技术解析

PHP2Shell 的原理

一个根因、两个 CVE、四个漏洞:PHP 在请求处理中途遇到致命错误——尤其是内存耗尽——时,无法可靠地完成清理。

最后核实 2026-10-22

根因

PHP 触及 memory_limit 时会抛出致命错误,并用 longjmp() 回退。这会跳过本应完成初始化或重置状态的代码,从而导致两个问题:

  • 半初始化的数据(CVE-2026-109618)。已分配但尚未初始化的内存,会在请求关闭阶段被当作有效数据读取。
  • 残留的全局变量(CVE-2026-109619)。请求级全局指针(错误、会话、对象、扩展状态)没有被清空,但它们指向的内存在请求结束时已被释放。同一 worker 的下一个请求会在正常初始化之前使用它们。

攻击者通过构造的 multipart 请求触达这些路径,在 PHP 解析输入时把它推入 OOM——这发生在你的路由、框架或认证逻辑运行之前。所以一个 “Hello World” 脚本和 WordPress 一样暴露。

利用链

  1. 步骤 1

    泄露

    利用内存泄露读取 worker 的实时内存,绕过 ASLR。

  2. 步骤 2

    破坏

    利用释放后使用(反序列化器或回溯)写入可控数据。

  3. 步骤 3

    执行

    把内存破坏转化为以 worker 用户身份执行代码——全程无需登录。

利用不是一次就成功的:攻击者需要反复试探,才能适配每个目标的内存布局。研究人员把整条利用链合并成一个名为 php2shell 的脚本(未公开),并在当时最新的 wordpress、drupal 和 joomla 容器镜像上完成演示。

四个漏洞

CVE-2026-109618

上传进度反序列化器中的释放后使用

会话模块在请求初始化完成之前就为上传进度跟踪解码数据,并在 BG(unserialize) 中记录状态。一次内存分配失败会销毁请求堆,却留下这个全局变量。同一 worker 的下一个请求会把被攻击者重新占用的内存当作有效的反序列化器状态。

CVE-2026-109618

远程读取进程内存

在早期的 RFC 1867 multipart 解析中,上传进度代码可能把会话 ID 写入 PS(id)。OOM 中断后,这个指针变成悬空指针。之后的 multipart 请求会信任该指针和长度,把那里现存的内容 URL 编码后写进 PHPSESSID Cookie——即使应用从未调用 session_start()。

CVE-2026-109619

致命错误回溯中的释放后使用

EG(last_fatal_error_backtrace) 在销毁请求堆的 OOM 之后仍然存在。下一个请求中较早的诊断逻辑会沿着这个悬空指针访问。在开启 fatal_error_backtraces 的 PHP 8.5 上最为直接。

CVE-2026-109619

OPcache 共享内存中的类型混淆

在 zend_accel_script_persist() 中,CG(map_ptr_last) 先于共享边界发布而推进。两者之间的内存分配失败会留下各 worker 认知不一致的 map-pointer 槽位,使某个 worker 把槽位复用于另一种对象类型。

漏洞与 CVE 的对应关系依据 Wiz Research 的文章。PHP 官方公告对漏洞类别的描述更宽泛,并提到还有其他变种。

按服务器部署方式的暴露情况

依据 PHP 官方公告。“存在风险”指源码分析表明有漏洞,但尚无针对该 SAPI 的公开通用利用。

部署方式…109618…109619说明
Apache + mod_php已证实 RCE已证实 RCE两个 CVE 均已在默认限制下验证可达 RCE。风险最高的部署方式。
PHP-FPM(nginx、Apache、Caddy……)存在风险已证实 RCE已在 FPM 上验证通过会话生命周期实现 RCE;OOM 路径已通过应用层代码路径证实,但尚无公开的通用 FPM 利用。
FrankenPHP / LiteSpeed LSAPI很可能受影响很可能受影响公告评估为很可能受影响。常驻 worker 只会让跨请求残留状态的问题更突出。
CLI(cron、队列 worker、artisan)公告未涉及公告未涉及公告未讨论,正常使用时也无法通过 HTTP 请求触达。但仍要升级——它们来自同一个软件包。

CVE 记录

CVE-2026-109618GHSA-w8hw-w9fx-j79wCVSS 4.0 · 9.2

内存耗尽(OOM)导致的内存损坏 RCE

PHP 在内存耗尽时会用 longjmp() 中断当前上下文。如果此时请求内存只初始化了一部分,请求关闭阶段就会读取这些无效数据或据此跳转。多条 multipart 解析路径可以在正常限制内把 PHP 推入 OOM,而且发生在任何应用代码运行之前。

影响:
无需认证即可以 PHP Web worker 用户身份执行代码。
已演示:
已在 WordPress、Drupal 和 Joomla(mod_php,默认限制)上演示认证前 RCE。
受影响版本
<8.2.35、<8.3.36、<8.4.27、<8.5.12(以及一直到 5.4 的已停止支持分支)
CWE
CWE-416, CWE-755, CWE-824, CWE-1325, CWE-1419
CVE-2026-109619GHSA-6wmf-4rv5-p3rpCVSS 4.0 · 9.2

跨请求残留全局状态导致的 RCE

致命错误(包括 OOM)可能跳过每个请求结束时的重置,导致错误、会话、对象或扩展相关的全局状态仍指向请求结束时已释放的内存。同一 worker 上的下一个请求可能在正常初始化之前使用这块已释放的内存。

影响:
释放后使用(UAF),可导致 worker 内存泄露或代码执行。
已演示:
已在 WordPress 和 Drupal 上演示认证前 RCE;还演示了与应用无关的远程内存泄露。
受影响版本
<8.2.35、<8.3.36、<8.4.27、<8.5.12(以及一直到 5.4 的已停止支持分支)

向量(两者相同):CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N。可通过网络触达,无需权限或用户交互;“AC:H / AT:P” 反映了利用必须针对目标做适配。早期泄露的报道给出的 CVSS 10.0 是错误的。

补丁改了什么

补丁不再在内存耗尽时使用 longjmp()。现在触及 memory_limit 会设置一个软 OOM,在虚拟机下一个状态一致的安全点抛出。另一个更高的阈值会触发硬 OOM,用 _exit() 直接结束进程——不运行关闭处理函数,也不会留下清理到一半的状态。

由于修复针对的是故障模式本身而非单个入口点,PHP 团队表示它同样覆盖了已知的变种。

副作用:依赖 OOM 后关闭函数仍会运行的代码路径,在触及硬限制后行为会有所不同。

相关页面