截至 2026 年 10 月 23 日,事态仍在发展。本文基于 PHP 官方安全公告与各国 CERT 的公开通报整理,不含任何未公开的利用细节。
一句话说清这件事
2026 年 10 月 21 日,PHP 官方一次性发布了 8.5.12、8.4.27、8.3.36、8.2.35 四个版本,修补两个"不开玩笑"的漏洞:
- CVE-2026-109618 —— 内存耗尽(OOM)处理不当导致内存损坏
- CVE-2026-109619 —— 残留的旧状态被下一个请求误用
合称 PHP2Shell,CVSS 4.0 评分均为 9.2 / 10(Critical)。
它们的共同点是:攻击者不需要任何账号、不需要用户点击任何东西,只要往你的网站发一个特制的 HTTP 请求,就可能在你服务器上执行任意代码。
PHP 官方博客用的措辞是"imperative that you update now"(必须立刻升级)。这种语气,在 PHP 二十多年的历史里都不多见。
一、为什么说它是"地基级"漏洞
绝大多数安全漏洞出在某个插件、某个主题、某个业务功能里。修掉它,换掉那个组件就行。
这次不一样。漏洞在 PHP 解释器内核本身,落在"内存管理"和"错误处理"这两个每处理一个网页请求都会走一遍的模块上。
这意味着:
| 维度 | 情况 |
|---|---|
| 受影响版本 | PHP 所有版本(8.1 及更早需靠发行版或厂商回移植补丁) |
| 受影响部署方式 | PHP-FPM、Apache mod_php、php-cgi、FrankenPHP、LiteSpeed 全在列 |
| 受影响的程序 | WordPress、Drupal、Joomla、Magento、Laravel、Symfony、Nextcloud、phpMyAdmin、MediaWiki、Roundcube、WHMCS、OpenCart…… 基本覆盖了 PHP 生态的主流框架和建站系统 |
| 攻击门槛 | 无需登录、无需交互,一个 HTTP 请求即可 |
| 攻击者能拿到什么 | Web 进程权限:篡改网站、窃取数据库、植入后门、把服务器当跳板 |
按公开数据,仅已确认暴露在公网的受影响服务器就超过百万台。荷兰、芬兰、日本等多国政府机构和企业选择临时关站避险。国家信息安全漏洞共享平台(CNVD)、国家网络安全通报中心、英国 NCSC、新加坡 CSA、瑞士 BACS 都发布了高危预警,香港网络安全事故协调中心更是把它定为"极高度风险"。
目前漏洞技术细节与 PoC 已在网上广泛流传,并且已有大规模的扫描和利用尝试。这不是"先看看再说"的漏洞。
二、先搞懂一个前提:PHP 进程是"常驻"的
要理解这两个漏洞,只需要先理解一件事。
很多人以为 PHP 像 CGI 一样,来一个请求就启动一个进程,处理完就退出。不是的。在生产环境里,PHP-FPM 或 Apache mod_php 会维持一组长期存活的 worker 进程,反复处理请求。
打个比方:worker 就像一个服务员,一桌客人走了,他擦擦桌子就接下一桌。
这套模式能安全运转,全靠一个隐含约定:上一桌客人走后,桌子必须彻底擦干净。
而这次的漏洞,本质上都是"桌子没擦干净"——只不过擦不干净的原因,藏在 PHP 自己的内存管理和错误处理代码里。
三、漏洞一:上一桌客人的东西没收拾干净
CVE-2026-109619(RCE via stale state,残留状态导致远程代码执行)
PHP 的 worker 进程内部有一批"全局状态"——错误状态、会话状态、对象状态、扩展状态。这些状态由 worker 持有,但它们指向的内存属于某一个具体的请求。
正常情况下,请求内存释放之前,这些全局状态必须被复位。问题是:有些操作会在"下一个请求的正常初始化"之前就执行,它们会在初始化还没来得及替换全局状态的时候,摸到上一个请求留下的旧值。
具体是这样一条链:
- 第一个请求(生产者)处理到一半,触发了致命错误——比如请求体解析失败、脚本执行超时、未捕获的异常、或者内存耗尽。这些中断会让"复位全局状态"这段代码来不及跑完。
- 请求堆内存被释放,但 worker 里那个全局指针还指着它。
- 第二个请求(消费者)由同一个 worker 处理。在正常初始化重置之前,有一批操作会先碰到这个旧指针。
- 把已经释放的内存当成有效数据来读、来释放、来跳转——这就是教科书级的 Use-After-Free(释放后使用,CWE-416)。堆布局摆得好,一次崩溃就能升级成任意代码执行。
那"第二批操作"具体是什么?官方公告点名了几条已确认的路径:
session.upload_progress(会话上传进度):只要处理multipart/form-data上传就会碰到,默认开启,而且业务代码从没调用过session_start()也没关系- PHP 8.5 的
fatal_error_backtraces(致命错误回溯):致命错误报告会把回溯状态存进全局,而这段全局状态在收尾阶段总是被漏掉重置 .htaccess里、或由反代传给 FPM 的无效/已弃用的 INI 设置:这类设置会在初始化早期就产生一条警告,而这条警告本身就会去消费那份旧状态
有两个细节特别值得记住:
第一,这是"两阶段"攻击。 生产者和消费者必须是同一个 worker。worker 越多、无关流量越大,单次尝试的成功率越低——但攻击者多试几次总归能撞上。降低成功率不等于消除风险。
第二,改配置躲不掉。 全局状态是在 PHP 决定"要不要报这个错"之前就被访问的。所以改 error_reporting、用 @ 抑制符、关掉 display_errors、关掉 log_errors——统统无效。
四、漏洞二:内存爆掉时,PHP 选择了"跳楼"
CVE-2026-109618(RCE via OOM corruption,内存耗尽导致内存损坏)
这个漏洞的根源,是 PHP 处理"内存不够用"的方式很粗暴。
当一次内存分配失败时,PHP 用 longjmp() 直接从当前位置跳出去——不是走楼梯下楼,是从二楼直接跳下来。跳是跳出来了,但中间所有该做的收拾动作,全被跳过去了。
问题在于,PHP 有一批代码运行在这样一个瞬间:请求的内存结构已经能被访问了,但还没初始化完。 如果恰好在这个时候发生一次内存分配失败——注意,失败的那次分配甚至和这块内存毫无关系——PHP 就无法正确记账这次提前退出,留下一个非法状态。
等到请求收尾,收尾函数把这个半成品当成有效数据去读、去分支判断……内存就坏了。
于是关键问题变成:攻击者能不能精确地把内存打爆,同时又不触发别的限制?
官方公告直接给了三种"用常规请求就能做到"的手法:
- 不声明可用的
Content-Length的 multipart 请求,能绕过最初的post_max_size检查,继续往里灌解码后的数据; - 单个 part 里塞入不限量的头部字段。这些头部在网络上传的时候很小,但要在 PHP 内存里表示出来,体积可能膨胀几十倍——一个"看起来没超
post_max_size"的请求,就能把memory_limit撑爆; - 空的文件上传控件不计入
max_file_uploads限制,却照样创建文件元数据数组;再叠上嵌套的字段名,数组开销还会成倍放大。
还有一个差别,决定了谁是重灾区:
- Apache + mod_php:即使请求启动阶段就失败了,Apache 也无条件执行请求收尾流程。而自动解析请求体恰恰发生在应用代码执行之前。结果就是——你的一行 PHP 代码都还没跑,攻击已经完成了。官方已在默认配置下验证了完整的远程代码执行。
- PHP-FPM:请求启动失败时会直接杀掉子进程,这条"应用无关"的路被堵了。但通过应用层代码路径,RCE 同样已被验证。FrankenPHP 与 mod_php 行为类似,被认为很可能受影响;LSAPI 与 FPM 类似,属于"很可能但未验证"。
所以如果你还在用 mod_php 跑老站点,优先级要拉到最高。
五、为什么拖了将近两年才修
这一段比漏洞本身更值得读。PHP 官方在公告里罕见地公开承认了问题。
| 时间 | 发生了什么 |
|---|---|
| 2024 年 | PHP 进行多年来首次外部安全审计。该审计由 Sovereign Tech Agency 出资、OSTIF 组织、Quarkslab 执行。在修其中一个问题的过程中,PHP 核心开发者第一次发现了 OOM 问题的苗头 |
| 2024 年 | 团队已推演出"理论上可能被利用",但找不到具体攻击路径,于是被其他影响更明确的问题盖了过去 |
| 2024—2026 年 | 修复方案有好几个,各有代价:破坏向后兼容性、引入新的拒绝服务风险、或者要求大量第三方扩展配合改造(几乎无法协调)。安全团队按"共识制"决策,谁也不敢拍板。加上掌握引擎深层细节的核心开发者本就稀少、个个身兼数职——修了停,停了修,反复数月 |
| 2026 年上半年 | 引入 AI 辅助:与 Alpha-Omega 合作,用 Anthropic 的 Claude Mythos 扫源码,用 OpenAI 的 Daybreak Red 验证报告和利用;同时配合 sanitizer 构建和全平台复现环境。多位独立研究者(lcfr-hax、sagitzadik-wiz、tomash-areion、amyb-oai 等)参与,PoC 被打通 |
| 2026-10-21 | 补丁发布 |
官方博文里有一句话说得非常直白:这次的延迟是"结构性失败"(structural failure),原因是安全团队的决策流程,以及资源不足。
修复思路也很有代表性:不再用 longjmp 处理 OOM,改成"延迟的软 OOM(留到下一个虚拟机安全点再处理)+ 达到第二道更高阈值时用 _exit() 硬退"。这样收尾流程再也不会看到非法状态,同时 PHP 限制内存的能力还保留着。
这件事的另一面是:开源基础设施的安全,长期是靠少数人的业余时间撑着的。 AI 让漏洞的发现速度上了量级,也必然会让"发现到被利用"的时间窗口进一步压缩。
六、现在该做什么
按顺序来,不要跳步。
第 1 步:升级,然后重启
升级到以下任一版本:
| 版本线 | 修复版本 |
|---|---|
| 8.5.x | 8.5.12 |
| 8.4.x | 8.4.27 |
| 8.3.x | 8.3.36 |
| 8.2.x | 8.2.35 |
升级完必须重启所有 PHP worker。 容器环境要重新构建并重新部署,否则跑的还是旧进程。
对 Linux 发行版来说,补丁可能已经回移植到原有包版本里——所以"大版本号没变"不代表没修,要看具体包版本和厂商的安全公告。
第 2 步:核实"真正在跑"的版本
这是最容易被忽略的一步。php -v 查到的是命令行版本,Web 用的可能是另一套。
# 命令行
php -v
# Web 实际使用的(按需替换版本号)
php-fpm -v
php-fpm8.3 -v
# 找出机器上所有 PHP 相关二进制
ls -l /usr/bin/php* /usr/local/bin/php* 2>/dev/null
# 确认 Web 服务器用的是哪种方式跑 PHP
httpd -M 2>/dev/null | grep -i php # Apache:有 php_module 就是 mod_php
apache2ctl -M 2>/dev/null | grep -i php别忘了这三类"隐形"的 PHP:
- 控制面板 / 一键部署环境(面板、容器镜像、LNMP 套件)里的 PHP
- 内嵌 PHP 的设备和管理系统:路由器、NAS、监控平台、工单系统、打印机管理页
- 第三方交付的整套系统:供应商装的,你自己都不一定知道里面有 PHP
瑞士 BACS 特别提醒:排查范围不要只盯着公网网站,内部系统、管理后台、托管平台、容器、一体机都要算进去。
第 3 步:暂时升不了,就做这五件事
官方给的临时措施只能降低特定攻击路径的暴露面,不等于安全。但只要还没升级,这些都得做:
- 管理端强制把
memory_limit设为-1
在 FPM pool 的php_admin_value或 Apache 配置里设,不能靠ini_set()。设完检查一遍,确保没有哪个 pool、目录配置或应用把它又加回来。 - 收紧请求体大小
不需要上传的应用把post_max_size设成8M;需要上传的设成32M。upload_max_filesize必须小于post_max_size。同时在 Web 服务器/代理层独立限制解码后的请求大小、输入字段数、multipart part 数、每个 part 的头部数、嵌套深度——特别是没有Content-Length的请求,这类限制必须也能覆盖到。 - 关掉会话上传进度
session.upload_progress.enabled = 0,然后在php.ini、FPM pool 或服务器配置这一层生效,最后重启所有 worker。注意:不能靠ini_set(),也不能靠 FPM 的.user.ini(生效时机太晚)。就算打了补丁,如果业务根本不用这个功能,也建议一直关着。 - PHP 8.5 额外一步
fatal_error_backtraces = 0,同样要重启所有 worker。 - 清理无效/过时的 INI 配置
那些会在请求初始化前产生警告或弃用提示的设置,全部清掉。注意:用error_reporting、display_errors、@去"藏"这些诊断信息没有用。
第 4 步:前端兜底
- 业务不使用
multipart/form-data的,在 Web 服务器或代理层直接拦掉这类请求,不让它到 PHP; - 用容器/cgroup 的
memory.max做可用性边界(不是完整防护,而且要注意:除非每个 worker 有独立 cgroup,否则杀容器会把 master 和所有 worker 一起带走); - 不要用
RLIMIT_AS或RLIMIT_DATA。这两个限制一旦触顶会让mmap()失败,反而正好走进那条漏洞路径。
第 5 步:查有没有已经被打
补丁不能清除已经发生的入侵。升级前后都要看:
- 异常请求日志、worker 反复崩溃/重启记录
- 被改动或新增的文件(尤其 Web 目录里的可疑 PHP 文件)
- 意外出现的进程、计划任务、SSH 公钥、数据库里的新账号
- 如果自己没把握,找专业应急响应团队
七、几个不要踩的坑
| 坑 | 为什么错 |
|---|---|
"我改了 error_reporting / 加了 @" | 全局状态在被判断"要不要报错"之前就被访问了,改这些完全无效 |
| "我装了 WAF,应该没事" | WAF 能挡一些已知的请求特征,但官方明确说了:WAF 本身不够 |
| "我的 PHP 版本很老,不在受影响列表里" | 恰恰相反,所有版本都受影响,老版本还拿不到官方补丁 |
| "升级完就完事了" | 不重启 worker 等于没升;容器不重建等于没升 |
| "我这是内网系统,不用管" | 内网横向移动是常规操作;而且很多"内网系统"其实就挂在公网上 |
| "看日志没发现异常就是安全的" | 官方说得很清楚:日志里没有错误,不等于安全。有些失败发生在 PHP 写日志之前 |
八、最后两句
如果你在跑 PHP:现在就去查版本、升级、重启 worker。 其他所有事情都排在这后面。
如果你不跑 PHP:把这篇转给那个跑 PHP 的人。 你的公司官网、客户系统、供应商后台,背面很可能就是 PHP——而且没有任何人告诉过你这件事。
另外还有一个时间点要记住:PHP 8.2 将在 2026 年 12 月 31 日结束支持(EOL)。到那天之后,8.2 线不再收到任何安全修复。趁着这次升级,顺便把版本规划一起做了。
参考资料
- PHP 官方安全公告(2026-10-21):https://www.php.net/archive/2026.php#2026-10-21-5
- PHP Foundation 官方博文《Today's PHP Security Release, and How We're Strengthening PHP Security》:https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/
- GHSA-w8hw-w9fx-j79w(CVE-2026-109618):https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
- GHSA-6wmf-4rv5-p3rp(CVE-2026-109619):https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp
- 国家信息安全漏洞共享平台(CNVD):CNVD-2026-41674、CNVD-2026-41675
- 新加坡网络安全局 CSA 公告 AL-2026-151
- 英国国家网络安全中心 NCSC 公告
- 瑞士联邦网络安全办公室 BACS 公告
本文仅用于安全防护与风险排查,请勿用于任何未经授权的测试或攻击行为。
首发于 2026 年 10 月 23 日