严重级漏洞:PHP 网站不用登录就能被远程执行代码,Linux 虚拟机能读到宿主机内存(CVE-2026-109618 / 109619 / 99999)
自查方法、修复版本和临时缓解都放在第 3 节「自查与升级」,需要直接处理的可以跳到那里;前两节讲漏洞机制和披露经过。
截至 2026-10-22,这三个编号都已正式发布,分属两组。CVE-2026-99999 出在 Linux 内核 IPv4 校验和路径上,研究者给它起名 PwnEscape 。CVE-2026-109618 和 CVE-2026-109619 都属于 PHP,走的是同一次协调披露,修复也是同一组提交,合称 PHP2Shell。
两组之间在产品和补丁上没有关联。三个编号的产品、影响和官方修复版本我先列在一起:
| CVE | 产品 | 影响 | 修复版本(官方矩阵) |
|---|---|---|---|
| CVE-2026-99999(PwnEscape) | Linux 内核 IPv4 | guest→host 内存泄露、主机崩溃;研究者在自有 KVM 上演示了有界主机权限执行 | 5.10.272、5.15.223、6.1.190、6.6.159、6.12.113、6.18.57、7.2.11;主线 7.3 |
| CVE-2026-109618(PHP2Shell / OOM) | PHP(php-src) | 未认证 RCE(web worker 权限);GHSA 在 Apache mod_php 上做了完整验证 | 8.2.35、8.3.36、8.4.27、8.5.12 |
| CVE-2026-109619(PHP2Shell / stale state) | PHP(php-src) | 未认证堆 UAF (Use After Free,释放后使用)、内存泄露、RCE;GHSA 写明 mod_php 和 PHP-FPM 都已演示 | 同上 |
评分各家不一样,我按出处分开列。
99999 这边,CISA-ADP 标的是 CWE-20,Red Hat 标的是 CWE-787。Linux CNA 给的 CVSS 3.1 是 9.3(CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H),Red Hat 给的是 8.8,向量的差别在 PR:L。
109618 / 109619 标的 CWE 是 416 / 755 / 824 / 1325 / 1419。PHP CNA 给的 CVSS 4.0 是 9.2(CVSS:4.0/AV:N/AC:H/AT:P/PR:N/...),NVD 的 Primary 3.1 是 9.8(CVSS:3.1/AV:N/AC:L/PR:N/...)。泄露前还有一个错误的 10.0 分数跟着 CVE 号传开过,出处是 PHP Foundation / Tenable。
收录和利用状态截至发稿还在变。99999 的 CVE 记录是 PUBLISHED,NVD 还停在 Received,暂时没有 NVD Primary CVSS;OSV 已经收录;CISA SSVC 在 2026-10-16 标的是 Exploitation=none。109618 / 109619 的 CVE 记录同样是 PUBLISHED,NVD 的状态是 Modified,OSV 截至发稿仍然返回 404。
三个编号都还没进 CISA KEV,我对照的目录是 dateReleased=2026-10-21T14:04:39.8983Z、count=1754。PHP 这两个的在野利用,各方说法不一,放在第 2 节细说。
1. PwnEscape:内核 IPv4 checksum 元数据
背景机制
现在的网卡和虚拟网络设备,经常把传输层校验和交给硬件或者对端去算。内核为此用了 checksum offload (校验和卸载):在 sk_buff 上挂一份元数据,标记校验和还没写回。常见字段有 ip_summed=CHECKSUM_PARTIAL,以及相对 skb->head 的 csum_start / csum_offset。
需要软件补算的时候,协议栈会调 skb_checksum_help(),按这几个坐标把校验和写回去。这条路径能追到 2008 年的提交 f43798c27684(tun: Allow GSO using virtio_net_hdr),这个提交让 TUN/TAP 允许 guest 通过 virtio_net_hdr 带上 GSO 和校验和元数据。元数据来自低信任的一方时,写回到哪里也就由这份元数据决定。
根因与触发链路
下面这条链路综合了研究者 Paulos Yibelo(pwn.ai)的文章和上游提交说明。
入站这一侧做校验时,用的是相对 skb->data 的 transport offset,没法保证它和 network_header 之间的距离。L2 头剥掉之后,guest 给的 csum_start 就可能落在已经解析过的 IPv4 头上。
旧代码在 ip_do_fragment() 里先调 skb_checksum_help(),再去读 iph->ihl。畸形元数据触发一次 deferred write 之后,IPv4 头已经被改过了。研究文里的例子是 0x45 变成 0x4f,IHL 从 20 字节扩到 60 字节。
分片路径接着按新的 IHL 拷贝「选项」,拷贝范围越过了 skb_headlen() 的逻辑线性数据,大约 32 字节主机内存就这样出现在发出去的 IPv4 options 里。
研究者在自己的、配置匹配的 KVM / Firecracker 环境上,把同一个原语继续推进成选中的内核写,并做了有界 host-root 标记演示。完整写链他说留到 Part 2 再讲,截至发稿我没找到公开细节。
Vercel 在 $1M Sandbox 挑战相关的博文里确认,研究者在它的生产 sandbox 上演示了主机内存泄露。主机崩溃和管理员权限命令执行,公开材料写的是在研究者自己的环境里做的。在某家云厂商生产环境上执行成功的公开说法,截至发稿我没找到。
修复、时间线与 PoC
主线修复是 Michael S. Tsirkin 的提交 6785011f8b16(2026-10-07,ipv4: validate checksum_start before completing checksum)。它在调用 skb_checksum_help() 之前加了一道守卫,要求 csum_start >= network_header + IPv4 header length。
c
/* include/net/ip.h --- commit 6785011f8b16 */
static inline int ip_check_csum_start(const struct sk_buff *skb)
{
if (unlikely(skb->csum_start <
skb->network_header + ip_hdrlen(skb)))
return -EINVAL;
return 0;
}
这个检查一起挂到了五个消费点上:ip_do_fragment、bridge conntrack 分片、nfnetlink_queue、xt_CHECKSUM、xfrm_output。提交说明里写明这是 defense in depth,更完整的入站校验「forthcoming」。那部分会不会另开 CVE,截至发稿我没找到公开结论。
受影响的语义版本从 2.6.27 开始,一直到各稳定分支的修复点之前。公开材料给的修复办法,就是升级到包含这个提交及其稳定分支回移植的内核。
披露经过按各来源原文里的日期排了一下:
| 日期 | 事件 | 来源 |
|---|---|---|
| 2026-08-18 | Vercel 公布 $1M Sandbox 挑战 | pwn.ai;Vercel blog |
| 2026-08-20 | 提交有界主机内存泄露报告 | pwn.ai |
| 2026-09-04 | 私下向内核安全联系人和网络维护者报告根因与候选补丁 | pwn.ai |
| 2026-09-07 | Michael S. Tsirkin 确认复现 | pwn.ai |
| 2026-09-19--22 | netdev 上公开补丁系列和修订 | pwn.ai;lore.kernel.org |
| 2026-09-25 | Linux CNA reserved CVE | CVE metadata |
| 2026-10-07--08 | 修订后的 guard 合入 net.git |
提交 Date;pwn.ai |
| 2026-10-15 | CVE PUBLISHED;oss-security 转发 | CVE;openwall 2026/10/15/5 |
CNA 对触发场景的说明是:只要能在 microVM 或客户机里,通过分配给它的 virtio-net 发报文并控制校验和元数据,就能触发,不需要主机凭据,也不需要 /dev/net/tun。路径是 guest→VMM→host TAP→IPv4,CVSS 记的是 AV:L。没有竞态或特殊布局要求,记为 AC:L;另外两项是 UI:N 和跨 guest/host 的 S:C。
发行版这边,Debian 跟踪器上的修复包就例如 bookworm 6.1.190-1(DLA-4841-1)、trixie 6.12.113-1(DSA-6568-1)、sid 7.2.11-1。Ubuntu 的页面上 medium 文案和 Critical 图标同时出现,前后对不上,具体包的修复版本以 Ubuntu 安全页 的实时状态为准。Red Hat 把这个 CVE 标为 Important(bugzilla 2553429),CWE 记的是 CWE-787。
PoC 方面,研究文里有机制说明和泄露证明。第三方索引里能找到泄露复现仓库,以及非破坏性的 metadata 可达性测试仓库,这次我没审计它们的代码是否正确。CISA KEV 没有列入,SSVC 在 2026-10-16 标的是 Exploitation=none。
2. PHP2Shell:109618 与 109619
CVE-2026-109618 和 CVE-2026-109619 在同一天公开(2026-10-21 14:00 UTC),修复提交用的是同一个标题 Avoid state corruption via bailout,就例如 01a40e89fbfc / 506e87fa7d / 31c0ea7d,分别对应 8.2.35 / 8.4.27 / 8.5.12 分支。两份 GHSA 也互相引用了对方的缓解建议。
两份对根因的描述不一样,落到最后都是请求边界上的状态不变量被破坏。第 1 节的「校验完之后状态又被改写」也属于状态不变量问题,两者的产品和攻击面没有关系。
109618:OOM 与 longjmp
PHP 官方的说法是:PHP 用 longjmp() 处理 OOM,直接跳出当前上下文。有些上下文在内存还没完全初始化时就能走到,这时分配一失败,请求内存就处在无效状态。之后请求清理阶段读写这个状态,或者根据它做分支,就可能导致 RCE。
Calif(calif.io)公开的中断点里,写得最细的之一是 _zend_hash_str_add_or_update_i。它先递增 nNumUsed / nNumOfElements,再调 zend_string_init,而这一步可能 bailout ,也就是通过 longjmp 式的硬跳转离开当前调用栈。到了 teardown 阶段,这个没初始化的 bucket 会被当成有效条目。
能把请求推到 OOM 的入口有这几类:
- 绕过或撑大
post_max_size,就例如没有有效 Content-Length 的 chunked / HTTP2 请求,或者解压。 - 空
filename=""的上传,它不计入max_file_uploads,却照样建立$_FILES的六元组。 - 用嵌套字段名放大分配。
Session upload-progress 在 modules_activated=0 时就能初始化 PS(id) 等状态。fatal 会跳过 RSHUTDOWN,但仍然走 shutdown_memory_manager,这样就留下了悬空的全局状态,下一个 multipart 请求会在 RINIT 之前用到它。
不同 SAPI 的情况差别挺大,GHSA 和研究文里都有写:
- mod_php:startup 失败后仍会跑
php_request_shutdown,畸形的表会被消费。GHSA 在默认 memory/input 限制、常规 prefork 多 worker 的配置上做了完整验证,应用代码执行之前就能触发。 - FPM:startup 失败会直接干掉 worker,这条路径活不下来。GHSA 写明目前还没有完整的 FPM exploit 演示(109619 另有覆盖 FPM 的路径)。
- FrankenPHP 和 mod_php 类似,LSAPI 和 FPM 类似(未验证)。
Wiz(wiz.io)归到这个 CVE 下的实例,包括 upload-progress unserializer 的 UAF,以及 PS(id) 被带进 PHPSESSID cookie 造成的远程进程内存披露。
修复对 OOM 路径的改动,可以从提交 31c0ea7d(PHP-8.5.12;同源的还有 01a40e89fbfc / 506e87fa7d)里看出来。原来 zend_mm_safe_error 上立即 zend_bailout() / longjmp 的做法被拆掉了,现在分两种情况处理:
- Soft OOM 走
zend_mm_delayed_error,填写EG(delayed_oom)、置上EG(vm_interrupt),等到下一个 VM 安全点,再由zend_handle_delayed_oom()/UnwindExit处理。 - Hard OOM 指超过
hard_limit的情况(默认大约是 soft 的 200%,或者 +1G 缓冲,对应 INImemory_hard_limit_percent),会走zend_mm_hard_limit_error→_exit(137)。
109619:跨请求全局状态
PHP 官方的说法是:有多种全局状态(error / session / object / extension)会临时指向请求内存,必须在释放请求堆之前复位。fatal(包括 OOM),或者 shutdown 之后警告又把它们重新填上,这类情况都会留下悬空指针。下一个请求在完全初始化之前的操作会用到这些指针,形成 UAF,并可能进一步发展到 RCE。
GHSA 用生产者 / 消费者模型来描述这个问题:
- Producer:把请求内存挂到 worker 的全局状态上,复位被 fatal 打断;或者 shutdown 之后,非致命诊断又写了一次。
- Consumer(已确认):请求初始化之前的 INI 诊断(
.htaccess/php_value、FPMPHP_VALUE等);Session upload progress(默认开启,不需要session_start())。
已经确认的强路径有两条:
- PHP 8.5 开启
fatal_error_backtraces时,EG(last_fatal_error_backtrace)漏了复位,这是 mod_php RCE 的基础。 - session upload-progress 的生命周期,在 mod_php 和 FPM 上都有验证过的路径。FPM 上需要先有 startup 成功之后的应用层 fatal,再接上后续的 upload-progress consumer。
Wiz 还把 fatal-error backtrace UAF 和 OPcache type confusion(CG(map_ptr_last) 和共享 frontier 的发布不同步)也归到了这个 CVE 下。
producer 和 consumer 这两个请求必须落在同一个 worker 上。多 worker 和 worker 回收会降低成功率,公开材料的判断是成功率下降,路径还在。
同一组提交在请求边界上把全局复位收得更紧,就例如:
zend_shutdown_executor_values显式ZVAL_UNDEF(&EG(last_fatal_error_backtrace));ext/opcache/zend_persist.c用pending_ce_cache做 begin / commit / discard;ext/session/session.c增加了PS(in_rfc1867_update)和 incomplete-class-only 序列化;main/main.c在 shutdown 时清空http_globals、调用clear_last_error()、置PG(modules_activated)=0。
GHSA-w8hw-w9fx-j79w 和 GHSA-6wmf-4rv5-p3rp 在这同一批提交上都标了 Fix。
披露时间线与在野说法
协调披露的主要节点列在下面。Calif 和 Wiz 记的报告日期对不太上,我在表里都保留了。
| 日期 | 事件 | 来源 |
|---|---|---|
| 2026-08-01--14 | Calif 发现多条通向 RCE 的路径,并向 PHP 提供 RCE PoC | calif.io |
| 2026-08-15 | PHP 确认 PoC | calif.io |
| 2026-08-19--24 | Wiz 发现并确认 Drupal / WordPress 未认证 RCE | wiz.io;calif.io |
| 2026-09-21 | PHP 确认并开始协调修复(Wiz) | wiz.io |
| 2026-10-12 | 两份 CVE reserved | CVE metadata |
| 2026-10-17 | AWS MadPot 观测到利用尝试(公开披露前约 4 天) | AWS Security Blog |
| 2026-10-20 | CVE 号在 HN / 社媒泄露;PHP 发布「Upcoming Critical」预告(原计划 10-22,提前到 10-21 14:00 UTC) | php.net;Tenable FAQ;Huntress |
| 2026-10-21 ~02:00--08:00 UTC | AWS 看到更多 PoC 和针对蜜罐的尝试 | AWS |
| 2026-10-21 14:00 UTC | 补丁和 GHSA / oss-security 公开发布 | php.net;openwall |
在野利用这件事各方说法不一,截至发稿我先并列放在这里:
- AWS:10-17 起在蜜罐里看到利用尝试;10-21 之后称 broadly exploited in the wild,并给出了 IOC IP。
- Cloudflare CTO(Tenable 引了他的推文):seeing early attacks emerging。
- watchTowr(Tenable / Huntress 引述):已经复现,并观察到利用尝试。
- Huntress(看的是自有客户,截至 2026-10-21 16:30 ET):没在伙伴环境里看到在野利用。
- CISA SSVC:Exploitation=
none(时间戳 2026-10-21T14:34:39Z / 14:34:42Z,可能没有随后续观测更新)。 - CISA KEV:两份都没有列入(KEV
dateReleased=2026-10-21T14:04:39.8983Z)。
PoC 这块,Calif 的 PoC 是私下交给 PHP 的,Wiz 演示了 WordPress / Drupal / Joomla,研究 writeup 里写的是机制。第三方仓库也有,就例如 cozybear-dev/cve-2026-109618,这次我没验证它能不能跑。PHP Foundation 的说法经 Tenable 转述,是泄露后检查过的 PoC「发布时未识别为可工作 exploit」。
oss-security 的跟帖里有人怀疑 8.5.12 的修复不完整,后来又说可能是另一个问题。Canonical 向 php.org 核实后,得到的答复是「unrelated and not a real issue」(Aaron Rainbolt 转述,2026-10-21)。这条答复不算官方的 CVE 修订。
版本边界要分开看。CNA 的 affected 矩阵只列了 8.2.*--8.5.* 到修复版之前,defaultStatus: unaffected;NVD 的 CPE 写的是 <8.2.35、8.3.0--<8.3.36、8.4.0--<8.4.27、8.5.0--<8.5.12。
Calif 把最早受影响的路径追到了 PHP 5.4.0。php.net 说明 8.1 及更早的版本已经不在 PHP 项目的常规更新范围内,同时给 7.4 / 8.0 / 8.1 提供了最小源码 backport,发行版那边可能另有包。CNA 列表和研究、运维角度看到的风险面不一定一致。
3. 自查与升级
这两组漏洞要完整修复,都得靠升级。从公开材料看,内核这边没有配置层面的缓解办法;PHP 这边两份 GHSA 给了临时缓解,也都写明了不完整。
3.1 内核(CVE-2026-99999)
优先排查跑不可信客户机、用 TAP + virtio-net 给客户机供网的宿主机,就例如 sandbox、microVM(Firecracker 等)和多租户 KVM。按 CNA 的场景说明,客户机这边只要能控制 virtio-net 报文的校验和元数据就能触发,不需要主机凭据。
-
在宿主机上看一下运行中的内核:
uname -r。 -
上游内核按所在的稳定分支,对照下面的修复版本:
分支 修复版本 5.10 5.10.2725.15 5.15.2236.1 6.1.1906.6 6.6.1596.12 6.12.1136.18 6.18.577.2 7.2.11主线 7.3受影响范围从
2.6.27开始。官方矩阵没列出来的分支没有对应的修复点,需要换到上表里的分支,或者用发行版的回移植包。 -
发行版内核的版本号没法直接和上游对照,要看发行版安全公告或包 changelog 里有没有包含本 CVE / 提交
6785011f8b16。Debian 的修复包是 bookworm6.1.190-1(DLA-4841-1)、trixie6.12.113-1(DSA-6568-1)、sid7.2.11-1;Ubuntu 以安全页的实时状态为准;Red Hat 的跟踪在 bugzilla 2553429。 -
新内核装好后要重启进去,再用
uname -r确认一遍。 -
上游提交说明里提到,更完整的入站校验会另外提交,截至发稿还没看到公开结论。升级之后也要继续跟进稳定分支的后续更新。
3.2 PHP(CVE-2026-109618、CVE-2026-109619)
-
先确认 Web 实际加载的是哪个版本。
php -v只反映 CLI;FPM 要用php-fpm -v看(发行版里常带版本后缀,就例如php-fpm8.2 -v),mod_php 以 Apache 加载的模块包版本为准。 -
升级到
8.2.35/8.3.36/8.4.27/8.5.12或者发行版的等价包,然后重启所有 PHP worker(FPM 的各个 pool、Apache)。发行版的例子:- Debian bookworm 的
php8.2修复包是8.2.35-1~deb12u1(DLA-4848-1);trixie 的php8.4是8.4.27-1~deb13u1(DSA-6574-1)。 - Ubuntu 的修复包有
8.1.2-1ubuntu2.28、8.3.6-0ubuntu0.24.04.13、8.5.9-0ubuntu2.1等,以 Ubuntu 安全页 为准。发行版包的版本号可能比上游修复版本低,判断依据是安全公告。
- Debian bookworm 的
-
还在用 8.1 / 8.0 / 7.4 的,去查发行版的安全包,或者用 php.net 给这几个分支提供的最小源码 backport。CNA 矩阵没列这些分支,风险怎么判断可以参考第 2 节的版本边界。
-
暂时没法升级的话,可以先用两份 GHSA 给的临时缓解。GHSA 要求两份的建议同时执行,改完配置要重启所有 worker。
措施 对应 GHSA 写明的限制 管理性强制 memory_limit=-1,并确认 pool、目录和应用配置都不会改回有限值109618 主要配置缓解; mmap()等底层分配失败时仍可能走longjmp();同时去掉了可预测的单请求内存上限用容器或 cgroup 的 memory.max代替 PHP 自身的内存上限109618 不保证阻止分配失败;整组被杀时 master 和所有 worker 一起退出,除非每个 worker 单独成组 保守设置 post_max_size,前端独立限制解码后的请求大小;降低max_input_nesting_level,限制输入变量、multipart 段数、每段 header 数与上传数;不用 multipart 就在前端拦截109618 打补丁后仍能降低 hard OOM 与 DoS 风险,不能代替升级 PHP 8.5 设 fatal_error_backtraces=0109619 去掉已确认 RCE 用到的 producer,其他全局状态不覆盖 session.upload_progress.enabled=0109619 只阻断 session 生命周期这条路径;不用该功能时,GHSA 建议打补丁后也保持关闭 消除请求初始化前的 INI 诊断:改用 php.ini、FPM pool 等启动期配置,减少.htaccess的php_value和前端下发的PHP_VALUE,删除过时指令109619 列为减少剩余风险的做法,不是完整缓解 消除攻击者可达路径上的 fatal、执行超时,以及扩展与服务器清理阶段的警告 109619 列为减少剩余风险的做法,不是完整缓解 性能允许时设 pm.max_requests=1109619 列为减少剩余风险的做法,不是完整缓解 有几处容易踩坑:
- 别用
RLIMIT_AS/RLIMIT_DATA来限内存。一旦触顶,mmap()失败,走进去的正好是有漏洞的longjmp()路径。 - 调
error_reporting、加@、关掉display_errors或log_errors,对 109619 都起不到防护作用。全局状态在 PHP 决定要不要报告之前就已经被访问了。 - 109619 的 GHSA 写明,没有完整的通用配置缓解。
- 别用
-
查日志。GHSA 的建议是在隔离的审计环境里,在启动配置中打开
log_errors=On、可用的error_log和error_reporting=E_ALL,然后在 Apache、PHP-FPM、反向代理和服务日志里找这几类内容:- 目标脚本运行之前反复出现的警告和弃用提示;
- 扩展或服务器清理阶段的诊断;
- fatal、parse 和 uncaught 错误,以及
Maximum execution time exceeded; - 只在攻击者可控的路由、内容类型、文件或插件选项上出现的失败。
日志里没有相关报错,并不能说明就安全。访问日志还可以拿 AWS 公布的 IOC IP(见第 2 节)对照一下。