-
-
[分享]PHP2Shell(CVE-2026-109618)UAF 路径分析与隔离环境复现记录
-
发表于: 2天前 371
-
PHP2Shell(CVE-2026-109618)UAF 路径分析与隔离环境复现记录
【测试声明】
(1)本文全部验证均在本地隔离环境完成(127.0.0.1,Docker 单机靶场,与生产网络物理隔离),未对任何第三方系统发起请求;
(2)文中出现的脚本调用均指向官方公开仓库中的验证脚本,仅用于确认脆弱代码路径可达;
(3)本文不提供、也不讨论可直接武器化的利用工具;涉及链路细节的部分,目的是帮助防守方理解攻击面与前置条件;
(4)任何对非自有资产的测试行为均需获得书面授权,否则涉嫌违法。读者因不当使用产生的后果自行承担。
0x00 背景与结论
10 月 21 日 PHP 披露两个无需认证的 RCE 漏洞,代号 PHP2Shell,CVSS 4.0 评分 9.2。
我在 16G 内存的轻薄本上搭了隔离靶场做验证,结论和传播中的标题有出入:
一、UAF 原语很容易验证,3 轮全部命中;
二、官方 PoC 本身没有实现代码执行(README 的 Limitations 里写得很清楚);
三、我最终拿到的 uid=33(www-data),走的是另一条完全独立的路径,与 UAF 无关。
所以这篇文章的重点不是"打通了",而是把这两条路径拆开讲清楚:哪一条是漏洞本身,哪一条是配置问题。
0x01 漏洞成因:指针活得比容器久
漏洞编号:CVE-2026-109618 / CVE-2026-109619
漏洞类型:堆释放后使用(Use-After-Free)
评分:CVSS 4.0 = 9.2
影响版本:PHP 5.4.0 – 8.2.34 / 8.3.35 / 8.4.26 / 8.5.11
修复版本:8.2.35 / 8.3.36 / 8.4.27 / 8.5.12
根因在 ext/session/session.c,与 multipart 上传进度特性相关。
该特性会把上传进度数据缓存进 session 的 HashTable,供进度查询使用。问题在于缓存进去的是 zval* 裸指针:
session.c:3292 → post_bytes_processed
session.c:3318 → current_file_bytes_processed
而进度更新存在节流:两次 FILE_DATA 事件间隔超过 min_freq(默认 1 秒)时,第二次事件会重新初始化 session:
php_session_rfc1867_update()
→ php_session_initialize()
→ php_session_track_init() ← 旧 HashTable 在此释放
旧表释放了,缓存的裸指针却仍然指向那块内存。随后 session.c:3331-3332 通过失效指针写入字节数,构成一次 UAF 写。
这类缺陷的隐蔽性来自它的"安静":不崩溃、不报错、请求返回 200。它写坏的东西在应用层看起来一切正常,因此既难以被发现,也难以用"服务还活着"来反证安全。
0x02 复现环境
宿主系统:Windows 11,版本 10.0.26200
处理器:Ryzen 7 8745HS,8C/16T
内存:16 GB
Linux 层:WSL2,版本 3.0.3.0
发行版:Ubuntu 24.04.3 LTS
容器:Docker 29.9.2
靶场组件:nginx 1.22.1 + php-fpm 8.4.4-fpm
轻薄本完全够用,单容器运行时占用 200-300 MB,CPU 基本空转。
0x03 UAF 路径验证
使用官方公开仓库中的隔离靶场:
git clone --depth 1 b4eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6K6P5i4u0W2P5o6p5H3x3e0y4Q4x3V1k6H3K9s2l9J5M7$3S2W2L8r3H3`.
cd php2shell/lab-uaf
docker compose up -d --build
确认环境处于漏洞状态:
curl -s 571K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5J5y4#2)9J5k6e0m8Q4x3X3f1H3i4K6u0W2x3g2)9K6b7e0p5&6x3U0j5^5i4K6u0r3
返回内容:
PHP 8.4.4 (fpm-fcgi)
session.upload_progress.enabled = 1
session.upload_progress.min_freq = 1
session.upload_progress.name = PHP_SESSION_UPLOAD_PROGRESS
运行官方验证脚本:
python3 scripts/php2shell_poc.py --monitor --verbose --rounds 5
输出:
── Round 1/5 ──
[>] Phase 1 sent: 5591 bytes
[>] Phase 2 sent: 10267 bytes
SID: fb719e2894ed430b9ff05fabb60dc294
Status: 200 OK
Armed: YES — bytes_processed = 5331
► Cached zval* pointers were written through (stale path reached)
汇总:
Rounds: 5
Pointers armed: 5/5
Worker crashes: 0/5
注意最后一行。0 崩溃不代表漏洞不存在,恰恰相反——静默写入才是有意义的信号:写入落在了可控位置,只是那块内存尚未被重新分配。一旦堆布局对上,同一个动作就会从"静默"变成"可控"。
0x04 为什么我最后拿到了 shell:两条路径的辨析
官方 README 在 Limitations 中明确写着:
"This PoC proves the vulnerable code path is reachable. It does not achieve a crash or code execution."
也就是说,官方 PoC 只证明代码路径可达,没有实现代码执行。
要把 UAF 变成 RCE,还需要堆喷精确回收 ZendMM 的 320 字节 slab bin,并适配具体构建版本的符号偏移。这属于另一个量级的工程工作,其中涉及的时序与布局问题,公开资料里并没有完整实现。
那我拿到的 uid=33(www-data) 从哪来?走的是另一条与 UAF 无关的路径:
session 投毒(把 PHP 代码写进 session 存储文件)
↓
php-fpm 9000 端口从外部可达
↓
FastCGI 直连,注入 PHP_VALUE=auto_prepend_file=<被投毒的 session 文件>
↓
index.php 执行前 include 该文件,导致代码执行
两条路径的差异如下。
关于是否依赖本次漏洞:左边(UAF 到 RCE)依赖,右边(session 投毒 + FastCGI)不依赖。
关于额外前提:左边没有额外前提,右边需要 fpm 端口可达。
关于难度:左边极高,需要堆布局加版本适配;右边低。
[招生]科锐逆向工程师培训(2026年11月13日实地,远程教学同时开班, 第57期)