
Analyse étape par étape et configuration de laboratoire pour CVE-2023-39361, une injection SQL non authentifiée dans Cacti v1.2.24, avec une procédure d'exploitation et d'atténuation.
Cacti est un outil de surveillance opérationnelle open source écrit en PHP, MySQL/MariaDB, qui offre une interface conviviale. La vulnérabilité a été découverte en 2023 et affectait toutes les versions antérieures à 1.2.24. Cette faille de sécurité réside dans une implémentation incorrecte lors de l'insertion de valeurs dans une requête SQL. Il s'agit d'une injection SQL critique qui permet aux attaquants de modifier la base de données ainsi que d'exécuter du code à distance.
Dans cette analyse, je vais exécuter Cacti dans Docker par souci de simplicité. Tout d'abord, nous allons créer le fichier docker-compose.yml comme ci-dessous et exécuter la commande 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:
Après avoir construit le conteneur, nous pouvons accéder à Cacti en naviguant vers http://localhost:80. L'étape suivante consiste à mettre à jour Cacti vers la version 1.2.24. Vous pouvez télécharger le script de mise à jour ici : https://pastebin.com/NfRiHLjR. Enregistrez le fichier dans le même répertoire que le fichier docker-compose.yml et renommez-le en upgrade_cacti.sh. Exécutez ensuite ces 2 commandes :
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"
Maintenant, nous sommes prêts à plonger dans le code pour voir ce qui nous permet de mener une attaque par injection SQL. Le fichier vulnérable est graph_view.php et nous avons seulement besoin d'un utilisateur invité pour accéder à ce fichier, ce qui permet à tout le monde d'exploiter cette vulnérabilité. Dans ce fichier, la fonction non sécurisée est grow_right_pane_tree().

Avant d'approfondir cette fonction, nous devons remonter pour comprendre comment elle est exécutée.
<?php
switch (get_nfilter_request_var('action')) {
//....
// De nombreux autres cas
case 'tree_content':
//..... Du code ici
// ---------
if (isset_request_var('node')) {
$parts = explode('-', sanitize_search_string(get_request_var('node')));
// Vérifie l'ancre de l'arbre
if (strpos(get_nfilter_request_var('node'), 'tree_anchor') !== false) {
$tree_id = $parts[1];
$node_id = 0;
}
//..... Du code ici
if ($tree_id > 0) {
if (!is_tree_allowed($tree_id)) {
header('Location: permission_denied.php');
exit;
}
// La fonction vulnérable est appelée ici
grow_right_pane_tree($tree_id, $node_id, $hgdata);
}
D'après l'extrait de code ci-dessus, nous pouvons voir que la fonction sera exécutée si $tree_id > 0. Toutes les étapes peuvent être expliquées comme ci-dessous :
Tout d'abord, le programme prend la valeur du paramètre action de l'entrée utilisateur et entre dans une instruction switch/case.
Dans le cas tree_content, le code prend le paramètre node de la requête de l'utilisateur. Ensuite, il divise l'entrée par le caractère -, l'enregistre dans $part et vérifie enfin si tree_anchor apparaît dans node. Si toutes les conditions correspondent, $tree_id sera assigné comme second élément dans $part. Par exemple, tree_content-1 ou 1-2-tree_content seront valides et la valeur de $tree_id sera respectivement.
À partir de là, notre URL ressemblera à ceci : http://localhost:80?action=tree_content&node=tree_anchor-1. Il est maintenant temps d'analyser la fonction grow_right_pane_tree() pour exploiter le bug.

Le paramètre vulnérable est rfilter qui est directement passé dans l'opérande RLIKE dans la clause WHERE. Ce qui le rend vulnérable, c'est que rfilter est entouré de guillemets doubles ", mais le paramètre est vérifié par la fonction html_validate_tree_vars(), qui ressemble à ceci :
function html_validate_tree_vars() {
/* ================= validation des entrées et stockage en session ================= */
$filters = array(
// .......
'rfilter' => array(
'filter' => FILTER_VALIDATE_IS_REGEX,
'pageset' => true,
'default' => '',
),
// ..........
);
validate_store_request_vars($filters, 'sess_grt');
/* ================= validation des entrées ================= */
// ........
}
Le type de filtre de rfilter est défini sur FILTER_VALIDATE_IS_REGEX et stocké dans $filters, puis $filter est passé dans la fonction validate_store_request_vars(). Plongeons dans cette fonction, cela révélera le reste de notre analyse :
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 fonction validate_store_request_vars() est responsable de la validation des entrées utilisateur. Lorsque le type de filtre est FILTER_VALIDATE_IS_REGEX, la fonction appelle la fonction validate_is_regex() avec la fonction preg_match pour vérifier si notre entrée est valide. Selon la documentation PHP :
preg_match() retourne 1 si le
patterncorrespond ausubjectdonné, 0 si ce n'est pas le cas, ou false en cas d'échec.