AdnaneKhan/Wp2Shell-RCE

GitHub: AdnaneKhan/Wp2Shell-RCE

针对 WordPress 核心 REST API batch-route 混乱叠加 SQLi 漏洞链的 PoC 项目,验证在 FILE 权限前置条件下通过 `INTO OUTFILE` 实现匿名 RCE。

Stars: 2 | Forks: 0

# WordPress 6.9.x — REST batch-route 混乱 + `author__not_in` SQLi (**FILE 权限 RCE 变种**) 适用于 WordPress 原生核心的无需认证、**无需插件**的 PoC。这演示了该漏洞链的 **`INTO OUTFILE` (FILE 权限) RCE 变种**:GHSA-ff9f-jf42-662q (batch-route 混乱) + GHSA-fpp7-x2x2-2mjf (`author__not_in` SQLi)。由 Adam Kues (Assetnote / Searchlight Cyber) 发现。已在 **6.8.6 / 6.9.5 / 7.0.2** 中修复。完整 漏洞链影响 **6.9.0–6.9.4, 7.0.0–7.0.1**;单独的 SQLi sink 还会影响 6.8.0–6.8.5(已在 6.8.6 中修复)。 ``` POST /?rest_route=/batch/v1 (anonymous) → SQLi → INTO OUTFILE (needs FILE priv) → id as www-data ``` ## 结果 (原生 WP 6.9.4 + FILE 权限数据库用户,无需插件) ``` [rce] uid=33(www-data) gid=33(www-data) groups=33(www-data) [+] RCE CONFIRMED (anonymous batch-route confusion + author__not_in SQLi -> INTO OUTFILE) ``` ## 漏洞链 这两个漏洞组合使用。`serve_batch_request_v1()` (class-wp-rest-server.php) 会为每个子请求构建一个包含 [route,handler] 的 `$matches[]` 数组,但是如果子请求未通过 `wp_parse_url()`(例如 path 为 `"http://"`),则会变成 `parse_path_failed` `WP_Error`,它会被推入 `$requests`/`$validation` **但在构建 `$matches` 时会被跳过** —— 因此 `$matches` 相对于 `$requests` 会发生 **一位的偏移**。分发循环随后根据请求位置索引 `$matches[$i]`,将每个子请求与**错误**的 handler 配对。修复方案 (6.9.5) 在错误分支中也进行了追加到 `$matches` 的操作。 **阶段 1 — batch 自调用 (method 枚举绕过)。** 外部 batch: ``` [ "http://" , POST /wp/v2/posts (body=inner) , /batch/v1 ] ``` 这种偏移将 `/wp/v2/posts` 子请求(其自身的验证已通过)分发到了 取自末尾 `/batch/v1` 条目的 **batch handler** 上 → `serve_batch_request_v1()` 在该子请求的 body 上**递归**运行。由于递归调用*不是* batch endpoint 自身的分发,因此它永远不会重新运行 `method` 枚举检查 —— 内部子请求 可以使用 **GET**。(6.9.5 中对 `serve_request`/`rest_api_loaded` 加入的重入防护是相关的安全加固措施。) **阶段 2 — 路由混乱 → 未经处理的 `author_exclude` → SQLi。** 内部 batch: ``` [ "http://" , POST /wp/v2/categories?author_exclude= , GET /wp/v2/posts ] ``` 该偏移将 `categories` 子请求分发到了 posts `get_items` 上。`categories` **并未**注册 `author_exclude`,因此 `sanitize_params()` 永远不会处理它 (`if ( ! isset($attributes['args'][$key]) ) continue;`),它被原封不动地传入了 `WP_Query::author__not_in` → 插入到 `NOT IN (...)` 中 → 导致 SQL 注入。 两个细节使得 `INTO OUTFILE` 成功落地: - `categories` 是通过其 **create** 路由 (POST) 匹配的,该路由的参数同样遗漏了 `orderby`,因此 body 中的 `orderby:false` 被直接传递给了 `WP_Query`,这会 **清空 ORDER BY**(否则 `UNION` + 限定的全局 `ORDER BY` 会报错)。剩余的 `LIMIT` 可以被 `INTO OUTFILE` 容忍。 - 被分发的 `get_items` 刚好选择了 `wp_posts.ID`(1 列),因此 `UNION SELECT` 是单列的。 **阶段 3 — `INTO OUTFILE`。** 注入的查询将一个无害的 PHP dropper 写入 MySQL 的 `secure_file_priv` 目录中,实验环境将该目录绑定挂载在 webroot 下;通过 HTTP 请求它便会以 `www-data` 身份运行 `id`。 ``` 0) AND 1=0 UNION SELECT '' INTO OUTFILE '/var/lib/mysql-files/oo.php'-- - ``` ## 前置条件 / 影响范围 **无条件(每个受影响的站点):** - **匿名、无插件、原生 WP 核心**通过 REST batch-route 混乱对 `WP_Query::author__not_in` 进行 SQL 注入。直接的 `/wp/v2/posts?author_exclude=` 会被 **过滤处理 (HTTP 400)** —— 必须利用这种混乱才能绕过它。 - 这实现了对 WP 数据库的未授权**读取**(通过 `X-WP-Total` 进行盲注/布尔型数据提取,或者在 ORDER BY 被清空后通过 UNION 提取)。 **此处演示的 RCE 所需条件(`INTO OUTFILE` 变种):** - WP MySQL 用户必须拥有全局 **FILE 权限**。这**并非** WP 的默认配置: cPanel/托管主机通常会授予 `ALL ON wordpressdb.*`(基于单个数据库,**没有** FILE 权限)。FILE 权限主要出现在执行了 `GRANT ALL ON *.*` 的自管 VPS / DIY 环境中,以及 开发环境中。实验环境的 `init.sql` 已为此授权。 - 一个 httpd 能够提供服务的 `secure_file_priv` 目录。MySQL 8 默认将其设置为 `/var/lib/mysql-files/`(通常**不**作为 Web 目录提供服务);实验环境将该目录 绑定挂载到了 webroot 中。在 `secure_file_priv` 为 `NULL` 或非服务目录的主机上, 即使拥有 FILE 权限,`INTO OUTFILE` 也无法投放可用的 webshell。 ## 无 FILE 权限的路径(在没有 FILE 权限的情况下,哪些是可达的,哪些是不可达的) 针对原生 WP 进行了调查并予以排除: - **无法通过这种混乱绕过认证。** `$matches` 的错位*确实会*将 匿名子请求分发到写入 handler 上(`/wp/v2/posts` create, `/wp/v2/settings`, `/wp/v2/plugins`, `/wp/v2/media`),但是 `respond_to_request()` 仍然会运行 错位 handler 的 `permission_callback`,这会强制执行权限检查 → 在每种 情况下都会返回 **401**。没有任何核心写入路由缺少(或宽松设置)`permission_callback`。 - **无法通过 SQLi 进行数据库写入。** sink 是 `get_items`(一个 `SELECT`);`$wpdb` 使用 单语句 `mysqli_query`(没有堆叠的 `; UPDATE`);没有 FILE 权限时,MySQL 没有 文件写入原语。因此该注入是只读的。 - **无法实现读取→RCE。** 认证密钥 / 数据库密码位于 `wp-config.php` 中(是一个文件,而不是 数据库);`user_pass` 是一个哈希值(需要破解);`user_activation_key` 也以哈希形式存储。 唯一不需要 FILE 权限的提权方式是经典的 **盲提取 → 破解管理员哈希 → 登录 → 安装插件 / 文件编辑器** 漏洞链,这本身需要满足 及一个可破解的管理员密码) 和 取消设置 `DISALLOW_FILE_MODS`) —— 即额外的先决条件。此处 未作演示。 ## 文件 ``` poc/ compose.yaml STOCK target: wordpress:6.9.4 + mysql:8.0 (FILE priv, shared OUTFILE/webroot dir). No plugin. init.sql grants FILE to the WP DB user exploit.py PYTHON exploit: anonymous batch self-call -> SQLi -> INTO OUTFILE -> fetch shell run.sh up + install + exploit (./run.sh down to tear down) rce/ shared dir: MySQL writes here, httpd serves here ``` ## 运行 ``` cd poc && ./run.sh # stock target up + install + anonymous exploit WP_TARGET=http://host:port python3 exploit.py ``` dropper 仅运行 `id` —— **不提供 shell / 没有反向连接**,这是设计使然。
标签:CISA项目, PoC, WordPress, 暴力破解, 版权保护, 编程工具, 请求拦截, 远程代码执行, 逆向工具