PHP2Shell 原理拆解:内存出错的那一瞬间,怎么变成远程代码执行
先给结论:PHP2Shell 不是某个网站程序的漏洞,而是 PHP 运行时本身的漏洞。它由两个远程代码执行漏洞组成——CVE-2026-109618 与 CVE-2026-109619,CVSS v4.0 均为 9.2 分,无需登录、无需用户交互。问题出在请求异常中止之后的清理阶段:内存耗尽或致命错误会强行结束请求,而清理流程却可能把无效内存当成有效数据继续使用,攻击者利用的正是这一瞬间。
一个 PHP 请求的一生是怎样的?
先理解 PHP 处理请求的生命周期。以 PHP-FPM 为例:常驻的 worker 进程接收请求,先初始化本次请求的内存和全局状态,再执行应用代码,请求结束时统一清理、释放,回到空闲等待下一个请求。关键在于:初始化、执行、清理必须严格配对。执行阶段一旦被强行打断,而清理仍按"一切正常"的前提运行,问题就出现了。
CVE-2026-109618:内存耗尽时发生了什么?
当请求内存超过上限(OOM)时,PHP 会用 longjmp() 直接跳出执行流,中止这次请求。麻烦在于,此时部分请求内存可能尚未完成初始化。但清理流程并不检查,仍把这块内存当作有效数据读取、跳转。这属于 CWE-824:访问未初始化指针——程序在用一个"还没写好"的指针做事,它指向哪里,可能被攻击者预先布置的内容左右。
CVE-2026-109619:致命错误后为什么会读到已释放的内存?
第二个漏洞剧本类似,失效方式不同。请求触发致命错误被中止后,某些全局状态仍指向本次请求的内存,而这块内存会在请求结束时被释放。worker 是常驻进程,不随请求退出;它接着处理下一个请求时,若读到仍指向已释放内存的全局状态,就会发生释放后使用(use-after-free):以为在读合法数据,实际读到的是已回收、可能被新数据覆盖的区域。
为什么攻击者还要做堆布局、还要命中同一个 worker?
这两个漏洞都不是"发一个请求就立刻得手"的类型。要把"读到无效内存"推进到"执行攻击者安排的代码",攻击者需要用大量特制请求反复塑造堆内存布局(即堆布局 grooming),让被误读的位置恰好落入可控内容;同时请求必须命中同一个持久 worker,因为失效状态只存在于那个进程里。即:触发不难,稳定利用门槛较高。但门槛高不等于可以观望——公开后数小时内已出现概念验证与大规模扫描,且已有在野利用记录(有云厂商蜜罐在公开前的 10 月 17 日就记录到利用尝试)。
为什么说漏洞发生在"应用代码运行之前"?
因为问题在运行时层:出事的是请求初始化与清理环节,而不是某个页面、某个插件写错了代码。这意味着登录页、验证码、应用自身的权限校验都挡不住它——攻击不需要先成为合法用户。影响范围因此很广:所有处理 Web 请求的 PHP 安装都在范围内,包括 PHP-FPM、Apache mod_php、php-cgi、FrankenPHP 和 LiteSpeed。
两个漏洞有什么区别?
| 对比项 | CVE-2026-109618 | CVE-2026-109619 |
|---|---|---|
| 触发条件 | 请求内存耗尽(OOM) | 请求触发致命错误 |
| 失效点 | longjmp() 中止后,清理访问未初始化内存 | 中止后全局状态指向已释放内存 |
| 问题类型 | 均为 CWE-824,CVSS v4.0 均为 9.2 分 | |
| 共同前提 | 需要命中同一持久 worker,并配合堆内存布局 | |
防御的落点在哪里?
官方态度明确:没有完整的配置缓解办法,升级是唯一完整修复,WAF 只能临时缓冲。修复版本已于 2026 年 10 月 21 日紧急发布:PHP 8.5.12、8.4.27、8.3.36、8.2.35,升级后必须重启 PHP 服务才算生效。PHP 8.1 及更早版本已停止官方支持、没有官方版本更新,应优先安排迁移。企业侧的排查顺序和供应商确认清单,我们在常见问题栏目另有一篇专门说明;网站日常运维与版本管理,也可结合智盟CMS建站产品页一起规划。
扫码添加企业微信,预约演示。
