【天穹】PHP2Shell:一次内存耗尽,如何演变为远程代码执行
星图实验室 2026-10-28 11:07 北京

一、概述
2026 年 10 月 21 日,PHP 官方披露了两项与请求生命周期直接相关的核心漏洞,业内统称为 PHP2Shell:
- CVE-2026-109618:RCE via OOM corruption
PHP 使用
longjmp()处理内存耗尽(Out of Memory,OOM)异常并直接跳出当前上下文;部分结构在分配前尚未完成初始化,后续请求清理流程仍可能读取或依据这部分内存分支执行,最终导致远程代码执行。 - CVE-2026-109619:RCE via stale global state
PHP 的部分全局状态会临时指向当前请求内存;在异常路径下,这些状态可能未被正确重置,或在重置后再次被填充。请求内存释放后,同一工作进程处理的后续请求仍可能读取或析构这批陈旧状态,从而触发远程代码执行。
两项漏洞对应的 CWE 分类一致,均涉及 CWE-416(Use After Free)、CWE-755(异常条件处理不当)、CWE-824(访问未初始化指针)、CWE-1325(顺序内存分配控制不当) 与 CWE-1419(资源初始化不正确)。从攻防视角看,它们的共同危险点非常明确:异常请求结束后,PHP 进程内部并没有回到绝对干净的状态。攻击者无需先找到上层业务代码中的上传、反序列化或命令注入漏洞,只要能够向目标 PHP 入口投递特定格式的 HTTP 请求,就可能在业务脚本开始执行之前触发底层异常路径。
目前,漏洞技术细节与利用代码均已公开,综合触发位置、利用公开程度与命令执行后果,建议将这两项漏洞按高风险远程代码执行漏洞优先处置。
1.1 受影响版本与修复基线
本次资产检索采用 CPE 匹配标准 cpe:2.3:a:php:php:*:*:*:*:*:*:*:*(即覆盖 PHP 应用程序的所有版本)。根据 NVD 记录与 PHP 官方安全公告,上游受影响范围与修复版本如下:

两项漏洞对应的 CWE 分类一致,均涉及 CWE-416(Use After Free)、CWE-755(异常条件处理不当)、CWE-824(访问未初始化指针)、CWE-1325(顺序内存分配控制不当) 与 CWE-1419(资源初始化不正确)。从攻防视角看,它们的共同危险点非常明确:异常请求结束后,PHP 进程内部并没有回到绝对干净的状态。攻击者无需先找到上层业务代码中的上传、反序列化或命令注入漏洞,只要能够向目标 PHP 入口投递特定格式的 HTTP 请求,就可能在业务脚本开始执行之前触发底层异常路径。
目前,漏洞技术细节与利用代码均已公开,综合触发位置、利用公开程度与命令执行后果,建议将这两项漏洞按高风险远程代码执行漏洞优先处置。
1.1 受影响版本与修复基线
本次资产检索采用 CPE 匹配标准 cpe:2.3:a:php:php:*:*:*:*:*:*:*:*(即覆盖 PHP 应用程序的所有版本)。根据 NVD 记录与 PHP 官方安全公告,上游受影响范围与修复版本如下:
PHP 分支 | 受影响版本范围 | 上游修复版本 |
|---|---|---|
PHP 8.2 及更早分支 | < 8.2.35(CPE 未设下界) | 8.2.35 |
PHP 8.3 | >= 8.3.0, < 8.3.36 | 8.3.36 |
PHP 8.4 | >= 8.4.0, < 8.4.27 | 8.4.27 |
PHP 8.5 | >= 8.5.0, < 8.5.12 | 8.5.12 |
二、漏洞分析
在公开讨论中,session.upload_progress 高频出现在两条利用链里,容易让人误以为两个 CVE 只是同一个漏洞的不同表述。实际上,它们共享的是同一个异常触发前提,但破坏的是两条不同的安全边界:
- 共同前提
:PHP 在内存分配失败时通过
zend_bailout()调用longjmp()直接跳出当前 C 调用栈,跳过调用栈上原本位于分配成功之后的初始化、计数回滚与局部清理逻辑。 - CVE-2026-109618(同一请求内状态不完整)
:对象只初始化了一半,当前请求的 shutdown 清理流程却把它当作完整对象继续遍历和析构。
- CVE-2026-109619(跨请求全局状态残留)
:当前请求堆已经被 Zend 内存管理器(Zend MM)整块释放,但进程级全局变量仍指向旧请求内存,并被同一 worker 的后续请求继续消费。
因此,看到攻击流量里使用了超大 multipart 请求或触发了 OOM,并不等于后续利用一定归属 CVE-2026-109618。判断属于哪一个 CVE,关键看最终被消费的损坏状态位于同一请求内部,还是跨越到了同一 worker 的下一个请求.
2.1 为什么 session.upload_progress 能在应用代码之前触发
当 PHP 解析 multipart/form-data 请求时,只要请求中同时带有会话标识、PHP_SESSION_UPLOAD_PROGRESS 字段以及文件上传 part,RFC 1867 解析器就会注册并调用 Session 上传进度回调。这一步发生在目标 .php 脚本开始执行之前,并会在解析过程中连续触发多类内存分配:
提前初始化或重新打开 Session;
解码已有 Session 数据并创建上传进度数组;
保存
bytes_processed等内部元素指针;将更新后的进度信息重新编码写回 Session;
随着后续文件块、表单字段与数组结构继续分配内存。
这意味着攻击者完全不需要目标站点存在复杂的上传业务逻辑。哪怕站点根目录只有一个最简单的 <?php echo "OK\n";,只要请求能够进入 PHP 的 RFC 1867 解析器,并且分配量越过 memory_limit,PHP 就会在进入业务脚本前直接 zend_bailout()。
2.2 CVE-2026-109618:同一请求中的半初始化对象
该漏洞的典型形态是“先发布状态、后完成初始化”的代码顺序遇上 OOM。以 _zend_hash_str_add_or_update_i() 一类 HashTable 插入过程为例:
HashTable 先递增
nNumUsed,使最后一个 Bucket 在逻辑上已经被计入表中;随后的 key 或字符串拷贝触发内存分配失败;
zend_bailout()直接跳出当前调用栈,该 Bucket 的 key、hash 或
zval尚未完成写入,nNumUsed也没有回滚;当前请求进入 shutdown 阶段,HashTable 析构函数按照已经递增的
nNumUsed遍历到这个半初始化 Bucket;清理逻辑把旧堆上残留的可控字节当成合法
zval、对象指针或析构元数据解释,进而触发非法释放、错误函数指针调用,最终演变为远程代码执行.
2.3 CVE-2026-109619:跨请求的陈旧全局状态与 Worker 复用
与同一请求内的半初始化对象不同,CVE-2026-109619 的核心是进程级全局状态跨请求越界存活:
请求 A 在 RFC 1867 上传进度回调中提前初始化 Session,使
PS(id)、PS(http_session_vars)等全局状态指向当前请求堆;请求 A 随后触发 OOM,
zend_bailout()跳过了 multipart 的EVENT_END清理回调;此时
PG(modules_activated)尚未完全建立,常规的 SessionRSHUTDOWN清理流程可能被绕过;请求结束时,Zend MM 回收了整个请求堆,但
PS(id)或PS(http_session_vars)仍然保留着指向旧堆槽位的悬空指针;当同一个 Apache
mod_phpworker 紧接着处理请求 B 时,会在完成正常初始化前再次进入上传进度或 Session 初始化逻辑;请求 B 读取陈旧的
PS(id)(造成越界读取/地址泄露),或通过zval_ptr_dtor(&PS(http_session_vars))析构已经被攻击者重占的伪造结构,最终把控制流导向任意命令执行。
这也解释了为什么实验环境常将 Apache mpm_prefork 的 MaxRequestWorkers 设为 1:
在默认多 worker 环境(如
MaxRequestWorkers=150)中,请求会被分发给不同子进程,攻击脚本需要先通过 worker 轮转与并发连接占位,把A1 → B1 → A2 → B2四个阶段“钉”在同一个 worker 上;当
MaxRequestWorkers=1时,系统中天然只有一个活跃 worker,所有请求必然串行落在同一进程内,跨请求状态残留能够以最直观、最稳定的方式复现。
因此,MaxRequestWorkers=1 是实验室消除了进程调度不确定性的调试配置,而不是漏洞根因。
三、天穹沙箱中的两条验证链
为还原 PHP2Shell 的真实行为特征,天穹沙箱在 Ubuntu 18.04 + Apache 2.4.29(mpm_prefork) 环境中构建了两条互补的动态验证链,分别覆盖 CVE-2026-109618 的 multipart OOM 异常触发链 与 针对 PHP 7.2 堆布局标定的 pwn10052 四阶段完整利用链。
在展开实验前,先说明一条关键的技术边界:公开 Exploit-DB 52704 默认标定的是 php:8.4.26-apache 容器环境,其 Zend MM 分箱布局、结构偏移与符号地址不能直接搬到 Ubuntu 18.04 的 PHP 7.2 上运行。为此,实验中的完整利用链采用针对 PHP 7.2.34 重新标定的 pwn10052,并在靶机加载带 cleanup 兼容性观测逻辑的实验模块完成全链复现。通过把“链一:原版环境可达的 OOM 触发与状态损坏”与“链二:完整四阶段地址泄露 + 命令执行闭环”拆开验证,既能看清每个 CVE 的真实触发条件,也能完整检验沙箱对多阶段内存利用的检测能力。
报告链接1:
报告链接2:
https://sandbox.qianxin.com/tq/report/toNewReport.do?rid=892ad2ac4e1610442af67f37582a448e&sk=84061347
3.1 验证链一:Chunked Multipart 触发 OOM 与请求关闭状态损坏(CVE-2026-109618)
第一条验证链对应样本 php_sample_chainD.sh 与触发脚本 poc_d.py,目标是验证攻击者如何在不依赖上层应用漏洞、也不依赖 Content-Length 预检的前提下,直接把 Apache mod_php 打入 OOM 异常清理路径。
1. 环境准备与入口最小化
验证脚本首先将 /etc/php/7.2/apache2/php.ini 中的 memory_limit 调整为 32M,并在 /var/www/html/index.php 放置最小化入口页面(仅输出 PHP 版本、SAPI 名称与 OK),随后重启 apache2 获取干净的 worker 进程。
这一步说明:该触发链不要求目标页面调用 session_start(),也不要求页面中存在文件上传或包含代码。只要请求访问的是任意由 mod_php 处理的 PHP 入口,RFC 1867 解析器就会在脚本执行前介入。
2. 为什么使用 Transfer-Encoding: chunked
在很多默认配置中,post_max_size(例如 8M)小于或接近 memory_limit。如果攻击者直接发送带固定 Content-Length 的超大 POST 请求,PHP 往往会在进入深度解析前直接拒绝请求体,导致 OOM 根本无法触发。
poc_d.py 采用的是更贴近实战的绕过方式:构造 Content-Type: multipart/form-data; boundary=OOM_BOUNDARY_...,同时使用 Transfer-Encoding: chunked 流式发送 128MB 表单数据(每块 8192 字节,不提供 Content-Length)。这样 Apache 与 mod_php 会边接收分块数据边送入 RFC 1867 解析器持续分配内存,直至突破 memory_limit=32M 触发 zend_bailout()。
POST /index.php HTTP/1.1Host: 127.0.0.1Content-Type: multipart/form-data; boundary=OOM_BOUNDARY_<timestamp>Transfer-Encoding: chunkedConnection: close
3. 沙箱捕获的完整行为时间线
如图 1 与图 2 所示,天穹沙箱完整记录了该样本从环境变更、脚本投递到 Apache 内部异常响应的整条链路:
- 配置与入口准备
:沙箱捕获到样本备份
/etc/php/7.2/apache2/php.ini.bak-php2shell、修改memory_limit、写入/var/www/html/index.php,并对敏感文件修改行为给出风险标记; - 攻击脚本启动
:监控到
chmod +x ./poc_d.py及python3 ./poc_d.py --host 127.0.0.1 --port 80 --path /index.php --total-mb 128的执行命令; - 网络侧漏洞触发识别
:当
python3通过本地套接字向127.0.0.1:80发送分块 multipart 请求时,天穹沙箱直接识别出Content-Type: multipart/form-data; boundary=OOM_BOUNDARY_...与Transfer-Encoding: chunked特征,并触发告警规则,标记为触发 PHP OOM 攻击; - Apache 异常清理与错误日志落盘
:
apache2worker 接收流式数据后在请求初始化/解析阶段耗尽内存,向/var/log/apache2/error.log写入Unknown on line 0相关错误记录,并伴有异常内存权限调整行为,最终向客户端返回HTTP/1.0 500 Internal Server Error。


这条验证链的价值在于把“普通 500 报错”与“CVE-2026-109618 异常清理触发”区分开来:沙箱不仅看到了返回码 500,同时还关联到了前序的 chunked multipart 边界特征、apache2 对 error.log 的异常写入以及进程级内存操作。
3.2 验证链二:样本 pwn10052 四阶段 ASLR 绕过与远程代码执行(CVE-2026-109619)
第二条验证链是针对 PHP 7.2 标定的样本 pwn10052,目标是完整重现从跨请求陈旧全局状态、两阶段地址泄露绕过 ASLR,到伪造引用结构执行系统命令的闭环过程。
1. 样本 pwn10052 的关键调用路径
与 Exploit-DB 52704 的通用描述相比,样本 pwn10052 采用的是 9bie 风格的 fake-reference Chain D。从根因归属看,它的 Stage 1、Stage 2 与 Stage 3 消费的都是跨请求残留的 Session 全局状态(PS(id) 与 PS(http_session_vars)),主线归属 CVE-2026-109619:
请求A(制造陈旧全局状态):RFC 1867 upload progressphp_rinit_session()PS(id) / PS(http_session_vars) 指向当前请求堆12MB 字段触发 OOM -> zend_bailout() / longjmp()跳过 multipart EVENT_END 清理与常规 Session RSHUTDOWN请求堆被释放,但全局指针仍残留于同一 worker请求B(Stage 1 / Stage 2 地址泄露):重占已释放的请求堆槽位php_session_initialize() / 上传进度清理路径读取陈旧 PS(id)php_url_encode()通过超长或定长 Set-Cookie 回显越界堆内存请求B(Stage 3 伪造对象触发 RCE):喷射伪造的zend_reference + HashTable 载体php_session_track_init() / Session 清理析构路径zval_ptr_dtor(&PS(http_session_vars))zend_reference_destroy() -> zend_array_destroy() -> zend_hash_destroy()调用伪造 HashTable.pDestructor -> 执行系统命令
2. 四个阶段的精细堆布局与实测输出
在 PHP 7.2.34 目标模块上,样本 pwn10052 使用的关键符号偏移为:zif_system = 0x35d4d0、_zval_ptr_dtor = 0x427ce0、HTDATA_OFFSET = 0x71000。整个利用过程分为四个阶段:
- Stage 0——Worker 状态准备
:确保目标请求序列落入同一个状态干净的
apache2worker,使后续请求共享同一个 Zend MM 堆空间。 - Stage 1——通过 4096 字节
Set-Cookie泄露 Zend MM 堆基址:
请求
stage1-A先发送1001个填充字段、PHPSESSID=\x00、PHP_SESSION_UPLOAD_PROGRESS、文件字段以及 12MB 超大字段,在 Session 初始化后立刻触发 OOM,留下悬空的PS(id);请求
stage1-B紧接着向同一 worker 发送800个精心排布的表单字段与伪造字符串,使悬空PS(id)的长度域被重占为0x1000(4096);服务器在
Set-Cookie响应头中吐出长度4096的 URL 编码数据,脚本从中恢复出 Zend MM chunk 基址0x7fdfe3800000,完成第一次 ASLR 绕过。- Stage 2——通过 16 字节
Set-Cookie泄露libphp基址:
请求
stage2-A再次利用 OOM 让PS(id)悬空在特定大小的 bin 槽位上;请求
stage2-B通过构造983个数组元素触发 HashTable 扩容,使新建 HashTable 恰好重占该槽位,并将偏移+0x28处的nNextFreeElement | pDestructor(16 字节原始数据\x01\x00\x00\x00\x00\x00\x00\x00\xe0\xdc\xe1\xe7\xdf\x7f\x00\x00)作为 Cookie 回显;脚本解析出
_zval_ptr_dtor的运行期地址,减去固定偏移0x427ce0得到libphp.so基址0x7fdfe79f6000,再加0x35d4d0算出zif_system = 0x7fdfe7d534d0,完成第二次 ASLR 绕过。- Stage 3——喷射 167 字节伪造 HashTable 载体触发命令执行
:
请求
stage3-A使用167字节 Session ID(落入bin-192槽位)配合 12MB 字段触发 OOM,使PS(id)与PS(http_session_vars)悬空;请求
stage3-B将 167 字节的伪造 HashTable 载体放在 multipart 表单最前面(后跟PHPSESSID、PHP_SESSION_UPLOAD_PROGRESS与400次引用喷射),确保载体优先重占chunk + 0x71000处的悬空槽位,避免被前面的表单字段提前抢占;当 Session 析构路径解引用
PS(http_session_vars)时,控制流沿伪造结构进入析构回调,执行命令id>/dev/shm/p1。
如图 3 所示,样本 pwn10052 在第一轮尝试(attempt 1/3)中即依次拿到堆基址、libphp 基址与 zif_system 地址,并在 /dev/shm/p1 中写入回显证明:
[*] target http://127.0.0.1/index.php mode=full[*] offsets: zif=0x35d4d0 dtor=0x427ce0 htdata=0x71000[*] stage 1: heap leak (attempt 1/3)[*] stage1 cookie len=4096 LPRE=true[+] chunk base = 0x7fdfe3800000[*] stage 2: libphp leak (attempt 1/3)[*] stage2 cookie len=16 raw="\x01\x00\x00\x00\x00\x00\x00\x00\xe0\xdc\xe1\xe7\xdf\x7f\x00\x00"[+] libphp base = 0x7fdfe79f6000[+] zif_system = 0x7fdfe7d534d0[*] stage 3: HTDATA=0x7fdfe3871018 ZIF=0x7fdfe7d534d0 cmd="id>/dev/shm/p1"[+] W3 attempt 1PROOF: uid=33(www-data) gid=33(www-data) groups=33(www-data)

3. 沙箱对跨进程利用闭环的检测结果
在攻击者视角下,Stage 3 只是向 /index.php 发送了两个看似普通的 multipart POST 请求;但在主机行为侧,天穹沙箱清晰捕获到了完整的利用闭环(见图 4):
- 网络接收与会话文件交互
:
apache2从本地套接字接收多阶段请求,打开/var/lib/php/sessions/sess_1979667a308c87dc446d0fce65d3a834,并在/tmp/php65REbv创建上传临时文件; - 清理旧证明文件
:样本进程在发起 Stage 3 前清理
/dev/shm/p1; - 精准命中漏洞命令执行告警
:
apache2派生sh执行系统命令sh -c id > /dev/shm/p1,并直接命中高危规则CVE-2026-109619 Web 进程 apache2 执行系统命令: sh -c id > /dev/shm/p1。

四、供应链分析
PHP2Shell 的治理难点,不只在于漏洞位于解释器核心,更在于 PHP 作为基础运行时,会沿着操作系统软件源、开源 Web 应用、第三方依赖链以及硬件固件管理界面层层传递。上层应用即使自身代码没有任何高危写法,只要底层绑定了未修复的 PHP 运行时,并暴露了可达的 Web 请求入口,就可能被动继承风险。
围绕 Ubuntu、Kali、CentOS、Arch 四类软件生态及固件样本的供应链测绘结果如下:

需要注意:组件版本命中、依赖关联与真实部署可利用性属于三个不同层次。上述数字存在交集,不能直接累加为“受影响资产总数”,更不能把广义依赖候选直接等同于可被远程 RCE 的生产系统。它们的作用是为企业提供一张从运行时、应用到设备的排查地图。
4.1 软件生态影响范围
在 Ubuntu、Kali、CentOS 与 Arch 四个生态中,共有 5,559 个 PHP 软件构建落入上游版本检索范围,另有 28 个 Ubuntu 构建早于发行版安全公告列出的修复版本:
如果按运行方式拆分已识别的上游版本范围匹配构建,攻击面差异会更加清晰:
在依赖层面,直接与间接关联软件包合并去重后共涉及 387,297 个“生态 + 包名”条目(两者有 1,155 个包名键重合)。企业在实际排查时,无需对全部间接依赖逐一停服,而应沿着生产环境中真正安装并对外提供服务的 PHP 运行时反向收敛影响面。
4.2 应用与热门产品
供应链测绘共识别出 2,911 个包含 PHP 源码的应用构建,涉及 441 个精确包名。其中既包括直接面向互联网部署的内容管理与协作平台,也包括大量政企内网常见的监控与运维系统:
Wiz Research 的公开分析已在特定容器部署的 WordPress、Drupal、Joomla 场景中演示了未认证远程代码执行。需要强调的是,这类应用在攻击链中扮演的是底层 PHP 运行时漏洞的承载入口,而非应用源码自身全部新增了同源漏洞。排查时必须把“应用版本”和“PHP 运行时版本”拆开核对:只升级 CMS 插件或前端代码,无法修复底层 libphp 或 php-fpm 的内存安全缺陷。
4.3 固件与硬件生态影响范围
除传统服务器与容器镜像外,网络设备、负载均衡器、NAS 等硬件产品也常在 Web 管理平面中内嵌 PHP。固件组件识别结果显示,共有 66 条固件记录中的 PHP 组件身份与版本落入两个 CVE 的 CPE 范围。以下为已识别到具体产品型号或固件构建的典型样本:

对硬件固件而言,组件版本命中代表该固件样本携带了落入受影响范围的 PHP 版本,但不能据此外推为相关品牌全部型号或当前最新固件均受影响。设备侧的最终处置应结合两点:一是厂商是否已发布包含修复的固件更新;二是设备 Web 管理接口是否直接暴露在不可信网络中。
五、排查与处置建议
结合官方安全公告、供应链测绘结果与沙箱复现经验,建议按以下步骤开展排查与加固:
升级上游或厂商修复版本:将 PHP 升级至 8.2.35、8.3.36、8.4.27、8.5.12 及以上安全版本;使用发行版软件源时,必须升级到包含两个 CVE 回补补丁的完整软件包版本。
对照 Ubuntu 官方安全公告核验:
Ubuntu 22.04 / 24.04 / 26.04 / 26.10 参考修复版本分别为
8.1.2-1ubuntu2.28、8.3.6-0ubuntu0.24.04.13、8.5.4-0ubuntu1.5、8.5.9-0ubuntu2.1(见 USN-8948-1);Ubuntu 20.04 / 18.04 / 16.04 / 14.04 等扩展维护版本参考更新分别为
7.4.3-4ubuntu2.29+esm5、7.2.24-0ubuntu0.18.04.17+esm14、7.0.33-0ubuntu0.16.04.16+esm22、5.5.9+dfsg-1ubuntu4.29+esm18(见 USN-8948-2)。
六、参考资料
- CVE-2026-109618:RCE via OOM corruption(https://www.cve.org/CVERecord?id=CVE-2026-109618)
- CVE-2026-109619:RCE via stale global state(https://www.cve.org/CVERecord?id=CVE-2026-109619)
- PHP 官方公告一(GHSA-w8hw-w9fx-j79w)(https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w)
- PHP 官方公告二(GHSA-6wmf-4rv5-p3rp)(https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)
- PHP 官方修复提交(31c0ea7d)(https://github.com/php/php-src/commit/31c0ea7d)
- Ubuntu 安全公告 USN-8948-1(https://ubuntu.com/security/notices/USN-8948-1)
- Ubuntu 安全公告 USN-8948-2(https://ubuntu.com/security/notices/USN-8948-2)
- Wiz Research:PHP2Shell 技术披露(https://www.wiz.io/blog/php2shell-php-rce)
- Calif:PHP2Shell 技术分析(https://calif.io/php2shell/)
- [Exploit-DB 52704](https://www.exploit-db.com/exploits/52704)
天穹智能分析平台(联系我们申请账号):https://sandbox.qianxin.com
天穹智能分析平台持续迭代升级,致力于为每一位样本分析人员打造更高效、更智能、更易用的分析平台——这始终是我们不变的初心与追求。
如果您希望深入了解平台功能,或在使用过程中遇到任何问题,欢迎随时联系我们。您的反馈,是我们进步的重要动力!
本文转载自微信公众号「奇安信技术研究院」,查看原文。版权归原作者所有,如有侵权请联系删除。