mcipekci/wp2shell

GitHub: mcipekci/wp2shell

wp2shell 是将两个 WordPress 核心漏洞组合实现未授权 RCE 的单文件概念验证脚本,用于授权渗透测试中的漏洞验证与利用。

Stars: 0 | Forks: 0

# wp2shell — WordPress 核心的未授权 RCE `wp2shell.py` 是一个单文件、仅依赖标准库的概念验证(PoC),它将 **两个** WordPress 核心漏洞组合成未授权的远程代码执行: | CVE | 组件 | 漏洞 | |-----|-----------|-----| | **CVE-2026-63030** | REST `/batch/v1` handler | 路由混淆 **不同步(desync)** — 一旦子请求路径解析失败,batch handler 就会在其 *handler* 表和 *validation* 表之间失去同步,导致子请求通过一个从未为其进行过验证的 handler 进行分发。 | | **CVE-2026-60137** | `WP_Query` | 标量 **SQL 注入** — `author__not_in` 的值被直接拼接到 SQL 中,因此任何传达到此处的字符串都是可注入的。 | 这两个漏洞单独来看都无法实现 RCE。Desync 是 *传递方式*(它将攻击者字符串走私到一个本不应到达的查询中);而注入是 *利用原语*。两者结合在一起,为未授权攻击者提供了一个完整的 SQL 读写 oracle,进而获得一个可用的 shell。 ## 受影响版本 存在漏洞:**6.8.0–6.8.5**,**6.9.0–6.9.4**,**7.0.0–7.0.1** 已修复:**6.8.6**,**6.9.5**,**7.0.2**(及更高版本) **报告者**:Adam Kues (Assetnote / Searchlight Cyber);SQLi 还归功于 TF1T、dtro、haongo。 **只有 6.9.0–7.0.1 能够实现 RCE。** 在 6.8.x 版本线上,desync 仍然会触发,但是会导致 handler 错位的那个分支会直接触发 `WP_Error::get_method()` 致命错误 — 因此 6.8.x 只是 **拒绝服务/崩溃**,而不是 shell。该工具会检测到这一点并告诉你当前可用的原语是什么。 ## 漏洞链的工作原理 此漏洞利用分层次构建;每一层都是通往下一层的桥梁。 1. **Desync (CVE-2026-63030)。** 一个批量请求中嵌套了第二个批量请求。其中一个子请求携带了一个故意格式错误的路径(`http://:`,被 `wp_parse_url()` 拒绝解析)。在 6.9.0+ 版本中,解析失败会导致 handler 表和 validation 表之间错开一个条目,从而使 *下一个* 子请求在一个从未针对其验证过的 handler — 即公开的文章集合 — 下运行。 2. **注入 (CVE-2026-60137)。** 这个被错误分发的调用带着由攻击者控制的 `author__not_in` 到达了 `WP_Query`。在那里的 `UNION ALL SELECT` 不会 *读取* 数据行 — 而是会 **伪造** 它们,让我们能在查询结果中伪造任意文章。 3. **SQL 到写入的桥梁。** 伪造的文章携带了 `[embed]` 标记。WordPress 会尽职地解析它们,并将其作为真实的 `oembed_cache` post-meta 行进行 **缓存** — 从而将只读注入转变为任意 *写入* 原语。 4. **权限枢纽转移。** 利用该写入桥梁,我们伪造了 以及一个自引用的 `request` 文章。它们共同迫使 WordPress **以该管理员身份重新运行其自身的请求流水线** — 这个时间窗口刚好足以让一个排队的 `POST /wp/v2/users` 调用创建一个新的管理员。 5. **Shell。** 以新管理员身份登录,上传插件,运行命令。该账户、其元数据、oembed 行以及 webshell 随后都会被 **清理干净** — 不留下任何持久的痕迹。 ### 提取 oracle 对于读取端(`--dump`、前缀解析、缓存的 ID),该工具会自动选择可用的最快 oracle,并 **在首次命中后停止**: 1. **`union`** — 内联 UNION:伪造行的 `post_title` 携带 `|||HEX(value)|||`,这会在 `/wp/v2/posts` 响应正文中被原样反射。在 **一个请求** 中读取整个值(任意长度)。如果 UNION 反射成功,就永远不会去探测布尔型 oracle。 2. **`bool`** — 布尔型 X-WP-Total oracle:混淆子响应的 `X-WP-Total` 标头在为真时是 `count>0`,为假时是 `0`。每个请求一位(二分法)。 3. **`time`** — 基于时间的 `SLEEP()`:当正文被剔除 *且* 响应为 500(例如由于缓存 mu-plugin 导致的分发后致命错误)时的兜底方案。即使在每次 REST 调用都会崩溃的目标上也能工作。 当 UNION 可用时,`--dump` 使用 `GROUP_CONCAT` 一次性提取整个凭证集。 ## 安装 无需安装。仅使用 Python 3.7+ 标准库 — 不需要 `pip install`。 ``` chmod +x wp2shell.py ``` ## 用法 ``` # 1. 它是否存在漏洞?(无 injection,无写入,无账户创建) ./wp2shell.py http://target --check # 2. 只读凭证转储 — 首个 administrator + option secrets ./wp2shell.py http://target --dump # …每个 administrator ./wp2shell.py http://target --dump all # 3. 完整 RCE — 创建临时 admin,运行命令,清理 ./wp2shell.py http://target --exec "id; uname -a" ``` ### 只读侦察(不创建管理员,绕过 2FA) ``` ./wp2shell.py http://target --user-list # full user table (logins/emails/roles) ./wp2shell.py http://target --content # every post type at status=any (private/draft) ./wp2shell.py http://target --content full # …including each item's raw body ``` ### 扫描多个主机 ``` ./wp2shell.py -f targets.txt --check --target-threads 8 ``` ## 选项 | 标志 | 用途 | |------|---------| | `url` / `-f FILE` | 单个目标,或包含多个目标的文件(每行一个 URL) | | `--check` | 仅漏洞检查 — 无注入、无写入、无创建账户 | | `--dump [all]` | 导出 **第一个** 管理员的哈希值和密钥;`all` = 每个管理员 | | `--exec CMD` | 创建临时管理员 → 运行 `CMD` → 清理 | | `--user-list` | 提升的内联读取整个用户表(无需登录,绕过 2FA) | | `--content [full]` | 提升读取 `status=any` 的所有文章类型;`full` 包含正文 | | `--oracle {auto,union,bool,time}` | 强制指定提取 oracle(默认 `auto`) | | `--prefix PFX` | 强制指定表前缀,而不是自动解析 | | `--impersonate UID` | 模拟该用户 ID,而不是第一个管理员 | | `--threads N` | 单个目标内的并发提取探测(默认 5) | | `--target-threads N` | 并发扫描 `-f` 中的 N 个目标(默认 1) | | `--proxy URL` | 通过代理路由(例如 Burp `http://127.0.0.1:8080`) | | `--timeout SEC` | 单次请求超时(默认 30) | | `--verbose` / `-v` | 转储原始批量子响应(用于调试) | ### WAF / 安全加固绕过 | 标志 | 用途 | |------|---------| | `--form` | 以 `multipart/form-data` 形式发送批量请求(干净的 URL,没有 `?rest_route=`) | | `--permalink [PREFIX]` | 通过 `//batch/v1` 路由(默认 `wp-json`);将 payload 置于 URL **和** `$_POST` 之外。提示:`--permalink index.php/wp-json` 通过 `PATH_INFO` 到达 REST,从而绕过锚定在 `/wp-json` 上的规则 | | `--dummy N` | **在正文中** 前置 N 字节的垃圾数据,以溢出 WAF 的检查缓冲区 | | `--donor {widgets,categories,tags}` | 如果某个混淆 donor 路由返回 500 错误,则切换路由(默认 `widgets`) | | `--per-page N` | 伪造 `per_page`(`-1` 或大的正数都会返回所有行;如果目标在 `-1` 时返回 500,请使用大的正数) | | `--users-route PATH` | 为创建用户调用使用不同的大小写/查询(`/wp/v2/Users`、`…?_=1`) | | `--primer PATH` | 如果 WAF 指纹识别了默认的 `http://:`,则使用另一个格式错误的 desync 路径 | | `--user-agent UA` | 固定 UA(默认:每次运行使用随机的真实浏览器 UA) | | `--embed-base URL` | 覆盖 oembed URL 基址(避免自嵌入回环) | ## 输出说明 - **密码哈希**(`$wp$2y$…` / `$P$…` / `$2y$…`)可以直接用于 hashcat (phpss 使用 `-m 3200`,或 WP bcrypt 模式)。WordPress 6.8+ 使用 `$wp$…` bcrypt。 - **`--dump`** 默认仅针对第一个管理员。使用 **`--dump all`** 获取完整的管理员列表。 - **500 错误 / 崩溃目标:** 如果分发后致命错误(例如 Pantheon 风格的缓存 mu-plugin)导致每个 REST 响应都变为 500,则管理员创建将不起作用,但读取仍然有效 — 切换至 `--oracle time --dump` 来恢复哈希值。 ## 为什么清理很重要 `--exec` 路径旨在做到 **不留痕迹**:临时管理员、其 usermeta、伪造的 `oembed_cache` 行以及上传的 webshell 都会在工具退出前被移除。这里没有 `--keep` 标志 — 操作员永远不应在客户的系统上留下一个由攻击者创建的活动管理员账户。 ## 文件 - `wp2shell.py` — 漏洞利用脚本(单文件,仅标准库)。 ## 参考 - Searchlight Cyber / Assetnote — *wp2shell: WordPress 核心中的未授权 RCE* - WordPress 7.0.2 版本发布(修复) — - 安全公告 — [GHSA-ff9f-jf42-662q](https://github.com/advisories/GHSA-ff9f-jf42-662q), [GHSA-fpp7-x2x2-2mjf](https://github.com/advisories/GHSA-fpp7-x2x2-2mjf) ## 免责声明 这是一个安全研究概念验证,用于授权的渗透测试和防御性验证。在您不拥有或没有书面测试许可的系统上运行它是违法的。作者对任何滥用行为不承担责任。
标签:CISA项目, WordPress, 无服务器架构, 漏洞PoC, 编程工具, 远程代码执行, 逆向工具