ILIAS 7 & 8 -  Multiple RCE vulnerabilities through import features - Security lab - rehme.infosec

Further publications

In my security lab, I write blog posts about identified security vulnerabilities, which are published in accordance with the responsible disclosure procedure.

Veröffentlichung

ILIAS 7 & 8 - Multiple RCE vulnerabilities through import features 

13.08.2024 - Rene Rehme

Im Zuge eines Penetrationstests haben wir mehrere Sicherheitslücken in ILIAS Version 7.30 und 8.9 identifiziert. Es handelt sich um Remote Code Execution (RCE) Sicherheitslücken, welche durch eine sog. Insecure Deserialization bewerkstelligt werden konnten. Eine RCE erlaubt einem Angreifer, serverseitig Code auszuführen.

Responsible Disclosure

May 10, 2024 - The security vulnerability was identified.

May 14, 2024 - The security vulnerability was reported to ILIAS.

May 24, 2024 - ILIAS open source e-Learning e.V. notes: Security updates will only be provided for ILIAS versions 8, 9, and 10 (trunk).

NO FIX RELEASED FOR VERSION 7. IT IS URGENTLY RECOMMENDED TO UPDATE TO THE NEXT MAJOR VERSION!

Allgemeine Beschreibung

Vulnerabel ist das Modul QuestionPool (Fragenpool für Tests) bzw. die Import Funktionalität dieses Moduls in ILIAS <= 8.9. Ein potenzieller Angreifer kann bei einer Ausnutzung dieser Sicherheitslücke serverseitig Code ausführen. Die Voraussetzung einer Ausnutzung ist ein User-Account mit den Rechten write und read, für die Erstellung und das Einsehen eines Fragepools für Tests.

 

Die Remote Code Execution (RCE) wird durch die vom Benutzer bereitgestellten Eingaben ermöglicht, da diese ungefiltert an eine PHP-Funktion unserialize() übergeben werden.

Proof of Concept (PoC)

Folgend die Logik, welche bei einem Import in ILIAS 8 bis zur betreffenden PHP-Funktion unserialize() durchläuft:

class.ilObjQuestionPoolGUI.php
public function uploadQplObject($questions_only = false)
{
	[...]
    ilUtil::moveUploadedFile($_FILES["xmldoc"]["tmp_name"], $_FILES["xmldoc"]["name"], $full_path);
	[...]
    if (strcmp($_FILES["xmldoc"]["type"], "text/xml") == 0) {
        $qti_file = $full_path;
        ilObjTest::_setImportDirectory($basedir);
    } else {
        // unzip file
        ilUtil::unzip($full_path);

        // determine filenames of xml files
        $subdir = basename($file["basename"], "." . $file["extension"]);
        ilObjQuestionPool::_setImportDirectory($basedir);
        $xml_file = ilObjQuestionPool::_getImportDirectory() . '/' . $subdir . '/' . $subdir . ".xml";
        $qti_file = ilObjQuestionPool::_getImportDirectory() . '/' . $subdir . '/' . str_replace("qpl", "qti", $subdir) . ".xml";
    }
	[...]
    $_SESSION["qpl_import_xml_file"] = $xml_file;
    [...]
}

Über die Methode uploadQplObject() wird die Import-Datei via $_FILES["xmldoc"]; behandelt. Dabei wird die hochgeladene Import-Datei (.zip) in ein Import Verzeichnis verschoben [1] und entpackt [2]. Der Pfad zum XML file wird bestimmt [3], um anschließend in einer Session qpl_import_xml_file [4] hinterlegt zu werden.

public function importVerifiedFileObject()
{
    if ($_POST["questions_only"] == 1) {
        [...]
    } else {
        include_once("./Modules/TestQuestionPool/classes/class.ilObjQuestionPool.php");
        // create new questionpool object
        $newObj = new ilObjQuestionPool(0, true);
        [...]
    }   
    if (is_file($_SESSION["qpl_import_dir"] . '/' . $_SESSION["qpl_import_subdir"] . "/manifest.xml")) {
        [...]
    } else {
        [...]
        // import page data
        if (strlen($_SESSION["qpl_import_xml_file"])) {
  			[...]
            $newObj->fromXML($_SESSION["qpl_import_xml_file"]);
        }
        [...]
    }
    [...]
}

 Ein ilObjQuestionPool Objekt wird initialisiert [1] und das XML aus der Session an die Methode fromXML() [2] übergeben. 

class.assTextQuestionImport.php
public function fromXML(&$item, $questionpool_id, &$tst_id, &$tst_object, &$question_counter, &$import_mapping)
{
    [...]
    if ($item->getMetadataEntry('termrelation') !== 'non'
        && $item->getMetadataEntry('termrelation') !== null) {
        $termscoring = $this->fetchTermScoring($item);
        [...]
    }
    [...]
}

In der Methode fromXML() wir u.a. überprüft, ob der Metatadata Eintrag termrelation gegeben ist und nicht non entspricht.  In der Import XML darf dieser Wert entsprechend nicht non, damit fetchTermScoring() [2] angesprochen wird. z.B.:

A part of the import XML
<qtimetadatafield>
    <fieldlabel>termrelation</fieldlabel>
    <fieldentry>1</fieldentry>
</qtimetadatafield>

Finale Codeausführung durch unserialize()

class.SurveyMatrixQuestion.php
protected function fetchTermScoring($item): array
{
    $termScoringString = $item->getMetadataEntry('termscoring');
    [...]
    $termScoring = @unserialize($termScoringString);
    [...]
    $termScoringString = base64_decode($termScoringString);
    $termScoring = unserialize($termScoringString);
    [...]
}

Die Funktion unserialize() wandelt den in der Variable $termScoringString [1] deklarierten String in eine Datenstruktur um. Da wir die Kontrolle über die Textdarstellung haben, können von uns definierte PHP-Objekte erstellt werden. 

 

Hintergrund: ILIAS erstellt bei einem Export für den Wert des Attributes fieldentry innerhalb von qtimetadatafield einen String, welcher beim termscoring Block ein serialisiertes PHP-Objekt darstellt. In umgekehrter Richtung wird die Textdarstellung bei einem Import in ein PHP-Objekt umgewandelt. PHP erledigt dies ohne jegliche Geschäftslogik via unserialize()

Es gibt mehrere Wege wie sich durch diese Begebenheit potenziell eine RCE ermöglichen lässt. Neben der denkbaren Ausnutzung von "Magic Methods" in ILIAS, haben wir eine Gadget chain verwendet. 

import.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE questestinterop SYSTEM "ims_qtiasiv1p2p1.dtd">
<!--Generated by ILIAS XmlWriter-->
<questestinterop>
	<item ident="il_0_qst_6" title="test" maxattempts="0">
		[...]
		<itemmetadata>
			<qtimetadata>
				[...]
				<qtimetadatafield>
					<fieldlabel>termscoring</fieldlabel>
					<fieldentry>[PAYLOAD]</fieldentry>
				</qtimetadatafield>
				<qtimetadatafield>
					<fieldlabel>termrelation</fieldlabel>
					<fieldentry>1</fieldentry>
				</qtimetadatafield>
			</qtimetadata>
		</itemmetadata>
		[...]
	</item>
</questestinterop>

Die Payload wird entsprechend dem XML Tag <fieldentry> unter <qtimetadatafield> als Wert angegeben [1].

 

Abschließend kann das XML File gezippt und regulär über die Import Funktionalität Option 2: Fragenpool für Umfragen importieren hochgeladen werden.

Die Payload wird während des Imports bei der Datenverarbeitung im Hintergrund ausgeführt. Eine Überprüfung ob der Schadcode ausgeführt wurde, kann via cat /tmp/poc.txt auf dem Webserver erfolgen:

 

content image

Sicherheitslücken in ILIAS 7, die zu einer RCE führen

Neben der Klasse class.SurveyMatrixQuestion.php gibt es noch weitere zur Durchführung einer RCE. Je nachdem welcher Typ von Frage importiert wird, können auch folgende unserialize() Funktionen in ILIAS <= 7.30 getriggert und ausgenutzt werden. Insgesamt handelt es sich um 7 Möglichkeiten einer potenziellen Ausnutzung unterschiedlicher Aktionen:

different files
/Modules/SurveyQuestionPool/Questions/class.SurveyMatrixQuestion.php
  1405:    $this->layout = unserialize($layout);

/Modules/TestQuestionPool/classes/import/qti12/class.assErrorTextImport.php:
  54:    $errordata = unserialize($item->getMetadataEntry("errordata");
  
/Modules/TestQuestionPool/classes/import/qti12/class.assFlashQuestionImport.php:
  54:    $this->object->setParameters(unserialize($item->getMetadataEntry("params")));

/Modules/TestQuestionPool/classes/import/qti12/class.assFormulaQuestionImport.php:
  53:    $data = unserialize($item->getMetadataEntry($variable));

/Modules/TestQuestionPool/classes/import/qti12/class.assFormulaQuestionImport.php:
  62:    $data = unserialize($item->getMetadataEntry($result));

/Modules/TestQuestionPool/classes/import/qti12/class.assTextQuestionImport.php:
  236:    $termScoring = unserialize($termScoringString);

/Modules/TestQuestionPool/classes/import/qti12/class.assTextQuestionImport.php:
  243:    $termScoring = unserialize($termScoringString);

Auf eine Code-Aufschlüsslung wurde verzichtet. Die unserialize() Funktionen in den aufgzählten Klassen sind in ILIAS 7 ausnutzbar.

Auswirkungen

Diese Sicherheitslücke kann erhebliche Auswirkungen auf die Sicherheit der betroffenen ILIAS-Installation und des zugrunde liegenden Betriebssystem haben. Alle auf dem Server gespeicherten Daten, Dateien und Komponenten, die der Webserver-User mit gegebenen Rechten lesen, bearbeiten oder löschen kann, sind potenziell betroffen. Ein Angreifer, der diese Sicherheitslücke erfolgreich ausnutzt, könnte zudem das System beschädigen oder weitere Angriffe in Kombination anderer Sicherheitslücken durchführen.

Klassifikation

CWE-20 Improper Input Validation

CWE-502 Deserialization of Untrusted Data

Bewertung

Exploitability Metrics

Attack Vector (AV): N
Es handelt sich um einen Netzwerkangriff. Die verwundbare Komponente ist "aus der Ferne ausnutzbar" und wird als ein Angriff betrachtet, der auf Protokollebene über das Internet stattfindet.
 

Attack Complexity (AC): L
Besondere Zugangsbedingungen oder mildernde Umstände bestehen nicht.
 

Attack Requirements (AT): N
Es wird davon ausgehen, dass die Payload in den meisten Fällen erfolgreich ausgeführt wird. Es werden keine besonderen Einstellungen oder Konfigurationen vorrausgesetzt.
 

Privileges Required (PR): H
Der Angreifer benötigt Privilegien, die Benutzerfunktionen eines Users bereitstellen, welche genau genommen das Lesen und Schreiben von "Fragenpool für Tests" erlaubt. Für eine Ausnutzung reicht i.d.R ein Tutoren-Account. Wir haben die Metrik PR mit "High" gewertet, da zwar keine administrativen (Root) Rechte erforderlich sind, Tutoren-Rechte aber in den meisten Fällen nicht durch eine reguläre Account-Registrierung gegeben sind.

User Interaction (UI): N
Es ist keine User-Interaktion durch ein Opfer notwendig.

Vulnerable System Impact Metrics

Confidentiality (VC): H
Bei einen erfolgreichen Angriff kommt es zu einem totalen Verlust der Vertraulichkeit. Ressourcen innerhalb der betroffenen Komponente werden dem Angreifer bei gegebenem Lesezugriff und entsprechender Berechtigung durch den Webserver-User www-data offengelegt. Code wird serverseitig ausgeführt.
 

Integrity (VI): H
Bei einem erfolgreichen Angriff kommt es zu einem totalen Verlust der Integrität. Der Angreifer ist in der Lage Dateien der betroffenen Anwendung zu verändern. Code wird serverseitig ausgeführt.
 

Availability (VA): H
Bei einem erfolgreichen Angriff ist der Angreifer ist in der Lage, den Zugang zu den Ressourcen der betroffenen Applikation vollständig zu verweigern, indem z.B. Dateien oder Daten gelöscht werden. Code wird serverseitig ausgeführt.

Subsequent System Impact Metrics

Confidentiality (SC): H
Bei einem erfolgreichen Angriff kommt es zu einem totalen Verlust der Vertraulichkeit. Ressourcen und Komponenten des Betriebssystems werden dem Angreifer bei gegebenem Lesezugriff und entsprechender Berechtigung durch den Webserver-User www-data offengelegt. Der Zugriff wird z.B. auf MySQL, sonstige Komponenten oder andere Applikationen tendenziell ermöglicht. Code wird serverseitig ausgeführt.
 

Integrity (SI): H
Bei einem erfolgreichen Angriff kommt es zu einem totalen Verlust der Integrität. Der Angreifer ist in der Lage Dateien des Betriebssystems oder anderen Applikationen zu verändern. Code wird serverseitig ausgeführt.
 

Availability (SA): H
Bei einem erfolgreichen Angriff ist der Angreifer ist in der Lage, den Zugang zu den Ressourcen aus Komponenten des Betriebssystems oder ggf. anderen Applikationen vollständig zu verweigern, indem z.B. Dateien oder Daten mit den Rechten des Webserver-Users gelöscht werden. Code wird serverseitig ausgeführt.