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。

时间

事件

来源

2026-10-21

PHP 官方发布修复版本 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35,两个 critical RCE

php.net

2026-10-21

PHP Foundation 发布复盘博客,披露问题发现与修复停滞过程

The PHP Foundation

2026-10-21

CNVD 收录 CNVD-2026-41674 / 41675

CNVD

2026-10-22

CNTA-2026-0006 风险提示,PoC 已广泛传播

CERT/ CNCERT

2026-10-22

CERT-MU 公告,命名 PHP2Shell,CVSS v4.0 9.2

CERT-MU

2026-10-23

追加 informational advisory:CVE-2026-115560(session upload progress 衍生路径)

php.net

2026-10-26

Windows 构建重建(-1 后缀),含 Brotli 1.2.1,修复 CVE-2026-111497

php.net

表 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 是一条已确认可达的路径。

维度

CVE-2026-109618

CVE-2026-109619

官方命名

RCE via OOM corruption

RCE via stale global state

GHSA

GHSA-w8hw-w9fx-j79w

GHSA-6wmf-4rv5-p3rp

缺陷类型

半初始化状态被当作有效数据(内存破坏)

use-after-free

所在路径

内存管理 / 请求清理

错误处理 / 请求态生命周期

外部可控因素

memory_limit触发时机、请求体大小

fatal error 触发时机

官方确认影响

mod_php 默认配置下完整 RCE;FPM 经应用层路径确认 RCE

UAF 与 worker 崩溃确认;真实应用演示 RCE

首要缓解

memory_limit=-1+ 限制post_max_size

session.upload_progress.enabled=0;8.5 加fatal_error_backtraces=0

表 2 双 CVE 机理对比

3. 影响面

3.1 版本影响矩阵

分支

受影响区间

修复版本

状态

8.5.x

< 8.5.12

8.5.12

受支持

8.4.x

< 8.4.27

8.4.27

受支持

8.3.x

< 8.3.36

8.3.36

受支持

8.2.x

< 8.2.35

8.2.35

受支持

8.1.x 及更早

全部

无官方版本

EOL(官方提供最小化源码 backport)

表 3 版本影响矩阵

官方为 7.4 / 8.0 / 8.1 提供了最小化源码 backport,这一非常规动作本身即反映存量问题的严重性——这些版本早已停止维护,正常情况下不会有任何补丁下发。

3.2 SAPI 风险矩阵

风险并非在所有运行形态上等价,差异来源于进程复用模型与请求解析阶段的归属。

运行形态

官方定性

触发前置条件

相对风险

Apache + mod_php

应用代码执行前即可触发,默认配置下完整 RCE 已演示

无(无需应用路径配合)

最高

PHP-FPM

经应用层代码路径确认 RCE

需应用内存在可踩路径

高

php-cgi

在影响范围内

视配置而定

中高

FrankenPHP

在影响范围内

视配置而定

中高

LiteSpeed

在影响范围内

视配置而定

中高

表 4 SAPI 风险矩阵

一个常见误判是:"我们使用 FPM,风险低于 mod_php,因此可以延后处理。"该判断仅对一半——FPM 规避的是 mod_php 那条最直接的时序路径(请求解析阶段即处于 PHP 管辖范围),并不意味着安全。若应用中存在大文件上传、复杂导入、回调中抛出 fatal error 等代码路径,FPM 同样落入已确认的可达范围。

4. 暴露面普查方法

引擎级漏洞的唯一根治手段是升级运行时,因此防守方的第一动作是确定自己在哪些位置运行着什么版本的 PHP。我实现了一套只读普查方法,包含三个子能力:probe(HTTP 指纹探测)、local(主机侧php -v核版)、selftest(判定逻辑离线自测)。全部只发送普通 GET 请求做指纹识别,不发送任何攻击载荷。

4.1 探测通道与置信度分级

通道

可得信息

置信度

失效条件

X-Powered-By响应头

完整版本号

high

expose_php=Off、反向代理/CDN 剥离响应头

phpinfo.php页面

完整版本号

high

文件不存在(同时该文件留存线上本身即高危遗留)

错误页签名

仅证明"这是 PHP"

medium

自定义错误页、display_errors=Off

PHPSESSIDCookie

仅证明会话在用

low

不使用原生 session

表 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 普查实测

对三台本地实例与三个本机二进制执行普查,结果如下:

目标

命中通道

置信度

版本

判定

localhost:8081

X-Powered-By、错误页、PHPSESSID

high

8.4.26

AFFECTED

localhost:8082

X-Powered-By、错误页、PHPSESSID

high

8.4.27

PATCHED

localhost:8083

错误页、PHPSESSID

不足

未泄露

UNKNOWN

本机php826\php.exe

php -v

—

8.4.26

AFFECTED

本机php827\php.exe

php -v

—

8.4.27

PATCHED

本机phpstudy\php7.3.4nts

php -v

—

7.3.4

EOL

表 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 结果

策略

可判定率均值 ± SD

95%CI (t)

95%CI (Bootstrap)

A HTTP 指纹(默认)

0.873 ± 0.038

[0.861, 0.885]

[0.862, 0.885]

B HTTP 指纹(藏版本)

0.152 ± 0.048

[0.137, 0.168]

[0.138, 0.167]

C 藏版本 + 二进制核版

0.995 ± 0.009

[0.992, 0.997]

[0.992, 0.997]

表 7 版本可判定率(N=60/轮 × R=40)

策略

漏判率均值 ± SD

95%CI (t)

A HTTP 指纹(默认)

0.128 ± 0.051

[0.112, 0.144]

B HTTP 指纹(藏版本)

0.844 ± 0.061

[0.825, 0.864]

C 藏版本 + 二进制核版

0.004 ± 0.010

[0.001, 0.007]

表 8 受影响目标漏判率(条件于目标真实受影响)

比较

U

z

p

Cliff's δ

A vs B(可判定率)

1600.0

7.723

1.13e-14

1.000

B vs C(可判定率)

0.0

−7.887

3.08e-15

−1.000

表 9 Mann-Whitney U 检验(α=0.05)

按目标计数跨 40 轮汇总的漏判概率与风险度量:

策略

漏判数 / 真实受影响数

P(漏判)

A HTTP 指纹(默认)

213 / 1664

0.128

B HTTP 指纹(藏版本)

1379 / 1635

0.843

C 藏版本 + 二进制核版

7 / 1603

0.004

表 10 漏判概率(按目标计数)

比较

ARR

RRR

B vs A(隐藏版本的观测代价)

0.715

84.8%

C vs B(二进制核版的补救效果)

0.839

99.5%

表 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. 缓解与加固

官方立场明确:只有升级是完整解决方案,其余措施仅降低特定攻击路径的暴露面。从披露到升级窗口之间存在时间差,因此缓解措施仍需落实,但必须清楚其边界。

措施

针对 CVE

生效位置要求

覆盖范围

残留风险

升级至修复版本

两者

—

完整

无

memory_limit=-1

109618

应用不可覆盖处(FPMphp_admin_value/ Apache 配置)

移除 PHP 层可控的内存限制失败点

底层分配失败仍可达漏洞路径

post_max_size8M(无上传)/ 32M(有上传)

109618

同上

压缩请求体触发面

不覆盖非体积型触发

session.upload_progress.enabled=0

109619

请求解析前生效(php.ini / FPM pool / server 配置),并重启每个 worker

已确认的 session 路径

不覆盖其他变体

fatal_error_backtraces=0(8.5)

109619

同上

已确认的 fatal backtrace 路径

仅该路径

容器/cgroup 可用性边界

109618(间接)

策略为仅终止受影响 worker

限制故障扩散

非漏洞修复

WAF 应急规则

两者(特征层)

边界设备

已知 PoC 特征

载荷形态可变,易绕过

表 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 形态,改头换面的载荷即可绕过。

规则

数据源

检测逻辑

级别

PHP worker 派生异常子进程

进程树 / EDR

父进程为 php-fpm / php-cgi / httpd,子进程为 sh、bash、cmd.exe、powershell、curl、wget、nc、python、perl

critical

PHP 运行时异常外连

网络连接 / NDR

源进程为 PHP 运行时,目的地址不在已知依赖清单内

high

worker 异常崩溃

进程退出码 / 日志

PHP worker 非正常退出频率突增(UAF 的典型伴随现象)

medium

表 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 终止,官方在随后五天又追加了两项公告,处置时须一并纳入。

编号

披露时间

内容

触发条件

处置

CVE-2026-115560

10-23

session upload progress 衍生 RCE 路径

需同时满足:配置session.serialize_handler为 php_binary/php_serialize、运行时用ini_set()切换为 php、再调session_start(),且 upload progress 仍启用

官方判定实践中少见,随下个常规版本发布完整公告;已设enabled=0并重启 worker 者保持现状即可

CVE-2026-111497

10-26

Brotli 解压越界写

PHP Windows 构建中,经 cURL 扩展的 HTTP 响应自动解压触达(cURL 默认关闭,应用/HTTP 客户端库可启用)

更新至带-1后缀的 Windows 重建包(含 Brotli 1.2.1)

—

10-21

PHP 8.6.0 RC4 亦含本次修复

运行 8.6 早期构建者

RC 不应上生产;已用者需更新

表 14 关联 CVE 与衍生风险

CVE-2026-115560 的价值在于它揭示了攻击面的延续性:109619 的修复并未穷尽 session 相关路径,只是堵住了已确认的那一批。这也印证了官方对缓解措施的一贯限定——"blocks the confirmed session paths but not other variants"。

10. 结论

基于上述分析,处置优先级建议如下:

  1. 版本清点(最高优先级)——覆盖所有运行位置,包括容器镜像、CI 构建工具、本地虚拟机、第三方设备中内嵌的 PHP 二进制,而非仅主 Web 服务器。清点主渠道为php -v/ 包管理器 / 镜像构建层,HTTP 指纹仅作补充(第 6 节已量化其局限)。

  2. 升级运行时——这是唯一根治手段。升级平台与插件无效。

  3. 部署运行时行为监控——聚焦 PHP worker 的异常子进程与外连,不依赖载荷特征。

  4. 落实缓解配置——注意生效位置与生效时机要求,memory_limit=-1与session.upload_progress.enabled=0均需配置在应用不可覆盖、且在请求解析前生效的位置。

  5. WAF 应急规则——设为阻断模式,但不得因此推迟升级。

本文的核心结论是:对引擎级漏洞,应用层的一切加固都无法替代运行时升级,而升级决策的前提是准确的版本清点——第 6 节的量化结果表明,清点方法本身的选择会直接决定结论的可信度(漏判概率在 0.128 与 0.843 之间波动)。在完成可靠的版本清点之前,任何关于暴露面的判断都只是估计。