CVE-2021-31933 Chamilo LMS File Upload RCE

Path traversal in File Upload leads to Remote Code Execution in Chamilo LMS


It’s been a bit since I spent some time looking for a web vuln… And this one was a great one to come back to.
This vulnerability allowed me to use a feature (which I later found was not needed any longer) that I found just by browsing the file system in the web root looking for interesting files. I identified a file named /main/upload/upload.php and started tampering with it. Through source code review, I identified a parameter that did not check user input which allowed me to specify any location on the file system through directory traversal. I could then upload to any location on the file system where the user running the web server process could write to.

Since I had complete control over the contents of the file, I next located a few places in the web root where I could write files (most directories were owned by root and restricted to root:root for writing, so I had limited options). One of them was not protected by the included .htaccess file, so that was a candidate for code execution.

The last hurdle was bypassing the php2phps() function that would rename php files and many of the variations to phps. However, phar was not renamed which enabled me to upload arbitrary php code and execute it as shell.phar.

I then had all of the pieces to achieve remote code execution from the perspective of an authenticated administrative user and was able to get a reverse shell as the www-data user. Shortly after, I wrote a full Proof of Concept in python and submitted a detailed report to, which I located through their security issues wiki page.

Chamilo was, by far, the most responsive organization I have ever dealt with for disclosing a vulnerability. Other organizations should view them as a model of how to handle a vulnerability being responsibly disclosed. Within two hours of me sending the report(at ~06:30 AM), they already had two patches pushed to GitHub and requested that I test them.


Operating System

Ubuntu Server 20.04.2 LTS (Focal Fossa)

Web server

Server version: Apache/2.4.41 (Ubuntu)
Server built: 2020-08-12T19:46:17

Chamilo Branch

Technical Details

Directory Traversal - File Upload

I discovered this through source code analysis while hunting for interesting files. I came across /main/upload/index.php and identified a parameter curdirpath that was not sufficiently sanitized.

if (isset($_POST['curdirpath'])) {
    $path = Security::remove_XSS($_POST['curdirpath']);
} else {
    $path = '/';


From there, the input is passed through remove_XSS() located here: /main/inc/lib/security.lib.php#L303

This originally tipped me off, but if you look further down on upload.document.php, you’ll see that curdirpath is passed directly to handle_uploaded_document(), which resides in fileUpload.lib.php

        $new_path = handle_uploaded_document(


So, that other stuff didn’t even matter, but it was enough to get me to see what happened if I posted ../../../../../../../tmp/ for curdirpath. XD

To continue, handle_uploaded_document() does a lot of stuff… curdirpath is passed in as the argument $uploadPath. The important thing is that there is no validation of the input prior to utilization here:

$whereToSave = $documentDir.$uploadPath;


It is verified that the file exists and is a directory, or if it doesn’t, then create the directory.
Then, after the filename is sanitized, $whereToSave is used in combination with $fileSystemName to create the variable $fullPath. Still, with no checking of the original value input through curdirpath. $filepath is also created using the original unchecked curdirpath value.

$fullPath = $whereToSave.$fileSystemName;

 // Example: /folder/picture.jpg
  $filePath = $uploadPath.$fileSystemName;

Then a switch statement determines whether to overwrite or rename based on whether or not the file already exists. For the video demo I do overwrite, but for the POC, I just leave the selection blank because it defaults to only save if the file doesn’t exist. That’s fine, because I generate a random name that probably doesn’t exist. The logic is pretty simple on how the file is put in place.

if (moveUploadedFile($uploadedFile, $fullPath)) {
    chmod($fullPath, $filePermissions);


Long story short here

  • Reading other peoples’ code is hard
  • Test your assumptions

Inadequate File Name Sanitization

Chamilo sanitizes the filename using the php2phps() function, which essentially uses regex to detect the usual suspects (php[234567], phtml) and changes the filename to .phps.

function php2phps($file_name)
    return preg_replace('/\.(php.?|phtml.?)(\.){0,1}.*$/i', '.phps', $file_name);


This is called through another function disable_dangerous_file() here: /main/inc/lib/fileUpload.lib.php#L300

The important catch, they forgot about .phar file types, which execute php code if you visit them in browser. So, that was my filetype of choice to upload. ### Finding a Place to Upload that will Execute Chamilo uses an .htaccess file to return a 403 if the user tries to access a specific file type in specific directories. Ideally, these conditions cover areas where the web server user www-data has write access. This is a good idea in the event that a user finds a flaw and is able to upload php files.

I found that most of the directories are owned by root. So, I did a find /var/www/html/chamilo-lms -type d -owner www-data 2>/dev/null to find all of the directories the web server user owned. One that stood out to me was the /web/ directory, as it was owned by www-data, but only the /web/css/ directory was protected by the htaccess file.

 # Prevent execution of PHP from directories used for different types of uploads
RedirectMatch 403 ^/app/(?!courses/proxy)(cache|courses|home|logs|upload|Resources/public/css)/.*\.ph(p[3457]?|t|tml|ar)$
RedirectMatch 403 ^/main/default_course_document/images/.*\.ph(p[3457]?|t|tml|ar)$
RedirectMatch 403 ^/main/lang/.*\.ph(p[3457]?|t|tml|ar)$
RedirectMatch 403 ^/web/css/.*\.ph(p[3457]?|t|tml|ar)$


So, I generally just test this via a shell on the system:
echo '<?php echo `id`;?>' > test.php
And visit it in browser to see if you get the output of the id command. I found that this worked, so I knew I had a good place to upload a file.

Proof of Concept


Next, I gave it a shot. This was my manual exploitation methodology.

Video here: chamilo-poc.mp4

Exploit source


[19 APR 2021 06:49] : Initial report sent to
[19 APR 2021 09:08] : Response received from Chamilo with two patches to test
[19 APR 2021 20:45] : I confirmed the patches addressed the issues
[28 APR 2021 ] Issue disclosed via Chamilo Security Issues page. They requested that I wait until after 12 MAY 2021 to disclose in order to give users a chance to patch. I was happy to do so.
[30 APR 2021] : CVE-2021-31933 assigned. [13 MAY 2021] : This writeup is released.


Back to top ↑


CVE-2020-28328 SuiteCRM RCE

Remediation testing I found another vulnerability during remediation testing, and that writeup can be found here.

Terminal Access on routers via UART

How to get a Shell on your Router (hopefully) Vulnerability hunting is hard, and it’s even harder if you don’t have access to the source. Hardware devices ma...

Back to top ↑