别照抄网上的 php2shell 复现脚本:PHP 内核这次到底碎在哪,4 个真相(2026)

别照抄网上的 php2shell 复现脚本:PHP 内核这次到底碎在哪,4 个真相(2026)

📖 摘要:2026 年 10 月 21 日,PHP 官方紧急发布 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12,修复 CVSS 9.2 的 php2shell(CVE-2026-109618 / 109619)。本文拆清 longjmp 导致的内存破坏根因,教你 30 分钟判断自己是否真的可达、怎么升级与检测,并逐条纠正网上流传的错误复现结论。

🏷️ 关键词:php2shell,PHP 安全,远程代码执行,内存破坏,应急响应

目录

  • [一、48 小时:一场被提前点燃的火](#一、48 小时:一场被提前点燃的火 "#sec-1")
    • [1.1 从泄露到百万级攻击的时间线](#1.1 从泄露到百万级攻击的时间线 "#sec-1-1")
    • [1.2 为什么这次和"某个 CMS 出漏洞"不是一个量级](#1.2 为什么这次和"某个 CMS 出漏洞"不是一个量级 "#sec-1-2")
  • [二、根因:longjmp 跳过了 C 栈展开](#二、根因:longjmp 跳过了 C 栈展开 "#sec-2")
    • [2.1 PHP 遇到致命错误时到底做了什么](#2.1 PHP 遇到致命错误时到底做了什么 "#sec-2-1")
    • [2.2 CVE-2026-109618:先记账,后分配](#2.2 CVE-2026-109618:先记账,后分配 "#sec-2-2")
    • [2.3 CVE-2026-109619:跨请求的僵尸全局](#2.3 CVE-2026-109619:跨请求的僵尸全局 "#sec-2-3")
  • [三、谁最危险:SAPI 与配置的可达性](#三、谁最危险:SAPI 与配置的可达性 "#sec-3")
    • [3.1 worker 复用是放大器](#3.1 worker 复用是放大器 "#sec-3-1")
    • [3.2 memory_limit 与 post_max_size 的比值](#3.2 memory_limit 与 post_max_size 的比值 "#sec-3-2")
  • [四、实战:30 分钟完成自查与修复](#四、实战:30 分钟完成自查与修复 "#sec-4")
    • [4.1 清点:那些"看不见的 PHP"](#4.1 清点:那些"看不见的 PHP" "#sec-4-1")
    • [4.2 判定:你的实例是否真的可达](#4.2 判定:你的实例是否真的可达 "#sec-4-2")
    • [4.3 升级:为什么必须重启 worker](#4.3 升级:为什么必须重启 worker "#sec-4-3")
    • [4.4 缓解:打不了补丁时的临时方案](#4.4 缓解:打不了补丁时的临时方案 "#sec-4-4")
  • 五、检测:怎么判断已经被打了
    • [5.1 唯一的高保真信号](#5.1 唯一的高保真信号 "#sec-5-1")
    • [5.2 进程与文件层的后渗透痕迹](#5.2 进程与文件层的后渗透痕迹 "#sec-5-2")
    • [5.3 已中招后的处置顺序](#5.3 已中招后的处置顺序 "#sec-5-3")
  • 六、辟谣:网上那些复现结论错在哪
    • [6.1 说法一:根因是 session.upload_progress 的 zval UAF](#6.1 说法一:根因是 session.upload_progress 的 zval UAF "#sec-6-1")
    • [6.2 说法二:CVSS 10.0,只影响 PHP 8](#6.2 说法二:CVSS 10.0,只影响 PHP 8 "#sec-6-2")
    • [6.3 说法三:上了 WAF 或改了配置就等于修好了](#6.3 说法三:上了 WAF 或改了配置就等于修好了 "#sec-6-3")
    • [6.4 说法四:升级完内核就万事大吉](#6.4 说法四:升级完内核就万事大吉 "#sec-6-4")
  • 七、这件事留下的三个长期启示
    • [7.1 影子 PHP 资产清单](#7.1 影子 PHP 资产清单 "#sec-7-1")
    • [7.2 AI 找洞时代,补丁窗口正在消失](#7.2 AI 找洞时代,补丁窗口正在消失 "#sec-7-2")
    • [7.3 把"重启才生效"写进变更流程](#7.3 把"重启才生效"写进变更流程 "#sec-7-3")
  • 八、总结

一、48 小时:一场被提前点燃的火

2026 年 10 月 20 日,php.net 首页挂出四行字:Upcoming Critical PHP Security Release ,说明次日 14:00 UTC 会发布两个严重漏洞的修复。四行字的公告,撑不起一个周末的热度------真正让它炸开的是:细节在公告之前就先漏了。

漏洞代号 php2shell,两个 CVE:CVE-2026-109618(OOM 内存破坏)和 CVE-2026-109619(跨请求陈旧全局状态),CVSS 4.0 均为 9.2。它们不需要账号、不需要会话、不需要目标站点上存在任何一行有漏洞的应用代码。

1.1 从泄露到百万级攻击的时间线

把公开披露的时间点拼在一起,能看到一条相当紧张的曲线:

时间(UTC) 事件
2026-08-11 Calif(Ryan)与 OpenAI 独立报告 OOM → RCE 链路;随后 Wiz、OpenAI 补充新的攻击向量
2026-10-17 亚马逊 MadPot 蜜罐记录到第一次利用尝试,来源是商用匿名化出口 IP(比披露早 4 天)
2026-10-20 PHP 官方发布预告公告;同日 CVE 编号、session.upload_progress 等碎片在社交媒体与 HN 扩散,官方被迫加速
2026-10-21 02:00 左右 最早的公开 PoC 出现,比禁令解除早约 12 小时
2026-10-21 13:30--14:00 官方发布 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35,并为 7.4 / 8.0 / 8.1 提供最小源码 backport
2026-10-21 白天 英国 NCSC 发布警报;荷兰 NCSC 及鹿特丹港、Overheid.nl、KNVB、多家医院、若干政党网站临时下线;芬兰关闭部分政府公共服务;英国预算责任办公室网站下线
2026-10-21 晚 Cloudflare CEO 公布攻击量:公告后 45 分钟到数万,2 小时内到数十万,随后到数百万
2026-10-22 新加坡 CSA 发公告「威胁就在眼前」;韩国科技部将国家网络警报上调至「注意」;国内 CNVD、工信部 NVDB、腾讯云、阿里云、奇安信相继发布通告

几个量级参考,能说明为什么各国政府选择"先下线再说":

  • W3Techs 的统计口径里,PHP 覆盖约 69%--77% 的已知服务端语言站点;
  • VirusTotal 的口径是 510 万+ 台 PHP 主机暴露在公网,其中约 380 万台有至少一个端口开放;
  • 受影响的不只是"你写的 PHP 代码",而是 WordPress、Drupal、Joomla、MediaWiki、Magento、Laravel、Symfony、WHMCS、Nextcloud、phpMyAdmin、Roundcube、OpenCart 这一整片运行时底座。

⚠️ 关键点:这不是"某个插件有洞",是宿主语言本身有洞。你 WordPress 插件全升级、主题全审计、后台强密码,在这两个 CVE 面前一分不加。

1.2 为什么这次和"某个 CMS 出漏洞"不是一个量级

区别在三个"与业务无关":

  1. 与代码质量无关 。请求解析发生在扩展初始化之前、脚本编译之前。也就是说,漏洞在你 index.php 第一行执行之前就已经被触发。代码写得再干净,暴露面和一团乱麻的老站完全一样。
  2. 与框架无关 。Laravel 的中间件、Symfony 的防火墙、WordPress 的 wp_die() 全都在脚本阶段才生效,而这个洞在 SAPI 层。
  3. 与认证无关。单个匿名 POST 就能打到,没有"需要登录"这个降级条件。

CrowdStrike 的公开测试结论是:一个匿名 POST 请求即可让 Apache mod_php 的 worker 拉起 shell 进程执行任意命令,跨 x86-64 / ARM 一致可靠,worker 重启后仍可靠,并且在 ARM 主机上仅凭 HTTP 请求就能绕过 ASLR。

而最难受的是它"安静":worker 不崩、HTTP 返回 200、应用日志里什么都没有------因为应用代码压根没跑。

二、根因:longjmp 跳过了 C 栈展开

要看懂这两个洞,得先接受一个反直觉的事实:PHP 处理"内存不够了"的方式,不是返回错误,而是直接跳走。

2.1 PHP 遇到致命错误时到底做了什么

PHP 的致命错误路径终点是 zend_bailout():

c 复制代码
// Zend/zend.c(官方公开源码,节选)
ZEND_API ZEND_COLD ZEND_NORETURN void _zend_bailout(const char *filename, uint32_t lineno)
{
   ...
   CG(unclean_shutdown) = 1;   // 只留一个"这次没收干净"的标记
   ...
   LONGJMP(*EG(bailout), FAILURE);   // 直接跳回外层 setjmp 点
}

LONGJMP 是 C 里的"非局部跳转"。它不做栈展开 :从出错点到 zend_catch 之间的所有 C 函数帧被直接丢弃------析构函数不执行、引用计数不回滚、清理函数不会被调用、也没有任何地方记录"这个数据结构只建了一半"。

跳回来之后,PHP 继续走它那套假设一切结构都处于一致状态 的常规请求清理流程(php_request_shutdown())。两个 CVE 讲的就是这个假设在哪两处破了。

另外记住两个组件:

  • HashTable :PHP 里所有数组的底层结构,包括 $_POST 这个超全局变量;
  • Zend MM :PHP 自带的 per-request 分配器,位于 PHP 和 OS 之间。小分配走按块大小分档的空闲链表,严格 LIFO,且回收的内存块从不清零------这是"陈旧字节可被攻击者控制"的物理基础。

2.2 CVE-2026-109618:先记账,后分配

出问题的函数是所有字符串键插入的底层实现 _zend_hash_str_add_or_update_i():

c 复制代码
// Zend/zend_hash.c(官方公开源码,节选)
add_to_hash:
   idx = ht->nNumUsed++;          // ① 先记账:表里"多了一个元素"
   ht->nNumOfElements++;
   p = ht->arData + idx;
   p->key = key = zend_string_init(str, len, ...);   // ② 后分配:这一步可能 OOM

顺序是关键。① 已经把 nNumUsed 加一了,② 才去分配 key 的副本。如果 ② 正好撞上 memory_limit,zend_bailout() 就把请求掀翻,而这个 bucket(ht->arData[idx])永远没被填完:它的 value、hash、key 指针全是分配器留下来的陈旧堆数据。

然后清理流程进场:zend_array_destroy() 老老实实遍历 nNumUsed 个 bucket,把这条"幽灵条目"当成正常的 zval 处理------i_zval_ptr_dtor() 会去解引用里面的指针、在指到的地址上递减引用计数,甚至在计数归零时按陈旧字节里的 type tag 去索引函数指针表并分发调用。

攻击者要控两件事:

  1. 控"在哪 OOM" 。发一个 URL-encoded POST,字段名做成 ZZZZ...ZZZZ[x]=1 这种超长嵌套形式,php_register_variable_ex() 会把外层名字作为新数组键插入------键的长度由攻击者直接决定 ,调到刚好越过 memory_limit,bailout 就精确落在 key 分配那一行。
  2. 控"陈旧字节是什么" 。Zend MM 的分档 + LIFO 回收行为足够确定。同请求里更早的一个字段,其值被 PHP 复制解码后立即释放,就能让释放出来的内容刚好占住随后被"认领"的那个块。于是幽灵条目变成一个完全由攻击者定义的伪造结构。

💡 一句话概括 109618:表说"我多了一个元素",而那个元素的字节是上一次分配剩下的垃圾,清理流程信了。

2.3 CVE-2026-109619:跨请求的僵尸全局

109619 是同一个机制,但损害落在进程级全局变量上,利用窗口被推到了下一个请求。

被中断的操作会把状态缓存在模块级全局里(错误状态、session 标识与处理器状态、序列化上下文、上传进度状态等),并且依赖执行到自己的 cleanup 才清除。致命错误一跳,teardown 被跳过,全局仍然指着 per-request 堆、嵌套计数器仍然声称"操作正在进行中"。

请求关闭时整个 per-request 堆被释放------但全局变量属于进程而不是请求,它就活下来了。于是 PHP 手里握着一个指向已释放内存的模块级指针。

两个因素让这份僵尸状态既可达又可控:

  • worker 复用:Apache mod_php 这类常驻 worker 部署,同一个进程接着处理下一个请求,陈旧全局被原样读到;
  • 启动顺序 :php_request_startup() 在跑扩展的 request-init 回调之前就解析请求变量。所以 multipart 上传进度处理会在"本该重置这些全局的扩展"有机会动手之前,就去消费 session / 序列化代码。

所以 109619 是两阶段 的:请求一当"生产者"污染 worker,请求二当"消费者"触发对已释放内存的读写。触发入口不止内存耗尽------执行超时 和客户端断连 也走同一条 zend_bailout() 路径。

三、谁最危险:SAPI 与配置的可达性

3.1 worker 复用是放大器

官方明确说 mod_php 风险最高,不是随口一说:

维度 Apache mod_php PHP-FPM
worker 复用 同一进程连续处理请求,跨请求利用天然可行(109619 的核心暴露面) 启动阶段失败的 worker 会被杀掉;但已进入脚本阶段被中断的 worker 仍存活,同样暴露
内存布局稳定性 prefork 下所有 worker 派生自同一父进程,重启后布局依旧,payload 可零成本反复精修 相对不稳定
官方判断 风险最高,单个匿名请求可绕过全部应用代码 部分缓解,绝非免疫

受影响 SAPI 全清单:PHP-FPM、Apache mod_php、php-cgi、FrankenPHP、LiteSpeed。凡是"对外提供 Web 请求处理能力"的 PHP 安装,都在范围内------包括你 docker pull php:8.1-apache 拉下来的那个镜像。

3.2 memory_limit 与 post_max_size 的比值

这是最容易做、也最容易被忽略的一次判断。攻击者要制造"解析请求时恰好耗尽内存",前提是请求体能在不超过 post_max_size 的情况下把 memory_limit 吃穿。经验判据是:

text 复制代码
可达条件 ≈ memory_limit < 4 × post_max_size
配置组合 是否可达 说明
post_max_size=8M / memory_limit=128M 默认组合,URL-encoded 路径不可达 但 multipart 路径仍要看字段数与嵌套深度
post_max_size=64M / memory_limit=128M 可达 常见于"能传大附件"的业务
post_max_size=100M / memory_limit=256M 可达 后台导入、素材站常见
post_max_size=2M / memory_limit=32M 不可达 小站常见,风险显著降低

⚠️ 注意:post_max_size 可以在 .htaccess 或虚拟主机里按目录设置。共享主机上,单个站点自己就能把自己搞成脆弱配置------所以"我们的默认镜像是安全的"这句话不成立。

四、实战:30 分钟完成自查与修复

4.1 清点:那些"看不见的 PHP"

先别急着 apt upgrade。真正让人踩坑的不是你亲手装的 PHP,而是别人替你装的。

bash 复制代码
# 1) 找所有 PHP 可执行文件(绕过包管理器装的也要算)
for p in /usr/bin/php* /usr/local/bin/php* /opt/*/bin/php*; do
  [ -x "$p" ] && printf "%-40s %s\n" "$p" "$("$p" -r 'echo PHP_VERSION;' 2>/dev/null)"
done

# 2) 正在监听/对外服务的进程里,有哪些是 PHP 系的
sudo lsof -nP -iTCP -sTCP:LISTEN | grep -Ei 'php|httpd|apache|nginx|litespeed|frankenphp'

# 3) 容器镜像(Podman 把 docker 换成 podman 即可)
docker ps --format '{{.Names}}\t{{.Image}}' | grep -Ei 'php|wordpress|nextcloud|matomo|whmcs'

# 4) 面板/自带运行时的产品,单独列出来人工核对
#    常见形态:控制面板、工单系统、电商 SaaS、监控探针、路由/NAS 的 Web 管理页

清点结果建议直接落成一张表,避免"我以为只有两台":

资产类型 例子 常见漏项
自建站点 业务后台、官网 .htaccess 里按目录改过 post_max_size
CMS WordPress / Drupal / Joomla 插件自带的 composer 依赖锁定了 PHP 版本,不敢升
面板/工具 各类运维面板、工单、监控 自带 PHP 运行时,不走系统包管理器
容器 php:8.x-fpm、php:8.x-apache 镜像构建时间早,基础层没重建
设备/一体机 防火墙、NAS、打印服务器 厂商固件内嵌 PHP,永远不告诉你有这回事

4.2 判定:你的实例是否真的可达

单机一行命令拿到关键四项:

bash 复制代码
php -v | head -1
php -i | grep -E '^(memory_limit|post_max_size|upload_max_filesize|session\.upload_progress\.enabled|Loaded Configuration File)'

对着一行表判断:

PHP 分支 受影响的最高版本 修复版本
8.5.x ≤ 8.5.11 8.5.12
8.4.x ≤ 8.4.26 8.4.27
8.3.x ≤ 8.3.35 8.3.36
8.2.x ≤ 8.2.34 8.2.35
8.1 及更早 全部 官方不再出新版本,只有 7.4 / 8.0 / 8.1 的源码级 backport

💡 顺带提醒:8.2 分支在 2026-12-31 EOL。还在 8.2 上的项目,这次修完之后应该顺手把"升 8.3/8.4"排进十二月的计划,而不是明年再被打一次。

4.3 升级:为什么必须重启 worker

官方在公告里专门写了一句很多人会划过去的话:更新后要重启服务,让运行中的进程加载修复后的代码。

原因很直白:PHP-FPM 的 master 进程、Apache 的 prefork worker、OPcache 的共享内存,都是"进程活着就一直用老代码"。你 apt install 完,磁盘上是新版本,内存里跑的还是旧的那套 Zend。

bash 复制代码
# Debian / Ubuntu 系
sudo apt update && sudo apt install --only-upgrade php8.4-fpm php8.4-cli php8.4-common

# RHEL / Rocky / Alma 系(第三方 remi 源在发布当天没有更新,官方建议切回发行版包或自行编译)
sudo dnf upgrade --refresh 'php*'

# 关键:重启,而不只是 reload(reload 不重建 worker 内存布局)
sudo systemctl restart php8.4-fpm
sudo systemctl restart apache2        # 用 mod_php 的必须重启 httpd/apache2

# 容器:重建而不是重启
docker compose up -d --force-recreate php

# 验证:确认运行中的版本,而不是磁盘上的版本
php -v | head -1
sudo lsof -p "$(pgrep -o php-fpm)" | grep -E 'libphp|php$' | head

⚠️ 用 mod_php 的机器优先级最高。如果一台机器上同时跑着 php-fpm 和 mod_php(历史遗留很常见),两个都要重启,别只重启你记得的那个。

4.4 缓解:打不了补丁时的临时方案

官方措辞很克制也很诚实:这些措施"只降低特定攻击路径的暴露面,不能让安装变安全 "。换句话说,它们是买时间 ,不是修问题。

针对 CVE-2026-109618(OOM 破坏):

ini 复制代码
; php.ini 或 FPM pool 配置
; 1) 关掉可控的内存上限失败(必须让应用无法覆盖)
;    FPM: php_admin_value[memory_limit] = -1
;    Apache: php_admin_value memory_limit -1
;    并逐一检查 pool / <Directory> / .htaccess 有没有把有限值又设回来
memory_limit = -1

; 2) 压低请求体上限(不需要上传就 8M,需要上传就 32M)
post_max_size = 8M
upload_max_filesize = 6M   ; 必须小于 post_max_size

LiteSpeed / Nginx / 反代层再补一道"解码后请求大小、字段数、multipart 段数、header 数、嵌套深度"的限制,并且要覆盖没有可用 Content-Length 的请求(chunked 传输会被漏掉):

nginx 复制代码
# nginx 侧示例:限制请求体与缓冲,减少单请求可支配内存
client_max_body_size 8m;
client_body_buffer_size 128k;
large_client_header_buffers 4 8k;

针对 CVE-2026-109619(陈旧全局):

ini 复制代码
; 必须在"请求解析之前"就生效的位置配置:
;   php.ini / FPM pool / 服务器配置
; 不要指望 ini_set() 或 FPM 的 .user.ini ------ 它们的生效时机太晚
session.upload_progress.enabled = 0

; 仅 PHP 8.5:关掉致命错误回溯,堵住已确认的那条 backtrace 路径
fatal_error_backtraces = 0

改完每个 worker 都要重启,否则改的只是配置文件。另外还要做两件"配置解决不了"的事:

  • 清掉那些"在初始化之前就吐诊断信息"的废弃 per-request 配置------用 error_reporting / display_errors / @ 去藏消息没有用,因为要堵的是"产生诊断"这个动作本身;
  • 修掉攻击者能触达的致命错误与执行超时(回调、上传、导入、清理逻辑里的那几处)。

⚠️ 还有一个反直觉的坑:不要用 RLIMIT_AS / RLIMIT_DATA 去当内存护栏。它们会让分配直接失败,恰好把执行流推回那条有漏洞的分配路径。要用就用容器 / cgroup 的内存上限,并且配成"只杀受影响的 worker"的策略。

五、检测:怎么判断已经被打了

5.1 唯一的高保真信号

成功利用几乎不留痕:worker 不崩、HTTP 200、应用日志安静。唯一稳定的产物是 Web 服务器错误日志里的一条通用致命错误:

text 复制代码
PHP Fatal error:  Allowed memory size of 16777216 bytes exhausted
(tried to allocate 3670048 bytes) in Unknown on line 0

鉴别点全在最后半句:in Unknown on line 0。

它意味着当时没有任何 PHP 文件在执行------错误发生在脚本启动之前。这就把它和大量"脚本内部数组太大导致 OOM"的噪音彻底分开了。抓这一条:

bash 复制代码
# 从 10-17 开始回溯(蜜罐记录到的最早利用尝试日)
sudo zgrep -hE 'Allowed memory size of .* exhausted' /var/log/apache2/*error*.gz \
  /var/log/nginx/*error*.gz 2>/dev/null \
  | grep 'in Unknown on line 0' | sort | uniq -c | sort -rn | head -50

# FPM 侧
sudo journalctl -u php8.4-fpm --since '2026-10-17' \
  | grep -E 'memory size of .* exhausted' | grep 'Unknown on line 0' | wc -l

如果这条规则命中量从 10-17 之后突然从 0 变成几十上百,基本可以按"已被扫描/尝试利用"处理。

5.2 进程与文件层的后渗透痕迹

日志只能证明"有人在打",证明"打穿了"要看结果:

层次 看什么 命令/位置
进程树 httpd / php-fpm / lshttpd 之下出现 sh、bash、curl、wget、python 子进程 ps -eo pid,ppid,user,lstart,cmd --forest
文件 Web 根目录下新增的 .php 文件,尤其是文件名随机、内容以 eval/assert/base64_decode 开头的 find /var/www -name '*.php' -newermt '2026-10-17'
计划任务 www-data 身份写入的 cron / systemd timer crontab -u www-data -l、/etc/cron.d/
凭据 .env、wp-config.php、config/database.php 被读取的 atime 变化 find /var/www -name '.env' -newerat '2026-10-17'
出网 Web 进程发起的异常外连 出口防火墙/NetFlow,关注 www-data 的 established 连接

💡 提醒一句:find -newer 这类判据在有频繁发布的机器上噪音很大,建议先只在静态站点和 WordPress 站上用,业务仓库用文件完整性基线比对更靠谱。

5.3 已中招后的处置顺序

顺序错了会丢证据,也会漏后门:

  1. 先隔离,后取证 :摘掉公网入口或挂维护页,不要直接关机(内存里的证据会没);
  2. 保留副本 :磁盘快照 + /var/log 整包 + 当时的进程列表;
  3. 打补丁、重启 worker------先堵住入口,否则清理完又被重打;
  4. 轮换全部凭据 :数据库口令、对象存储 AK/SK、第三方 API Key、.env 里的一切、以及 wp-config.php 里的盐值。这类漏洞能读进程内存,内存里就是这些明文;
  5. 清理后门再恢复上线 :WebShell、cron、SSH authorized_keys、被改过的 .htaccess、被注入的 mu-plugin;
  6. 最后再复盘:为什么这台机器的 PHP 版本不在清单里。

六、辟谣:网上那些复现结论错在哪

这一段可能是本文最有价值的部分。漏洞是真的,但围绕它的二手信息里,错误的比正确的传播得更快。

6.1 说法一:根因是 session.upload_progress 的 zval UAF

这是流传最广的一条。几个公开 PoC 仓库把根因定位成:rfc1867 上传进度处理器把 zval* 缓存进 session 的 HashTable,上传停顿超过 session.upload_progress.min_freq 后,后一个 FILE_DATA 事件重新初始化并释放了该表,缓存指针变成悬垂指针,下一次写入 bytes_processed 就打穿了。

听起来非常自洽,也确实能对上 CVE-2026-109618 / 109619 在 ext/session/session.c 里的两处行号。但有人按公开 PoC 在 ASAN 构建下逐项验证(PHP 8.4.4、8.5.4、5.6.40,关 ASLR、投毒调用地址都试了),结论是:这两条 CVE 的 UAF/RCE 没复现出来。而对照官方 patch diff 后可以看到,补丁实际做的是三件事:

  1. 重做 OOM 跳出路径 :移除 OOM 场景下的 longjmp(),改为在下一个 VM 安全点触发延迟的"软 OOM",到更高阈值时用 _exit() 做"硬 OOM"。这样关闭流程不会再看到半成品状态,同时保留了 memory_limit 的正常能力;
  2. 在 RINIT 之前初始化 session 状态;
  3. 重置上一个请求遗留下来的状态。

也就是说:根因说法错了,攻击面说法对了一半 。session.upload_progress 确实是 109619 已确认的一条 consumer 路径(官方公告自己写了),但它是一个放大器 ,不是根因。把放大器当根因的后果很实在------你会以为"我关掉 session.upload_progress.enabled 就安全了",而官方明确说这只堵住已确认的 session 路径,堵不住其他变体。

💡 判断一条漏洞分析靠不靠谱,最简单的办法:看它有没有对官方 patch 的 diff。只对着 PoC 反推根因的,八成是猜的。

6.2 说法二:CVSS 10.0,只影响 PHP 8

两个数字都被传歪了:

流传说法 实际情况
CVSS 10.0 官方 CVSS 4.0 为 9.2;部分机构按 CVSS 3.1 口径给到 9.8,都不是 10.0
只影响 PHP 8.x 缺陷代码可追溯到 PHP 5.4 时代,7.x / 8.x 全在范围内;只是官方只为 8.2--8.5 出了正式版本
8.1 和更早也有人修 官方不再 为 8.1 及更早出新版本,只发了 7.4 / 8.0 / 8.1 的最小源码 backport;发行版和 LTS 厂商可能提供自己的回补

"只影响 8.x"这条误传最要命:它会让一堆跑 7.4 的老站直接放弃排查,而那些恰恰是最不可能有 WAF、最没人维护、最容易被当成跳板的一批。

6.3 说法三:上了 WAF 或改了配置就等于修好了

PHP Foundation 的原话值得抄下来贴在群里:

缓解措施"降低特定攻击路径的暴露面,但不能让安装变安全"。

拆开看:

  • WAF 是概率性的。Cloudflare 自己在公开披露前约 9 小时就上了规则,公告后攻击量仍然从数万涨到数百万------说明规则能挡掉一批已知 pattern,挡不住变形。而且自建站的 WAF 往往只覆盖 HTTP 层,而这条链的 payload 是"合法的 POST 结构 + 精心调过的字段长度",长得非常正常。
  • memory_limit=-1 是双刃剑。它确实去掉了"可控的内存上限失败",但你这台机器的可用内存变成了攻击者的拒绝服务预算。必须配 cgroup 上限兜底。
  • session.upload_progress.enabled=0 只堵一条路。官方说的是 "blocks the confirmed session paths but not other variants"。
  • 改配置不重启 = 没改。这条重复三遍都不过分。

6.4 说法四:升级完内核就万事大吉

这是最隐蔽的一条。内核补丁修的是 PHP 自身的内存破坏,但应用层的攻击链不会因为内核升级而消失。

验证过的一个仍然成立的链路:session.upload_progress 把上传的文件名原样写进 session 文件(不做净化),并且 multipart 处理器接受 POST body 里的 PHPSESSID,从而绕过 session.use_only_cookies。如果目标应用本身存在 LFI,就能把会话文件包含进来执行注入的代码。这个组合在 cleanup=0 下成功,在默认 cleanup=1 的竞态下 3/3 成功,在 enabled=0 下换成"包含上传的临时文件"仍然成功。

结论要分两层说:

  • 内核 CVE 是真的,升级是唯一完整修复;
  • 但"应用层有 LFI"这个前提本身就是一个独立的高危问题,它不会因为你把 PHP 升到 8.5.12 就消失。

所以正确的动作是两条并行:今天升内核 ,这周排一遍 include($_GET[...]) 这类 sink。

七、这件事留下的三个长期启示

7.1 影子 PHP 资产清单

这次让各国政府手忙脚乱的,不是"我们的主站用了 PHP",而是没人能在一小时内答出"我们到底有多少个 PHP 实例"。面板、工单、监控探针、NAS 管理页、打印机 Web 控制台、厂商一体机------它们都跑 PHP,都不在 CMDB 里。

一个可落地的做法:把"运行时清单"当成独立资产,而不是附属在应用下面。最小字段就四个:运行时 + 精确版本 + 对外可达性 + 负责人。这次能在一小时内筛出所有 php < 8.2.35 的团队,下一次就能少熬一个通宵。

7.2 AI 找洞时代,补丁窗口正在消失

这条时间线值得单独记一下:2020 年就有人发现过这个 OOM 缺陷,2024 年在一次 PHP 审计里被重新发现,但因为没有证明可利用性,就没有修。

十几年里它一直躺在那儿,因为"从 OOM 到 RCE"需要一条人类研究员很难手工走完的链。前沿模型把这条链走通了。Wiz 的说法是,AI"显著降低了把一个长期存在的缺陷变成可用 exploit 的门槛";美国某云安全公司 CTO 的评论更直白:这不会是最后一次,AI 会持续在广泛使用的软件里挖出同类问题。

对我们这些写业务代码的人,实际含义只有一个:"这个洞太复杂了,应该没人能利用"不再是一个可以接受的排期理由。复杂度的护城河,正在被算力填平。

7.3 把"重启才生效"写进变更流程

这次有一批团队会经历一个非常难受的场景:补丁装了、版本号对了、还是被打穿。因为跑在内存里的还是旧的那套 Zend。

这不是新问题,但它在"语言运行时级"的安全更新上后果最严重。建议把变更检查单里的那一行固定成:

text 复制代码
[ ] 安全更新已应用
[ ] 运行中的进程已确认加载新版本(不只是磁盘上的版本号)
[ ] 无残留的旧 worker / 旧容器 / 旧 master

八、总结

把整件事压缩成一张表:

维度 结论
是什么 PHP 内核两个无需认证的 RCE,代号 php2shell,CVSS 4.0 = 9.2
编号 CVE-2026-109618(OOM 内存破坏)、CVE-2026-109619(跨请求陈旧全局)
根因 zend_bailout() 用 longjmp() 跳出,不做 C 栈展开;清理流程信任了一个"只建了一半"的结构
修复版本 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35,7.4 / 8.0 / 8.1 仅源码 backport
最危险配置 Apache mod_php 常驻 worker + memory_limit < 4 × post_max_size
唯一完整修复 升级 + 重启 worker
检测信号 in Unknown on line 0 的 Allowed memory size ... exhausted
最大误区 把 session.upload_progress 当根因;以为 WAF / 改配置 / 只升内核就够了

如果要留一句话给还在排期的同事:

先把 php -v 跑一遍,比读完任何一篇分析都重要。 这两个洞不挑框架、不挑代码质量、不挑你有没有登录态------它挑的只是"你什么时候重启"。


如果本文对你有帮助,欢迎点赞、收藏。你们团队这次是怎么盘的(有没有在面板/设备里翻出藏起来的 PHP)?评论区聊聊,也给后面排查的人留个线索。

相关推荐
前端老干部1 小时前
文案写完了,Codex 能接着做视频吗?用飞影跑了一条咖啡口播
人工智能
我想问问天1 小时前
AI 越来越会写代码,工程师的价值在哪里?
人工智能
桌管小李1 小时前
终端安全一体化实战:AI 审计、行为分析、零信任、防拍照与远程运维全解析
运维·服务器·网络·人工智能·windows·安全·macos
AI日报派送佬1 小时前
2026年10月22日AI行业日报|AI智能体正面开战、云厂商模型生态洗牌、算力基建加速落地,同时智能体安全、高危漏洞风险持续拉响警报
人工智能·机器学习·openai·英伟达·ai安全·anthropic·ai日报
王晖_Oscar1 小时前
如何搭建生产级的长链路 AI 工作流?
人工智能
米小虾1 小时前
Agent架构10——八条穿越模型迭代的设计原则
人工智能·agent
米小虾1 小时前
Agent架构09——从提示工程到 Graph 工程:五个范式的包含关系
人工智能·agent
田里的水稻1 小时前
FA_CNN卷积神经网络十二_CIFAR10全流程PyTorch
人工智能·pytorch·深度学习·神经网络·cnn
水如烟1 小时前
孤能子视角:姜还是老的辣?——年轻与资深的创新优势:"切"的锐度(高CD)与"续指"的厚度(高PD)
人工智能