PHP2Shell 复现与分析:请求生命周期缺陷及利用边界
原创
XMOS
XMOS
雷石安全实验室
2026年10月23日 17:31
浙江
去阅读
PHP2Shell 复现与分析:请求生命周期缺陷及利用边界
编者荐语
PHP2Shell 刚公开,一些号称“通杀”的 EXP 就开始流传。安全研究员 Marcus Hutchins 在对早期公开代码的评论中指出,他看到的部分所谓 PHP2Shell 利用,实际沿用了旧的“会话投毒+本地文件包含(LFI)”技术,需要应用本身存在 LFI 漏洞。两者都涉及 PHP_SESSION_UPLOAD_PROGRESS,利用的缺陷和前提却相去甚远。[6]
把 Session、上传进度这几个关键词交给大模型,模型可能找来过去的利用方法,拼出一份带着新 CVE 编号的脚本。代码有 payload、有执行流程,甚至写好了成功提示;如果发布者自己也不理解原理,就很容易把“生成了一份脚本”当成“完成了漏洞利用”,未经验证便发布、转发。
随后,其他人拿着脚本去测试,发现打不通,讨论又变成了“EXP 太垃圾”“漏洞雷声大雨点小”。但失败可能来自代码利用了另一个问题,也可能来自目标部署、构建和触发条件不匹配。只看最后有没有弹出成功提示,既无法确认脚本是否实现了这个漏洞,也无法评估漏洞本身的风险。
大模型可以辅助研究,代码的验证仍需要人来完成。下面这篇分析从隔离环境的复现和对照结果谈起,交代利用走了哪条路径、在哪一步中断,以及这些结果能够支持多大范围的判断。
遵循《网络产品安全漏洞管理规定》关于漏洞信息发布的相关要求,本文仅用于原理分析与安全防护研究,不提供 PoC、EXP 或可直接复用的攻击代码及载荷。
2026 年 10 月 21 日,PHP 发布 8.5.12、8.4.27、8.3.36、8.2.35,修复 CVE-2026-109618 和 CVE-2026-109619。两项漏洞的 CVSS v4 评分均为 9.2,相关研究以 PHP2Shell 为名公开。[1][2][3]
在隔离环境的复现中,WordPress 和 Laravel 的 Apache mod\_php 环境完成了命令执行;PHP-FPM 环境在请求启动阶段触发内存耗尽后,子进程退出,原利用路径中断。进一步加入自建测试入口后,FPM 环境复现到了释放后使用,但没有完成代码执行。
这些结果的差异,与 PHP 在什么阶段发生错误、错误后执行哪些清理,以及处理请求的进程是否继续存活有关。本文从这组对照出发,分析两项漏洞的机制、公开利用的适用条件,以及修复时需要核实的部署信息。
一、复现结果与测试条件
本地复现使用的镜像包括 wordpress:php7.4-apache 和 php:8.5.11-apache。下表整理了各环境的结果,应用名称用于标识测试载体。
| 测试环境 | PHP 部署方式 | 记录中的结果 |
|---|---|---|
| WordPress 6.1.1,PHP 7.4.33 | Apache mod\_php | 完成命令执行,回读结果为 www-data |
| Laravel 测试应用,PHP 7.4.33 | Apache mod\_php | 完成命令执行,回读结果为 www-data |
| PHP 8.5.11 | Apache prefork + mod\_php | 两次运行均在第一次尝试完成代码执行 |
| PHP 8.5.12 | 与上一项配置相同 | 原利用脚本在第一阶段失败,未完成信息泄漏及后续代码执行 |
| WordPress 6.1.1,PHP 7.4 | PHP-FPM | 启动期 OOM,子进程以退出码 70 结束 |
WordPress 的一组运行输出如下。脚本先获得堆和 libphp 的地址信息,再通过内存破坏进入命令执行阶段。
WordPress 环境的利用脚本运行输出
图 1:WordPress 环境的运行输出摘录。图中地址对应单次运行,PHP 7.4 测试的内存上限为 16M。
命令执行的验证还包括目标侧输出文件的回读。下图来自另一组运行记录,WordPress 和 Laravel 容器中的文件内容均为 uid=33(www-data),文件属主也是 www-data。
WordPress 和 Laravel 环境的输出文件回读
图 2:两个环境的命令输出及文件属主记录。
本地 PHP 7.4 的复现记录包含 16M、32M 两种内存上限,相应的填充方案也有调整。后文引用的 128M、多 worker 测试来自公开项目,不能与本地结果合并计算成功率。
8.5.11 与 8.5.12 的对照支持修复版本阻断了本次测试使用的利用链。这里的验证范围是该环境、该脚本及其执行路径;两次成功不足以评估生产环境的利用稳定性,修复后的第一阶段失败也不能替代对其他变体的验证。
二、异常退出如何留下可利用状态
请求内存与 worker 的寿命不同
在 mod\_php 和常规 PHP-FPM 部署中,处理请求的 worker 会被复用。一个请求结束后,请求堆被释放,进程继续处理后续请求。
PHP 的部分全局字段由 worker 持有,却指向某次请求分配的内存。这类引用需要在请求内存释放前清除。否则,进程虽然仍在运行,字段里保存的地址已经失效。
下一个请求也并非等所有状态完成初始化后,才开始处理输入。与本次会话利用路径有关的正常处理顺序如下:
PHP 请求生命周期及上传进度的早期处理位置
图 3:正常请求的处理顺序。上传进度处理可在 session 模块正常的请求初始化之前发生,应用代码此时尚未执行。
session.upload_progress.enabled 默认开启。相关处理可以早于 session 模块正常的请求初始化,因此不要求业务代码先调用 session_start()。如果前一请求遗留了失效状态,早期操作可能在初始化重置它之前,就已经访问了它。[3]
这也解释了业务层检查的局限:在这些启动期路径中,应用的登录校验、路由和上传接口判断尚未执行。要阻断请求,需要在请求到达 PHP 解析器之前实施相应控制。
CVE-2026-109618:OOM 后,收尾流程访问了无效状态
修复前,PHP 在内存耗尽时可以通过 longjmp() 跳出当前处理上下文。发生跳转的位置如果没有为这种退出方式做好准备,局部的配对清理就可能被绕过,尚未完成初始化的请求状态仍然可达。
mod\_php 即使发现请求启动失败,仍会调用请求关闭流程。关闭流程随后读取、释放或依据这些无效状态执行分支,便可能造成内存破坏。[2]
multipart 解析还存在内存放大问题。字段、嵌套数组和文件元数据在 PHP 内部的占用,可以明显大于其网络传输大小;缺少可用声明长度的请求,也可能越过解析器最初的大小检查。因此,限制上传文件大小或请求体字节数,不能单独证明 PHP 内部不会到达危险的分配失败点。
CVE-2026-109619:请求结束后,失效引用仍被保留
这项漏洞涉及跨请求保留的状态。某次操作把请求级状态写入全局字段,随后遭遇 fatal error 或 OOM,没有完成配对重置;请求堆释放后,全局字段仍保留旧地址。另一些路径则可能在清理之后,又通过诊断信息重新写入请求所属的状态。[3]
在会话状态这条路径中,需要同时找到两个操作:
- producer:产生请求所属状态,并使该状态在请求结束后仍被引用。
- consumer:在后续请求正常初始化之前,访问这块已经失效的状态。
同一 worker 中的状态残留与后续访问
图 4:会话状态路径的一种触发过程。请求 A 遗留失效引用,请求 B 在正常初始化之前访问它。
这条路径要求两个请求到达同一 worker。worker 数量、连接分配及进程回收会影响命中条件,但它们不能修复失效引用本身。
上述路径分别涉及两个生命周期问题:
| 漏洞 | 主要问题 | 本文讨论的典型路径 |
|---|---|---|
| CVE-2026-109618 | 分配失败后提前跳出,留下不完整或无效状态 | 请求启动失败后,mod\_php 的关闭流程继续访问该状态 |
| CVE-2026-109619 | 请求所属状态未在生命周期边界正确重置 | 后续请求在正常初始化前访问前一请求留下的状态 |
同一条利用链可能组合多种缺陷,各变体的触发条件仍需分别分析。Wiz 还公开了归入 CVE-2026-109619 的 OPcache 类型混淆:局部与共享状态更新不一致,导致其他 worker 后续使用共享结构时发生类型混淆。这条路径涉及跨 worker 的共享状态,与上图会话路径的同一 worker 条件不同。[4]
三、FPM 对照中,利用在哪一步中断
启动失败后,原 worker 退出
在 PHP-FPM 对照环境中,测试请求包含约 60 MB 的 multipart 普通字段,PHP 的 memory_limit 为 16M。请求在启动阶段触发内存耗尽,响应记录为:
Fatal error: Allowed memory size of 16777216 bytes exhausted
(tried to allocate 8390042 bytes) in Unknown on line 0
FPM 日志随后记录了旧子进程退出和新子进程启动:
child 374 exited with code 70 after 526.003774 seconds from start
child 512 started
这里的 526 秒是子进程自启动以来的存活时间,不是该请求的处理耗时。
请求启动失败后 mod\_php 与 PHP-FPM 的处理分支
图 5:启动期 OOM 的处理差异。mod\_php 仍调用关闭流程;FPM 终止当前 child,使本次依赖旧进程的启动期会话状态链中断。应用执行期间的路径另行分析。
PHP-FPM 在 php_request_startup() 失败后终止当前 child。对于依赖同一进程继续处理请求的会话状态链,旧 worker 退出意味着失效状态不会再由它的下一个请求消费。本地结果与 PHP 公告描述的启动失败行为一致。[2][3]
应用执行期间发生错误,条件会改变
启动失败的结果不能覆盖应用执行期间的所有路径。PHP 官方验证的另一条 FPM 会话状态路径发生在正常启动之后:应用中的某个操作发布了 session 或解码器状态,随后遇到 fatal error,配对重置被跳过;worker 继续存活,后续请求的上传进度处理再访问失效状态。[3]
本地进一步测试使用了一个额外编写的 producer 页面。记录显示,producer 发生 OOM 后 worker 仍然存活,下一请求访问到了残留的解码器状态,并复现了释放后使用(UAF)。
这组测试没有完成 RCE。当时的消费阶段早于 WordPress 在当前请求中注册相关类,预期的对象利用条件没有成立;实验也没有完成后续所需的信息泄漏和执行链。
这组结果的验证范围是自建入口条件下的 UAF,尚未证明默认 WordPress + FPM 可完成未认证代码执行。 对 WordPress 6.1.1 测试容器的源码检索也没有找到可直接替代该页面的入口,其他版本、插件、主题和业务扩展不在这次检索范围内。
此外,本地 FPM 环境没有加载 OPcache,也未覆盖前述共享状态变体。评价一套 FPM 部署时,需要继续检查具体应用操作、扩展及配置,不能仅根据 SAPI 名称作出安全判断。
四、公开利用降低了哪些成本
截至 10 月 23 日,一份公开利用项目的 README 报告了以下测试组合:PHP 8.4.26 NTS mod\_php、Apache prefork、官方镜像,使用随 PHP 提供的 php.ini-production;display_errors=Off,memory_limit=128M,MaxRequestWorkers=150。目标页面只输出一行文本,没有调用 session_start()。[5]
项目报告该组合下 10 次测试均完成代码执行,另有单 worker 基线 20 次成功的记录。这些数字属于项目作者报告,本文没有独立复跑该组配置。
这份公开结果补充了本地测试未能说明的两点:已知利用可以不依赖低内存上限和页面错误显示;多 worker 配置也存在满足跨请求命中条件的办法。单凭“关闭了错误显示”或“使用多个 worker”,不足以排除这条利用路径。
从适配过程看,仍需区分两类条件。
一类由二进制构建决定,包括函数和内部结构的位置。PHP 版本、发行版补丁、编译参数及 CPU 架构都可能影响这些位置。利用中的地址泄漏可以帮助确定运行时基址,但不能自动消除不同构建之间的差异。
另一类来自运行配置和实际执行过程。扩展、session handler、内存上限、前端请求处理方式及分配器布局,都可能改变利用条件。前述 PHP 7.4 在 16M、32M 下使用不同填充方案的记录,就说明即使没有更换二进制,利用参数也可能需要调整。PHP 公告同样将构建、扩展、前端行为和分配器布局列为影响可移植性与可靠性的因素。[2]
据此可以判断,批量采用相同镜像或软件包的部署,可能共享一部分利用适配成果,攻击成本未必需要按站点重复支付。但从版本号或响应头识别出 PHP,只能提供环境线索,不能证明目标使用了某一确定构建,更不能直接推导出利用成功率。
风险排查应把相同镜像、软件包和配置的资产放在一起核查。公开利用已经覆盖的组合,应优先处置;尚无适配记录的受影响构建,也应按漏洞公告完成修复。
五、修复应落实到实际处理请求的 PHP
PHP 已发布的修复版本如下,同分支可使用这些版本或包含相同修复的后续版本、供应商安全更新。[1]
| 分支 | 首个修复版本 |
|---|---|
| PHP 8.5 | 8.5.12 |
| PHP 8.4 | 8.4.27 |
| PHP 8.3 | 8.3.36 |
| PHP 8.2 | 8.2.35 |
PHP 安全团队另为 7.4、8.0、8.1 提供了最小源码回移植,供自行构建或缺少 LTS 供应商支持的用户使用。使用旧分支的系统,需要核查实际安装的补丁包是否包含完整修复;缺少受支持补丁来源的系统,应安排迁移,并在过渡期间限制暴露范围。[1]
本次修复同时处理了两方面问题:OOM 的退出方式,以及跨请求状态的重置。前者引入安全点上的延迟退出和更高阈值下的直接进程退出,后者清理可能在下一请求早期被访问的失效状态。[2][3]
实施升级时,需要找到真正承接 HTTP 请求的 PHP。终端中的 php -v 只能说明对应 CLI 的版本,面板、容器、FPM 池和 Apache 模块可能使用不同安装。验收记录应包括实际 SAPI、构建或供应商补丁版本,以及加载的配置。
替换软件包后,还要确认旧进程已经退出。mod\_php 需要完成 Apache 进程更新,FPM 需要完成池内 worker 更新;同套运行时下的队列及其他常驻进程也应纳入升级。使用平滑重载时,应核实旧 worker 已排空退出。
无法立即升级时的临时控制
临时措施需要对应到具体路径。PHP 官方明确指出,配置调整不能完整替代此次更新。[1][2][3]
| 措施 | 作用与适用范围 |
|---|---|
| 在 Web 服务器或网关层拒绝业务不需要的 multipart 请求 | 在到达 PHP 前缩小输入面;需要确认正常上传业务的依赖 |
关闭 session.upload_progress.enabled 并更新所有 worker |
阻断已确认的早期 session 产生、消费路径,不能覆盖其他全局状态 |
PHP 8.5 关闭 fatal_error_backtraces 并更新所有 worker |
移除已确认的 fatal backtrace producer,不能覆盖其他状态变体 |
| 在前端限制实际解码后的请求大小,并约束字段、part、header 数及嵌套深度 | 降低内存放大和资源耗尽风险;限制也要覆盖缺少可用 Content-Length 的请求 |
针对 CVE-2026-109618,官方还给出了由管理员强制设置 memory_limit=-1 的临时措施,以移除可预测的 PHP 请求内存上限触发条件。但底层分配失败仍可能进入脆弱路径,而且取消该上限后,需要另行评估进程或容器的资源边界。它不构成完整修复,也不能替代对 CVE-2026-109619 的处理。[1][2]
排查期间,可关联观察异常 multipart 请求、内存耗尽、重复 500、worker 退出,以及异常长度的 session 响应 Cookie。这些是调查线索;判断是否已经完成利用,还需要结合命令执行、异常文件、进程行为或其他证据。
参考资料
资料核对截至 2026 年 10 月 23 日。
- PHP 官方发布公告及修复说明
- PHP 安全公告:CVE-2026-109618 — RCE via OOM corruption
- PHP 安全公告:CVE-2026-109619 — RCE via stale global state
- Wiz Research:PHP2Shell
- 公开利用项目:README 与测试条件
- Marcus Hutchins 对早期 PHP2Shell 利用代码的评论(截图)
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论