
Analisi passo-passo e configurazione del laboratorio per CVE-2023-39361, un'iniezione SQL non autenticata in Cacti v1.2.24, con procedura di sfruttamento e mitigazione.
Cacti è uno strumento open-source di monitoraggio operativo scritto in PHP, MySQL/MariaDB, che fornisce un'interfaccia amichevole. La vulnerabilità è stata trovata nel 2023 e ha interessato tutte le versioni precedenti alla 1.2.24. Questo difetto di sicurezza risiede nell'implementazione impropria durante l'inserimento dei valori nella query SQL. Si tratta di una vulnerabilità critica di SQL injection che consente agli attaccanti di modificare il database e anche di eseguire codice in remoto.
In questa analisi, eseguirò Cacti in Docker per semplicità. Per prima cosa, creeremo il file docker-compose.yml come di seguito ed eseguiremo il comando docker-compose up -d.
version: '3.5'
services:
cacti:
image: "smcline06/cacti"
container_name: CVE-2023-39361
domainname: example.com
hostname: cacti
ports:
- "80:80"
- "443:443"
environment:
- DB_NAME=cacti_master
- DB_USER=cactiuser
- DB_PASS=cactipassword
- DB_HOST=db
- DB_PORT=3306
- DB_ROOT_PASS=rootpassword
- INITIALIZE_DB=1
- TZ=America/Los_Angeles
volumes:
- cacti-data:/cacti
- cacti-spine:/spine
- cacti-backups:/backups
links:
- db
db:
image: "mariadb:10.3"
container_name: CVE-2023-39361_db
domainname: example.com
hostname: db
ports:
- "3306:3306"
command:
- mysqld
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --max_connections=200
- --max_heap_table_size=128M
- --max_allowed_packet=32M
- --tmp_table_size=128M
- --join_buffer_size=128M
- --innodb_buffer_pool_size=1G
- --innodb_doublewrite=ON
- --innodb_flush_log_at_timeout=3
- --innodb_read_io_threads=32
- --innodb_write_io_threads=16
- --innodb_buffer_pool_instances=9
- --innodb_file_format=Barracuda
- --innodb_large_prefix=1
- --innodb_io_capacity=5000
- --innodb_io_capacity_max=10000
environment:
- MYSQL_ROOT_PASSWORD=rootpassword
- TZ=America/Los_Angeles
volumes:
- cacti-db:/var/lib/mysql
volumes:
cacti-db:
cacti-data:
cacti-spine:
cacti-backups:
Dopo aver creato il container, possiamo accedere a Cacti visitando http://localhost:80. Il passo successivo è aggiornare Cacti alla versione 1.2.24. Puoi scaricare lo script di aggiornamento qui: https://pastebin.com/NfRiHLjR. Salva il file nella stessa directory del file docker-compose.yml e rinominalo in upgrade_cacti.sh. Quindi esegui questi 2 comandi:
docker cp upgrade_cacti.sh CVE-2023-39361:/tmp/upgrade_cacti.sh && docker exec -it CVE-2023-39361 bash -c "bash /tmp/upgrade_cacti.sh"
Ora siamo pronti, immergiamoci nel codice per vedere cosa ci permette di effettuare un attacco di SQL injection. Il file vulnerabile è graph_view.php e per accedervi è sufficiente l'utente guest, il che consente a chiunque di sfruttare questa vulnerabilità. In questo file, la funzione insicura è grow_right_pane_tree().

Prima di addentrarci in questa funzione, dobbiamo rintracciare a ritroso come viene eseguita.
<?php
switch (get_nfilter_request_var('action')) {
//....
// Many other cases
case 'tree_content':
//..... Some code here
// ---------
if (isset_request_var('node')) {
$parts = explode('-', sanitize_search_string(get_request_var('node')));
// Check for tree anchor
if (strpos(get_nfilter_request_var('node'), 'tree_anchor') !== false) {
$tree_id = $parts[1];
$node_id = 0;
}
//..... Some code here
if ($tree_id > 0) {
if (!is_tree_allowed($tree_id)) {
header('Location: permission_denied.php');
exit;
}
// Vulnerable function is called here
grow_right_pane_tree($tree_id, $node_id, $hgdata);
}
Dal frammento di codice qui sopra, possiamo vedere che la funzione viene eseguita se $tree_id > 0. Tutti i passaggi possono essere spiegati come segue:
In primo luogo, il programma prende il valore del parametro action dall'input dell'utente e entra in un'istruzione switch/case.
Nel caso tree_content, il codice prende il parametro di richiesta node dall'utente. Successivamente, divide l'input tramite il carattere -, lo salva in $part e infine controlla se tree_anchor appare in node. Se tutte le condizioni corrispondono, $tree_id verrà assegnato come secondo elemento di $part. Ad esempio, tree_content-1 o 1-2-tree_content saranno validi e il valore di $tree_id sarà rispettivamente assegnato.
Da questo punto, la nostra URL sarà simile a questa: http://localhost:80?action=tree_content&node=tree_anchor-1. Ora è il momento di analizzare la funzione grow_right_pane_tree() per sfruttare il bug.

Il parametro vulnerabile è rfilter, che viene passato direttamente nell'operando RLIKE all'interno della clausola WHERE. Ciò che lo rende vulnerabile è che rfilter è racchiuso tra doppi apici ", ma il parametro viene controllato dalla funzione html_validate_tree_vars(), che si presenta così:
function html_validate_tree_vars() {
/* ================= input validation and session storage ================= */
$filters = array(
// .......
'rfilter' => array(
'filter' => FILTER_VALIDATE_IS_REGEX,
'pageset' => true,
'default' => '',
),
// ..........
);
validate_store_request_vars($filters, 'sess_grt');
/* ================= input validation ================= */
// ........
}
Il tipo di filtro di rfilter è impostato su FILTER_VALIDATE_IS_REGEX e salvato all'interno di $filters, poi $filter viene passato alla funzione validate_store_request_vars(). Immergiamoci in questa funzione, rivelerà il resto della nostra analisi:
function validate_store_request_vars($filters, $sess_prefix = '') {
// .......
if (cacti_sizeof($filters)) {
foreach($filters as $variable => $options) {
// ....
elseif ($options['filter'] == FILTER_VALIDATE_IS_REGEX) {
if (is_base64_encoded($_REQUEST[$variable])) {
$_REQUEST[$variable] = base64_decode($_REQUEST[$variable]);
}
$valid = validate_is_regex($_REQUEST[$variable]);
if ($valid === true) {
$value = $_REQUEST[$variable];
} else {
$value = false;
$custom_error = $valid;
}
}
// ........
function validate_is_regex($regex) {
// ........
if (@preg_match("'" . $regex . "'", NULL) !== false) {
ini_set('track_errors', $track_errors);
return true;
}
La funzione validate_store_request_vars() è responsabile della validazione dell'input dell'utente. Quando il tipo di filter è FILTER_VALIDATE_IS_REGEX, la funzione chiama validate_is_regex() con la funzione preg_match per verificare se il nostro input è valido. Secondo la documentazione PHP:
preg_match() restituisce 1 se
patterncorrisponde alsubjectdato, 0 se non corrisponde, o false in caso di errore.
L'istruzione if utilizza il confronto stretto, il che significa che solo quando si verifica un errore non possiamo entrare nell'istruzione if. $regex, che è il nostro input, è racchiuso tra due apici singoli '. L'intenzione degli sviluppatori è di evitare che l'utente inietti l'apice singolo ', spesso usato per iniziare un attacco di SQL injection. Questa sembra essere un'implementazione di sicurezza robusta, ma diamo un'altra occhiata a come rfilter viene passato nella clausola WHERE:
$sql_where .= ' (gtg.title_cache RLIKE "' . get_request_var('rfilter') . '" OR gtg.title RLIKE "' . get_request_var('rfilter') . '")';
Ah-oh, lo sforzo di validare l'input dell'utente diventa inutile poiché rfilter è racchiuso tra doppi apici ", il che consente agli attaccanti di iniettare facilmente " per uscire dall'istruzione.
Avendo capito perché è vulnerabile, ora andremo più a fondo nella query in cui viene passato $sql_where.

Dopo che $sql_where viene assegnato, il programma chiama la funzione get_allowed_tree_header_graphs(). La funzione si presenta così:
function get_allowed_tree_header_graphs($tree_id, $leaf_id = 0, $sql_where = '', $sql_order = 'gti.position', $sql_limit = '', &$total_rows = 0, $user_id = 0) {
// ......
if ($sql_where != '') {
$sql_where = " AND ($sql_where)";
}
$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;
$graphs = db_fetch_assoc("SELECT gti.id, gti.title, gtg.local_graph_id, h.description, gt.name AS template_name, gtg.title_cache, gtg.width, gtg.height, gl.snmp_index, gl.snmp_query_id
FROM graph_templates_graph AS gtg
INNER JOIN graph_local AS gl
ON gl.id = gtg.local_graph_id
INNER JOIN graph_tree_items AS gti
ON gti.local_graph_id = gl.id
LEFT JOIN graph_templates AS gt
ON gt.id = gl.graph_template_id
LEFT JOIN host AS h
ON h.id = gl.host_id
$sql_where
$sql_order
$sql_limit");
$sql = "SELECT COUNT(*)
FROM graph_templates_graph AS gtg
INNER JOIN graph_local AS gl
ON gl.id=gtg.local_graph_id
INNER JOIN graph_tree_items AS gti
ON gti.local_graph_id=gl.id
LEFT JOIN graph_templates AS gt
ON gt.id=gl.graph_template_id
LEFT JOIN host AS h
ON h.id=gl.host_id
$sql_where";
$total_rows = get_total_row_data($user_id, $sql, array(), 'graph');
return $graphs;
}
È qui che $sql_where viene passato in una query. Nota che la variabile viene assegnata altre 2 volte prima di essere passata, ovvero $sql_where = " AND ($sql_where)"; e $sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;. Combinato con la prima assegnazione, ecco come viene creato finalmente $sql_where:
$sql_where .= ' (gtg.title_cache RLIKE "' . get_request_var('rfilter') . '" OR gtg.title RLIKE "' . get_request_var('rfilter') . '")';
$sql_where = " AND ($sql_where)";
$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;
Dopo queste 3 assegnazioni, $sql_where sarà simile a questo:
$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id) AND ((gtg.title_cache RLIKE "' . get_request_var('rfilter') . '" OR gtg.title RLIKE "' . get_request_var('rfilter') . '"))";
La query in cui viene passato $sql_where sembra complicata, ma possiamo creare un'altra semplice query con una struttura simile:
select * from users WHERE (TRUE) AND ((username RLIKE "' get_request_var('rfilter') '" -- qualcosa dietro che non è necessario))
Per iniettare con successo il nostro payload, dobbiamo uscire da " e da )). Pertanto, questo payload potrebbe funzionare "));SELECT SLEEP(5) --. Il nostro payload sarà simile a questo: select * from users WHERE (TRUE) AND **((username RLIKE "'"));SELECT SLEEP(5) --. Sembra buono! Proviamo su Cacti

Il motivo per cui il server non resta in pausa per 2 secondi è che abbiamo causato un errore nella funzione preg_match(). Diamo un'altra occhiata alla funzione
if (@preg_match("'" . $regex . "'", NULL) !== false) {
ini_set('track_errors', $track_errors);
return true;
}
Questa funzione prende il nostro input come espressione regolare. E ) viene usato per raggruppare un insieme di caratteri. Poiché non è chiuso, la funzione restituisce un errore. D'altra parte, se proviamo "(());SELECT SLEEP(5) --, non riusciamo a uscire da (( nella query SQL. Tuttavia, c'è un trucco qui, ed è "OR"(("));SELECT SLEEP(5) --. Lascia che ti spieghi: racchiudo (( tra doppi apici per trasformarlo in una stringa nella query SQL e uso OR per farlo corrispondere al username precedente. Il doppio apice " nell'espressione regolare, però, è considerato solo un carattere normale, ecco perché possiamo sfruttare questa caratteristica per uscire dalla query SQL.

Basandoci sul tempo di risposta, possiamo essere certi di aver iniettato con successo SQL nel database. Questa è una vulnerabilità critica di SQL injection. L'attaccante può sfruttare questo bug per prendere il controllo dell'account amministratore o persino eseguire comandi in remoto.
Come al solito, le vulnerabilità di injection in generale si verificano a causa dell'assenza di validazione dell'input. Tuttavia, in questo caso, gli sviluppatori hanno applicato delle validazioni, ma in modo improprio. Per risolvere il problema, basta semplicemente racchiudere il parametro rfilter tra apici singoli ' invece che tra doppi apici ".

Con questa semplice correzione, lo sforzo di validare l'input dell'utente funzionerà ora correttamente. Proviamo di nuovo il payload per vedere se è ancora vulnerabile.

Con lo stesso payload, ora la risposta è molto veloce, il che significa che non possiamo più iniettare comandi SQL.
Da questa analisi CVE, possiamo imparare che anche un errore minore può portare a un esito catastrofico. Dovremmo rivedere attentamente il codice e testare frequentemente il nostro sito web per renderlo sempre più sicuro. Questa è la fine dell'analisi, spero che tu abbia imparato qualcosa di utile oggi. Buon hacking!