首页
社区
课程
招聘
[分享]PHP2Shell(CVE-2026-109618)UAF 路径分析与隔离环境复现记录
发表于: 1天前 102

[分享]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内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
点赞 ・0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回