PHP2Shell:PHP 引擎级双 UAF,从 5.4 潜伏到 8.5
原创
APT-101
APT-101
APT-101
2026年10月22日 18:39
陕西
去阅读
漏洞分析#PHP安全#CVE#RCE#安全研究 #PHP2SHELL
01 平地惊雷:洞在 PHP 引擎里,5.4 就埋下了
PHP2SHELL 这个名字点得很直白:PHP 到 shell。它不是哪个框架的锅,是 PHP 引擎自己的两个漏洞,编号 CVE-2026-109618 和 CVE-2026-109619,横跨从 PHP 5.4.0 到 8.5.11 的所有版本,修复落在 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 四条维护分支上。
两个数字先记一下:受害面从 5.4.0 一直到 8.5.11,修复只落在 8.2 系往上。十来个大版本跨度,说明这缺陷在引擎里潜伏了很久,跟大多数跑在 web 上的 PHP 站都有关系。麻烦的是 5.x/7.x/8.0/8.1 早就停止维护,永远等不到补丁,老站只能裸奔。
严重在哪:PHP 的主流部署方式是 nginx/Apache + PHP-FPM,发行版、官方 php:fpm 镜像、宝塔/1Panel/Plesk 这类面板基本都是它。mod\_php 的存量主要在共享主机和老 LAMP。官方实测下来,mod\_php 上两个洞的验证最充分,FPM 上 619 已经跑通完整 RCE。
02 揭开盖子:一次 OOM,把请求生命周期拦腰截断
两个洞的根子是同一条路径。PHP 处理一次 HTTP 请求时,可能在应用代码还没执行、光解析请求体(multipart body)的阶段就内存耗尽(OOM),引擎会走 zend_bailout() 做 longjmp 中断。问题在于:longjmp 会跳过中断点之后还没做完的初始化,扩展的请求清理(RSHUTDOWN)也会被跳过。
正常流程里,请求结束应该先重置各类状态,再释放请求内存。OOM 一打断,顺序就乱了,于是出现两类独立缺陷:
CVE-2026-109618,同请求内:longjmp 后请求内存可能压根没初始化完成,request shutdown 阶段却把这份无效状态当成有效对象去访问。这是\"一次请求从启动到 shutdown\"内部的状态错乱,完全不依赖应用代码——你在应用层写多好的代码都管不了它。
CVE-2026-109619,跨请求:一次请求发生 fatal 或 OOM 后,worker 进程里存着指向请求堆的全局状态(session、错误回溯这类),本该先重置再释放堆。RSHUTDOWN 被跳过,重置没发生,堆却照常释放了,指针悬空。同一 worker 处理下一个请求时,在正常初始化之前就可能访问到这些过期指针——Use-After-Free。FPM 的 worker 常驻进程池天然满足\"同一 worker 接下一请求\"这个前提。
图1 一次 OOM 打断请求生命周期:跳过未完成的初始化和 RSHUTDOWN,618 同请求内错乱、619 下一请求踩悬空指针
03 618 的钥匙:upload\_progress + LFI,三把锁都开才进得去
618 的实际利用面,挂在 PHP 一个老功能上:session.upload\_progress(上传进度)。PHP ≥ 5.4 默认开启。它想解决的是\"大文件上传时给个进度条\":浏览器在 multipart 里塞一个魔术字段 PHP_SESSION_UPLOAD_PROGRESS,PHP 就把上传进度写进 session。
攻击者手里其实握着三个可控点:
- PHPSESSID 可控——cookie 里的值自己定,session 文件名就是
sess_<PHPSESSID>; - multipart 里的 filename 可控——它会被原样写进 session 文件;
- 落盘位置已知——
session.save_path默认/tmp。
组合起来:发一个带特制 PHPSESSID 和 PHP_SESSION_UPLOAD_PROGRESS 的 multipart 请求,PHP 就把塞了攻击载荷(filename 字段)的 session 文件写进 /tmp/sess_<自己编的id>。
但这只是\"能写一个内容可控的文件\",离执行代码还差一步。链路能不能走通,看三把锁:
session.upload_progress.enabled = On(默认开,多数配置直接满足);session.upload_progress.cleanup = Off或存在竞态窗口(cleanup 默认是 On,请求完就把文件删了,要赶在删除前包含它);- 目标存在 LFI,能 include
/tmp/sess_<PHPSESSID>。
缺任何一把,618 这条链在目标上就断。尤其 LFI 是硬前提,没有 LFI,这个洞对你来说只是\"丢一个 session 文件\"。
图2 618 三把锁:enabled=On、cleanup=Off 或竞态、有 LFI 包含 sess 文件,缺一把链就断——LFI 是硬前提
04 619 的另一面:不用文件系统,unserialize 也能 RCE
619 走的是另一条路,干净得多。unserialize 的释放路径里有个 var_destroy() UAF:攻击者控制一个序列化对象,就能在对象释放阶段触发任意 destructor(__destruct 类魔术方法),配合 POP(Property Oriented Programming)链直接构成 RCE。整个过程不需要任何文件系统交互——不落盘、不需要 LFI。
它要的两个前提:目标存在可控的反序列化入口(入参来自 $_POST/$_COOKIE/外部数据源,进 unserialize()),以及应用里有可用的 POP gadget(框架自带的魔术方法类通常是最现成的,Laravel、PHPUnit 这些组件都有过经典链)。
版本探测也省事:X-Powered-By: PHP/x.y.z 响应头会直接泄漏 PHP 版本,落在受影响区间内,目标就进了潜在名单。
口径说明:公开材料对 619 的机制描述有两种说法——一说跨请求全局状态(session、错误回溯)指针悬空,一说 unserialize 路径的 var\_destroy UAF。这两条对着同一类失效现象,但官方还没给正式定稿,本文都摆出来,以 CVE 详情为准。
图3 619:控制序列化对象 → unserialize 释放 → var\_destroy UAF → destructor gadget → POP 链,全程不碰文件系统
05 谁中招了:一张矩阵对号入座
| CVE | 机制 | 受影响版本 | 修复版本 |
|---|---|---|---|
| 109618 | session.upload\_progress UAF / OOM | 5.4.0 – 8.5.11 | 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 |
| 109619 | var\_destroy UAF via unserialize | 5.4.0 – 8.5.11 | 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 |
部署方式 × 验证效果(官方口径):
| 部署方式 | 618 | 619 |
|---|---|---|
| mod\_php | CRASH PoC(首版口径为完整 RCE) | 完整 RCE 已验证 |
| PHP-FPM | 源码分析认为受影响,利用链分析中 | 完整 RCE 已验证 |
判定就一句:版本在 [5.4.0, 8.5.11] 区间、且低于对应分支修复号的,潜在受影响;8.2.35+ / 8.3.36+ / 8.4.27+ / 8.5.12+ 已修复;低于 8.2 的老分支没有补丁,一律视为受影响。
图4 版本对号入座:5.4.0–8.5.11 受影响,修复线 8.2.35+ / 8.3.36+ / 8.4.27+ / 8.5.12+,EOL 旧分支无补丁视为受影响
06 白盒自查:六条线,分清\"受影响\"和\"可利用\"
先分清楚两件事:中不中看引擎版本,能不能打成 RCE 看配置和应用把前置凑齐没。自查动作照下面的线走:
① 版本与补丁状态(权威判据):
php -vphp -r 'echo PHP_VERSION;' # 运行时常量,防伪装dpkg -l | grep -i php # 包管理部署apt-get changelog php8.2 | grep -iE \"109618|109619\" # 包是否已 backport 修复git -C /path/php-src log --oneline | grep -iE \"109618|109619|var_destroy\" # 源码编译看补丁
注意以 Web SAPI 实际加载的引擎为准,CLI 和 php-fpm 版本可能不一样。
② SAPI 形态(决定哪个 CVE 适用面):
php -r 'echo php_sapi_name();' # apache2handler / fpm-fcgi / cliapache2ctl -M | grep -i php # mod_php 是否加载
③ 618 配置三件套:
php --ini && php -i | grep -iE \"upload_progress|save_path\"
enabled=On 是前提(默认);cleanup=On 需要打竞态窗口、Off 条件最优;save_path=/tmp 是 LFI 的包含目标。用 Laravel 这类框架时注意 session 可能被框架接管,upload\_progress 是否仍生效要实测。
④ LFI 面(618 的另一半):
grep -rnE \"include|require|file_get_contents\" --include=\"*.php\" /app | grep -v vendor/
找\"include/require + 用户可控变量\"且没有规范化/白名单的点。没有 LFI,618 链在目标上就是断的。
⑤ 619 入口:
grep -rn \"unserialize(\" --include=\"*.php\" /app | grep -v vendor/
入参来自外部数据源的调用才有意义,同时盘一下框架里有没有现成 gadget。
⑥ 合流判定:
版本命中 [5.4.0, 8.5.11] ? ├─ 否 → 不受影响 └─ 是 → 618:enabled=On 且 (cleanup=Off 或竞态) 且 有 LFI 619:存在可控 unserialize 入口 且 有 gadget 缺任一前置 → 该链当前配置下不可利用(引擎仍要升级)
补充一点:UAF 类的 PoC 验证在测试集群上做,别在生产打——619 崩的是 worker,可能连累一片请求。
07 收束:升级四条分支,老版本别指望救
修复动作其实很整齐:
- 升级:8.2.35+ / 8.3.36+ / 8.4.27+ / 8.5.12+(四条分支已全部出包);
- 应用层兜底(升不了级时):618 锁 LFI(路径规范化/白名单)、
session.upload_progress.enabled可整关;619 收敛unserialize()入口、禁危险魔术方法类; - 现实是:5.x/7.x/8.0/8.1 早已 EOL,官方不会出补丁,停更老站等于把这两个洞常驻在自己系统里——该迁移迁移,该隔离隔离。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论