CISA 在 10 月 26 日把 CVE-2026-109618 与 CVE-2026-109619 同时列入已知被利用漏洞目录(KEV),并给联邦民事机构划出 10 月 29 日的修复截止线。对不在联邦体系内的企业来说,这条截止线没有法律约束力,但它传递的信号非常明确:这两个被合称为 PHP2Shell 的漏洞,已经从“理论可利用”变成“正在被利用”,处置节奏也从“排进下个迭代”压缩到“按小时计”。更值得警惕的是时间线——PHP 在 10 月 21 日发布安全版本,而 Amazon Threat Intelligence 观察到的利用尝试出现在 10 月 17 日,比公开披露早了四天。这意味着披露即窗口期结束,任何还在等“观察几天再动”的团队,实际上已经处在被动位置。

KEV 收录改变了什么
KEV 目录本身不是技术公告,它记录的是“已被确认在真实环境中利用”这一事实判断。对安全团队而言,它的价值在于帮你重新排序:同样一个 CVSS 9.2,如果没有在野利用证据,你可以按季度节奏走变更流程;一旦进了 KEV,风险模型里的“可能性”项就被拉满,剩下的只有“暴露面有多大”和“多久能关掉”。
PHP2Shell 的情况还多一层复杂性。根据 IPA 在 10 月 22 日发布的公告,CVE-2026-109618 出在内存不足(OOM)的错误处理路径上,CVE-2026-109619 与请求处理相关,两者组合后可由远程未认证的第三方触发任意代码执行。PHP 安全团队给出的 CVSSv4.0 评分为 9.2。这两个漏洞分开看严重性已经很高,组合成一条链之后,攻击者不需要任何凭据、不需要登录后台,只要能向目标发 HTTP 请求,就具备了前置条件。
“Unauthenticated RCE”这五个字真正可怕的不是技术细节,而是它把受影响资产的范围界定得极宽。任何跑着受影响 PHP 版本、并且对外提供 HTTP 服务的实例,都算在名单里;而这些实例往往不止一处——主站、后台管理系统、对外开放的 API、被遗忘的旧版测试环境、外包交付的小程序服务端,全都是。
先明确受影响范围,再谈处置
动手之前有一件事必须先做:确认自己到底该拿哪一份清单去比对版本。这一步骤如果靠记忆或者靠“我们用的是最新的 PHP”来回答,后面的工作基本等于白做。
需要确认的事实有三项。第一,受影响的是哪些 PHP 分支——补丁目前覆盖受支持的 PHP 8.x 分支,具体版本号以 PHP 官方 security release 公告为准;那些已经停止维护的旧分支不会获得修复,运行在这些分支上的实例只能走升级或下线隔离的路径。第二,要留意官方随后披露的回归情况:10 月 26 日前后,PHP 8.4.27 与 8.5.12 被确认存在回归问题,如果你的升级目标正好落在这两个版本上,验证环节需要额外核对功能是否正常,而不是打完补丁就收工。第三,同一批次里还有与 session upload progress 相关的修复,以及 Windows 构建上另一个独立问题(社区称 Brotli2Shell),这些不应与 PHP2Shell 混为一谈,但会影响你对同一批变更的风险评估。
把这三项确认清楚,你手上就有了一份可以拿去和资产清单做交集的判据,而不是一个模糊的“赶紧打补丁”的口头指令。
资产盘点:把 PHP 运行时的真实边界画出来
绝大多数团队在这一步失手,不是因为工具不够,而是因为盘点的目标定错了。有效的盘点不是列出“哪些服务器装了 PHP”,而是列出“哪些进程在加载 PHP 运行时,以及它们的版本是什么”。
容易被漏掉的几类位置
- 容器镜像与编排清单。 基础镜像里带的 PHP 往往和业务代码声明的版本不一致。镜像的 tag 会变,digest 不会,核验时要落到 digest 上。
- 独立的 CLI 进程与常驻 worker。 队列消费者、定时任务、数据导出脚本,这些通常由独立的 PHP CLI 二进制执行,可能和 FPM 用的不是同一个版本,甚至来自不同的安装路径。
- 第三方设备与一体化交付产品。 网络设备管理界面、监控面板、堡垒机、打印服务器,很多内置 PHP 且不提供版本查询入口,只能靠厂商公告确认。IPA 的公告也特别提示:自研或采购的产品是否受影响,需要向该产品的开发者确认。
- 制品仓库里的历史版本。 vendor 目录、打包好的 phar、离线安装包、备份恢复到一半的旧镜像,都可能在某个时刻被重新拉起。
- CI/CD 与构建缓存。 构建机上的 PHP 版本如果落后,产出的制品会把这个版本一路带进生产。
一条可以照着执行的收敛路径
先从入口系统(负载均衡、Ingress、服务发现)导出全部对外服务清单,再逐项回溯到运行实例,最后落到实例上的 PHP 二进制。顺序反过来做——从主机清单出发去找哪些对外——几乎必然漏掉那些“不在 CMDB 里但还在跑”的实例。
版本核验:以运行时为准,不以配置文件为准
composer.json 里的 version 约束、Dockerfile 里的 FROM php:8.x、文档里写的“PHP 8.3”,这些都不是证据。真正的证据是进程当前加载的二进制版本。
# 确认 FPM 实际运行的版本
php-fpm -v
# 确认 CLI 版本,注意两者可能不同
php -v
# 找到加载的配置文件,避免改了 php.ini 却在读另一个
php -i | grep -E "Loaded Configuration File|Scan this dir"
对于容器环境,版本核验要落到镜像层:
docker ps --format '{{.Names}}t{{.Image}}' | grep -i php
docker inspect --format '{{.Image}}' <container>
# 直接进容器确认,而不是相信镜像 tag
docker exec <container> php -v
对于系统包安装的实例:
dpkg -l | grep -E '^iis+php' | awk '{print $2, $3}' # Debian/Ubuntu
rpm -qa | grep -i php # RHEL 系
systemctl list-units --type=service | grep -i php
这里有一个经常被忽视的细节:同一台主机上可能存在多个 PHP 版本——系统包一个、宝塔或面板自带一个、容器里再一个。核验时必须逐个确认,不能看到一个版本号就认为全机通过。
核验结果要落成可复核的记录:主机名或容器 digest、进程路径、版本号、核验时间、核验人。这份记录在后续的变更审批和事后复盘中,是你唯一能拿出来的东西。
打补丁:把停机风险拆开处理
补丁本身不复杂,复杂的是“不中断业务”这个约束。可行的做法是把变更拆成三步,每步都有独立的验证点。
第一步:分批与灰度
先用一组低流量实例做金丝雀,观察一个完整的业务周期(至少覆盖一个高峰和一个低谷)。灰度组的选择标准不是“最不重要的机器”,而是“流量特征最接近主站、但故障影响可控”的实例。这一步的目标不是证明补丁有效——那由官方公告保证——而是证明补丁在你的业务代码上没有引入行为变化。
第二步:重启,而不只是替换文件
这是本次事件中最容易出错的地方。Tenable 在更新说明里专门加了一条提示:更新后必须重启 PHP 服务。仅仅替换磁盘上的二进制或扩展文件,正在运行的进程仍然加载着旧的内存映像,漏洞窗口不会关闭。
# 平滑重载(推荐,适用于 php-fpm)
kill -USR2 "$(cat /run/php/php-fpm.pid)"
# 或通过服务管理器
systemctl reload php8.x-fpm
# 确认新进程已生效
ps -o lstart= -p "$(cat /run/php/php-fpm.pid)"
php-fpm -v
同时要把 OPcache 一并考虑进去。OPcache 驻留在 FPM 进程内存中,进程重载会带走它;但如果你的部署方式是 opcache.file_cache 或独立的缓存层,需要单独清理,否则旧的字节码可能继续被服务。
对于 Apache 的 mod_php 部署,重载 httpd 即可;对于使用 suexec 或每站点独立 FPM pool 的架构,要确认所有 pool 都被重载,而不是只重载了默认 pool。
第三步:准备能真正执行的回滚
回滚预案的价值取决于它有多具体。“回滚到上一版本”不算预案。有效的预案至少要包含:上一个已知良好版本的二进制或镜像 digest、回滚的命令、回滚后需要重启哪些服务、回滚决策的触发条件(例如错误率超过多少、或某个核心接口连续失败多久),以及谁有权拍板。
需要注意的是,回滚意味着重新暴露在 PHP2Shell 之下。所以回滚只应作为“争取时间定位问题”的手段,而不是终点。回滚之后必须并行准备下一版补丁的验证,并且在这段时间里把边界防护(WAF 规则、访问控制、临时下线非必要入口)顶上去。

日志排查:判断是否已经被打过
补丁打完不等于事件结束。从 10 月 17 日首次观察到利用尝试到 KEV 收录,中间有九天时间差,这段窗口足够攻击者做完探测甚至落地。所以补丁上线后必须补一轮回溯排查。
需要看的不是某一条特定的 payload 特征,而是行为模式:
- 对外入口在异常时间收到的、指向 PHP 入口文件的 POST 请求,尤其是 Content-Length 明显偏离正常业务分布的请求。
- 短时间内同一来源对多个不同路径的高频探测,特别是针对
index.php、wp-login.php等通用入口的探测。 - PHP 进程异常退出、OOM 相关日志的突增。CVE-2026-109618 出在 OOM 错误处理路径上,异常的内存耗尽记录值得单独拉出来看。
- Web 根目录及上传目录中新出现的、非预期的可执行文件或权限变更。
- 由 Web 服务用户(如
www-data)发起的异常外连、异常子进程。
参考信息方面,Adel Karimi 报告了针对蜜罐服务器的成功利用,Trail of Bits 报告了针对未打补丁 PHP 版本的可用 exploit。这些公开信息说明利用门槛已经不高,不能假设“我们的站点太小,不会被盯上”——自动化扫描不挑目标。
日志的保留期要在这时候确认一遍。如果 access log 只留 7 天,而暴露窗口是 9 天,你连回溯的原料都不够,这个缺口本身就应该写进复盘。
复盘:把这次的动作固化成下次能直接跑的流程
事件处置结束后,复盘的重点不应落在“我们响应得有多快”,而应落在“哪些环节依赖了个人经验”。可以从四个问题入手。
盘点是否可复现? 如果这次是靠某位同事凭记忆列出的服务器,那么下次还得靠他。把资产发现脚本、版本核验命令、记录模板固化成可重复执行的流程,比写一份事后报告有用得多。
版本核验的判据是否唯一? 这次有没有出现“文档说 8.3、实际跑 8.1”的情况?如果有,说明配置管理与实际状态之间存在漂移,需要把运行时版本纳入常态化采集,而不是等出事了才去查。
变更窗口能否压缩? 从 KEV 收录到 10 月 29 日只有三天。你们这次从接到通知到完成全量重启用了多久?卡在审批、卡在测试环境、还是卡在没人知道某台机器是谁的?卡点在哪,改进就落在哪。
回滚决策是否有人敢拍板? 回滚预案写得再细,如果没人有权在凌晨三点按下那个按钮,它就只是一份文档。把授权边界提前写清楚。
还有一条容易被忽略的:把这次涉及的 CVE 编号、受影响的内部资产清单、补丁版本、验证结论整理成一份可检索的内部记录。半年后当有人问“我们当时是怎么处理 PHP2Shell 的”,这份记录应该能在几分钟内被找到,而不是靠翻聊天记录拼出来。
PHP2Shell 不会是最后一个被列入 KEV 的运行时漏洞。PHP 被嵌入在太多地方——CMS、控制面板、自研服务、第三方设备——这个特性决定了它的暴露面永远比清单上写的更宽。真正能降低下次冲击的,不是这次补丁打得多快,而是这次之后,你们手里有没有一套“拿到一个 CVE 编号,就能在几小时内定位、验证、处置、回溯”的固定动作。KEV 的截止日期是给联邦机构定的,但那条时间线所代表的攻击节奏,对所有对外提供服务的组织都成立。
- CI/CD
- it2021
- it2021.com
- PHP 安全
- tag
- 代码执行
- 安全运维
- 服务管理
- 访问控制
- 资产发现

1F
容器镜像里的PHP版本确实容易漏掉,得逐个digest核实