9.2 分,但没有通杀 PoC:PHP2Shell 的两个内存缺陷与真实门槛
原创
YNsec
YNsec
YNsec安全实验室
2026年10月22日 12:05
四川
去阅读
9.2 分,但没有通杀 PoC:PHP2Shell 的两个内存缺陷与真实门槛
10 月 21 日,多家安全厂商几乎同时发出通告,PHP 核心的两个远程代码执行漏洞被标注为【已复现】。
我们是在这批通告出来的当天看到消息的。灵隐·星戍网络安全实验室当天组织团队做了两件事:先按公开线索复现,同时对当晚官方更新前的最后一个版本 PHP 8.5.11 做全量代码审计。
复现的过程不算顺利,审计的结论也经过了几轮推翻。团队前后出了多版内部报告,最后收敛到一个判断上:这不是一个典型的通杀漏洞。
这个判断让我们做了一个不太\"划算\"的决定——没有在第一时间跟进发文。
说实话,流量和关注我们也要。但做安全研究这件事,抢在前面说错一句话,代价比晚发一天大得多。所以我们等到官方 10 月 21 日 14:00 UTC 的补丁落地,等到社区各家把技术分析陆续公开,把官方口径、厂商结论和我们的审计结果放在一起交叉比对,才有了这篇文章。
它是团队研究成果和社区公开成果的合集。文中凡属社区推断而非官方定论的内容,我们会逐处标出来。也在此感谢社区里每一位愿意把细节写清楚的创作者——这一次,公开分析的密度直接决定了防守方能不能在 24 小时内做出正确判断。
一、先把事实钉死
这件事最特殊的地方不在漏洞本身,在时间线。补丁公开的那一刻,漏洞点就等于公开了。
| 时间(UTC) | 事件 |
|---|---|
| 10-20 晨 | Hacker News 出现爆料帖(已删除,发帖者自认无证据),截图在 X、Mastodon 扩散,早期传言称评分为 10.0 |
| 10-20 19:33 | PHP 安全团队发布罕见的\"预告公告\",定档 10-21 14:00,并保留\"若细节进一步公开或出现利用迹象则继续提前\"的权利 |
| 10-20 | watchTowr 确认漏洞真实并复现 CVE-2026-109618;CERT-EU 发布公告 2026-018;研究者 Kirill Firsov 放出演示视频,攻击链被命名为 php2shell(署名 LCFR 与 Calif.io) |
| 10-21 07:43 | Cloudflare 观测到补丁发布之前的早期攻击 |
| 10-21 13:47 | Windows 构建已出现在 downloads.php.net |
| 10-21 14:00 | PHP 官方发布 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 |
| 10-21 14:04 | CISA KEV 目录更新,两个 CVE 均未收录 |
| 10-21 14:17 | NVD 为 CVE-2026-109618 建档,状态仍是 Undergoing Analysis |
| 10-21 当天 | watchTowr 蜜罐网络观测到利用尝试(未成功);Hadrian 称已观测到\"大规模\"在野利用;Patchstack 称收到托管侧的活跃利用报告但未公布证据 |
协调披露原定 10 月 22 日解除,因细节提前泄露,官方被迫抢跑一天。防守方的时间差不是\"天\",是\"分钟\"。
两个 CVE,两处必须正名
先说清楚两件被很多报道写错的事。
第一,php2shell 不是官方名称。 它是研究者在社交媒体上提出的社区代号,PHP 官方公告全程没有使用这个词。写文章时把它当\"事件代号\"用没问题,当\"CVE 别名\"用就错了。
第二,官方定性为两个独立根因,不是一条利用链。 社区流传的 PoC 里有\"用一个链把两个编号串起来\"的叙事,但官方公告把两者分开描述:
- CVE-2026-109618(GHSA-w8hw-w9fx-j79w)—— RCE via OOM corruption:内存耗尽的处理逻辑可能在 PHP 内部请求数据只完成部分初始化时中断它,随后的请求清理流程会把这段无效数据当作有效数据处理。
- CVE-2026-109619(GHSA-6wmf-4rv5-p3rp)—— RCE via stale global state:请求所拥有的内部状态可以在其底层内存已被释放之后继续存活,同一 worker 处理的后续请求可能在正常初始化将其重置之前用到这段残留状态。
两者都是内存安全缺陷(CWE-416,109619 另涉及异常条件处理不当 CWE-755),本质同一类:被释放的、或未正确初始化的内存,被后续流程继续使用。CVSS v4.0 均为 9.2。
影响与修复
| PHP 分支 | 受影响版本 | 首个修复版本 |
|---|---|---|
| 8.5 | < 8.5.12 | 8.5.12 |
| 8.4 | < 8.4.27 | 8.4.27 |
| 8.3 | < 8.3.36 | 8.3.36 |
| 8.2 | < 8.2.35 | 8.2.35(2026-12-31 EOL) |
| 8.1 及更早 | 全部版本 | 无官方发布版,仅 7.4 / 8.0 / 8.1 有最小化源码回溯补丁 |
影响区间从 PHP 5.4.0 起算,近十年发布的版本几乎全部涉及。官方明确:所有处理 Web 请求的 PHP 安装都在风险范围内,包括 PHP-FPM、Apache + mod\_php、php-cgi、FrankenPHP 和 LiteSpeed;其中 mod\_php 风险最高,因为已有一份在默认 input 与内存限制下、无需认证即可在应用代码执行之前触发攻击的验证。
二、机制:两个缺陷各自坏在哪
这一节讲的是\"为什么会坏\",不是\"怎么打\"。前者是必要的,后者我们放到最后解释为什么不写。
109618:OOM 处理里的提前退出
PHP 用 longjmp() 处理内存耗尽,目的是跳出当前上下文、中止本次请求。这套机制在正常情况下是有效的。
问题出在\"正常情况下\"这个前提。有些上下文里,请求内存在完全初始化之前就已经可达——也就是结构体指针已经挂上去了,但字段还没填完。如果恰好在这个窗口里发生分配失败,PHP 的退出路径没有充分考虑\"这次是提前退出的\",于是关闭流程读到的是一段半成品状态。半成品状态里如果有无效的指针或类型标记,后续的析构与分支就会走错。
官方的修法很能说明问题:移除 OOM 场景下的 longjmp(),改为在下一个 VM 安全点触发延迟的\"软 OOM\",并在达到更高阈值时用 _exit() 执行\"硬 OOM\"。这样关闭流程再也不会看到无效状态,同时保留了 memory_limit 正常的内存限制能力。
这是一个必须动到控制流的修复。改动面之大,也是这个洞在 2024 年那次审计里被看到、却没被修掉的原因之一。
109619:活得比内存还久的状态
第二个更隐蔽:请求级的内部状态,在它作为底层支撑的内存被释放之后,仍然存活。
触发点通常是致命错误。发生致命错误时 PHP 的清理路径并不总是完整,会留下指向已释放内存的全局指针。而 PHP 的 worker 是复用的——一个 php-fpm 子进程、或 mod\_php 下的 Apache 子进程,要连续处理成百上千个请求。于是后续某个请求在该指针被正常初始化重置之前,就可能用上这段已经不属于它的内存。
两个 CVE 的差异用一张表说更清楚:
| CVE-2026-109618 | CVE-2026-109619 | |
|---|---|---|
| 机制 | OOM 处理中的内存状态破坏 | 陈旧的全局状态 |
| 触发 | 请求启动阶段内存耗尽(如 multipart 解析) | 致命错误(含 OOM、执行超时)留下已发布但未清理的请求状态 |
| 所需请求数 | 1 个,无效状态在同一次请求的关闭阶段即被处理 | 2 个,前一个污染 worker,后一个消费残留 |
| 已验证代码执行 | mod\_php,默认 input 与内存限制 | mod\_php;PHP-FPM 经源码分析属可受影响,但未见完整利用 |
为什么大家把矛头指向 session.upload\_progress
截至成文,官方公告没有点名具体函数。以下属于社区分析,最终以官方技术文档为准。
多家机构的分析方向一致,指向 ext/session/session.c 里的 php_session_rfc1867_callback(),也就是 PHP 的会话上传进度功能。这个功能有三个叠加起来很危险的特点:
- 客户端在
multipart/form-data请求里带上PHP_SESSION_UPLOAD_PROGRESS字段,PHP 在解析 multipart 请求体的过程中(不是解析完之后)就开始往会话里写数据; - 这段逻辑发生在
RINIT之后、你的应用脚本执行之前——也就是说,业务代码还没跑,会话数据已经被改写过; - 它由
session.upload_progress.enabled控制,从 PHP 5.4 起默认开启。
第 3 点是整件事最讽刺的地方:这个功能是给前端做上传进度条用的。绝大多数站点没主动开它,也没人主动关它,它就这么在默认配置里待了十年。
而这个功能本质上是一个流式上传与会话生命周期之间的持续竞态:上传在流式推进,会话在同时被更新、销毁、重建、GC。UAF 最喜欢长在这种地方。
Wiz 的技术分析里有一个判断值得单独拎出来:这两个 CVE 归组了 PHP 中的四个内存安全问题,但并未覆盖全部变体——已被追踪的变体和\"可能存在但尚未被追踪\"的变体同时存在。这意味着 8.5.12 这一批补丁不能理解为\"这个方向已经彻底干净了\"。
三、为什么它和\"又一个 CMS 插件洞\"不是一个量级
我们见过太多高危漏洞,绝大多数的影响面可以画出一个边界:装了某个插件、某个版本、开了某个功能。这次没有边界。
第一层:它在运行时,穿透其上所有应用。 Sansec 公布的验证清单里,以下应用的原生安装均可被未认证 RCE 打穿:Magento 2.4.9、WordPress 7.1.2、Drupal 11.4.3、Joomla 6.1.4、MediaWiki 1.46.2、Roundcube 1.7.4、phpMyAdmin 5.2.3、Nextcloud 35.0.1、Laravel 13.11、Symfony 8.1、WHMCS。升级 CMS、换框架、打插件补丁,对这件事统统无效。
第二层:它发生在应用代码执行之前。 这是本次最要命的一点。框架中间件、CMS 的审计插件、应用层的 WAF 规则、登录校验、CSRF 防护——全部\"来不及\"。哪怕站点上只有一个内容为 hello world 的 PHP 文件,只要它能被访问,就在射程内。
第三层:暴露面数据不好看。
- PHP 驱动了已知服务端语言网站中约 70% 的站点(W3Techs)
- 公开测绘显示,约 88.4 万台对外主机报告 PHP 8.1 或更早版本(Shodan,10-20)——这批机器拿不到官方补丁
- 另有约 74 万台只报告 “PHP”、不标版本
- Palo Alto Networks 的遥测给出的暴露面约 2260 万实例
- 而且大部分服务器关闭了
X-Powered-By,每五台里约四台隐藏了精确版本,主动扫描的可见性远低于直觉
需要说明口径:以上是 banner 计数,不是已确认失陷的系统数量。
结论一句话:插件漏洞考验的是开发者的响应速度,运行时漏洞考验的是整个组织的资产可见性。 \"更新 PHP\"在真实环境里不是一个动作,而是一张\"分支 × 站点 handler × 容器镜像\"的矩阵。
四、9.2 分与真实门槛的落差
这是全文我们最想讲清楚的一节,也是我们当初决定不抢发的直接原因。
一侧的证据:它确实很难稳定打穿
VulnCheck 复现了两个漏洞,结论很具体:
- 在 ARM64 上可以实现代码执行
- 在 x86 上,关闭 ASLR 时可以代码执行
- 公开流传的 PoC 有数十个,但能用的极少,没有一个能在 x86-64 上绕过 ASLR
Red Hat 的评估同样是:在默认的 PHP-FPM 部署下,RCE 实际很难。
另一侧的证据:它确实已经在被打了
- 泄露的利用工具 PHP2Shell 横幅宣称\"适用于 5.4.0 起的每一个 PHP 版本\"
- Cloudflare 在 10-21 07:43,也就是补丁发布前近六小时,就观测到早期攻击,并已为全部客户(含免费用户)部署针对已知利用向量的 WAF 规则
- Hadrian 在补丁发布前数小时表示\"已观测到 php2shell 在大规模被利用,多数攻击滥用 session upload-progress 路径\",并预计补丁发布前后会出现更多利用路径
- Sansec 的预判更直接:补丁公开后 15 分钟内就会出现大规模利用尝试——因为拿到补丁只需对比新旧代码就能定位漏洞点
- watchTowr 的 Jake Knott 说得最克制也最准确:“目前还没有人成功,但我们不认为这能维持多久。”
两边都对,为什么
因为内存破坏类漏洞的\"可利用性\"从来不是一个布尔值,而是一组条件的函数:
- 构建参数:编译选项、是否开启
ZEND_DEBUG、分配器实现 - 进程内的环境:已加载的扩展集合、扩展的加载顺序,都会改变堆布局
- worker 历史:109619 依赖前一个请求留下的残留,攻击者能控制的东西比想象中少
- 配置:
memory_limit、post_max_size决定了 OOM 能不能被精确制造 - 地址空间随机化:x86-64 上的 ASLR 正是 VulnCheck 说\"没有一个 PoC 能绕过\"的那道墙
也就是说,所谓\"通杀 PoC\"在这种漏洞上几乎不存在。PoC 的通用性被内存布局的确定性死死绑住——你为 Ubuntu 24.04 的 php8.3 标准包算出的偏移,换到 Debian 或 Alpine 上大概率就是一堆垃圾数据、直接段错误。这也是我们审计后得出\"不是通杀漏洞\"的落点:它在理论上限上极其严重,在现实可达性上高度依赖环境。
但\"难\"绝不等于\"可以等\"
这个推论必须打断,理由有三个:
一,攻击者的成本和防守方的成本不对称。 攻击者只需要命中一个高价值构建——比如某个主流发行版的标准包、某个托管面板的默认环境、某个云厂商的镜像。一旦有人针对这个构建完成了利用,打击面立刻从\"一个偏移\"变成\"一整批同构实例\"。而防守方需要在所有实例上都不出错,一次失手就是全部。
二,攻击者可以无限试错,你不能。 网上那数十个 PoC 里能用的极少,但攻击者可以挂着扫描器一直试,试到某个实例崩了为止。而崩溃这件事本身就是信号——我们后面会讲,502 突增可能就是有人在试。
三,这个洞还有一条不需要完整 RCE 的收益路径:读进程内存。 官方与厂商分析都提到,这两个缺陷可被用于泄露敏感的进程内存,里面可能有数据库凭据、API Key、会话密钥。偷凭据不需要绕过 ASLR 那么完整的链条,只要能把内存读出来。
所以 9.2 分不是虚高。CVSS 衡量的是\"未认证 + 网络可达 + 默认配置 + 影响全部版本 + 可读进程内存\"这一组潜在影响的上限,不是\"今天的 PoC 成熟度\"。把评分当成宣传、把\"难\"当成缓冲,是这次最贵的误判。
五、SAPI 差异:为什么官方点名 mod\_php
同一份代码,跑在不同的 Server API 上,风险不一样。
| 部署形态 | 特征 | 官方风险判断 |
|---|---|---|
| Apache + mod\_php | PHP 作为模块嵌入 httpd 进程,请求在进程内直接处理,进程长期复用 | 最高,已有默认配置下的未认证利用验证 |
| PHP-FPM | 独立 master/worker 进程池,worker 复用 | 受影响;109619 经源码分析可受影响,未见完整利用 |
| php-cgi | CGI 模式,每请求一进程 | 受影响 |
| FrankenPHP | Go 编写的现代应用服务器,内嵌 PHP | 受影响 |
| LiteSpeed | LSAPI | 受影响 |
mod\_php 排在第一,原因不神秘:它的进程模型最\"省\",一个 httpd 子进程要连续处理大量请求,109619 那种\"前一个请求污染、后一个请求消费\"的模式在进程内最容易稳定复现;而模块化嵌入也意味着没有 FPM 那层额外的清理与隔离路径。
一个必须提醒的实操问题:php -v 看到的是 CLI 版本,不等于承载 Web 请求的那个二进制。 CLI 与 FPM、mod\_php 常常是两个二进制、两份 php.ini。最可靠的做法是临时放一个页面输出 PHP_VERSION、PHP_SAPI、php_ini_loaded_file(),看完立刻删掉。
六、检测:官方没给 IoC,只能自己造
官方没有发布任何 IoC。这类\"在应用代码之前完成的攻击\"本来也不容易留痕,所以检测要往进程侧和崩溃侧去靠。
进程侧(高置信):PHP 的工作进程派生出了它不该派生的东西。
- 重点关注
php-fpm、php-cgi、httpd、apache2这几个父进程 - 异常子进程:
sh、bash、cmd.exe、powershell.exe、python、perl、curl、wget、nc、certutil.exe - 命令行特征:
/dev/tcp/、mkfifo、fsockopen - 还有一类容易被忽略的:直接由 Web 服务账号执行的侦察命令,
whoami、id、uname -a、ipconfig
一条快速排查命令:
pstree -p | grep -A2 php-fpm
如果一个 worker 下面挂的不是 socket,就得细看。
Huntress 记录过一个很典型的模式,虽然不是本次漏洞,但可以作为\"PHP 派生了不该派生的事\"的参考样本:一个 PHP FastCGI 进程反复执行一行命令,把带随机标记字符串的小 PHP 文件写入它能找到的第一个可写临时目录,执行后删除;如果所有目录都不可写,它会打印 !notwrtbl! 来标记失败。
崩溃侧(高置信代理指标):这次的漏洞本质是内存破坏,利用尝试极易导致 worker 崩溃。
- PHP 日志里 OOM、fatal error、异常段错误突然增多
- php-fpm worker 反复重启
- 反向代理(nginx)侧的 502 突增——在你不发布、不重启的时间窗里出现,值得当成主动探测来对待
回溯排查:Web 根目录下新增的混淆单文件、新增的定时任务、异常外联、未知的管理员账号。补丁完成之后建议至少回溯一段时间,把\"打补丁\"和\"确认没被打\"当成两件事做。
如果怀疑已经失陷:先隔离、先保全日志,再谈清理。顺序反了,证据就没了。
七、处置:补丁矩阵与缓解的真实边界
第一优先,升级。 受支持分支升到 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35,升级后必须重启 PHP-FPM 或 Web 服务器,确保新解释器真的被加载。不改进程只改文件,等于没改。
8.1 及更早的现实是:没有官方发布版。PHP 安全团队提供了 7.4 / 8.0 / 8.1 的最小化源码回溯补丁,面向自行编译或没有 LTS 供应商的场景;其余情况要么靠发行版回移,要么迁移。PHP 8.2 将于 2026-12-31 结束支持,升到 8.2 只是过渡,不是终点。
如果你在 RHEL 系上依赖 Remi 仓库(rpms.remirepo.net):发布当天该仓库没有更新,官方明确建议切换到发行版软件包或从源码编译。
缓解措施(官方原话:没有任何配置是完整缓解,更新是唯一解)
针对 109618,思路是让 OOM 不要在错误的时机发生:
memory_limit=-1,通过php_admin_value(FPM pool 或 Apache 配置)下发,防止应用层覆盖;下发后要确认没有 pool、目录级配置或应用代码再把有限值设回去- 下调
post_max_size:无上传业务设 8M,有上传设 32M,并保持upload_max_filesize < post_max_size - 在 Web 服务器或代理层限制解码后的请求体大小、输入字段数、multipart part 数、header 数与嵌套深度
- 不要用
RLIMIT_AS/RLIMIT_DATA来限内存——它们可能恰好触发那个有问题的分配失败路径
针对 109619,思路是掐掉最受关注的触发面:
session.upload_progress.enabled=0,必须在php.ini、FPM pool 或服务器级配置里生效,并重启全部 worker;不要依赖ini_set()或 FPM 的.user.ini- PHP 8.5 上再设
fatal_error_backtraces=0 - 清理那些会在初始化之前就产生诊断输出的、已废弃的 per-request 设置。注意:用
error_reporting、display_errors或@把诊断藏起来没用,藏起来不等于不产生
三个最容易踩的自查坑
- CLI 不等于 Web。
php -v与 FPM/mod\_php 实际加载的版本可能完全不同,两份php.ini也是独立的。 - 发行版版本号不等于上游版本号。 例如 Ubuntu 24.04 的修复包版本是
php8.3 8.3.6-0ubuntu0.24.04.13,页面上的PHP_VERSION仍显示8.3.6——照 8.3.36 去比对,会把已修复的系统误判成未修复。要查包版本和 changelog 里的 CVE 记录。 - 面板与容器。 宝塔等面板显示的版本未必是真正在跑的版本;改完必须重启 PHP 服务,只重启站点无效。Docker 部署要
docker build --pull重建,本地缓存的基础镜像不带补丁。
八、为什么不放 PoC
这篇文章里没有 PoC,也不会给。原因不是什么\"安全研究者的操守\"之类的漂亮话,而是一个技术判断:
这个漏洞本质上没有稳定的通杀 PoC。
它坏在内存侧。能不能打通、打到什么程度,取决于目标机器的内存分布——构建参数、已加载扩展、worker 的历史状态、地址空间随机化、内存限制配置,全都是变量。同样一份利用代码,在 A 机器上可能一次成功,在 B 机器上就是段错误。VulnCheck 那句\"没有一个公开 PoC 能在 x86-64 上绕过 ASLR\",说的正是这件事。
于是所有声称\"一键通杀\"的工具,只有两种可能:要么它是针对某个特定构建(某个发行版的标准包、某个面板的默认环境)反复调出来的,要么它就是在赌。
把这种东西放出去,赌赢的是攻击者,赔的是还在打补丁的人。现在这个时间点尤其不合适——补丁才落地一天,还有大量系统在排队。
如果你手上还有没升级的 PHP,先去升级。文章回来看。
写在最后:两件值得沉淀的事
这次事件里,技术上最值得记住的不是那两个 CVE 编号,是它们的\"年龄\"。
那个 OOM 缺陷,2020 年就有人发现过,2024 年 OSTIF 与 Quarkslab 的审计里再次出现——但因为当时没人能证明它可利用,修复就搁置了。直到今年 8 月,在 OpenAI\"Patch the Planet\"计划下,Calif 与 OpenAI(8 月 11 日)独立上报了完整根因,Wiz(8 月 24 日)补充了更多攻击向量,整件事才被真正推动。PHP Foundation 自己把这段延误称为安全流程的\"结构性失败\"。Wiz 的评价更直接:这些缺陷存在了几十年,证明其可利用性需要异常复杂的链条,已经贴近人类研究者能做到的边缘。
“未证明可利用\"从来不等于\"安全”。 这是漏洞治理里最脆弱的一环,而这一次,补上这一环的是 AI 辅助审计。这个信号的意义可能比漏洞本身更大。
第二件事更朴素:资产台账必须下沉到运行时层。多数企业的 CMDB 里有应用、有中间件、有主机,但很少有\"这台机器跑的是 PHP 7.4.33\"这一行。没有这一行,任何应急响应都只能全网救火。这次是 PHP,下一次可能是别的运行时——问题不会消失,只会换一个名字再来。
关于本文的数据与口径
- 官方事实(CVE 定义、影响与修复版本、SAPI 范围、缓解措施)取自
php.net 2026-10-21 安全公告与四条 Release Note,以及两个 GitHub Advisory 编号
* 根因涉及 session.upload_progress / php_session_rfc1867_callback 的部分属社区分析方向(watchTowr、Sansec、GMO Flatt Security、Wiz 等),官方公告未点名具体函数
* 可利用性结论分别来自 VulnCheck(复现)与 Red Hat(评估),两者并不矛盾,讨论见第四节
* 在野利用程度各方表态存在分歧,文中已逐条标注来源
* 暴露面数据为 banner 计数,非已确认失陷系统;CISA KEV 状态截至 2026-10-22 发稿
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论