🎯 CVE-2026-63030 深度技术分析:漏洞根因 · PoC/EXP · 检测指纹
CVE-2026-63030 深度技术分析
CVE-2026-63030 WordPress Core 解释冲突漏洞深度技术分析
CVE-2026-63030 WordPress Core 解释冲突漏洞深度技术分析
REST Batch 路由混淆 → SQL Injection → 远程代码执行
摘要:WordPress 核心近期披露的 CVE-2026-63030 是一枚由 REST API 批量请求端点 /batch/v1 路由混淆引起的解释冲突(Interpretation Conflict)漏洞。未认证攻击者可通过精心构造的批量子请求,使 serve_batch_request_v1() 在匹配路由时发生数组索引错位,进而绕过子请求权限验证,触发 SQL 注入,最终与 CVE-2026-60137 链式利用实现远程代码执行。CISA KEV 已于 2026 年确认该漏洞在野外被积极利用,多个公开 PoC 仓库(wp2shell-poc 等)已发布,对运行受影响版本(6.9.0 – 6.9.4、7.0.0 – 7.0.1)的 WordPress 站点构成严重威胁。
📌 漏洞概述
| CVE 编号 | CVE-2026-63030 |
| 漏洞类型 | Interpretation Conflict(解释冲突)/ 逻辑错误(CWE-436) |
| 影响组件 | WordPress Core REST API Batch Endpoint(/wp-json/batch/v1) |
| 影响版本 | 6.9.0 – 6.9.4 7.0.0 – 7.0.1 |
| 不受影响版本 | ≤ 6.8.5 |
| 严重性 | Critical(严重) 未认证 SQL 注入 + RCE 利用链 |
| 利用状态 | CISA KEV 收录 已确认野外被利用,且存在活跃 PoC 仓库 |
| 关联漏洞 | CVE-2026-60137(用于链式利用) |
该漏洞由 Searchlight Cyber 的 wp2shell 安全研究首次披露,核心问题在于 WordPress REST API 批量处理机制中的路由匹配与权限校验过程存在数组索引失步,攻击者可以将一个经过权限校验的请求上下文“转嫁”给另一个完全未经验证的请求。由于全球有大量 WordPress 站点开放了 REST API 且无需认证即可访问 /batch/v1 端点,该漏洞的实际暴露面非常广泛。
🔬 漏洞根因分析
WordPress 从 6.9 版本开始,在 REST API 中提供了批量请求(Batch Request)能力。客户端可以通过一次对 /wp-json/batch/v1 的 POST 调用,在请求体 requests 数组中包含多个子请求。每个子请求都包含独立的 path、method 和 body。从设计意图上讲,服务端需要对每个子请求执行和普通 REST 请求完全一致的路由匹配、权限校验和参数处理。
根据 PoC 逆向分析,核心处理函数 serve_batch_request_v1() 在处理子请求时维护了两个关键数组:
$matches—— 记录每个子请求经由路由匹配后命中的处理程序(handler);$validation—— 记录每个子请求经过权限校验后的结果。
在正常情况下,两个数组的长度一致,相同索引对应同一个子请求。但当某个子请求的 URL 无法被 wp_parse_url() 正确解析时(例如包含畸形 scheme、非法字符或空 host),处理逻辑会“跳出”正常匹配分支:
- 该子请求的校验失败信息被追加至
$validation; - 但没有向
$matches追加任何占位元素。
这种不对称写入导致两个数组从该位置开始产生索引偏差。后续分发子请求时,代码使用同一个循环偏移量同时访问 $matches 和 $validation。于是,第 N 个子请求执行所依赖的 handler 可能来自第 N+1 个子请求的路由匹配结果,同时也可能携带第 N-1 个子请求的权限校验结果。这就是路由混淆(Route Confusion)的根因。
进一步分析,公开 PoC 中对该原语(primitive)进行了两次嵌套利用:
🧪 PoC 复现
从 GitHub 公开仓库抓取的实际 PoC 代码(仓库)。📋 代码元数据语言md来源Icex0/wp2shell-poc针对性⚠️ 疑似通用代码(未检测到 CVE 引用,仅供参考)依赖见代码注释/README用法详见代码注释中的使用说明
# wp2shell-poc
Independent proof-of-concept for the unauthenticated WordPress REST batch route-confusion
SQL injection associated with Searchlight Cyber's wp2shell advisory.
This repository is not Searchlight Cyber's official checker. `check` confirms the SQLi path,`read` demonstrates database read,
and `shell` opens a plugin-backed command shell either with
supplied administrator credentials or by first exercising the SQLi-to-admin bridge.

**Detection / IoCs:** Elastic Security Labs and Eye Security published detection guidance and indicators for this chain. See the [Elastic write-up](https://www.elastic.co/security-labs/wp2shell-wordpress-rce-detection-elastic-defend) and the [Eye Security defenders guide](https://labs.eye.security/wp2shell-defenders-guide/).
## Affected versions
Searchlight Cyber's advisory lists these wp2shell RCE exposure ranges:
|
Version range |Status ||------------- |------ ||<= 6.8.5 |Not affected ||6.9.0 – 6.9.4 |Affected ||7.0.0 – 7.0.1 |Affected |## How it works
The REST batch endpoint (`/batch/v1`) is unauthenticated and runs several sub-requests in one
call,
relying on each sub-request being validated and permission-checked on its own.
`serve_batch_request_v1()` builds two parallel arrays — `$matches` (the matched handler per
sub-request) and `$validation` (the validation result per sub-request) — then indexes both by
the same offset when dispatching. A sub-request whose path fails `wp_parse_url()` is appended to
`$validation` but not to `$matches`,
so the arrays fall out of step and a sub-request is
dispatched under a **different** sub-request's handler. That is the route confusion.
The PoC nests the primitive twice:
1. A `POST /wp/v2/posts` request that carries a `requests` body is dispatched under the batch
handler itself. Having been validated as a posts request,its `requests` list is never checked
against the batch schema,
so its sub-requests may use `GET` — the method allow-list is
bypassed.
2. Inside that inner batch,a `GET /wp/v2/posts/999999` item-route request carries posts collection
query params such as `author_exclude`,`orderby`,and `per_page`. The `999999` ID does not need
to exist;it is just an unlikely post ID used to match the item route,
whose schema does not
validate those collection-only params. The desync then dispatches the same request under posts
`get_items()`,where `author_exclude` maps to the `WP_Query` `author__not_in` query var,
which
the vulnerable build interpolates into SQL as a string.
The result is a boolean- and time-based blind SQL injection reachable pre-authentication. This PoC
also includes the UNION fake-post primitive used by the SQLi-to-admin chain.
The RCE path implemented here is:
1. Use UNION fake `wp_posts` rows to render attacker-controlled content through a posts collection.
The render bridge uses the `/wp/v2/posts/999999` item-route source — the same route the SQLi read
uses to reach `get_items()`.
2. Use that render to make WordPress create real oEmbed cache posts.
3. Recover those real cache post IDs through the SQLi.
4. In one poisoned batch request,
recast those IDs as a customizer changeset,navigation item,and
request hook shape.
5. Let the same request reach `POST /wp/v2/users`,creating a generated administrator.
6. Log in as that generated administrator and use plugin upload behavior to run a command.
Steps 1–5 are pre-authentication;
the command-execution step is authenticated admin plugin upload.
## Requirements
Python 3.8+ and the standard library. No third-party dependencies.
## Usage
Run it from the repository directory:
```
./wp2shell.py <command><url>
[options]
```
Or `pip install .` to get a `wp2shell` command on your `PATH`.
### check — non-destructive vulnerability check
Prints passive WordPress markers and public version hints first,then sends a benign batch marker
probe. A vulnerable batch implementation returns HTTP 207 with the route-confusion marker pattern
`parse_path_failed`,`block_cannot_read`,
and `rest_batch_not_allowed`.
The marker probe is based on the WordPress core fix. The malformed `///` request creates
`parse_path_failed`;a `/wp/v2/posts` request acts as a batch-allowed spacer;the
`/wp/v2/block-renderer/...` route is not batch-allowed but returns `block_cannot_read` if its
handler is reached anonymously;
`/batch/v1` gives `rest_batch_not_allowed`. On vulnerable builds
the parse error shifts the batch handler arrays out of step,so the spacer request is dispatched
under the block-renderer handler. Fixed builds keep the arrays aligned,so this exact all-three
pattern should not appear for the crafted probe.
By default,
`check` stops there and does not send an SQLi payload. Use `--confirm-sqli` when you
also want an active SQLi confirmation. The confirmation tries the UNION read primitive first and
falls back to paired timing probes if UNION reflection is unavailable.
The signals are independent: a version hint is only a hint,the marker pattern shows route
confusion,
and `--confirm-sqli` shows a payload reached the database. A WAF can block the payload,
so a failed confirmation doesn't prove the bug is absent.
```
./wp2shell.py check http://target
./wp2shell.py check targets.txt # scan every URL in the file
```
### read — extract data through SQL injection
```
./wp2shell.py read http://target # server fingerprint
./wp2shell.py read http://target --preset users # user logins and password hashes
./wp2shell.py read http://target --query "SELECT @@version"
```
By default extraction is `--technique auto`,
which tries the available methods in this order:
1. **union** — forges a fake `WP_Post` row via `UNION` and reads its title back from the REST
response as `||HEX(value)||`. The payload uses the same `/wp/v2/posts/999999` source route with
`orderby=none` and `per_page=500` so the fake row survives as a rendered post. One request per
value.
2. **error** — `EXTRACTVALUE`/`UPDATEXML` leak ~15 bytes per request,
when the target reflects
MySQL errors (e.g. `WP_DEBUG_DISPLAY` on).
3. **blind** — boolean binary search,~8 requests per character;reads the posts collection
`X-WP-Total` header as the true/false signal and needs no reflected value.
Force one with `--technique union|error|blind`. These read paths do not write database rows.
### shell — command execution
With `--user` and `--password`,
`shell` logs in with supplied administrator credentials and uses
WordPress plugin upload behavior.
Without credentials,`shell` first runs the pre-auth SQLi-to-admin bridge,logs in as the generated
administrator,
then uploads the plugin shell.
```
./wp2shell.py shell http://target --user admin --password '<recovered>' --cmd id
./wp2shell.py shell http://target --user admin --password '<recovered>' -i # interactive shell
./wp2shell.py shell http://target --cmd id # pre-auth bridge
./wp2shell.py shell http://target -i # pre-auth interactive
```
`shell` uploads a plugin webshell (locked behind a random path and a per-run token) and prints its
path. The uploaded webshell is removed automatically. When the pre-auth bridge creates an
administrator,
that generated account is removed automatically after the shell session finishes.
## Options
|Option |Applies to |Description ||------------------- |---------- |-------------------------------------------------------------------- ||`--proxy URL` |all |Route traffic through an HTTP proxy (for example,
Burp). ||`--timeout N` |all |Request timeout in seconds. ||`--sleep N` |check |Delay used by the timing fallback for `--confirm-sqli`. ||`--samples N` |check |Timing pairs used by the timing fallback for `--confirm-sqli`. ||`--confirm-sqli` |check |
Also send an active SQLi confirmation payload. ||`--preset` |read |`fingerprint` or `users`. ||`--technique` |read |`auto` (default),`union` (in-band,forges a fake post),`error` (in-band,needs visible DB errors),or `blind`. ||`--query` |read |
A scalar SQL expression to read. ||`--prefix` |read |Database table prefix (default `wp_`). ||`--max-length N` |read |Maximum characters read per value (default 128). ||`--user` / `--password` |shell |Optional admin credentials;omit both to use the pre-auth bridge. ||
`--cmd` |shell |Command to run (omit when using `-i`). ||`-i` / `--interactive` |shell |Open an interactive shell after deploying. |## Remediation
Update to WordPress 7.0.2,or 6.9.5 if the site is on the 6.9 branch. Until then,block both `/wp-json/batch/v1` and the `rest_route=/batch/v1` query parameter at
the edge,
or require authentication for the batch endpoint via the
`rest_pre_dispatch` filter.
## Legal
For authorized security testing only. Use it exclusively against systems you own or have explicit
written permission to test. No warranty is provided and no liability is accepted for misuse.
## References
- WordPress 7.0.2 release announcement — <https://wordpress.org/news/2026/07/wordpress-7-0-2-release/>
- Searchlight Cyber wp2shell advisory — <https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core/>- Hackify — wp2shell technical analysis — <https://hackify.nl/en/blog/wp2shell-wordpress-core-rce/>- sergiointel/wp2shell-poc SQLi-to-admin bridge — <https://github.com/sergiointel/wp2shell-poc>- Elastic Security Labs — wp2shell detection &
IoCs — <https://www.elastic.co/security-labs/wp2shell-wordpress-rce-detection-elastic-defend>- Eye Security — wp2shell defenders guide — <https://labs.eye.security/wp2shell-defenders-guide/>🤖 本文由漏洞情报系统自动聚合生成 · 2026-08-01 15:07 · 数据源: NVD/GitHub-Advisory/OSV/CISA-KEV/Exploit-DB/PoC-in-GitHub + 检测规则库