-
-
[分享]PHP2Shell(CVE-2026-109618)UAF 路径分析与隔离环境复现记录
-
发表于: 1天前 102
-
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 ecfK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6K6P5i4u0W2P5o6p5H3x3e0y4Q4x3V1k6H3K9s2l9J5M7$3S2W2L8r3H3`.
cd php2shell/lab-uaf
docker compose up -d --build
确认环境处于漏洞状态:
curl -s 9a1K9s2c8@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 端口可达。
关于难度:左边极高,需要堆布局加版本适配;右边低。
关于公开实现:左边没有,右边有。
只有左边才是本次漏洞真正需要警惕的地方。右边那条在任何一个 PHP 版本上都能成立——只要 fpm 端口暴露在攻击者可达的位置。换句话说,我这次拿到的"战果",本质上是部署配置问题,而不是漏洞利用的证明。
这也是媒体标题"510 万台服务器面临风险"容易误导人的地方:那是暴露面统计,回答的是"有多少台在射程内",不是"有多少台可被一条命令打穿"。两者的处置成本差了一个数量级——后者几乎无解,前者大多只要改几行配置。
下面一段仅用于说明该链路为何还需要额外前提条件,目的在帮助防守方识别攻击面,不构成可直接使用的利用步骤。
0x05 踩坑记录
坑 1:wsl --install 卡在 60.1%
报错原文:
与服务器的连接被重置=====60.1%===
不是被手动打断,是到下载服务器的连接被重置。
解法:从 GitHub 下 MSI 手动安装,装前必须验签名:
$sig = Get-AuthenticodeSignature -FilePath "wsl-setup.msi"
$sig.Status # 必须 Valid
$sig.SignerCertificate.Subject # 必须 CN=Microsoft Corporation
坑 2:dism 返回 3010 被误判成失败
启用 Windows 功能时,"成功但需重启"的返回码是 3010,不是 0。用 if errorlevel 1 会把它当失败,但日志里明明写着"操作成功完成"。
正确写法:
set RC=%ERRORLEVEL%
if "%RC%"=="0" echo OK
if "%RC%"=="3010" echo OK - REBOOT REQUIRED
if not "%RC%"=="0" if not "%RC%"=="3010" echo FAILED
坑 3:ubuntu-base 没有 CA 证书,apt 全线崩溃
报错原文:
Certificate verification failed: The certificate is NOT trusted.
最小根文件系统不带 ca-certificates,典型的鸡生蛋问题。
解法:临时用 http 源装上证书,再切回 https。操作前先备份源文件:
cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.bak
sed -i 's|b41K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0K9i4u0J5L8%4u0K6i4K6u0W2N6s2g2F1j5g2)9J5k6i4c8K6K9h3&6Y4K9s2g2S2i4K6u0W2k6h3c8#2i4K6u0W2j5$3&6Q4y4@1y4Z5N6s2c8H3i4K6y4m8i4K6u0r3i4K6u0r3L8h3W2J5M7X3!0J5M7#2)9J5k6i4c8#2L8X3q4Q4x3X3g2@1M7$3W2F1k6$3S2#2j5g2)9J5k6h3g2V1N6g2)9J5k6h3y4F1i4K6N6o6k6#2)9J5y4W2)9J5x3K6x3&6i4K6y4n7 /etc/apt/sources.list.d/ubuntu.sources
apt-get update -y
apt-get install -y --no-install-recommends ca-certificates
update-ca-certificates
(装完把 .bak 换回去)
坑 4:FastCGI 链路成功率几乎为 0(查了两小时)
两个原因叠加,且现象完全一样,所以极难定位。
原因一:cleanup = On。上传完成后 PHP 立刻清空 session,而脚本在上传结束之后才等待触发,那时 session 早空了。现场:session 目录里堆了几千个 0 字节文件。
原因二:投毒键缺少 x 前缀。官方实现里有一行注释点明了原因:
# 'x' prefix keeps the variable name parser happy until the <?php tag
return f"x<?php echo '{OUTPUT_DELIM}_start';system('{command}');echo '{OUTPUT_DELIM}_end';?>"
少了这个 x,session 变量名解析失败,payload 不生效——但表现出来的现象和"被 cleanup 清空了"一模一样。
两个问题同时存在时,修掉一个仍然失败,很容易误判成"这条路走不通"。
解法方向:让触发发生在上传仍在进行、session 尚未被 cleanup 的时间窗口内,而不是等上传结束。
坑 5:每次验证后容器 Exited (255)
UAF 触发后 php-fpm 异常退出,entrypoint 检测到主进程结束,整个容器退出。
这不是环境故障,而是缺陷影响的直接体现。日志里有对应的物证:
PHP Warning: Cannot modify header information - headers already sent by
(output started at /var/lib/php/sessions/sess_5009bc5dd3de4ad4aff2b1f95f713a3e:1)
这行说明 session 文件第 1 行产生了输出,导致 header 无法发送,代码确实被执行了。对防守方而言,这类 warning 可以作为排查历史是否被利用的起点之一。
0x06 加固建议
永久修复:升级到 8.5.12+ / 8.4.27+ / 8.3.36+ / 8.2.35+。
免补丁缓解:
session.upload_progress.enabled = Off
副作用:上传进度条失效。这是唯一代价,但对绝大多数业务来说可以接受。
三行自查:
php -v
php -i | grep session.upload_progress
ss -lntp | grep -E "9000|9001|9002|9003"
第三行最关键:输出里若有 0.0.0.0:9000,立刻收敛成 127.0.0.1:9000 或 unix socket。这一条防的不只是本漏洞,而是所有"fpm 裸奔"类问题。
纵深防御:
一、nginx 保持 fastcgi_request_buffering on(默认值,别照抄靶场配置);
二、容器不要给 cap_add: SYS_PTRACE;
三、边界 WAF 拦截异常 multipart(超长 Content-Length、分片异常、请求体与文件混投);
四、fpm worker 不以 root 运行;
五、排查 session 目录是否存在异常大量的小文件或非序列化内容。
0x07 小结
"能触发"和"能利用"之间隔着一整个工程。
这句话对攻防双方意义不同。对攻击方,这条鸿沟是成本;对防守方,它是时间窗口——补丁没跟上的那几天里,只要把配置收干净(关掉用不上的特性、把管理端口收回内网),实际风险会被压得很低。
但反过来也成立:不要因为"利用门槛高"就降低优先级。门槛只影响性价比,不改变漏洞存在的事实;一旦有人把链路工程化,门槛会在一夜之间塌掉。
参考
官方公开验证仓库:383K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6K6P5i4u0W2P5o6p5H3x3e0y4Q4x3V1k6H3K9s2l9J5M7$3S2W2L8r3H3`.
完整复现记录(含环境脚本与逐条踩坑):ff1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6J5k6h3&6&6j5h3&6Y4i4K6u0V1j5%4u0W2j5i4c8W2i4K6u0r3M7r3S2H3x3Y4y4Z5k6h3I4D9i4K6u0V1M7X3g2H3M7X3)9`.
免责声明(重申)
本文仅供安全研究、漏洞验证与防御加固讨论。任何测试须确保目标系统为自有资产或已获书面授权,且测试环境与生产环境网络隔离。严禁用于未授权的渗透测试、系统入侵或数据获取。作者不提供任何形式的攻击工具、定制化利用服务或技术支持。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。