PHP2Shell漏洞回顾与修复指南
一句话版:能升级就先升级——8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 或发行版回移植版本。升不了,就关掉
session.upload_progress、收紧请求体限额、收窄 multipart 入口;think-swoole 用户按机制分析风险极低,但仍应跟进运行时更新。
事件速览
2026 年 10 月 21 日 14:00 UTC,PHP 官方发布带外安全版本,修复两个核心引擎漏洞,CVSS v4.0 均为 9.2,无需认证、无需用户交互,攻击请求在应用代码执行之前就已生效。由于细节与 PoC 在补丁发布前泄露,社区将其称为 PHP2Shell;SANS 一度将 infocon 提升到黄色,Cloudflare 报告已观测到在野利用。
| 编号 | 成因 | 攻击者怎么打 | 影响面 |
|---|---|---|---|
| CVE-2026-109618 | 内存耗尽(OOM)打断请求初始化,清理阶段把半初始化的数据当成有效数据 | 用 multipart 请求把 PHP 的内存推到耗尽:不带 Content-Length 的 multipart 可绕过 post_max_size,请求头数量无上限,max_file_uploads 不计空文件 | mod_php 已验证可利用;FPM / php-cgi / FrankenPHP / LiteSpeed 同样在风险名单 |
| CVE-2026-109619 | 请求级全局状态未复位,形成 UAF(释放后使用) | 同一 worker 上先后两个请求:前一个以致命错误、OOM 或超时收场并留下残留状态,后一个在初始化阶段读到它 | 同上;已确认路径包括 session 上传进度(默认开启)与 PHP 8.5 的 fatal_error_backtraces |
修复版本:8.2.35 / 8.3.36 / 8.4.27 / 8.5.12。PHP 7.4 / 8.0 / 8.1 只提供源码级回移植,官方不再发版;Ubuntu 已发布 USN-8948-1(覆盖 22.04 / 24.04 / 26.04 / 26.10),其余发行版以各自追踪页为准。
关于“影响所有版本”:PHP 5 以来的版本在机制上都在风险范围,但官方只为 8.2–8.5 出构建。所以判断一台机器是否已修复,认发行版安全公告和包版本,而不是
php -v的主版本号。
先纠正三个流传的说法
漏洞刚公开时各种版本的“技术解读”满天飞,其中三个错误流传最广:
| 流传说法 | 实际情况 |
|---|---|
| 两个漏洞都是 session 上传进度的 UAF | 只有 109619 与它相关,而且它只是已确认的一条路径;109618 是 OOM 引发的状态损坏,跟 session 无关 |
| CVSS 10.0,史上最严重 | 官方评分是 9.2(v4.0),AC——利用条件偏苛刻,不等于“随便一打就穿” |
| ThinkPHP 出了新漏洞 | 漏洞在 PHP 引擎层,与框架无关,所有 PHP 站点都是同一处境;唯一跟框架有关的变量,是应用的 session 是否把请求级数据写进了 PHP 的全局会话状态 |
对号入座:你的运行方式该怎么办
| 你的运行方式 | 怎么办 |
|---|---|
| think-swoole(常驻协程) | 已确认的利用路径基本不成立,应用层通常无需改动,保持运行时更新即可(见方案一) |
| FPM / mod_php + TP 6.0+ | ① 升级 PHP(唯一完整修复);② 过渡期三件套:关上传进度、走 TP 内置 session、收紧限额(见方案二与加固清单) |
| FPM / mod_php + TP 5.x | 方案二对其不适用——TP5 的 session 本质是原生 session 封装,须先升级到 TP 6.0+ 再按上行处理 |
| 任何模式 | 无法立即升级又暴露公网:先在 WAF / 网关限制访问,或临时下线 |
方案一:think-swoole 用户——风险最低,但别当免死金牌
Swoole 常驻模式下,两个 CVE 的已验证利用链都缺少必要环节,四条机制依据值得说清楚:
不经过 PHP 的 multipart 解析器:请求体由 Swoole 自行解析并填充
$_POST/$_FILES,109618 的入口(PHP 的 multipart 解析与内存耗尽)不参与;PHP_SESSION_UPLOAD_PROGRESS驱动的上传进度回调也不会触发,109619 的已确认路径之一随之消失;没有按请求生效的 INI:
.htaccess/php_value/PHP_VALUE这类覆盖在 Swoole 下不生效,109619 所需的“初始化阶段触发告警”入口大幅减少;Session 自成体系:think-swoole 基于
swoole_table+ ThinkPHP 缓存的独立 Session 实现,不走session_start(),应用不会向 PHP 的全局会话状态发布请求级数据;仍要做的两件事:Swoole 扩展与 PHP 运行时保持更新(发行版会回移植补丁);避免在攻击者可达路径(上传、导入、错误页、析构)上制造致命错误和超时——那正是 109619 里“留下残留状态”的第一个请求。
判断依据说明:以上是基于公开通告与框架源码的机制分析,不是官方结论。如果你的 think-swoole 应用里仍在调用原生
$_SESSION/session_start(),或装有会触发 per-request INI 的扩展,请按方案二处理,不要套用“免处理”。
方案二:FPM / mod_php 走 TP 内置 session(仅 TP 6.0+)
版本前提:TP 6.0 / 6.1 / 8.x 的 think\session\Store 是纯 PHP 实现——读写全部走自有驱动(file 落 runtime/session/,redis / memcached 同理),全程不调用 session_start() / session_set_save_handler(),因此应用不会把请求级数据发布到 PHP 的全局会话状态,官方验证的 FPM 应用依赖型利用链随之断掉。TP 5.x 不适用:其 think\Session 是原生 session 的封装(session_set_save_handler() + session_start() + $_SESSION),必须先升级框架。
配置本身很简单,用内置驱动即可:
// config/session.phpreturn [
'type' => 'redis', // 或 file / memcached / memcache
'auto_start' => true,];三条注意:
统一用
session()助手或Session门面,不要混用$_SESSION、session_start()、session_set_save_handler()等原生 API,第三方扩展同样要排查;必须配套关闭上传进度——该处理器在引擎层运行,不要求应用调用
session_start(),因此还存在与 TP 无关的利用链;这是纵深防御而非完整修复,完整修复仍然只有升级 PHP。
; php.ini 或 FPM pool,启动期生效 session.upload_progress.enabled = Off
不要放在
.htaccess里。每请求生效的 INI 覆盖本身就是 109619 利用链的一环,用它做防护等于往火上浇油。ini_set()同样无效——PHP 在应用代码运行前就处理完上传了,函数会直接返回 false。
过渡期加固清单(无法立即升级时)
PHP 侧:
; php.ini / FPM pool 配置 session.upload_progress.enabled = Off fatal_error_backtraces = 0 ; 仅 PHP 8.5+,另一条已确认的 109619 路径 post_max_size = 8M ; 按业务收紧,并在 Nginx/Apache 侧独立强制请求体上限 max_file_uploads = 20max_input_vars = 1000max_input_nesting_level = 64; file_uploads = Off ; 业务确实没有上传时再开 memory_limit = -1 ; 管理员强制设置,让 OOM 路径打不中(见下方说明)
关于内存:PHP 给出的部分缓解是“由管理员显式设置
memory_limit=-1,让请求不会走到 OOM 分支”,同时配一个很小的post_max_size。反过来说,不要为了“省内存”去调小memory_limit,也不要用RLIMIT_AS/RLIMIT_DATA给 PHP 进程加 OS 级内存上限——那恰好是把请求推进 OOM 的路径。
Web 服务器侧:Nginx 收紧 client_max_body_size;对确认不接收上传的路由,直接拦掉 Content-Type: multipart/form-data。
# http {} 段
map $http_content_type $is_multipart { default 0; "~*^\s*multipart/form-data" 1; }map $uri $upload_route { default 0; "~^/api/upload$" 1; }# server {} 段
set $mp_block "$is_multipart$upload_route";if ($mp_block = "10") { return 415; }WAF 侧:拦掉任何请求体里带上传进度字段的请求,作为 PHP 之外的独立一层。
SecRule ARGS_POST_NAMES "@streq PHP_SESSION_UPLOAD_PROGRESS" \ "id:1026109618,phase:2,deny,status:403,log,t:none,msg:'PHP upload progress field'"
需要 SecRequestBodyAccess On;字段名若改过 session.upload_progress.name,规则同步替换。共享主机或官方 Docker 镜像等改不了主 php.ini 的场景,可以用站点根目录的 .user.ini 或在 /usr/local/etc/php/conf.d/ 下加一份配置后重建镜像。
应急清单
| 优先级 | 动作 |
|---|---|
| P0 · 立即 | 升级 PHP 至 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 或发行版回移植版本,重启 FPM / Web;无法升级且暴露公网 → WAF 限流、收窄 multipart 入口或直接下线 |
| P1 · 24 小时内 | session.upload_progress.enabled = Off 并重启 worker;PHP 8.5 设 fatal_error_backtraces = 0;收紧 post_max_size 等限额;Nginx 侧限制请求体并拦截非上传路由的 multipart |
| P2 · 一周内 | swoole 用户保持运行时更新;FPM 用户确认 session 走框架内置管理、无原生混用;清查容器镜像与面板、CDN 回源、第三方软件内嵌的 PHP;开启 E_ALL + log_errors,并监控 Web 用户派生 sh / curl / wget 的进程 |
注意事项
关
display_errors、加@、改error_reporting都没用:漏洞发生在引擎层,不在你的错误处理逻辑里;只封
PHP_SESSION_UPLOAD_PROGRESS不等于修复:它挡住的是 109619 的已知路径,109618 走的是 multipart 内存耗尽,且不排除有其它变体;打了 WAF 规则不等于打完补丁:WAF 解析 multipart 的方式与 PHP 不同,只能算一层缓冲;
不要运行来源不明的“一键检测脚本”和 PoC:漏洞细节在补丁前就已泄露,这类脚本本身就是投毒重灾区。