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 为空数组,additionalPropertiestrue。因此外层 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/categoriesposts 控制器的 get_items() 执行

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

阶段 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 (...) 子句,形成时间盲注。

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

相关推荐
网安蟹佬霸2 小时前
OSINT开源情报收集实战:从信息搜集到资产测绘(2026最新万字保姆级指南)
前端·网络·安全·web安全·网络安全·开源
ltl2 小时前
TLS 1.3 工程实践:1-RTT 与 0-RTT 的安全权衡
安全
80s7773 小时前
动态ip和静态ip有什么区别
服务器·网络·tcp/ip
鲁邦通物联网3 小时前
出海物联网设备的全球化网络接入与合规脱敏架构:基于 Node-RED 与 C++ 零拷贝的边缘系统设计
网络·物联网·系统架构·边缘计算·边缘计算网关·5g数采·工业级边缘计算网关
星恒讯工业路由器3 小时前
家庭无线组网方式全解析:从单路由到FTTR,哪种适合你?
网络·物联网·智能路由器·路由器·家庭组网·ac+ap·mesh组网
0566464 小时前
agent学习——流式响应与文本切分
网络·python·学习
mqiqe4 小时前
AgentScope Java 2.0 Agent 状态存储(AgentStateStore)深度解析:构建可恢复、可扩展的智能体运行时
java·运维·网络
mennekes4 小时前
数据中心安全配电设备如何选择?
运维·人工智能·科技·安全·制造
Flynt4 小时前
就编译了一下代码,浏览器密码没了——Rust 生态遭遇史上最狠供应链攻击
安全·rust·开源