A. 影响全球 70% 网站的 PHP RCE:PHP2Shell 从成因到修复一次讲清(含实测) B. PHP 远程代码执行漏洞(CVE-2026-109618/109619)修复指南:附一键自查脚本
10 月 21 日,PHP 官方发布 10 月安全更新,一次性修复了两个 CVSS v4.0 评分高达9.2(Critical)的远程代码执行漏洞:CVE-2026-109618和CVE-2026-109619。研究者们给这组漏洞起了个响亮的名字——PHP2Shell(名字来自 Wiz Research 把两个漏洞组合成的同名利用脚本)。
这两个漏洞的特别之处在于:它们藏在 PHP 引擎本身,和你跑什么应用无关。哪怕你的站点只有一个echo "Hello World";,在受影响版本 + 特定部署方式下也可能被未经身份验证的攻击者直接拿 shell——以 Web 工作进程的权限执行任意代码。据发现方 Wiz Research 的口径,全球约70% 的网站运行在 PHP 之上,WordPress、Drupal、Joomla 等主流 CMS 全部在受影响范围内(Wiz 已在三者上完成未授权 RCE 演示)。
事件还有个小插曲:原定 10 月 21 日的协调披露,因 10 月 20 日细节提前在网络流传,PHP 团队提前执行了发布。截至目前,公开渠道暂无大规模在野利用的报告,但这类"组件级"漏洞的攻击窗口从来不等人。
本文按"你是谁 → 中没中招 → 怎么修 → 修不动怎么缓 → 云上怎么防"的顺序展开,附 Docker 实测和一份可直接复制的自查脚本。
一、你是谁:影响范围速查
先说结论:过去十年内发布的所有 PHP 版本(5.4 及以上)至少受其中一个漏洞影响(Wiz 口径)。官方仍在对 8.2~8.5 四个分支提供修复:
你的 PHP 分支 | 修复版本 | 说明 |
8.5.x | 8.5.12 | 2026-10-21 发布 |
8.4.x | 8.4.27 | 同上 |
8.3.x | 8.3.36 | 同上 |
8.2.x | 8.2.35 | 8.2 将于 2026-12-31 停止维护,建议借机规划升级 |
≤ 8.1 | 无官方修复 | 已 EOL。发行版/LTS 厂商可能有 backport 补丁;源码构建可用 PHP 官方提供的 7.4 / 8.0 / 8.1 backport 分支 |
部署方式决定风险高低(这点很多文章没讲清楚):
- Apache mod_php:风险最高。官方确认在默认配置下、任何应用代码执行之前,就能被未授权请求触发完整 RCE;
- PHP-FPM:同样受影响,已确认可通过应用层路径达成 RCE(部分场景依赖应用配合);
- FrankenPHP、LiteSpeed(LSAPI)等也在受影响名单里。
另外,两个漏洞的利用都要求"生产者请求"和"消费者请求"落在同一个 worker上——多 worker、频繁回收会降低单次成功率,但不能消除风险。
二、漏洞是怎么发生的
两个漏洞同根同源,都是内存安全问题,官方公告的表述比大多数二手解读清晰,我按自己的理解转述一遍。
CVE-2026-109618:OOM(内存耗尽)处理中的内存破坏
PHP 处理内存耗尽的老办法是用longjmp()直接跳出当前上下文、中止请求。问题在于:某些时刻,请求的内部数据还没初始化完就已经可被访问。如果恰好在这一步发生内存分配失败,这个"半成品"状态就会被留在原地,而后续的请求清理流程会把它当成有效数据去读、去分支——内存破坏由此产生。
攻击者有办法"掐着点"制造这种失败:multipart 请求解析里有好几个放大器——不带可用 Content-Length 的请求体、单个 part 里塞无限多的 header(内部表示比线上传输占用多得多的内存)、不计数却产生元数据数组的空文件控件、嵌套字段名……这些都让攻击者能在不超出表面配额的前提下,精确地把内存耗尽安排在想要的那一步。
CVE-2026-109619:跨请求的"陈旧全局状态"(Use-After-Free)
PHP worker 进程里有一批全局状态(错误、session、对象、扩展状态),本该在每个请求结束时重置。但如果一个请求以致命错误收场(解析失败、未捕获异常、require失败、超过最大执行时间、OOM……),重置可能被跳过;甚至一个非致命的警告也可能在状态已被清理后重新"填入"请求私有的数据。于是 worker 全局指针指向了一块已释放的内存——同一个 worker 处理的下一个请求,在某些早于正常初始化的操作(比如 INI 配置告警、session upload progress 处理)里踩到这块内存,就是教科书级的 UAF。
两个已被官方确认的具体路径:
- PHP 8.5 的
fatal_error_backtraces:致命错误的回溯信息留在全局变量里,请求堆销毁后指针悬空——这是已实证的 mod_php RCE 基础。注意:该选项在 8.5 默认开启(我在官方 Docker 镜像里实测确认,见下文)。 - session upload progress 生命周期:文件上传进度处理会在正常初始化之前触碰 session/解码器状态,默认开启且不需要应用调用
session_start()。
对运维有价值的检测信号
Wiz 在其分析中给了一组排查信号,未打补丁期间值得在日志里盯着:
- 异常的 multipart 请求,随后出现内存限制报错或连续 HTTP 500;
- 响应里出现异常大或畸形的
PHPSESSIDcookie(内存泄露通道的痕迹); - 不明原因的 PHP worker 崩溃/重启。
三、本地实测:两分钟确认你中没中招
实操环境:Windows + WSL2(Ubuntu 发行版)+ Docker Desktop(开启 WSL 集成)。思路是拉修复前后的两个官方 PHP 镜像做对照:先看版本,再跑一个自查脚本。
第一步:起容器看版本
docker run --rm php:8.5.11 php -v # 修复版本 8.5.12 之前 → 受影响 docker run --rm php:8.5.12 php -v # 已包含修复看两个细节:两个容器的唯一差别就是镜像版本号(8.5.11 vs 8.5.12);更有意思的是 build 日期——8.5.11 是 10 月 21 日构建的、8.5.12 是 10 月 28 日构建的,都是漏洞披露前后的新鲜货。这说明这条镜像分支正处在高频更新期,用容器的同学近期最好把基础镜像拉取策略设成always,别让本地缓存的旧镜像坑了自己。
第二步:跑自查脚本
先交代下来历:下面这个check.php是我照着官方公告亲手写的——版本对照表来自 php.net 安全公告的修复版本(8.5.12 / 8.4.27 / 8.3.36 / 8.2.35),配置项检查来自两个 GHSA 公告的缓解措施原文,没有引用任何第三方代码,全文贴在下面可以直接复制走。逻辑很简单:按PHP_VERSION_ID对照第一节的修复版本表,顺带把官方缓解建议涉及的几个配置项打出来。
在 WSL 里把脚本复制到/tmp再挂进容器跑(挂载用 Linux 原生目录最稳,避免中文路径的幺蛾子):
cp <check.php 所在路径>/check.php /tmp/check.php docker run --rm -v /tmp/check.php:/src/check.php php:8.5.11 php /src/check.php docker run --rm -v /tmp/check.php:/src/check.php php:8.5.12 php /src/check.php<?php // PHP2Shell(CVE-2026-109618 / CVE-2026-109619)快速自查脚本 // 用法:php check.php // 请保存为 UTF-8 编码;建议在 Web 服务实际使用的 PHP 环境中运行 $fixMap = [ 80500 => '8.5.12', 80400 => '8.4.27', 80300 => '8.3.36', 80200 => '8.2.35', ]; $id = PHP_VERSION_ID; $branch = intdiv($id, 100) * 100; $patched = false; echo 'PHP 版本 : ' . PHP_VERSION . '(SAPI: ' . php_sapi_name() . ")\n"; if ($id >= 80600) { echo "[OK] 8.6 分支已包含修复(请勿在生产环境使用 RC 版本)\n"; $patched = true; } elseif ($id < 80200) { echo "[!!] 受影响,且该分支已停止官方维护(≤8.1 无官方修复版本)\n"; echo " 处置:升级到受支持分支;源码构建可参考 PHP 官方的 7.4/8.0/8.1 backport 分支,\n"; echo " 或使用发行版 / LTS 厂商提供的安全补丁\n"; } else { sscanf($fixMap[$branch], '%d.%d.%d', $a, $b, $c); $fixId = $a * 10000 + $b * 100 + $c; if ($id < $fixId) { echo "[!!] 受影响:当前分支(" . substr(PHP_VERSION, 0, 3) . ")的修复版本为 {$fixMap[$branch]},请尽快升级\n"; } else { echo "[OK] 当前版本不低于修复版本({$fixMap[$branch]}),已包含修复\n"; $patched = true; } } echo "\n-- " . ($patched ? '以下配置项仅为额外加固参考' : '升级前的临时缓解相关配置(过渡措施,不能替代升级)') . " --\n"; $iniChecks = [ ['session.upload_progress.enabled', '0', '官方缓解建议:设为 0 并重启所有 worker'], ['fatal_error_backtraces', '0', 'PHP 8.5 专用缓解:设为 0 并重启 worker'], ]; foreach ($iniChecks as [$key, $safeVal, $note]) { $val = ini_get($key); if ($val === false) { echo " -- {$key}:当前版本无此配置项\n"; } else { echo ($val === $safeVal ? ' [OK] ' : ' [!!] ') . "{$key} = {$val} ({$note})\n"; } } $memLimit = ini_get('memory_limit'); echo ($memLimit === '-1' ? ' [OK] ' : ' [!!] ') . "memory_limit = {$memLimit} (官方建议:以应用无法覆盖的方式设为 -1,如 FPM pool 的 php_admin_value)\n"; echo " -- post_max_size = " . ini_get('post_max_size') . " (官方建议:收紧到实际所需,无文件上传可设 8M)\n"; echo "\n注意:\n"; echo "1. CLI 与 Web 模块的 PHP 版本可能不同,请以 Web 服务实际加载的版本为准(phpinfo() / php -i 确认)。\n"; echo "2. 官方明确:配置缓解均不完整,升级到修复版本是唯一彻底方案。\n";脚本怎么读(不到 60 行、零依赖,核心就三段):
- 版本判定:PHP 内置常量
PHP_VERSION_ID是整数版本号(8.5.11 → 80511),$fixMap存四个分支的修复版本;intdiv($id, 100) * 100把 80511 归入 80500 分支查出修复版本 8.5.12,再sscanf还原成 80512 做整数比较,低于它就标[!!]。两个边界单独处理:≥ 8.6 已含修复;≤ 8.1 官方已 EOL,直接给处置出路(升级 / backport / 厂商补丁),而不是干巴巴报一句"受影响"。 - 配置体检:
ini_get()现场读值,和官方缓解建议的"安全值"逐项比对;当前版本不存在的配置项(比如 8.5 以下没有fatal_error_backtraces)标--跳过,避免误报。 - 措辞分级:
$patched记录版本判定结果,决定配置区的标题——未修复时叫"临时缓解(不能替代升级)",已修复时降级为"额外加固参考"。同一份体检报告、两种语境,已修复的用户不至于对着[!!]空紧张。
两个容器里的真实输出:
php:8.5.11(受影响)
PHP 版本 : 8.5.11(SAPI: cli) [!!] 受影响:当前分支(8.5)的修复版本为 8.5.12,请尽快升级 -- 升级前的临时缓解相关配置(过渡措施,不能替代升级)-- [!!] session.upload_progress.enabled = 1 (官方缓解建议:设为 0 并重启所有 worker) [!!] fatal_error_backtraces = 1 (PHP 8.5 专用缓解:设为 0 并重启 worker) [!!] memory_limit = 128M (官方建议:以应用无法覆盖的方式设为 -1,如 FPM pool 的 php_admin_value) -- post_max_size = 8M (官方建议:收紧到实际所需,无文件上传可设 8M)这份输出分三段读:第一行是版本判定(受影响,修复版本 8.5.12);中间三行[!!]是重点——默认配置恰好全踩在官方点名的位置上:session.upload_progress.enabled=1和fatal_error_backtraces=1正是第二节那两条已确认利用路径的"开关",memory_limit=128M这个有限值则是 CVE-2026-109618 那条 OOM 路径的触发前提;结尾两条注意里,第 1 条在实战中最容易踩坑——很多机器php -v显示的是新版本,Web 服务实际加载的还是老版本,排查时一定以phpinfo()为准。
php:8.5.12(已修复)
PHP 版本 : 8.5.12(SAPI: cli) [OK] 当前版本不低于修复版本(8.5.12),已包含修复 -- 以下配置项仅为额外加固参考 -- ...(下略,配置项同上)和上图对比两处变化:判定行变成[OK];配置区标题从"临时缓解相关配置"换成"仅为额外加固参考"。这就是$patched那个布尔值的意义——版本过了,同样的[!!]标记就从"风险告警"降级成"与建议值不同"的提示,改不改随你。
两个值得注意的实测细节:session.upload_progress.enabled和fatal_error_backtraces在官方镜像里默认都是开的,正好对应上文两条已确认的利用路径——也就是说,默认配置正好踩在官方点名的路线上,别指望"我配置很标准所以没事"。
四、修复:升级是唯一彻底方案
PHP 官方在公告里的措辞非常罕见地直接:没有任何完整的配置缓解,升级是唯一安全选择。对照第一节的表,把 PHP 升到对应分支的修复版本(或该分支更新版本),或安装发行版/LTS 厂商的对应补丁。
操作上别漏掉这几点:
- 升级前:备份 + 兼容性验证(跨大版本先读 UPGRADING 文档);
- 升级后:重启全部PHP worker,并确认实际提供 Web 服务的进程加载的确实是新版本——
php -v查的是 CLI,Web 模块以phpinfo()为准; - 容器化部署:更新基础镜像后替换旧实例,否则扩容/重新部署时旧镜像会带着漏洞回来;
- ≤8.1 的存量系统:这是本次最尴尬的一批。官方没有修复版本,能升级就升级;实在动不了的,用 backport 分支自己构建或找 LTS 厂商,同时把下一节的缓解措施拉满。
五、修不动怎么办:官方临时缓解清单
"老旧系统下线周期三个月、漏洞窗口期只有三天"是常态,以下是 PHP 官方给的缓解措施(注意:每一条官方都注明了不完整,只是降低特定攻击路径的暴露面):
措施 | 针对路径 | 要点 |
| CVE-2026-109618 | 必须用应用无法覆盖的方式设置(FPM pool / Apache 的 |
收紧 | 同上 | 建议在前置代理层限制请求体大小、字段数、part 数、header 数; |
| CVE-2026-109619 | 用 php.ini / FPM pool / 服务器配置设置,别用 |
| 同上 | 同样需要重启 worker |
| 通用 | 性能代价大,打破"同一 worker 处理两个请求"的利用前提,仅特殊情况考虑 |
两个官方点名的反面教材:
- 不要用
RLIMIT_AS/RLIMIT_DATA代替 memory_limit——它们恰恰会触发漏洞所在的mmap失败路径; - 调低
error_reporting、加@、关display_errors来"藏掉告警"没有防护作用——问题发生在 PHP 决定是否报告之前。
六、云上环境:把修复窗口期护起来
升级需要灰度、需要回归测试,窗口期里入口防护就是救命的那层窗户纸。阿里云在漏洞披露当日完成了监测评估并发布了快速防护指南(完整版见文末参考链接),对应产品上的动作:
- 摸清资产、排查主机风险:云安全中心 → 风险治理 → 漏洞管理,搜索这两个 CVE 编号,对已纳管的 PHP 主机执行检测。注意"支持列表里有"不等于"这台机器已经扫过",要看结果列表:
- 应用入口防护:WAF 3.0 → Web 核心防护 → 引擎配置 → 系统 → 防护规则,启用规则901222并确认为「拦截」动作;
- 网络侧虚拟补丁:云防火墙 → 防护配置 → IPS 配置 → 虚拟补丁,启用规则41000594(拦截),互联网边界和 VPC 边界都要确认实际业务流量经过防火墙:
- 图:云防火墙部署架构——流量只有经过防火墙,虚拟补丁才会生效
- 没人手兜底:安全管家可以协助排查与处置。
还没开通这些能力的读者:云安全中心有 15 天免费试用,WAF 3.0 有 2000CU 资源包,窗口期开起来成本很低。老规矩强调一遍:入口防护只是争取时间,不能替代 PHP 升级。
七、这次真正值得记住的事:攻击已经进入 AI 速度
最后聊点技术之外、但可能比漏洞本身更重要的东西。
这组漏洞的代码已经在 PHP 代码库里躺了十年以上。过去不是没人看过,而是把"理论缺陷"变成"可利用攻击链"的验证调试工作量太大:要串联多个 bug、要适应不同 PHP 版本和配置下的内存布局,一个部署能跑通的利用换个目标就要重新调。Wiz Research 在复盘里说得很直白:他们借助前沿模型(Gemini 4 Argon、GPT-5.6 Sol,以及自家的 Wiz Atlas)参与漏洞发现、利用开发、攻击检测和补丁验证,一名研究员一周完成了过去顶级团队数月的工作——8 月 19 日发现首个漏洞,8 月 20 日就在 Drupal 上确认了未授权 RCE,10 月 21 日修复随协调披露一起落地。
这说明一个变化正在发生:"太复杂、没人会真的去利用"这道防线,对攻击者来说正在失效。十年没人碰的代码,AI 可以几天把它的可利用性验证完。防御侧如果还停留在人工节奏,窗口期只会越拉越大。
其实阿里云在今年 9 月的云栖大会上就给出了他们的判断——"攻击以 AI 速度来,我们以 AI 速度防",浓缩成一句话就是:用 AI 速度,先于攻击,防御到位。落到产品上,是基于千问大模型对十余款安全产品做 Agentic 化升级,覆盖主机、网络、数据、应用和安全运营,让检测、研判、封禁的链路整体提速;本次 PHP2Shell 的响应中,云安全中心/WAF/云防火墙提供的防护能力就来自这批完成 Agentic 升级后的产品——漏洞披露当日即完成评估并上线防护指南,这个响应速度本身就是最好的注脚。这些升级也不是纸面概念——比如云安全中心新上线的 Skills 安全检测,专门扫 Agent 技能文件,恶意脚本、风险配置、恶意提示词三个维度一把抓,把 Agent 供应链攻击的口子从源头堵上(上面试用入口打开后就能看到这个新能力);已经在跑 Agent 工作流的同学,值得顺手开起来。想深入了解这套思路的,推荐读阿里云欧阳欣在云栖大会的演讲全文《重塑 Agentic 时代的安全体系》。
八、别搞混:CVE-2026-115560 是另一个洞
顺带提醒:10 月 23 日 PHP 官方又预告了一个 RCE 漏洞CVE-2026-115560(session 序列化处理器相关,特定配置组合下可利用,官方评估实际风险低),将在下一个常规版本修复,过渡缓解恰好也是session.upload_progress.enabled=0。看到"PHP 又爆 RCE"的标题别慌,也别和本文这两个 CVE 混为一谈——但反过来,这个配置项确实值得顺手关掉。
参考
- PHP 官方 10 月安全发布公告与各版本 Release Announcement(php.net)
- GHSA-w8hw-w9fx-j79w(CVE-2026-109618)、GHSA-6wmf-4rv5-p3rp(CVE-2026-109619),php/php-src Security Advisories
- Wiz Research:《PHP2Shell: AI uncovers critical RCE vulnerabilities affecting 70% of the Internet》
- PHP Foundation:《Today's PHP Security Release, and How We're Strengthening PHP Security》
- 阿里云《PHP 远程代码执行漏洞防护指南》:https://summit.aliyun.com/php2shell-securityresponse
- 阿里云欧阳欣:《重塑 Agentic 时代的安全体系》:https://developer.aliyun.com/article/1765586
以上是我基于官方公告 + 本地实测的完整复盘。如果你的环境里跑着 PHP,现在就花两分钟跑一下第三节的自查脚本——这类漏洞的历史经验是:公开披露之日起,留给防御者的时间就开始倒数了。觉得有用的话点个收藏,评论区交流你的升级进展。