
Schritt-für-Schritt-Analyse und Laboraufbau für CVE-2023-39361, eine nicht authentifizierte SQL-Injection in Cacti v1.2.24, mit Anleitung zur Ausnutzung und Eindämmung.
Cacti ist ein quelloffenes Betriebsüberwachungstool, das in PHP, MySQL/MariaDB geschrieben ist und eine benutzerfreundliche Oberfläche bietet. Die Schwachstelle wurde im Jahr 2023 gefunden und betrifft alle Versionen vor 1.2.24. Dieser Sicherheitsfehler liegt in der unsachgemäßen Implementierung beim Einfügen von Werten in eine SQL-Abfrage. Es handelt sich um eine kritische SQL-Injection-Schwachstelle, die es Angreifern ermöglicht, die Datenbank zu ändern und Code aus der Ferne auszuführen.
In dieser Analyse werde ich Cacti der Einfachheit halber in Docker ausführen. Zuerst erstellen wir die Datei docker-compose.yml wie unten gezeigt und führen den Befehl docker-compose up -d aus.
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:
Nach dem Erstellen des Containers können wir auf Cacti zugreifen, indem wir http://localhost:80 aufrufen. Der nächste Schritt ist, Cacti auf Version 1.2.24 zu aktualisieren. Sie können das Upgrade-Skript hier herunterladen: https://pastebin.com/NfRiHLjR. Speichern Sie die Datei im selben Verzeichnis wie die docker-compose.yml-Datei und benennen Sie sie in upgrade_cacti.sh um. Führen Sie dann diese beiden Befehle aus:
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"
Jetzt können wir loslegen. Tauchen wir in den Code ein, um zu sehen, was passiert ist, das uns einen SQL-Injection-Angriff ermöglicht. Die anfällige Datei ist graph_view.php und wir benötigen nur einen Gast-Benutzer, um auf diese Datei zuzugreifen, was es jedem ermöglicht, diese Schwachstelle auszunutzen. In dieser Datei ist die unsichere Funktion grow_right_pane_tree().

Bevor wir tiefer in diese Funktion eintauchen, müssen wir rückwärts verfolgen, wie die Funktion ausgeführt wird.
<?php
switch (get_nfilter_request_var('action')) {
//....
// Viele andere Fälle
case 'tree_content':
//..... Etwas Code hier
// ---------
if (isset_request_var('node')) {
$parts = explode('-', sanitize_search_string(get_request_var('node')));
// Prüfen auf Baumanker
if (strpos(get_nfilter_request_var('node'), 'tree_anchor') !== false) {
$tree_id = $parts[1];
$node_id = 0;
}
//..... Etwas Code hier
if ($tree_id > 0) {
if (!is_tree_allowed($tree_id)) {
header('Location: permission_denied.php');
exit;
}
// Anfällige Funktion wird hier aufgerufen
grow_right_pane_tree($tree_id, $node_id, $hgdata);
}
Aus dem obigen Code-Snippet können wir sehen, dass die Funktion ausgeführt wird, wenn $tree_id > 0 ist. Alle Schritte können wie folgt erklärt werden:
Zuerst nimmt das Programm den Wert des Parameters action aus der Benutzereingabe und tritt in eine switch/case-Anweisung ein.
Im Fall tree_content holt der Code den Parameter node aus der Anfrage des Benutzers. Als nächstes wird die Eingabe durch das Zeichen - geteilt, in $part gespeichert und schließlich geprüft, ob tree_anchor in node vorkommt. Wenn alle Bedingungen zutreffen, wird $tree_id als zweites Element in $part zugewiesen. Zum Beispiel sind tree_content-1 oder 1-2-tree_content gültig und der Wert von $tree_id wird entsprechend gesetzt.
Ab diesem Punkt wird unsere URL so aussehen: http://localhost:80?action=tree_content&node=tree_anchor-1. Jetzt ist es an der Zeit, die Funktion grow_right_pane_tree() zu analysieren, um den Fehler auszunutzen.

Der anfällige Parameter ist rfilter, der direkt in den RLIKE-Operanden innerhalb der WHERE-Klausel übergeben wird. Was ihn anfällig macht, ist, dass rfilter in doppelte Anführungszeichen " eingeschlossen ist, der Parameter jedoch von der Funktion html_validate_tree_vars() überprüft wird, die wie folgt aussieht:
function html_validate_tree_vars() {
/* ================= Eingabevalidierung und Sitzungsspeicherung ================= */
$filters = array(
// .......
'rfilter' => array(
'filter' => FILTER_VALIDATE_IS_REGEX,
'pageset' => true,
'default' => '',
),
// ..........
);
validate_store_request_vars($filters, 'sess_grt');
/* ================= Eingabevalidierung ================= */
// ........
}
Der Filtertyp von rfilter ist auf FILTER_VALIDATE_IS_REGEX gesetzt und in $filters gespeichert, dann wird $filter an die Funktion validate_store_request_vars() übergeben. Tauchen wir in diese Funktion ein, das wird den Rest unserer Analyse offenbaren:
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;
}
Die Funktion validate_store_request_vars() ist für die Validierung der Benutzereingabe verantwortlich. Wenn der filter-Typ FILTER_VALIDATE_IS_REGEX ist, ruft die Funktion validate_is_regex() mit der preg_match-Funktion auf, um zu überprüfen, ob unsere Eingabe gültig ist. Laut PHP-Dokumentation:
preg_match() gibt 1 zurück, wenn das
patternmit dem angegebenensubjectübereinstimmt, 0, wenn nicht, oder false bei Fehler.