腾讯云发布 php2shell 风险通告:两个 9.2 分漏洞并列为高风险,但关掉上传进度并不能保平安

腾讯云 10 月 21 日发布 php2shell 风险通告,CVE-2026-109618 与 109619 列为高风险。社区流传的关闭 session.upload_progress 挡不住 109618:它走的是内存耗尽与请求关停路径。本文对照五种措施的实际效果。

10 月 21 日晚间,腾讯云安全中心发布《PHP “php2shell” 远程代码执行漏洞风险通告》(编号 2525),把当天在 PHP 社区发酵的两枚漏洞正式定性为高风险。通告给出的影响范围是 PHP < 8.2.35、< 8.3.36、< 8.4.27、< 8.5.12 四个分支,安全版本对应为 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12。本站此前已从漏洞机理角度梳理过这次事件,这篇换一个角度:看厂商通告的措辞,以及它没有说出口的那部分。

先说结论:这条通告是准确的,但它刻意保持了克制——只讲“升级到安全版本”,没有给任何临时缓解方案。这个省略不是疏忽,而是因为目前流传最广的那个缓解方案,实际上挡不住。

通告里说了什么

腾讯云的通告把两枚漏洞并列陈述,各自的成因描述与 PHP 官方 advisory 基本一致。CVE-2026-109618 的问题出在内存耗尽(OOM)的处理路径上:PHP 用 longjmp() 提前退出当前上下文,而在某些情况下,请求内存在完全初始化之前就已经处于可访问状态。攻击者构造一个包含大量头部、嵌套字段名或空文件控件的 multipart 请求,体积看着没超过 post_max_size,却能一点点把 memory_limit 撑爆,触发 OOM。在 Apache mod_php 这类会无条件调用请求关闭流程的 SAPI 中,关闭过程会去处理这些无效状态,最终演变成内存破坏和远程代码执行。

CVE-2026-109619 则是另一个维度的问题:PHP 工作进程会保留指向前一个请求内存的全局状态(错误、会话、对象、扩展状态),在请求内存释放前没有完全重置。攻击者构造特定堆布局,让同一个工作进程处理的两个请求分别扮演生产者和消费者,就能实现未认证的堆释放后使用、内存泄露或远程代码执行。

风险定性部分,通告的表述是:未经身份验证的远程攻击者可在服务器上以 Web 工作进程账户身份执行任意代码,可能导致服务器完全沦陷、敏感数据泄露或恶意软件植入。

两条并列,但差别很大

通告把两枚漏洞并排列出,读起来像是一类问题。但从利用条件看,它们处在请求生命周期的两个不同端点——一个在请求“结束”时出事,一个在下一次请求“开始”时出事。

两个 CVE 在请求生命周期不同阶段触发:109618 走内存耗尽与请求关停路径,109619 走跨请求陈旧状态路径
两个 CVE 的分工:109618 在单个请求的收尾阶段触发,109619 依赖工作进程复用

这个区分有实际意义。109618 的攻击发生在请求失败之后的清理阶段,攻击者的输入从来没有进入过应用代码——这也是 Datadog 安全实验室能用一个“只打印一行字”的 PHP 脚本复现的原因:只要这个 URL 由 PHP 处理,路径就存在。应用层的登录校验、参数过滤、业务逻辑里的权限判断,在这里全部不起作用,因为它们根本还没被执行到。

“Our reproduction triggers the out-of-memory corruption described in CVE-2026-109618 on PHP 8.5.11 with an unauthenticated HTTP POST request. The request never reaches the target application’s code.”
——Datadog Security Labs,2026 年 10 月 21 日

109619 的条件则更苛刻一些:需要工作进程被复用(php-fpm 的常态,但要卡准时序)、需要构造堆布局。单独看它未必能稳定打通,但和 109618 组合起来,前者负责铺垫状态、后者负责收割,整条链就完整了。

为什么“关掉 upload_progress”不管用

漏洞披露后,社区最快流传开的缓解方案是:在 php.ini 或 .user.ini 里设置 session.upload_progress.enabled = Off,重载 PHP-FPM。这个建议看起来合理——漏洞代码确实在 ext/session/session.c 的 php_session_rfc1867_callback() 里,这个回调就是上传进度特性挂上去的。

但 109618 的入口不在那里。顺着通告的描述推一遍就清楚了:

  1. 攻击者发一个 multipart POST,靠大量头部和嵌套字段名累积内存占用;
  2. 每个字段都不大,所以 post_max_size 拦不住,但总量撑爆 memory_limit;
  3. OOM 触发 longjmp() 跳出,请求内存停在未初始化状态;
  4. SAPI 的请求关停流程照常执行,处理这块无效内存。

这条路径上没有任何一步需要上传进度功能参与。我在本机用 PHP 8.3.6 做了个最朴素的验证,证明第 3 步到第 4 步之间确实有一段“代码仍在跑”的窗口:

PHP 8.3.6 实测:memory_limit 耗尽触发 Fatal error 后,shutdown 函数仍被完整执行
memory_limit 耗尽后进程并未立即消失,收尾代码仍在执行——这正是 109618 的入口

这段输出里的 post_max_size=2M 和 memory_limit=16M 是非默认配置,只为了让 OOM 在一个无关紧要的脚本里快速发生。重点在最后两行:Fatal error 之后,注册的 shutdown 函数照样被调用了。PHP 的 bailout 机制不会因为内存耗尽就让进程立刻消失,它要先把该收的尾收完。攻击者利用的正是这段收尾代码。

Datadog 的结论更直接:Only patching fixes these vulnerabilities. Mitigations circulating in the community, such as setting session.upload_progress.enabled=0, don’t protect you. 上海交通大学网络信息中心的预警用词稍缓——说该措施“可切断本次已验证的中途 session 生命周期路径,但不能替代官方补丁”,指的其实就是 109619 那一半。

哪些措施有用,哪些是错觉

把各方说法摆在一起对照,五种常见处置的实际效果差别很大:

php2shell 缓解措施对照表:升级补丁、关闭上传进度、WAF 拦截、调低内存限制、停用 EOL 版本的效果差异
五种措施的实际覆盖面,只有升级补丁是根治手段

有一行值得单独拎出来说:调低 memory_limit 是反向操作。很多运维看到“内存耗尽导致漏洞”的第一反应是收紧内存,但 OOM 恰恰是触发条件,限额越低,攻击者用越小的请求就能把它打出来。这属于典型的“看起来在加固,实际在帮忙”。

WAF 拦截则处于中间位置。腾讯云的通告提到 T-Sec Web 应用防火墙已支持防护,这在当下能挡住大量现成的扫描器和脚本小子,确实有价值。但 Upwind 的分析指出,PHP 的 multipart 解析器比多数人以为的宽松:它接受折行头部、接受没有冒号的行(会被粘到上一个头部)、接受缺失 form-data 的 Content-Disposition、接受闭合引号后面的垃圾字符。只匹配单行里干净的 name=“PHP_SESSION_UPLOAD_PROGRESS” 的规则,是可以绕过去的。

一个容易被忽略的时间差

财联社在 10 月 21 日晚的滚动报道里提到一条细节:PHP 官方声明称,本周二(10 月 20 日)UTC 时间约 15:00,涉及本次漏洞的两个 CVE 编号就已经开始出现在在线论坛和社交媒体上。也就是说,编号先泄露,补丁后发布。

Upwind 的评估把这个时间差讲得更明白:泄露迫使官方加速发布了原定节奏的补丁,而技术细节——包括具体的释放与回收路径、需要按构建版本计算的偏移量——如今已经公开。GitHub 上也有研究者打包了可复现的 PoC,包含 Docker 靶场和多种回退利用路径(session 投毒 + LFI、日志投毒 + LFI、直连 FastCGI、PEAR webshell 写入)。

这意味着“等官方补丁再说”这个策略,在 10 月 20 日之后就失效了。漏洞细节在补丁之前就到了攻击者手里,从 21 日 14:00 UTC 补丁发布到批量扫描开始,中间可能只有几个小时。

给不同规模站点的处置建议

按投入产出排序,实际可执行的顺序大概是这样:

站点情况 首要动作 说明
PHP 8.2 及以上,有运维能力 直接升级到 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 宝塔、aaPanel 等面板一般已推送新版;升级前备份,注意扩展兼容
PHP 8.1 及更早 先隔离,再迁移 这些分支官方不再回补,只能下线、加白名单或前置代理,长期必须迁移
暂时无法停机升级 WAF 先顶上 + 关 upload_progress 两道一起上能压住大部分批量扫描,但要清楚它们都只是买时间
共享主机 / 虚拟主机 问主机商要确切版本号 面板显示的“PHP 8.3”不等于已修补,要看到 php -v 的完整版本号

自查最直接的办法是跑一条命令,看具体到补丁位的版本号:

php -v
# 或
php -r 'echo PHP_VERSION, PHP_EOL;'

拿到形如 8.3.35 的结果就说明还没修,8.3.36 才是安全的。这里特别提醒一句:面板里显示的版本号经常只到小数位,看到“PHP 8.3”无法判断,必须看第三位。

如果你怀疑已经被打过

这次漏洞的检测难点在于,利用成功不一定让进程崩溃。Datadog 明确指出攻击者可以用校准过的内存布局绕过 ASLR,不需要靠暴力触发崩溃;反过来,利用失败通常会让 worker 段错误退出,在前面有 nginx 的架构上表现为 502。

所以排查线索要分两类看:

  • 高置信度的失败信号:PHP worker 短时间大量 502 / 502 与 200 交替、php-fpm 日志里 worker 异常退出、内核日志里有 php-fpm 的 segfault 记录。这类通常是对着别人的 PoC 在试。
  • 成功后的痕迹:PHP 进程(www-data / apache / nginx 用户)启动了 sh、bash、curl、wget 等异常子进程;网站目录出现新文件且时间戳集中在同一分钟;请求日志里存在体积巨大、头部数量异常多的 multipart POST。

日志检索时重点看 Content-Type: multipart/form-data 且 Content-Length 明显偏大、或者完全没有 Content-Length 的 POST 请求。按 IP 聚合一下,如果某个 IP 在几分钟内反复发这类请求、且响应码在 200 和 5xx 之间跳动,基本可以判定是在尝试利用。

另外提醒一点:不要只看应用日志。109618 的攻击请求根本没有进入应用代码,很多应用层日志里根本不会留下记录,只有 Web 服务器和 PHP 侧的访问日志能看到。

各安全厂商的响应速度值得单独记一笔。腾讯云在这份通告里列出了三项已支持的能力:T-Sec 主机安全支持该漏洞检测、T-Sec Web 应用防火墙支持防护、T-Sec 云防火墙支持防护。也就是说,已经购买这些产品的用户可以在控制台里直接做资产排查,不必自己逐个登录服务器比对版本。

但要注意“支持防护”的具体含义。WAF 的虚拟补丁是规则层面的拦截,它解决的是“这一批已知的利用请求不要进来”,而不是“服务器上的 PHP 已经安全了”。规则发布之后,绕过手法通常会在数天内出现——这次尤其如此,因为漏洞细节在补丁之前就已经公开。虚拟补丁的定位是止损,不是修复。

如果同时跑了多个 PHP 版本,排查时容易漏。常见的情况是:站点主程序确实升级到了 8.3.36,但某个老插件目录或某个老站点的虚拟主机还挂在 8.1 上,命令行里的默认 PHP 又是另一个版本。php -v 只反映当前 PATH 里那一个,真正的全量排查要按 SAPI 逐个看——php-fpm 的进程、Apache 的 mod_php 模块版本、面板里各个站点的 PHP 版本设置,三处都要对齐。

最后补一句实操细节:手动编译过 PHP 的服务器,或者用了非官方源(如 sury、remi)的服务器,包管理器的升级路径和官方发布并不完全同步。这类环境升级完成后务必用 php -r 'echo PHP_VERSION;' 二次确认,而不是看 apt 或 yum 的“已是最新版本”提示。

写在最后

这次事件里最值得记住的不是漏洞本身,而是那条被广泛转发的错误建议。session.upload_progress.enabled=Off 之所以传得快,是因为它符合直觉——漏洞在 session 上传进度代码里,关掉这个功能当然就安全了。但通告原文写的是“内存耗尽”和“请求关停”,两个词都不在这条直觉链上。

厂商通告的价值,往往不只在它写了什么,还在于它把哪些并列、把哪些省略。腾讯云这份通告准确地分列了两条成因,也准确地没有给临时缓解方案——因为确实没有一个“改个配置就安全”的选项。遇到这种通告,读成因比读建议更有用。

一句话行动项:今天就去跑一句 php -r 'echo PHP_VERSION;'。如果第三位数字低于 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 对应值,这件事的优先级应该高于手上其他所有事。

给TA打赏
共{{data.count}}人
人已打赏
在线客服
在线客服
热线电话
QQ客服
电子邮箱
suduwangluo