2026 年 10 月 21 日,PHP 官方发布 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35,修复两个 CVSS v4.0 9.2 分的未认证 RCE(研究团队命名 php2shell)。未认证,一个 HTTP 请求,应用代码执行前即可完成 RCE,影响 5.4 以来全部版本。
| CVE | 根因 | 官方确认的利用路径 |
|---|---|---|
| 2026-109618 | OOM 时 longjmp 打断,半初始化内存被清理流程消费 | mod_php 默认配置,未认证请求直接触发 |
| 2026-109619 | worker 全局状态未重置,踩上一个请求释放的内存 | session.upload_progress;致命错误回溯 |
| 分支 | 安全线 | 状态 |
|---|---|---|
| 8.5 | 8.5.12 | 已修 |
| 8.4 | 8.4.27 | 已修 |
| 8.3 | 8.3.36 | 已修 |
| 8.2 | 8.2.35 | 已修 |
| 8.1 及更早 | 无补丁 | EOL,只能迁移或隔离 |
暴露面数据(均来自 2026 年 10 月 21 日前后报告,量级参考):
- Wiz:PHP 支撑全球约七成网站
- Dataminr:同一天扫到至少 200 万个暴露在公网的 PHP 实例
- Cisco Talos:全球约一半的 PHP 站点压根不会升级,因为版本太老,官方已不提供更新
- 奇安信 10 月 22 日称已发现在野利用;Talos 截至 21 日未见确认失陷
漏洞详情:两个洞,一个根因
CVE-2026-109618(OOM 损坏):PHP 通过 longjmp() 在请求超过 memory_limit 时中止。中止路径上请求内部数据可能只初始化了一半,request shutdown 的清理流程把这些数据当作有效数据处理,释放从未初始化的内存。攻击者构造 multipart 上传请求即可触发,官方在 Apache mod_php 默认配置下验证了未认证完整利用。Calif 放出的演示视频里,从发请求到拿下 shell,大约 15 秒。
CVE-2026-109619(残留全局状态):worker 的全局状态持有上一个请求已释放内存的指针,同一 worker 处理的下一个请求在正常初始化重置前访问到这些陈旧状态,构成 use-after-free。官方确认两条利用路径:session.upload_progress(mod_php 与 FPM 均可)和致命错误回溯(PHP 8.5)。
两个洞,一个根因:请求在异常结束的边界上,内存留在了一个损坏状态。
nginx
# 临时减伤(非完整修复):在前端限制解码后请求体大小
client_max_body_size 8m; # 不收文件上传的业务
# 注意:不要用 RLIMIT_AS / RLIMIT_DATA 顶替 memory_limit,
# 官方警告这会诱发 mmap 失败,正好踩进 longjmp 脆弱路径
为什么埋了十几年
2024 年 PHP 基金会出钱包了 Quarkslab 做外部审计,核心开发当时就注意到了 OOM 的最初迹象。理论上能写 exploit,但没人找到具体路径,搁置。基金会的复盘原话:"这次延迟是一次结构性失败。"
转折在今年:7 月 30 日 Calif 进场审计,8 月 11 日报告了无条件 RCE,OpenAI 同一天独立报了同一个根因,8 月 24 日 Wiz 的报告也到了。三家都用了前沿 AI 模型,Wiz 全程用 Wiz Atlas、Gemini 4 Argon 和 GPT-5.6 Sol。Wiz 的原话:"前沿 AI 把一个存在已久的缺陷变成了可用的 exploit。"
升级实操:七步,一步都别跳
有人甩过来一句:"先升了再说,别问那么多。"升肯定要升,但"只换二进制不重启"这种半吊子升级等于没升。七步照着走。
第一步:核版本,别信面板别信包管理器
命令行出来的才算数,CLI 和 FPM 可能不是同一个版本,两个都要看:
bash
php -v
php-fpm -v
# 机器上装了几个 PHP,当前指向哪个
which php
ls -l /usr/bin/php*
update-alternatives --display php
# 包管理器眼里装了哪些包、什么版本
dpkg -l | grep -E '^ii +php' | awk '{print $2, $3}'
rpm -qa | grep php
# Docker 的,进容器看,别信镜像 tag
docker exec -it <容器名> php -v
用宝塔、phpStudy、oneinstack 这类面板的注意:面板显示的版本号和 FPM 实际跑的版本不是一回事,以 php-fpm -v 为准。真实案例:面板显示 8.2,FPM 实际跑着 7.4,照着面板升了个寂寞,服务 restart 了十几次都没反应过来。
第二步:备份,两分钟的事
bash
# 配置整目录备份,-a 保住权限和属主
cp -a /etc/php /etc/php.bak.$(date +%F)
# 留一份升级前的完整配置快照
php -i > /root/php-info-before.txt
dpkg -l | grep php > /root/php-pkgs-before.txt
三年前一次小版本升级没做备份,配置被新包冲掉,凭记忆重写漏一个 opcache 参数,性能掉一半,查了两个多小时。这两行 cp 命令,升级前闭着眼睛敲。
第三步:升级,按场景挑一条路
场景 A,Debian / Ubuntu,发行版源已有新版本:
bash
apt update
apt install --only-upgrade \
php8.3-fpm php8.3-cli php8.3-common
源里还没有的,加 ondrej PPA:
bash
apt install software-properties-common
add-apt-repository ppa:ondrej/php
apt update
场景 B,RHEL / Rocky / Alma,走 Remi 源:
bash
dnf install \
https://rpms.remirepo.net/enterprise/remi-release-9.rpm
dnf module reset php
dnf module install php:remi-8.3
重置模块那一步别跳过,不然 dnf 会拿系统默认流跟你打架。
场景 C,Docker。改 Dockerfile,别在容器里手升,tag 钉死小版本,--no-cache 重建,复用带旧版的 layer 是白升第一大坑:
bash
docker compose build --no-cache
docker compose up -d
docker exec -it <容器名> php -v
场景 D,编译安装。保留旧安装目录,新版本装到新目录,软链接切流量,出事切回来。
四个场景共同的坑:发行版仓库同步有延迟,官方发了公告,源里未必有包。等不了的就近换源,别干等。
第四步:重启,重灾区在这
换二进制不重启 worker,等于没升。注意是 restart,不是 reload------reload 只重读配置,不换进程里的旧代码。
bash
# Nginx + PHP-FPM,服务名各发行版不一样,挨个试
systemctl restart php8.3-fpm
systemctl restart php-fpm
systemctl restart php-fpm.service
# Apache mod_php
systemctl restart apache2 # Debian 系
systemctl restart httpd # RHEL 系
# K8s,滚动重启后确认旧 ReplicaSet 缩到零
kubectl rollout restart deployment/php-app
kubectl rollout status deployment/php-app
kubectl get rs
# 最后一道保险:看进程启动时间是不是真的变了
systemctl show php8.3-fpm -p MainPID
ps -o lstart= -p <上面的PID>
时间还是升级前的,就是没重启到位,回去查服务名。
第五步:验证,升完不算完
版本验证,CLI 和 FPM 都看,少一个都不算升完:
bash
php -v
php-fpm -v
CLI 升了 FPM 没升的情况真实存在:几年前一次升级,CLI 已是新版,探活全绿,日志干净,当场收工;三天后随手敲 php-fpm -v,旧版------业务一直在旧进程上跑,升了个命令行。
配置验证,确认现有配置没被升级冲掉:
bash
php -i | grep -E \
'memory_limit|post_max_size|upload_max_filesize'
业务验证,探活加看日志,上传、登录、支付等 core 功能手动点一遍:
bash
curl -sS -o /dev/null -w '%{http_code}\n' https://你的站点/
tail -f /var/log/php8.3-fpm.log
第六步:升不动的,按洞设缓解
EOL 分支、没人敢动的最老那台,官方缓解配置照抄。
CVE-2026-109618 的 OOM 洞,把 memory_limit 调到 -1(不触发 OOM 路径),再把 post_max_size 压小:
ini
; /etc/php/8.3/fpm/php.ini 或 pool.d/www.conf
php_admin_value memory_limit -1
php_admin_value post_max_size 8M
不接收文件上传的业务压到 8M,要传的压到 32M。32M 是上限,别超。
CVE-2026-109619 的残留状态洞,两个开关:
ini
; 关掉上传进度会话
session.upload_progress.enabled = 0
; PHP 8.5 再加这条
fatal_error_backtraces = 0
位置在 php.ini 或 FPM 池配置里,别用 ini_set()------请求解析之前不生效,挡不住这个洞。改完必须 systemctl restart FPM,再用 php -i 验证配置真的读到了。
两条红线,官方原话照抄:千万别拿 RLIMIT_AS 或 RLIMIT_DATA 顶替 memory_limit,这俩会诱发 mmap 失败,正好踩进 longjmp 那条脆弱路径。
第七步:回滚预案,先想好再动手
bash
# 配置回滚
cp -a /etc/php.bak.$(date +%F)/* /etc/php/
systemctl restart php8.3-fpm
# 包级回滚(Debian 系,版本号填备份里的旧版本)
apt install --allow-downgrades php8.3-common=8.3.35-1
稳妥做法是蓝绿:新版本起新 pool 或新容器,探活全过再切流量,旧环境原地留着,观察期过了再下线。
中招排查与 CI 卡版本
排查清单(Talos 和 Rescana 给的迹象):
- PHP-FPM 日志里段错误、worker 异常退出变多
- worker 内存占用不对劲,同一批请求响应时间抖动
- 响应头 Set-Cookie 里出现不该有的数据(CVE-2026-109619 的泄露路径会从这个口子漏内存内容)
- 业务进程名下冒出陌生子进程,尤其是 shell、curl、wget
- 服务器出现不认识的出站连接
- 站点目录里多出没部署过的文件
占任何一条,先隔离再深查,日志留证别急着清。
CI 硬断言,低于安全线直接挂:
bash
# Dockerfile 里钉死小版本
FROM php:8.3.36-fpm-alpine
# CI 里加一条
php -v | grep -qE 'PHP 8\.3\.(3[6-9]|[4-9][0-9])' || exit 1
AI 攻防之辩
AI 帮着找漏洞,帮了攻还是防,没有结论。守的一方,Wiz、Calif、OpenAI、Gemini 把一个十几年的洞变成了带补丁的公开记录;攻的一方,Cloudflare 看到攻击者改 payload 绕 WAF,watchTowr 看到了活跃利用尝试。同一把刀,磨得更快了。
信源
- PHP 安全公告 GHSA-w8hw-w9fx-j79w / GHSA-6wmf-4rv5-p3rp:github.com/php/php-src...
- Wiz php2shell 分析:www.wiz.io/blog/php2sh...
- CERT-EU Security Advisory 2026-018:cert.europa.eu/publication...
- Cisco Talos:blog.talosintelligence.com/two-critica...
版本核对脚本、PHP 缓解配置模板、排查清单已打包,见文末。
通过网盘分享的文件:2026年10月22日-PHP-REC漏洞链接: pan.baidu.com/s/1dWxv7Ypi... 提取码: 3wss
原创技术博客 · 开源项目分享 · AI全栈创作社区 idao.fun