HP2Shell 双 RCE 机理拆解与防御
1. 事件概述与响应状态
PHP 官方于 2026-10-21 发布 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 四个分支的修复版本。本次公告的措辞强度在 PHP 历年安全公告中少见——公告标题直接包含 "update now",正文使用 "all PHP users are strongly encouraged to update immediately",并明确指出在 Apache + mod_php 上未认证请求可在任何应用代码执行之前触发攻击,且默认配置下已演示出完整 RCE。
国内响应在两日内完成:CNVD 于 10-21 收录为 CNVD-2026-41674 / CNVD-2026-41675,国家互联网应急中心于 10-22 发布 CNTA-2026-0006 风险提示,明确指出"漏洞技术细节与 PoC 已广泛传播"。国际侧,毛里求斯 CERT(CERT-MU)于 10-22 发布公告并给出 CVSS v4.0 9.2 的评分,将两者合称 PHP2Shell。
表 1 事件时间线
需要强调的是修复边界:CERT-MU 公告明确指出,升级 WordPress、Drupal、Laravel 等平台或插件不能修复此问题,必须更新服务器上的 PHP 运行时本体。这是引擎级漏洞与框架级漏洞的根本区别——补丁的作用位置在解释器,而不在应用代码。
2. 机理分析
两个 CVE 位于相邻代码区,但成因、触发前提与利用条件彼此独立,修复方式也不同,因此应分开评估。
2.1 CVE-2026-109618:OOM 中断导致的半初始化状态被误用
PHP 在处理请求过程中维护一批内部请求数据。当请求在特定时刻触及memory_limit时,PHP 会中断当前执行流程;若中断发生在内部数据仅完成部分初始化之后,系统便处于一种"数据不完整但结构指针已存在"的中间态。问题出在收尾阶段:请求清理(request cleanup)逻辑未校验该数据的完整性,将其作为合法结构继续处理,由此产生内存破坏。
该漏洞的可利用性关键在于那个"特定时刻"是外部可控的——memory_limit是可配置项,请求何时触发它与请求体大小、解析结构直接相关。这也是官方将memory_limit=-1列为首要缓解措施的原因:移除这个攻击者可控的失败点。官方同时说明,底层分配失败仍可能抵达漏洞路径,故该措施不构成根治。

2.2 CVE-2026-109619:请求态残留导致的 use-after-free
请求持有的内部状态,在其背后内存已释放后仍然存活;同一 worker 处理后续请求时,若该状态在常规初始化重置之前被访问,即构成 use-after-free。官方已确认 UAF 与 worker 崩溃,漏洞报告者进一步在真实应用(Apache + mod_php 环境)上演示了 RCE。
触发前提是 fatal error 出现在错误的时机。官方因此为 PHP 8.5 单独给出fatal_error_backtraces=0——fatal backtrace 是一条已确认可达的路径。
表 2 双 CVE 机理对比
3. 影响面
3.1 版本影响矩阵
表 3 版本影响矩阵
官方为 7.4 / 8.0 / 8.1 提供了最小化源码 backport,这一非常规动作本身即反映存量问题的严重性——这些版本早已停止维护,正常情况下不会有任何补丁下发。
3.2 SAPI 风险矩阵
风险并非在所有运行形态上等价,差异来源于进程复用模型与请求解析阶段的归属。
表 4 SAPI 风险矩阵
一个常见误判是:"我们使用 FPM,风险低于 mod_php,因此可以延后处理。"该判断仅对一半——FPM 规避的是 mod_php 那条最直接的时序路径(请求解析阶段即处于 PHP 管辖范围),并不意味着安全。若应用中存在大文件上传、复杂导入、回调中抛出 fatal error 等代码路径,FPM 同样落入已确认的可达范围。
4. 暴露面普查方法
引擎级漏洞的唯一根治手段是升级运行时,因此防守方的第一动作是确定自己在哪些位置运行着什么版本的 PHP。我实现了一套只读普查方法,包含三个子能力:probe(HTTP 指纹探测)、local(主机侧php -v核版)、selftest(判定逻辑离线自测)。全部只发送普通 GET 请求做指纹识别,不发送任何攻击载荷。
4.1 探测通道与置信度分级
表 5 探测通道与置信度分级

只有前两档能给出确切版本号,后两档的判定价值在于"确认此处存在 PHP 运行时",而非版本。当版本不可见时,正确做法是如实输出"置信度不足",而不是推断一个版本填充报告——这一点在 5.2 节的实测中得到了体现。
4.2 判定阈值与比较陷阱
判定逻辑的核心是一张阈值表加分组比较。修复补丁号在各分支间不同,因此必须按次版本分组后再比较补丁号:
# 官方修复版本阈值(次版本 -> 修复补丁号)
PATCHED = {"8.2": 35, "8.3": 36, "8.4": 27, "8.5": 12}
def verdict_of(version):
"""返回 PATCHED / AFFECTED / EOL。version 形如 '8.4.26'。"""
v = tuple(int(x) for x in version.split("."))
minor = f"{v[0]}.{v[1]}"
if minor not in PATCHED:
return "EOL" # 已停止维护,按受影响处置
return "PATCHED" if v[2] >= PATCHED[minor] else "AFFECTED"
版本号必须解析为整数元组再比较。直接做字符串比较会产生错误结论——例如"8.4.9" > "8.4.27"在字符串序下成立,而实际 8.4.9 早于修复版本。EOL 分支需单列:8.1 及更早版本虽不在阈值表中,但因其没有官方补丁,风险只高不低,按受影响处置才正确。
5. 实测结果
5.1 判定逻辑自测
判定逻辑被拆为四条离线用例验证(loopback mock,不涉及网络):
==============================================================
php2shell_scan 自测报告(离线,loopback mock)
==============================================================
T1 受影响判定 : PASS
T2 修复判定 : PASS
T3 加固场景 : PASS
T4 阈值边界 : PASS
结论:4/4 PASS
T4(阈值边界)是最易出错的一条,其针对的正是 4.2 节所述的分组比较问题。
5.2 普查实测
对三台本地实例与三个本机二进制执行普查,结果如下:
表 6 普查实测结果矩阵

8081 与 8082 仅相差一个补丁号,判定结果相反。这种"差一号"的情形在真实环境中极为常见——升级窗口排在披露之后,恰好错过。
8083 是本次实测中最有说明价值的样本。该实例隐藏了版本号,仅留下 Fatal error 页面签名与PHPSESSID证明 PHP 运行时的存在,工具如实给出"置信度不足"而非猜测版本。指纹不可见不等于不受影响:expose_php=Off解决的是"他人能否看见",完全没有改变"是否受影响"这一事实。

主机侧核版过程中遇到一个典型的 Windows 环境问题:集成环境(如 phpstudy)自行管理 PHP 路径,php不在系统 PATH 中,首轮local检测直接返回"未在本机发现 PHP 安装",需显式指定php.exe绝对路径方可完成。本机 phpstudy 自带的 PHP 7.3.4 判定为 EOL——该分支早在 2021 年底即停止维护,仅有官方本次破例提供的最小化 backport 可用,使用集成环境的用户通常只能等待发行方跟进或迁移至受支持分支。

6. 普查方法有效性量化
5.2 节的 8083 样本提出了一个更一般的问题:在版本可见性受限的环境里,基于 HTTP 指纹的普查到底还剩下多少有效性?这一问题对防守方具有直接的决策意义——如果普查本身会大量漏判,那么"已完成清点"的结论就是虚假的。
6.1 实验设计
采用蒙特卡洛重复普查实验:
每轮抽样 N = 60个目标,版本按存量环境分布加权抽取(版本池含 8.2/8.3/8.4/8.5 的受影响与已修复版本,以及 7.4、8.1 两个 EOL 版本;真实受影响占比 0.68)
三种普查策略:
AHTTP 指纹,目标为默认配置(版本可见)
BHTTP 指纹,目标开启
expose_php=Off(版本不可见)C策略 B 基础上,对 HTTP 指纹失败的目标改用主机侧
php -v核版
版本可见性建模为伯努利事件:默认配置下 P=0.88(存在反向代理剥离响应头、phpinfo 缺失等失败情形);加固配置下 P=0.15(仅错误页泄露版本等残留路径);主机侧核版可达率 P=0.95
重复 R = 40轮,固定随机种子
seed=20261031保证可复现统计指标:版本可判定率、受影响目标漏判率;采用 t 分布与 Bootstrap 百分位双法计算 95% 置信区间,组间比较采用 Mann-Whitney U 非参数检验并报告 Cliff's δ 效应量
6.2 结果
表 7 版本可判定率(N=60/轮 × R=40)
表 8 受影响目标漏判率(条件于目标真实受影响)
表 9 Mann-Whitney U 检验(α=0.05)
按目标计数跨 40 轮汇总的漏判概率与风险度量:
表 10 漏判概率(按目标计数)
表 11 风险度量(以漏判为事件)
6.3 结果解读
(1)HTTP 指纹在默认配置下已非完备。策略 A 的可判定率为 0.873(95%CI [0.861, 0.885]),意味着即使目标未做任何版本隐藏,仍有约 12.7% 的目标无法给出确切版本——反向代理剥离响应头、phpinfo 不存在、错误页被自定义覆盖,都会导致指纹失败。相应的漏判概率为 0.128,即每 8 个真实受影响的目标中约有 1 个被判为"未知"。
(2)版本隐藏使普查近乎失效。策略 B 的可判定率降至 0.152,受影响目标漏判概率升至 0.843。两组差异极为显著(p=1.13e-14,Cliff's δ=1.000,即两组几乎完全分离),相对于默认配置,漏判风险上升 84.8%(ARR=0.715)。这一结果的实践含义是:在开了expose_php=Off的环境里,仅凭 HTTP 指纹得出的"未发现受影响版本"结论不具备参考价值,它反映的是观测能力缺失,而非风险缺失。
(3)主机侧核版是决定性补救。策略 C 将可判定率恢复到 0.995,漏判概率降至 0.004,相对策略 B 改善 99.5%(ARR=0.839)。这说明版本清点的主渠道应当是能够直接触达二进制的方式——php -v、包管理器记录、容器镜像构建层——HTTP 指纹只能作为补充。
实验局限需如实说明:版本可见性概率(0.88 / 0.15 / 0.95)与版本池权重为建模假设,用于刻画普查方法的相对有效性趋势,不代表任何具体环境的实测命中率;接入自有环境时,应以实际普查结果替换这三个参数后重跑,结论方具备该环境的代表性。判定逻辑本身与实扫脚本完全一致,且通过了 5.1 节的 4/4 自测。
7. 缓解与加固
官方立场明确:只有升级是完整解决方案,其余措施仅降低特定攻击路径的暴露面。从披露到升级窗口之间存在时间差,因此缓解措施仍需落实,但必须清楚其边界。
表 12 缓解措施矩阵
三个容易被忽略的技术细节:
memory_limit=-1的反直觉性。常规安全基线要求设置有限的内存上限,此处官方的建议恰恰相反。原因是该漏洞依赖"请求在错误时刻撞上内存上限"这一可控失败点,移除上限即拆掉触发条件。但官方明确否定了用RLIMIT_AS或RLIMIT_DATA替代的做法——这两种限制会制造出它本想避免的分配失败,等于自行构造触发条件。可用性边界应通过容器或 cgroup 实现,且策略最好是仅终止受影响的 worker。
缓解配置的生效时机。session.upload_progress.enabled=0与fatal_error_backtraces=0必须配置在 php.ini、FPM pool 或 server 配置这类请求解析之前生效的位置,并重启每一个 worker。ini_set()与 FPM 的.user.ini无效——它们生效于漏洞所关注的时间窗之后。同理,memory_limit若写在应用可用ini_set()覆盖的位置,等于未设。
错误处理层面的真正修复。官方还要求修复可被攻击者触达的 fatal error 与执行超时(含回调、上传/导入处理、清理阶段的失败),并移除那些在初始化前即产生诊断输出的无效或废弃配置。需要指出:用error_reporting、display_errors或@隐藏诊断输出没有帮助——问题在于 fatal 本身的发生时机,而非其可见性。

8. 检测思路
漏洞的最终表现为 RCE,因此检测点应置于运行时行为,而非请求特征——后者依赖已知 PoC 形态,改头换面的载荷即可绕过。
表 13 检测规则
title: Suspicious Child Process Spawned by PHP Worker
detection:
process:
parent_process: "php|php-fpm|php-cgi|httpd"
child_process: "sh|bash|cmd.exe|powershell|curl|wget|nc|python|perl"
condition: process
level: critical
title: Unexpected Outbound Connection from PHP Runtime
detection:
network:
source_process: "php-fpm|php-cgi|httpd"
destination: "not_in_known_dependency_list"
condition: network
level: high
CERT-MU 公告中的建议与此一致:为 PHP worker 进程派生子进程(shell、命令解释器、异常外连)配置告警。这类检测不依赖任何 PoC 特征,具备对载荷变体的稳健性。
9. 关联与衍生风险
本次事件并未在 10-21 终止,官方在随后五天又追加了两项公告,处置时须一并纳入。
表 14 关联 CVE 与衍生风险
CVE-2026-115560 的价值在于它揭示了攻击面的延续性:109619 的修复并未穷尽 session 相关路径,只是堵住了已确认的那一批。这也印证了官方对缓解措施的一贯限定——"blocks the confirmed session paths but not other variants"。
10. 结论
基于上述分析,处置优先级建议如下:
版本清点(最高优先级)——覆盖所有运行位置,包括容器镜像、CI 构建工具、本地虚拟机、第三方设备中内嵌的 PHP 二进制,而非仅主 Web 服务器。清点主渠道为
php -v/ 包管理器 / 镜像构建层,HTTP 指纹仅作补充(第 6 节已量化其局限)。升级运行时——这是唯一根治手段。升级平台与插件无效。
部署运行时行为监控——聚焦 PHP worker 的异常子进程与外连,不依赖载荷特征。
落实缓解配置——注意生效位置与生效时机要求,
memory_limit=-1与session.upload_progress.enabled=0均需配置在应用不可覆盖、且在请求解析前生效的位置。WAF 应急规则——设为阻断模式,但不得因此推迟升级。
本文的核心结论是:对引擎级漏洞,应用层的一切加固都无法替代运行时升级,而升级决策的前提是准确的版本清点——第 6 节的量化结果表明,清点方法本身的选择会直接决定结论的可信度(漏判概率在 0.128 与 0.843 之间波动)。在完成可靠的版本清点之前,任何关于暴露面的判断都只是估计。