AI 挖出的 PHP 漏洞让银行当晚停了站,而同一周还有第二颗雷

先说结论

一句话:这周有两件事,被很多人当成了一件。

  • 第一件:PHP 语言本身的远程代码执行漏洞,编号 CVE-2026-109618 和 CVE-2026-109619,合称 PHP2Shell。10 月 21 日发布补丁。这是真漏洞,代码写错的那种。
  • 第二件:一家叫 eMudhra 的证书机构,8 张中间 CA 的私钥可以被任何人算出来。这不是漏洞,这是信任根被人抄了——拿到私钥的人,能为任意网站签发"所有浏览器都认"的证书。

第二件看上去没第一件吓人,其实更麻烦。原因很朴素:漏洞修的是代码,更新一个包就完事;信任根修的是每一份自带的 CA 文件,而很多系统的 CA 文件从装机那天起就再也没动过。

最容易被漏掉的一块放在最后一节,那才是绝大多数人真正中招的地方。

第一颗雷:不用登录,一个请求就能进 PHP

它到底漏在哪

PHP 有个特性叫"内存超限时中断当前请求",内部用 longjmp() 跳出去。问题是跳的那一刻,请求相关的内部数据可能只初始化了一半,之后的清理流程却把它当合法数据用——这是 109618。

109619 是另一回事:worker 是复用的,上一个请求留下的全局状态没清干净,就还指着已释放的内存。同一个 worker 处理的下一个请求,会在正常初始化之前读到这块旧内存,典型的 use-after-free。

两个都是 9.2 分(CVSS v4.0)。泄露时传的是 10.0 满分,PHP 官方后来更正了。

顺带更正一个流传很广的说法:泄露时说的是"影响 PHP 5 开始的全部版本",但按 Calif 的技术写作,最早能追到 PHP 5.4.0(2012 年)。

这个洞是 AI 找出来的

这件事还有个容易被忽略的背景:两个漏洞都是 AI 帮忙找出来的。

Calif 的研究者在 OpenAI 的 Patch the Planet 计划下,用 GPT-5.6 Sol 审 PHP 源码,2026 年 7 月 30 日进场,8 月 1 日就挖出了第一批。Wiz 的团队则在几周后用自家 Atlas、Google Gemini 4 Argon 和 GPT-5.6 Sol 独立发现并复现了同样的问题。

但研究者的复盘里有一句挺反直觉的话:

没有一个模型能靠一句提示词把"所有 bug"翻出来。LLM 默认的变体分析会从最新、最有名的 bug 往附近的旧代码爬,不会主动从二十年前的冷门利用原语开始。真正起作用的是"知道自己目标的漏洞史",然后把这个方向明确指给模型。

也就是说,这次不是"AI 自动扫出了 PHP 的底",而是人先知道该往哪儿看,AI 负责把那段路走完。

它带来的问题也在这里:日经报道亚马逊的说法是,同样的能力放到攻击侧,AI 几分钟就能把目标点出来;今年的漏洞披露数量预计会是去年(已是历史最高)的两倍左右。今天这种"本周两个 9 分"的节奏,往后不会少见。

为什么这次特别难办

三个原因叠在一起:

  1. 它是运行时的洞,不是应用的洞。 一个只输出 Hello World 的页面,只要跑在受影响版本上,一样中招。这跟以往那种"你装了某个插件才危险"完全不是一个性质。
  2. 不需要登录。 PHP 官方确认在 Apache mod_php 默认配置下,一个 HTTP 请求就能在应用代码执行之前拿到执行权。攻击面从"某个接口写错了"变成"你这个端口开着"。
  3. 补丁提前泄露了。 原定 10 月 22 日统一公布,10 月 20 日细节在社交网络上传开,PHP 团队只好提前到 21 日 14:00 UTC 发布。而 Cloudflare 在当天 07:43 UTC 就观察到了攻击——比补丁早六个多小时。补丁空窗期不是几天,是负数。

威胁范围有多大

PHP 在"服务端语言已知"的网站里占 69.3%。Wiz 的团队实测打通了 WordPress、Drupal、Joomla 的最新容器镜像,还能从进程内存里读到密码、token、API 密钥。公网上暴露的 PHP 主机,估算在 130 万到 190 万台。

日本那边的反应可以当参考:补丁发布当晚,三菱 UFJ 银行、三井住友信用卡、大发/斯巴鲁/铃木的官网全都临时停了。CERT-EU 发了预警 2026-018,NSA 更直接——修不了的,先断网。

修法

分支 修复版本
PHP 8.5 8.5.12
PHP 8.4 8.4.27
PHP 8.3 8.3.36
PHP 8.2 8.2.35(注意 8.2 年内就 EOL 了)
PHP 7.4 / 8.0 / 8.1 官方不发版,只给了源码 backport 分支
PHP 5.4 – 7.3 无修复

改配置只能"堵路",不能"修洞"。临时顶一下:memory_limit 设成 -1 且应用没法覆盖、session.upload_progress.enabled=0、PHP 8.5 再加 fatal_error_backtraces=0——改完必须重启所有 worker。用 error_reporting、@、display_errors 把警告藏起来完全没用,因为那条路径根本不需要警告被显示出来。

那 Windows 和 IIS 上的 PHP 呢?

很多人以为这波只影响 Linux 机器。不是。

IIS 上跑 PHP 走的是 FastCGI → php-cgi.exe,而 php-cgi 就写在 PHP 官方公告的受影响清单里(和 PHP-FPM、mod_php、FrankenPHP、LiteSpeed 并列)。但档位确实低一级:

运行方式 官方定性 公开的可靠利用
Apache mod_php 最高危,请求在应用代码执行前就被打穿 官方验证过完整 RCE
PHP-FPM 受影响 Calif 明确说做到了可靠利用
php-cgi(IIS / FastCGI) 受影响 在清单里,但没有公开的可靠利用

不能因此当没事,因为 109619 的前提在 IIS 上正好成立、而且更贴近:IIS FastCGI 的 php-cgi.exe 是常驻进程,一个进程按 PHP_FCGI_MAX_REQUESTS(默认 500)连轴处理多个请求——"同一个 worker 的下一个请求读到上一个请求残留的状态",这就是它的默认架构。而 session.upload_progress.enabled 在 Windows 版里默认也是开的,那正是官方确认的两条 consumer 路径之一。

补一条历史:CVE-2024-4577 是 Windows php-cgi 的 Best-Fit 编码问题,只在中日文 codepage 下可利用。这次两个洞跟编码无关,但 Windows 加 php-cgi 这条链被专门盯过。

在 IIS 上改配置,别照抄 Linux 那套

官方给的是 php_admin_value(FPM pool 或 Apache 配置),IIS 上根本没有这个指令。只能改 php.ini:

memory_limit = -1
session.upload_progress.enabled = 0
fatal_error_backtraces = 0        ; 只有 8.5 需要
post_max_size = 32M               ; 不做上传就设 8M
upload_max_filesize = 24M         ; 必须小于 post_max_size

改完必须让所有常驻的 php-cgi 进程重生,否则等于没改:

net stop WAS /y
net start W3SVC

回收应用池也行(FastCGI 子进程会跟着重启),iisreset 更重,一般不必。php_value / php_admin_value 是 Apache 的 context 指令,写在 IIS 上会被直接忽略。

Windows 上先跑这几条

& "C:\PHP\php-cgi.exe" -v
& "C:\PHP\php-cgi.exe" -i | Select-String "Loaded Configuration File|memory_limit|upload_progress"
%windir%\system32\inetsrv\appcmd.exe list config -section:system.webServer/fastCgi

低于 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 的就是有问题。换二进制时有个坑:IIS FastCGI 必须用 NTS(Non-Thread-Safe) 版本,下成 TS 会出别的问题。

第二颗雷:一份私钥,值一个「4」

然后第二天,另一件事。

先补一句背景,因为这件事的机制其实很简单:你的设备里预装了一张短名单,叫根证书;根给中间证书背书,中间证书再给网站签名。链对得上,你的浏览器就信这个网站。整条链的安全性,最终压在那几把签发用的私钥上。

10 月 17 日,一位署名 "Wayne" 的研究者向 Chrome Root Program 报告:印度证书机构 eMudhra(emSign 品牌)的 8 张中间 CA 私钥可以算出来。证明方式很干脆——他用这 8 个私钥分别签了一句 "We have a problem here - Wayne"。

原因是密钥生成时的随机性严重不足。其中一张 EM OV TLS CA - G3C 的私钥值,实际就是 4。研究者 Flüpke 干脆把它贴到了 GitHub 上。

Mozilla 收到报告两小时后,自己独立复算出了同样的私钥——没问原始报告者用了什么方法。这说明不是巧合,是流程本身有洞。

为什么这件事的定性完全不同

PHP 那个洞,是"代码写错了"。这个不是。没有任何一行代码出错,是签发证书的那把钥匙本来就不保密。 拿到它,就可以为任何域名签一张所有设备都认的证书。

这 8 张里 6 张签 TLS、2 张签代码签名。每一张都还有一份由 Sectigo AAA Certificate Services 交叉签名的副本,所以真正要拉黑的是 16 张证书、2 个根,其中 emSign ECC Root CA - G3 是绝大多数设备信任库都有的根。CT 日志里从 10 月 19 日起已经能看到伪造的 google.com 等域名证书。

各家封得有多快

浏览器动作极快:Firefox 和 Chrome 在收到报告当天(10/17)就通过 OneCRL / CRLSet 封了,Apple 10/19,微软 10/20。Chrome 会在 10 月 27 日把 emSign ECC Root CA - G3 从根存储里彻底移除——到那天,一些正常签发的合法证书也会开始报错。

问题在于:浏览器封了,不等于你封了。 浏览器的封禁走它自己的吊销机制,而大量软件根本不查吊销——它只是装机时自带一份 CA 文件,然后一直信到有人想起来去更新它为止。

所以 Go、Java、Node 为什么全在更新

因为它们都自带这份 CA 文件,或者依赖一份不带吊销状态的根列表。于是各家只能在自己的发布里硬编码"这 8 个公钥我不要了"。

栈 修复版本 日期 说明
Go 1.27.3 / 1.26.10 10/19 CVE-2026-111943,crypto/x509 里硬编码 8 个 SPKI 哈希,覆盖全部 16 张证书
Java Oracle 带外安全警告 CVE-2026-91403,CVSS 9.1 10/21 组件 JSSE,波及 8u511 / 11.0.33 / 17.0.21 / 21.0.13 / 25.0.5 / 27.0.1
Node.js 22.23.4 / 24.22.1 / 26.12.1 10/21 除 ca-cert 外还带了 c-ares / OpenSSL / SQLite / Undici,另有 CVE-2026-105482(high,dns.resolve 遇 NUL 截断)
Python certifi 2026.10.19 / pip 26.2.2 / uv 0.13.4 10/20 certifi 是独立的一份 CA 副本,不跟系统走
Rust rustls-webpki 0.103.16 / webpki-roots 1.0.10 10/20
Electron 44.9.1 / 43.8.1 / 42.11.16 —
Debian / Ubuntu ca-certificates 20250419+deb13u1 10/20 Red Hat 对应 RHSB-2026-012
PHP / Composer ca-bundle 1.4.16 / 1.6.0 —

Go 那边有个细节值得记住:哈希算在 RawSubjectPublicKeyInfo 上——也就是公钥,不是证书指纹。所以不管是谁签的、在链上第几层,只要带着这把钥匙就会被拒,也没有 GODEBUG 能关掉。而且必须重新编译,光升级工具链不会让已经编译好的二进制变安全。

另外,Go 在 10 月 8 日还发过 1.27.2 / 1.26.9,修了 15 个问题(HTTP/2 崩溃、请求走私、模板转义等)。那是另一次发布,和这里的 CA 事件无关,也不是 9 分级别的无认证 RCE。别把两次混成一次。

有个地方容易看错:Java 那个 9.1 分不是你想的那种洞

这条要单独说,因为它特别容易被误读成"又一个 PHP2Shell"。

CVSS 9.1、组件 JSSE、AV:N/AC:L/PR:N/UI:N/C:H/I:H/A:N——光看这些指标确实像无认证远程代码执行。但 OpenJDK 10 月 21 日的公告把 CVE-2026-91403 直接对到了那 8 张被泄露的中间 CA 上。这是证书信任问题,不是 JVM 里藏着一段能被执行的内存错误。

Oracle 的公告里没有给出任何 RCE 链。得分高,是因为"不用认证、网络可达、机密性和完整性都受影响"这套评分维度对伪造证书同样适用。

这两件事的区别不在分数,在你要做什么:Java 这条是"把 JDK 里的信任列表更新掉",PHP 那条是"把运行时换掉"。

顺手在一台 Debian 13 的机器上验了一遍

说了这么多,不如看一个真实状态。Debian 13 / arm64,一台日常在用的机器:

$ dpkg-query -W ca-certificates
ca-certificates  20250419              ← 就是有问题的那版

$ ls /etc/ssl/certs | grep -i emsign
emSign_ECC_Root_CA_-_C3.pem
emSign_ECC_Root_CA_-_G3.pem            ← 还在
emSign_Root_CA_-_C1.pem
emSign_Root_CA_-_G1.pem

$ curl https://dv-g3b-emudhra.we-have-a-problem.com/
200                                    ← 应该是证书错误才对

$ curl https://dv-g3b-aaa-emudhra.we-have-a-problem.com/
200                                    ← Sectigo 交叉签名那条路也通

两个探测点都返回 200,意思是这台机器上的 curl、git、pip 以及跑在上面的服务,仍然信任那 8 把已经公开的私钥。

这两个域名是社区做的检查点:客户端若已不再信任被泄的中间证书,请求应该直接以证书错误失败。返回 200 就是中招。

修复包在 Debian 的 security 池里已有,但很多机器的 apt 列表还是旧的:

sudo apt-get update
sudo apt-get install -y --only-upgrade ca-certificates
# 复验,必须变成证书错误
curl https://dv-g3b-emudhra.we-have-a-problem.com/

顺带把自带 CA 副本的那几样也带上:pip install -U certifi、Node.js 升到 22.23.4 以上。

真打算动手,按这个顺序最省事

不谈"应该建立资产台账"那套。就按痛感排序:

  1. 先跑那两个 curl 探测点。 三秒钟一台,五台就是十五秒。返回 200 的接着往下做,直接报证书错的跳过这一节。
  2. 再查有没有 PHP 在对外服务。 mod_php 优先,它是"请求在应用代码跑起来之前就被打穿"的那一种;Windows 上的 IIS + FastCGI 也算,见前文那一节。查不到 PHP 就不用管第一颗雷,别被新闻带着跑。
  3. 然后才更新系统 CA。 动手前先想清楚:Chrome 10/27 移除那个根以后,任何还链到 emSign ECC Root CA - G3 的合法证书都会报错。对外服务用的是 eMudhra 签发的证书,现在就去换。
  4. 最后清点容器与 CI。 最费时,放到最后做,但它恰恰是最容易漏的(见下一节)。

最容易漏掉的,是那些从来不联网更新 CA 的东西

前面列的那些"修复版本"有个共同的坑:它们都需要你重新构建或重新部署,而很多人以为自己已经修好了。

具体长这样:

  • 容器镜像。 基础镜像里的 /etc/ssl/certs/ca-certificates.crt 是构建时打进去的,不重建镜像就永远带着旧的那份。
  • CI runner。 构建机上的 CA 通常没人管,但它负责拉代码、拉依赖、推制品——正好是最值钱的那条链路。
  • 已经编译好的 Go 二进制。 kubectl、terraform、各家云 CLI 都算。升级了工具链但没重新编译,跑的还是旧代码。Linux 上 Go 读系统 CA 文件,macOS 和 Windows 上它问操作系统——同一份二进制在不同平台受影响程度并不一样。
  • 打包进 App 的 CA 副本。 Node.js、Electron、Python 的 certifi、各家 SDK,都是自带一份 Mozilla 快照,跟操作系统完全无关。系统打全补丁的电脑上,这些照样中招。
  • 固件不更新的设备。 物联网、老式打印机、几年前的 NAS,它们的证书库到证书过期为止都不会变。

这一类东西的共同点是:它们的"已修复"状态不会自己出现,也不会在扫描报告里自己变绿。 你得主动去数一遍。

回到开头那个问题:这周两颗雷,先修哪个?

看法是——PHP 那个更急,但 eMudhra 这个更漏。 PHP 是有人在打你,你知道要打补丁;CA 这件事是没人打你,但你从上周末开始就一直在信任一把全世界都能算出来的钥匙,而且这份信任还躺在一堆你不会想起来去更新的地方。

参考