2026-10-21-php2shell
php2shell:我把它从一个「未授权 RCE」跑成了一个段错误
这几天 X 和几个群里都在传 php2shell,CVE-2026-109618 / CVE-2026-109619。
说法很统一:两个请求、未授权、RCE,PHP 8.4.26 及以下全中。GitHub 上跟着冒出来好几个 PoC 仓库,每个都说自己”已验证”。
我一开始也半信。直到我把它们挨个跑了一遍。
第一步 · 先把流传的 PoC 全跑一遍
不读代码,先看行为。四个仓库 + 一条 X 上的 payload,全都丢进同一个 8.4.26 环境里跑:

结果有点尴尬:
- 号称拿到 shell 的那个,第一次上传就 OOM 死了,第二个请求连不上,没有 shell,也没有 flag;
- 0x4148 那个 lab 其实很老实,README 自己写着”不崩、不执行代码”,它是个可达性检查器;
- ikow 和 Quackster 的链是真能跑通的,但前提是应用里已经存在 unserialize() 汇聚点,也就是要先有一个反序列化漏洞——那不叫 php2shell 打穿,那叫拿反序列化漏洞打穿;
- X 上那条
filename="|O:4:\"Evil\":0:{}"确实能写进 session 文件,但默认的 session.serialize_handler 根本不会去实例化它。
顺带说一句,我自己最早也传过一个(allureluoli/php2shell-…-PoC),当时按计数器 UAF 的思路做的,硬编码偏移 + 让人自己填泄漏地址。现在回头看,它也是不崩的那一类。这个仓库我留在那儿当反面教材。
所以问题变成:如果这些都不是,那 php2shell 到底是个什么东西?
第二步 · 回到源码,看 8.4.27 改了什么
8.4.26 到 8.4.27 之间,PHP 官方那条 commit 的标题很朴素:
506e87fa Avoid state corruption via bailout
“避免 bailout 造成的状态损坏”。bailout 是 PHP 里的长跳转:内存耗尽、超时、致命错误时,直接 longjmp 跳出去,不跑清理代码。这条 commit 做的事是:让解析过程中的内存分配变成可失败的(fallible),把 OOM 的处理延后,并且在 RINIT 之前把受影响的全局变量重置掉。
重点在最后半句:在 RINIT 之前。
第三步 · 崩溃点为什么在 RINIT 之前
PHP 处理请求的顺序是这样的:
php_request_startup()
→ php_hash_environment() // 建 $_POST 这些超全局变量
→ sapi_handle_post()
→ rfc1867_post_handler() // 解析 multipart
→ php_session_rfc1867_callback() // 有 session.upload_progress 时才走
→ php_session_initialize()
→ php_session_decode()
注意这条链的位置:它在 php_request_startup() 里面。也就是说,框架还没启动、路由还没匹配、鉴权还没跑、中间件一个都没执行,PHP 已经在解析你的 multipart body,并且在处理 session 了。而 session.upload_progress.enabled 默认是 1。
两条请求加起来是这样:

第一步请求让 session 数据解码成功,但编码时超了 memory_limit。这时 PHP 走 bailout 直接退出,序列化上下文里那个指针没被清理——它活着,但指向的位置已经不会再被维护了。
第二个请求进来,只要这个进程还活着,它就会在 RINIT 之前再去碰那个上下文。然后:
第四步 · 8.4.26 上的崩溃
环境:PHP 8.4.26,--enable-debug + -fsanitize=address 从源码编的,php -S 起单进程。

AddressSanitizer: SEGV on unknown address 0x000000000138 (READ)
#0 tmp_var ext/standard/var_unserializer.c:168
#2 ps_srlzr_decode_php ext/session/session.c:1134
#4 php_session_initialize ext/session/session.c:502
#5 php_session_rfc1867_cleanup ext/session/session.c:3211
#7 rfc1867_post_handler main/rfc1867.c:1273
#13 php_request_startup main/main.c:1873
栈顶在 tmp_var(),就是反序列化里读临时变量的那个位置,它在读一个已经死掉的地址,0x138 是零页偏移,读一下就 SEGV。
关键是:栈里没有一行应用代码。
第五步 · 8.4.27 什么都不发生
同样的两个请求,同样的 body,同样的 memory_limit=8M、session 6MB,只换 PHP 源码:

注意一个容易看错的地方:修复版也会 OOM,也会抛 Allowed memory size exhausted。它和漏洞版的区别不是”会不会耗尽内存”,而是”耗尽之后进程里还剩下什么”。8.4.27 在 bailout 之前把那些全局状态重置了,所以第二个请求进来时是干净的。
这里有个细节值得记一下:我实测的六个版本号里,8.4.27 的 fatal 报在 zend_string.h:198,8.4.26 报在 zend_string.h:176——同一个函数,不同的分配路径,这就是 fallible alloc 那条改动。
第六步 · 不需要 ASAN 也能看出来
ASAN 只是为了拿到可读的栈。这个 bug 在普通发行版 PHP 上一样崩:拿系统自带的 8.4.16(无 debug、无 ASAN)跑同一套两个请求,第二个请求直接没有响应,进程 exit 139。
所以验证成本可以压得很低:两个现成的 php 可执行文件,一个 30 秒的脚本,不编译、不用 docker。
第七步 · 做成差分,别做成单边演示
这类漏洞最容易做错的地方,是只演示”漏洞版崩了”。那说明不了任何问题——内存耗尽的请求本来就可能把进程搞挂。
所以脚本是差分的:两版各跑 3 次,只有 3/3 vs 0/3 才给正面结论,否则 exit 1。
# 就地验证,不用编译(8.4.16 vs 8.4.27)
PHP_VULN=/usr/bin/php PHP_FIXED=$HOME/pb/p8427/bin/php lab/quickstart.sh
# 严谨路线:两版都按 sha256 固定源码、ASAN 从源码编,各跑 3 次
lab/bootstrap.sh
lab/run-ab.sh
而且不只在我机器上跑:
VERDICT
8.4.26 ASAN hits: 3/3
8.4.27 ASAN hits: 0/3
RESULT: reproduced (vulnerable crashes, fixed does not)
GitHub Actions 上每次 push 都会重编两版再跑一遍,日志作为 artifact 上传。这部分我特意检查过 runner 的日志——是真的下载 tarball、真的 make、真的跑出 SEGV var_unserializer.c:168 in tmp_var,不是缓存命中后直接跳过。
第八步 · 所以这不是 RCE
这一点必须说清楚,因为网上那批”两请求拿 shell”的说法,跑起来都是不成立的:
- 我们能稳定复现的,是 RINIT 之前的状态损坏(SEGV / heap-use-after-free);
- 从崩溃到代码执行,中间还差两步:reclaim(把释放掉的那块内存用可控内容填回去)和劫持(让程序按你的数据去跳)。公开材料里没有人做到这两步;
- 还有一层前提:要让编码阶段 OOM,session 里得先有一个足够大的值。我的 harness 是自己写这个 session 文件的(
tools/seed_session.py),因为这是 lab 侧的前提。真实攻击者能不能把大值塞进自己的 session,取决于应用怎么写——这一点我没有证明,我也不打算假装证明了。
另外两个负向对照也顺手记一下:
- 换一个干净的 SID(session 里没有大值):两版都正常返回 200,不崩。所以不是”任何上传进度请求都会崩”;
- 用 php-fpm:第一个请求就把 worker 干掉了,根本没有”下一个请求”去复用任何东西。这也解释了为什么这个 bug 的适用面是 prefork mod_php 和 CLI server 这类能扛过启动失败的 SAPI。
修复版本
8.4.27、8.3.36、8.2.35、8.5.12。低于这些版本的,不管你的应用写得多干净,都建议直接升——这个漏洞不需要你的代码有 bug。
最后
整个东西我做成了一个可复现的 lab,两版 PHP 都从源码编、差分脚本、6 份原始日志、逐条 PoC 实测记录都在里面:
https://github.com/allureluoli/php2shell-lab
MIT,只用 127.0.0.1,payload 是一个大小而不是一条命令。
如果你手上有真能打到代码执行的链,欢迎来 issue 里贴——那个 issue 表单我按”能验证的复现”设计的,光有截图不算。