本文转载自微信公众号「网络安全透视镜」,作者:网络安全透视镜
原文标题:全网都在等的PHP2Shell 远程命令执行漏洞复现,漏洞通用,POC不通用! ! !
内容版权归原作者及原公众号所有,转载仅为传播资讯;若有侵权请联系管理员删除。
PHP2Shell 是 2026-10-21 紧急披露的两个 PHP 内核远程代码执行漏洞,CVSS v4.0 均为 9.2,不需要任何凭据。本报告在 PHP 8.4.26 + Apache prefork 单 worker 上已真实打出代码执行,并给出了一条可以直接给审阅者用的判断结论。
本文看点
01
两个 CVE 同源,一口一个 bug
02
为什么没有通用 PoC
03
真实打出 uid=33 的完整链路
CVE-2026-109618 / CVE-2026-109619
文档性质
实验室漏洞分析与定点复现记录
编写日期
2026-10-22
分析对象
php-src 内核(session + multipart + Zend MM)
本地结论
补丁存在;在 PHP 8.4.26 NTS + Apache 2.4.68 prefork 单 worker 上 已打出 RCE(uid=33(www-data))
关键结论
漏洞通用,RCE exploit 不通用。 不存在「一个 HTTP 包打所有未修复 PHP」的 PoC。
CHAPTER
漏洞概述
PHP2Shell 是 2026-10-21 紧急披露的两个 PHP 内核远程代码执行漏洞,CVSS v4.0 均为 9.2(AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H)。CWE 以 CWE-416 Use After Free 为主,并涉及异常路径处理(CWE-755)、未初始化指针(CWE-824)。
两个 CVE 是同一类「bailout 后状态损坏」问题的两面:
CVE-2026-109618 — OOM 时 `longjmp` 撕开半初始化状态
PHP 用 memory_limit 限制单请求内存。触顶时走:
longjmp 从任意 C 栈帧直接跳回请求顶层,中间所有尚未跑完的构造函数、链表挂接、全局赋值都被跳过。若此时请求内存(executor globals、session 模块全局、multipart 解析器)只初始化了一半,随后 php_request_shutdown() 仍按「这是一次完整请求」去遍历、释放这些指针。
官方说明最高危 SAPI 是 Apache mod_php:startup 失败也照样 shutdown,且发生在任何应用代码之前。
补丁前关键代码(php-8.4.26 Zend/zend_alloc.c):
CVE-2026-109619 — 跨请求残留的进程级全局
php-fpm / mod_php worker 是长驻进程。一次请求可以把「属于本请求堆」的对象挂到进程全局:
、(session 模块)
PS(id)
PS(http_session_vars)
错误对象 / 8.5 的 fatal backtrace
扩展残留
请求以 OOM/longjmp/fatal 结束时如果没把这些全局清成 UNDEF/NULL,下一个 HTTP 请求会把悬空指针当活对象:zval_ptr_dtor(PS(http_session_vars)) → UAF。
已确认、且不需要应用配合的生产者/消费者:
session.upload_progress(PHP 5.4 起默认 On)。 multipart 解析回调运行在 RINIT 之前:
目标页面可以是 <?php echo “OK”; ?>,不必调用 session_start()。
两者如何拼成 RCE
流量全是普通 multipart/form-data POST,没有自定义协议,也没有「把 PHP 源码 POST 上去就会执行」这种错误模型。
CHAPTER
影响范围
|
分支
|
受影响
|
官方修复包
|
|---|---|---|
|
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
|
|
8.1 / 8.0 / 7.x / 5.4+
|
受影响(upload_progress 自 5.4 默认开启)
|
无官方发行版,仅有源码 backport
|
SAPI:
**mod_php + Apache prefork**:官方确认未认证 RCE,本实验室已复现。
**PHP-FPM**:源码面可利用(session 路径);官方未给出完整 FPM exploit。FPM 在 startup 失败时会杀掉 child,618 的「startup 失败仍 shutdown」这条在 FPM 上不成立,619 仍可能。
FrankenPHP / LSAPI:模型类似。
应用层(WordPress、Laravel、Drupal 等)只要解释器未打补丁即在范围内,与业务代码是否使用 session 无关。
CHAPTER
利用条件
分两层看:触发 UAF 的条件相对宽;把 UAF 打成稳定 RCE 的条件极窄。
触发层(相对通用)
PHP 未升级到上述修复版本。
session.upload_progress.enabled=On(默认;必须在 php.ini / php_admin_value 关,ini_set() 来不及)。
memory_limit 为有限值(默认 128M 也可,实验室为了稳定打 OOM 收到 8M)。
post_max_size 大于用于打 OOM 的字段(否则进不了 rfc1867)。
攻击者可对任意 .php 发 HTTP POST。
两次请求打到同一个 worker(prefork 单 worker 时为 100%;多 worker 只是降概率)。
满足以上即可观察到:
Fatal error: Allowed memory size … exhausted in Unknown on line 0
下一请求 session 状态异常(非法 PHPSESSID、读 session 失败)
或 / worker
zend_mm_heap corrupted
SIGABRT
这已经证明漏洞存在,但还不是 RCE。
RCE 层(本实验室打通时的全部约束)
|
条件
|
本实验室取值
|
若不满足
|
|---|---|---|
|
精确构建
|
php:8.4.26-apache-bookworm
|
堆喷尺寸全部作废
|
|
SAPI
|
mod_php + mpm_prefork
|
FPM 要另写一条链
|
|
Worker 数
|
1
|
无法稳定 A→B 同进程
|
|
display_errors
|
On
|
当前公开链的 Set-Cookie 泄漏依赖 headers_sent
|
|
max_input_vars
|
1000(默认)
|
超限 warning 的时机变了
|
|
memory_limit
|
8M
|
OOM 点漂移,对象落点变
|
|
session.use_strict_mode
|
Off
|
\x00 PHPSESSID 无法迫使换 id
|
|
libphp.so 偏移
|
zif_system=0x45a3b0 等
|
stage3 调到错误函数
|
|
堆常数
|
HTDATA=+0x690d8,filler=841…
|
重占失败,只会崩
|
|
用户态指针区间
|
本机内核映射在 0x79…
|
泄漏解析器会当「没漏到」
|
命令嵌入 167 字节 zend_string,有效命令 ≤ 14 字节。
CHAPTER
为什么该漏洞没有通用 PoC
这是本报告要单独说清楚的一点:CVE 编号是通用的,RCE 载荷不是。
「通用 PoC」在这里如果指「任意未打补丁 PHP,发固定 HTTP 包就能 system()」,则不存在。原因不是信息披露不全,而是利用原语本身决定的。
这是堆 UAF,不是「参数进了 `system()`」
对比典型 PHP 应用 RCE(反序列化 gadget、文件上传、eval($_GET)):
|
|
应用层 RCE
|
PHP2Shell RCE
|
|---|---|---|
|
稳定原语
|
字符串/对象图,语义确定
|
悬空指针 + 堆块尺寸
|
|
载荷
|
逻辑 payload,跨版本常还能用
|
fake HashTable 二进制,对 Zend MM bin 敏感
|
|
ASLR
|
通常不需要
|
必须先泄 chunk 基址和 libphp 基址
|
|
目标代码
|
某条业务逻辑
|
libphp.so 里 zif_system 的文件偏移
|
UAF 能转 RCE 的前提是:攻击者知道「释放块的尺寸、下一次分配会落到哪、伪造对象里每个字段的偏移」。这些对 PHP 小版本、编译选项、发行版、Apache/PHP 谁先分配堆 全部敏感。
本机对照实验:同一条公开 exp,换一层构建就失败
实验室用的是同一套 [9bie/php2shell-bypass-aslr-exp](https://github.com/9bie/php2shell-bypass-aslr-exp) 源码:
1
Hub 默认 php:8.4.26-apache(Debian 13 Trixie,Apache 2.4.69)
stage1 HEAP_LEAK_FAILED(5/5)
2
php:8.4.26-apache-bookworm(Debian 12,Apache 2.4.68)
cookie 已泄漏,但脚本只认 0x7f… 指针,仍判失败
3
同上,只把指针窗口放到 0x55…–0x80…(本机内核 6.8 堆在 0x79…)
第一次即 RCE
注意:1 和 2 的 PHP 都是 8.4.26。只换了 Debian 小版本 / Apache 小版本,libphp.so 里:
堆喷个数(841 / 406 / +0x690d8)是按 bookworm 镜像标定的,拿到 Trixie 上全部对不齐。 同一 PHP 版本号都做不到通用,更不可能覆盖 5.4~8.5。
必须按目标现场 leak 的量
完整链至少要在这个 worker 的这次地址空间里得到:
Zend MM chunk 基址(2MiB 对齐)。来源:伪造超长 zend_string,让 php_url_encode(PS(id)) 越界读,从 Set-Cookie: PHPSESSID= 里捞指针。本机一次泄漏 4096 字节 cookie、98 次命中 0x79cdb5400000。
libphp.so 基址。来源:把 pDestructor 读成 zval_ptr_dtor,减去该构建的文件偏移。
若干相对 chunk 的常量(HTDATA、R0、pad 尺寸)。这些不能从泄漏里直接算出来,只能对「这一个 ELF + 这一套 INI」预先标定。
少任何一项,stage3 的 fake HashTable 就会指到随机内存:表现是 worker 崩溃(看起来像 DoS),而不是 system()。
这也是为什么分析过程必须看堆、看偏移——不是「研究怎么打得更花」,而是 RCE 载荷把布局写死了。
SAPI / INI / MPM 会改掉对象生命周期
即使二进制相同:
**多 worker**:A 的悬空指针在 worker-3,B 打到 worker-7,消费的是干净状态。只能靠大量请求碰概率,不能当稳定 PoC。
**FPM**:startup 失败直接 child,618 的「半初始化 shutdown」路径被剪掉;419 的 session 路径还在,但堆与 Apache prefork 完全不同,bookworm 常数作废。
_exit
****:当前链靠 超限 warning 置 ,才能在 OOM 之后仍通过 把堆指针带出来。关掉就没了这条泄漏。公开仓库自己也写了「 的完整 RCE 稍后提交」。
display_errors=Off
max_input_vars
headers_sent=1
Set-Cookie
display_errors=0
** 不是 8M**:OOM 发生的分配点变了,session 对象和后续 zend_string 落在不同 bin。
memory_limit
****: 无法迫使 ,泄漏阶段缺 cookie。
session.use_strict_mode=On
PHPSESSID=\x00
send_cookie=1
这些都是默认或常见生产配置,所以「默认 PHP」四个字并不能推出「默认能 RCE」。
能通用的只有「验洞触发包」,不是 RCE
可以相对通用的是 A+B 两包(见第 6.3 节):
任意未修复 PHP + 默认 upload_progress + 有限
memory_limit
效果:OOM fatal、跨请求 session 异常、可能打崩 worker
**效果不是命令执行**
把「能打崩 / 能脏状态」写成「通用 RCE PoC」是错误的。本仓库因此拆成两份:
:通用触发(无命令执行)
lab/trigger_upload_progress_oom.py
:**仅** 的 RCE
lab/php-8.4.26-apache-bookworm/exp.py
php:8.4.26-apache-bookworm
小结
「PHP2Shell 的漏洞面是内核级、默认配置即可触发的 UAF;把它变成稳定 RCE 需要「同 worker + 预先标定的堆喷尺寸 + 该次构建的 libphp.so 符号偏移 + 一次成功的 Cookie 泄漏」。 因此:不存在跨版本、跨发行版、跨 SAPI 的通用 RCE PoC;只存在「某一精确构建上的定点 exploit」和「跨版本的崩溃/状态损坏触发包」。」
CHAPTER
定点复现环境
以下环境已两次独立打出 uid=33(www-data)。
宿主机与靶场
宿主机
Ubuntu 24.04.4 LTS,内核 6.8.0,Docker 29.2.1
镜像
php:8.4.26-apache-bookworm
Image ID
sha256:df868eae5dacd34acf4722610c7275b43e4648d7710dd1c5e7dafd0b498d59d1
容器 OS
Debian GNU/Linux 12.15 (bookworm)
PHP
8.4.26 NTS,Zend Engine 4.4.26,mod_php
Apache
2.4.68,mpm_prefork,MaxRequestWorkers=1,KeepAlive Off
libphp.so
21172088 B,sha256 ff3ea77fb119541f6d10ddf0fa7e9e3fd6a3043e049a536b530dbbec35db68d6
监听
127.0.0.1:8088
目标页
<?php echo “OK\n”; ?>(无 session_start())
HTTP 指纹(exp 启动时会校验,不对则拒绝):
PHP / Apache 配置
php.ini:
Apache MPM:
启动靶场
官方 php:8.4.26-apache 当前默认是 Trixie,堆布局与本 exp 不一致。必须显式用 bookworm 标签:
本实验室对应容器名:php2shell-target。./out 挂到容器 /out,RCE 默认把 id 写到 /out/p1。
CHAPTER
复现步骤
一键 RCE(推荐,完整代码)
完整利用代码(已按第 5 节构建写死偏移,启动时校验指纹):
本机两次运行均在 stage3 第 1 次尝试成功:
![图片[1]-全网都在等的PHP2Shell 远程命令执行漏洞复现,漏洞通用,POC不通用! ! !-阻击者联盟](https://blog.h-acker.cn/wp-content/uploads/2026/10/ea9457a2c020261022100547.png)
proof 文件(容器内 /out/p1):
站点随后仍返回 OK。Apache 日志会出现 zend_mm_heap corrupted 和 child SIGABRT/SIGSEGV——这是 UAF 被打成 zif_system 时的副作用,不是「只打崩了服务」。prefork 会拉起新 worker。
写死的常数(相对 ASLR,不随 slide 变):
exp 会拒绝非 8.4.26 / 非 Apache 2.4.68 的目标。确认是这一版再加 –force。
利用链对应的 HTTP 形态
全部请求都是 POST /index.php,Content-Type: multipart/form-data。下面给出可独立复现的 Raw;12MiB 的 OOM 字段在文本里截断,生成方式见 6.3 的 Python。
作用:RINIT 之前 php_session_initialize(),再用超大字段打到 memory_limit,longjmp 留下悬空 PS(*)。
实测响应:
in Unknown on line 0 = 用户脚本还没开始。
作用:同一 worker 上再次进入 upload_progress;无文件 part 时走 MULTIPART_EVENT_END → php_session_rfc1867_cleanup() → zval_ptr_dtor(残留指针)。
单独打 A+B(不做堆喷)时,B 已能看到跨请求残留:
这两包就是「相对通用的验洞 PoC」。它们证明 UAF/状态残留,不会执行命令。
A 在 6.2.1 基础上改为:
1001 个 、值为 (触发 , → )
name=p0000..p1000
x
max_input_vars
display_errors=On
headers_sent=1
值为单个 (迫使新 id 且 )
PHPSESSID
\x00
send_cookie=1
同样的文件 part + 12MiB
big
B 在 6.2.2 基础上追加 841 个 48 字节字段,再跟 79 字节伪造块(ZSTR_LEN=0x1000)。响应中:
本机从中解析出 chunk base = 0x79cdb5400000。
A:progress + 6 组 31/31 pad + 文件 + 12MiB,并带 Cookie: PHPSESSID=aaa…a(32 个 a),避免 Set-Cookie 覆盖伪造 reference 的 val.type。
B:progress + 15 字节 carrier(改写悬空 reference 的 val.value 低 7 字节)+ 15 个携带 fake HashTable 的 167 字节字段。
触发:
fake HashTable 的二进制随 chunk/libphp 基址变化,不能写成与泄漏无关的固定 Raw 包。完整构造见 exp.py 的 _fake_payload() / stage3()。把 stage3 body 当「通用 HTTP exp」发布,换一台机器就会变成纯崩溃。
仅验证漏洞(无 RCE)的完整代码
脚本生成 6.2.1 / 6.2.2 两包并打印响应。成功标志是 A 出现 Unknown on line 0 的 memory exhausted,B 出现 session 警告或 worker 异常,而不是 PROOF: uid=…。
CHAPTER
补丁情况与补丁前后差别
官方补丁已发布(2026-10-21,embargo 从 10-22 提前)。
发行版:8.5.12、8.4.27、8.3.36、8.2.35
NEWS:*Fixed GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp (Avoid state corruption via bailout). (CVE-2026-109618, CVE-2026-109619)*
提交:[](https://github.com/php/php-src/commit/31c0ea7d7f)
31c0ea7d7ff1d1fccb6492e52dab544011628065
本报告对比了官方 tarball **php-8.4.26 vs php-8.4.27**(完整 diff 在 )
report/evidence/
618:禁止 OOM `longjmp`
zend_mm_safe_error() → zend_bailout()
函数删除
任意 C 帧跳回请求顶层
软 OOM:写入 EG(delayed_oom),置 vm_interrupt,到 VM 安全点再 zend_throw_unwind_exit()
shutdown 看到半初始化状态
硬 OOM:新 INI memory_hard_limit_percent,超限 _exit(137),进程直接死,shutdown 看不到坏状态
619:session 全局必须先复位
ext/session/session.c:
**立刻** ,切断上一请求的 /
MULTIPART_EVENT_START
php_rinit_session_globals()
PS(id)
PS(http_session_vars)
检查 返回值,失败即退
php_session_rfc1867_update/cleanup
php_session_initialize()
:解析阶段反序列化只生成
PS(in_rfc1867_update)
__PHP_Incomplete_Class
之后不再跑这条回调
PG(modules_activated)
main/rfc1867.c:
每个 part 只保留 Content-Disposition / Content-Type,去掉「海量自定义头放大内存」
解析循环检测 并有序释放
ZEND_HAS_OOM()
读 body 再次强制
post_max_size
升级后必须重启全部 worker / 容器。发行版 backport 可能版本号不变,以 changelog 是否出现两个 GHSA 为准。
截至本报告编写时,Docker Hub 尚无 php:8.4.27-apache 标签(源码 tarball 已发布)。
CHAPTER
修复建议
唯一完整修复:升级到 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 或厂商 backport,然后重启所有 PHP 进程。
无法立即升级时的不完整缓解(需 php_admin_value,重启生效):
(挡已展示的 session 链,不是全部 619)
session.upload_progress.enabled=0
(去掉可预测 cap,缓解已确认的 618;底层 失败仍可能 bailout)
memory_limit=-1
mmap
调小 ;反代限制 body
post_max_size
不用上传的应用,反代直接丢弃
multipart/form-data
FPM: 缩小跨请求窗口
pm.max_requests=1
PHP 8.5:
fatal_error_backtraces=0
不要设 RLIMIT_AS / RLIMIT_DATA:它们让 mmap 失败,正好走进脆弱 longjmp。
应急排查:zend_mm_heap corrupted、exhausted in Unknown on line 0、异常超长 Set-Cookie: PHPSESSID=、对任意 PHP 的超大 multipart + PHP_SESSION_UPLOAD_PROGRESS。按失陷处理:轮换密钥、查 webshell。
CHAPTER
参考
https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp
https://www.php.net/ChangeLog-8.php
https://github.com/php/php-src/commit/31c0ea7d7f
公开定点利用(实验室参考):https://github.com/9bie/php2shell-bypass-aslr-exp
https://unit42.paloaltonetworks.com/cve-2026-109618-cve-2026-109619/
END
对应的exp,关注微信公众号,私信 php2shell 获取
我是网络安全透视镜。
如果你觉得今天这篇有收获,欢迎点赞、在看、转发三连,我们下篇见。
— 阅读原文请访问微信公众号「网络安全透视镜」 · 原文链接 —













暂无评论内容