技术分享 | CVE-2026-87902 WordPress 页面模版路径穿越漏洞分析

匿名者  55分钟前

cfdc979d-b852-43c7-bf5c-ea50218a95fe.png

▌前言

在漏洞验证及利用过程中,路径遍历、文件读取与本地文件包含经常会被放在同一类场景中讨论,但三者在实际检测时并不能简单地共用一套规则。

以文件读取为例,研究人员通常会选择 "/etc/passwd" 或 "win.ini" 作为验证目标;而文件包含漏洞的关键并不只是“能否访问到文件”,还需要确认目标文件是否真正进入了应用的模板加载或代码执行流程。如果仍然只依赖状态码、页面长度或单个关键字进行判断,就很容易把普通报错、主题自带内容或代理缓存误判为漏洞。

2026 年 9 月 22 日,WordPress 官方公开了 CVE-2026-87902。该漏洞位于 WordPress Core 的页面模板解析链路中,未认证攻击者可以通过双重编码的pagename 参数绕过前置清洗,使get_page_template()将主题目录之外的本地 PHP 文件加入模板候选列表。随后, locate_template() 与模板加载器在缺少真实路径边界校验的情况下,对该文件执行 include 。

漏洞本身提供的是一个未认证本地 PHP 文件包含能力;当服务器同时满足 PEAR、 register_argc_argv=On 、可写目录等额外条件时,还可以进一步形成远程代码执行链路。

本期内容将以 CVE-2026-87902 为例,详细拆解漏洞从匿名请求进入 WordPress、穿过 slug 清洗、进入模板层级并最终触发文件包含的完整过程,并结合 Goby POC 中最关键的检测代码说明如何完成非破坏性验证。
本期内容将以 CVE-2026-87902 为例,详细拆解漏洞从匿名请求进入 WordPress、穿过 slug 清洗、进入模板层级并最终触发文件包含的完整过程,并结合 Goby POC 中最关键的检测代码说明如何完成非破坏性验证。

▌漏洞触发的关键前提

CVE-2026-87902 并不是向任意 WordPress 站点发送一个固定数据包就一定能够成功。完整的文件包含链路至少依赖以下条件:

1. 目标存在一个已发布、匿名用户可以正常访问的 Page,POC 需要通过有效的 page_id 让 WordPress 进入页面模板解析流程。

2. 当前子主题或父主题的根目录下存在以page- 开头的目录,例如常见的page-templates/。

3. 被包含的目标是 PHP 进程可读的本地.php文件。

4. 页面未配置优先级更高、能够正常解析的自定义模板,否则 WordPress 会优先命中该模板。

5. open_ba sedir、容器安全策略或 MAC 访问控制没有阻止 PHP 进程访问目标文件。

其中,page- 目录是利用链中非常容易被忽略的一环。WordPress 在构造模板名称时会自动增加 page- 前缀,攻击者不能直接让路径从 ../ 开始。因此,请求中的第一个目录片段通常写为 templates/,最终与系统增加的前缀组合成:

page-templates/../../../../wp-links-opml.php

这也是为什么部分主题具备漏洞利用条件,而另一些主题即使 WordPress Core 版本受影响,也无法进入目录穿越路径。

若要从文件包含继续扩展为代码执行,还需要更多环境条件。例如服务器存在可读的 pearcmd.php、Web SAPI 开启register_argc_argv,并且 PHP 进程能够向某个目录写入文件。PEAR 只是已公开的一种条件性利用路径,并不是 WordPress 的依赖,也不能代表所有受影响版本都可以直接远程代码执行。

▌完整漏洞调用拆解

攻击者利用该漏洞的过程可以分为五个核心步骤。

1. 匿名 POST 请求进入公共查询变量

WordPress 在WP::$public_query_vars中公开了pagename与 page_id。在请求解析阶段,WP::parse_request()会直接从$_POST中读取这些参数,因此整个入口不需要账户、Cookie、Nonce 或插件配合。

image.png

       图 1:pagename、page_id 属于公共查询变量,并可由 POST 请求写入查询对象。

POC 使用用POST 而不是 GET 还有另一个原因:redirect_canonical()对非 GET/HEAD 请求会直接返回,不会在模板解析前把异常路径规范化或重定向掉。

2. 双重编码让遍历序列活过 slug 清洗

如果直接提交 ../,WordPress 的路径和标题清洗逻辑会将斜杠、点号等字符改写,无法形成有效穿越。因此,POC 将../ 表示为双重编码:

../        -> %2E%2E%2F
再次编码   -> %252E%252E%252F

请求体第一次被 PHP 解析后,%25 会还原为%,数据变成%2E%2E%2F。此时 wp_ba sename()还看不到真实的斜杠,而 sanitize_title_with_dashes()又会主动保留合法的%xx转义字节,导致编码后的遍历片段继续留在 pagename中。

image.png

             图 2:sanitize_title_with_dashes()会暂存并恢复合法的 %xx 编码字节。

这里的核心问题不是 WordPress 完全没有清洗,而是清洗与解码发生在不同阶段:前一阶段把编码字节当作普通 slug 内容保留下来,后一阶段又把它们恢复成具有路径语义的 ../。

3. page_id 保证页面对象存在

恶意pagename本身通常无法匹配真实页面。如果只提交这一参数,WordPress 查询可能直接落入 404 流程,模板解析链也就无法稳定触发。

因此 POC 会同时提供一个真实存在的 page_id。数字 ID 用于选中已发布页面,而恶意 pagename继续保留在查询对象中,最终被 get_page_template()读取。

这也是固定写死page_id=2的检测脚本在真实环境中经常失败的原因:不同站点的页面 ID、删除记录和初始化方式并不一致。Goby POC 没有把 Sample Page 的 ID 当作固定前提,而是先动态寻找一个可以正常渲染的页面。

4. 延迟 urldecode 激活目录穿越

真正让编码内容恢复路径含义的位置位于 get_page_template():

image.png

          图 3:编码内容在模板解析阶段才被urldecode()还原,并进入模板候选列表。

经过urldecode()后,之前被保留的 %2E%2E%2F重新变成../。WordPress 再为它增加 page- 前缀和.php 后缀,生成类似下面的模板候选。

page-templates/../../../../wp-links-opml.php

这里也决定了漏洞的一个重要限制:模板解析器会自动补全.php,因此它更适合包含 PHP 文件,而不是直接读取 /etc/passwd 这类无后缀文本文件。

5. locate_template 缺少真实路径边界校验

在受影响版本中,locate_template() 会把模板名称拼接到主题根目录之后,并通过 file_exists() 判断文件是否存在:

但这段代码没有确认规范化后的真实路径是否仍然位于主题目录内部。模板加载器虽然会调用 realpath(),并检查文件扩展名、可读性和文件类型,却同样没有把结果与允许的主题根目录进行比较,最终直接执行:

image.png

    图 4:locate_template()只判断文件是否存在,模板加载器规范化路径后直接执行include。

至此,路径遍历不再只是一个字符串问题,而是转化为真实的本地 PHP 文件包含。

▌从文件包含到条件性RCE

公开研究中给出了一条基于 PEAR 的条件性代码执行链:第一次请求通过路径穿越包含pearmd.php,并利用其命令行参数处理能力在可写目录生成 PHP 文件;第二次请求再通过同一个模板穿越入口包含该文件,使 PHP 代码在 Web 进程权限下执行。

这条链路的价值在于证明文件包含能够进一步扩大影响,但它有明确的环境依赖:

· Web 环境需要存在可读取的 PEAR 入口文件及其依赖;

· register_argc_argv 需要在 Web SAPI 中开启;

· PHP 进程需要拥有可写目录;

· WAF、反向代理或 CDN 不能破坏其特殊查询字符串;

· open_ba sedir 等限制不能阻断相关文件访问。

因此,在自动化检测阶段直接执行完整 RCE 链并不是最佳选择。一方面,不同环境的 PEAR 路径和 PHP 配置差异很大;另一方面,写入文件、执行命令会给目标系统带来不必要的副作用。

Goby POC 针对这一问题选择了更加稳妥的验证方式——包含 WordPress 安装根目录中原本就存在的核心 PHP 文件,通过文件独有的响应特征确认漏洞,而不是向目标写入任何内容。

▌Goby POC 关键实现

POC 选择包含 WordPress 自带的 wp-links-opml.php 进行非破坏性验证。该文件执行后会返回 OPML 文档,因此可通过 <opml与 </opml>两个特征确认目标文件确实进入了模板执行流程。

先获取一个有效的page_id

GET /index.php?rest_route=/wp/v2/pages&per_page=1&_fields=id  HTTP/1.1
POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded

page_id=<有效页面ID>&pagename=templates%252F%252E%252E%252F...%252Fwp-links-opml

image.png

发送匿名POST请求

POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded

page_id=<有效页面ID>&pagename=templates%252F%252E%252E%252F...%252Fwp-links-opml

image.png

关键检测代码只做三件事:构造双重编码的遍历路径、检查 OPML 双特征、再以随机不存在文件进行一次反向校验。未命中反证条件后,才记录漏洞地址并返回成功。

image.png

              图 5:POC 的关键验证代码。检测过程不写入文件,也不执行命令。

▌官方补丁分析

WordPress 7.1.2 的修复不是只在一个位置过滤../,而是增加了两层防线。

第一层位于 get_page_template()。解码后的pagename只有通过 validate_file()
检查,才能被加入页面模板候选列表:

image.png

          图 6:WordPress 7.1.2 在模板候选生成前调用validate_file()拒绝路径穿越。

第二层位于模板定位阶段。新版本增加_ wp_is_template_path_allowed():当候选路径包含 ..时,先调用realpath()获取真实路径,再与子主题目录、父主题目录和 theme-compat等允许目录逐一比较。只有规范化后的路径确实位于允许根目录内部,locate_template()才会返回该文件。

image.png

               图 7:补丁通过 realpath() 计算候选路径,并建立允许的主题目录集合。
image.png

              图 8:locate_template() 仅在候选文件通过真实路径边界检查后返回模板。

这种“输入校验 + Sink 前真实路径边界校验”的组合,比单独过滤编码形式更可靠。即使未来出现新的编码差异、路径表示方法或上游调用入口,最终模板文件仍然需要通过目录包含关系检查。

▌影响版本与修复建议

官方公告显示,受影响范围覆盖 WordPress 4.7.0 至 7.1.1 的多个维护分支,并已将修复回移至 4.7 分支。主要修复版本包括:

· WordPress 7.1.2

· WordPress 7.0.6

· WordPress 6.9.9

· WordPress 6.8.10

· 以及各旧分支对应的安全更新,最低回移至 4.7.37

建议站点管理员优先升级到当前分支的已修复版本,并同步采取以下缓解措施:

1. 检查当前子主题与父主题根目录是否存在page-* 目录。

2. 在升级完成前,针对请求参数中的单次、双重编码路径穿越序列设置 WAF 或反向代理规则。

3. 为 Web SAPI 关闭不必要的register_argc_argv 。

4. 从生产镜像中移除不使用的 PEAR 命令入口及其他可被包含的工具文件。

5. 使用 open_ba sedir 、容器文件系统权限或 MAC 策略限制 PHP 进程的可读、可写范围。

6. 排查 Web 进程可写目录中的异常 PHP 文件及近期模板访问日志。

▌总结

CVE-2026-87902 的关键并不在某一个孤立的危险函数,而在多个看似合理的处理步骤组合后产生了安全边界缺口:

· pagename 与page_id可以由匿名 POST 请求控制;

· slug 清洗为了兼容 URL 编码而保留 %xx 字节;

· 页面模板解析阶段又进行延迟 urldecode();

· 模板名称被拼接到主题目录,但没有验证真实路径是否仍在主题根内;

· 模板加载器最终对可读 PHP 文件执行include。

Goby POC 使用 WordPress 核心 PHP 文件作为非破坏性验证目标,并通过明确的文件特征和一次反向请求降低误报,不需要执行完整 RCE 链即可证明文件包含成立。

从固定请求到动态环境适配,从单一关键字到正反双向确认,这类设计可以让漏洞验证结果更加稳定,也能在减少副作用的同时保留足够清晰的技术证据。

▌参考

  1. WordPress 官方安全公告:https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-7hp8-65ch-5whp

  2. Robert Ressl 技术分析:https://ressl.ch/blog/cve-2026-87902-wordpress/

  3. 公开验证环境与 PoC:https://github.com/ressl/cve-2026-87902-poc

  4.  WordPress 主题页面模板文档:https://developer.wordpress.org/themes/classic-themes/templates/page-template-files/

最新评论

昵称
邮箱
提交评论