PHP2Shell 漏洞(CVE-2026-109618、CVE-2026-109619)临时缓解记录
记录时间:2026-11-03
环境:CentOS 7.9(el7)/ Zabbix 4.4.6 / PHP 7.2.34(自编译,/data/app/php72)/ Tengine / 站点根目录 /data/app/tengine/html
一、问题现象
安全那边同步了两个 PHP 漏洞:CVE-2026-109618 和 CVE-2026-109619,等级 Important,都能导致远程代码执行。阿里云安全中心的修复文档把这组漏洞叫 PHP2Shell。
这个漏洞我早就知道,只是公司业务基本都是 Java 服务,我没往 PHP 上想。安全提醒之后才反应过来,这台 Zabbix 4.4 的前端是 PHP 跑的。
机器上查到的是系统 RPM 的 PHP:
php-5.4.16-48.el7.x86_64
php-cli-5.4.16-48.el7.x86_64
php-common-5.4.16-48.el7.x86_64
which php 指向 /usr/bin/php,php --ini 读 /etc/php.ini。但系统 RPM 的 5.4 只是 CLI,Web 实际跑的是 /data/app/php72 这套 2020 年编译的 PHP 7.2.34,FPM 由 /etc/init.d/php-fpm 以 -c /data/app/php72/etc/php.ini 启动。要处理的是这个 7.2。
阿里 ALinux 2 的修复版 php-5.4.16-48.2.al7 在公网和内网源都取不到,PHP 官方也只修到 8.2.35、8.3.36、8.4.27、8.5.12,7.2 没有官方补丁。业务不能停机,也没有 PHP 升级窗口,我先按官方缓解措施做配置收敛,升级后续再排期。
二、根因分析
CVE-2026-109618 出在 PHP 处理 OOM 的路径上:内存耗尽时用 longjmp 退出当前上下文,部分场景下请求内存还没初始化就被后续清理逻辑读取或分支,最终可以走到 RCE。触发点是 memory_limit 耗尽。
CVE-2026-109619 是跨请求的全局状态问题:全局变量临时指向请求内存,某些场景下没被正确重置,或者重置后又被重新填回。请求内存释放后,全局状态仍然指向那块内存,后续请求读到脏状态,同样可以 RCE。
这台机器的风险点有三个:
- Web PHP 是 7.2.34,2020 年编译,上游早已 EOL,没有任何官方补丁
- Zabbix 4.4.6 也是 2021 年 EOL 的版本
- FPM worker 原本
pm.max_requests = 5000,低流量下能活好几天,跨请求状态复用窗口很长
官方文档明确写了没有能完整消除这两个漏洞的通用配置,配置只能降低特定触发路径的风险。
三、解决方案
3.1 修改前备份
cp -a /data/app/php72/etc/php.ini \
/data/app/php72/etc/php.ini.bak.$(date +%Y%m%d%H%M%S)
cp -a /data/app/php72/etc/php-fpm.d/www.conf \
/data/app/php72/etc/php-fpm.d/www.conf.bak.$(date +%Y%m%d%H%M%S)
本次生成的备份:
/data/app/php72/etc/php.ini.bak.20261103214011
/data/app/php72/etc/php.ini.bak2.<时间戳>
/data/app/php72/etc/php-fpm.d/www.conf.bak.<时间戳>
3.2 关闭 session 上传进度
编辑 /data/app/php72/etc/php.ini,保留原来的注释行,新增生效行:
;session.upload_progress.enabled = On
session.upload_progress.enabled = Off
也可以用命令改:
sed -i -E 's/^[[:space:]]*;?[[:space:]]*session\.upload_progress\.enabled[[:space:]]*=.*/session.upload_progress.enabled = 0/' \
/data/app/php72/etc/php.ini
3.3 取消内存上限
先看内存余量,确认这台机器扛得住:
free -m
total used free shared buff/cache available
Mem: 31985 5196 835 372 25953 26023
Swap: 10239 1942 8297
32GB 内存,available 26GB,dmesg 里没有 OOM 记录。这个余量下按官方建议设 -1:
sed -i -E 's/^[[:space:]]*;?[[:space:]]*memory_limit[[:space:]]*=.*/memory_limit = -1/' \
/data/app/php72/etc/php.ini
grep -n '^memory_limit' /data/app/php72/etc/php.ini
# 401:memory_limit = -1
3.4 限制 FPM worker 请求数
原来的值是 5000:
grep -n 'pm.max_requests' /data/app/php72/etc/php-fpm.d/www.conf
# 139:pm.max_requests = 5000
把旧值用 ; 注释掉,新增一行。php-fpm.conf 的注释符是 ;,不是 #:
;pm.max_requests = 5000
pm.max_requests = 1
sed -i -E 's/^[[:space:]]*[#;]?[[:space:]]*pm\.max_requests[[:space:]]*=.*/pm.max_requests = 1/' \
/data/app/php72/etc/php-fpm.d/www.conf
/data/app/php72/sbin/php-fpm -t -y /data/app/php72/etc/php-fpm.conf
3.5 重启
/etc/init.d/php-fpm restart
配置汇总:
| 参数 | 值 | 说明 |
|---|---|---|
session.upload_progress.enabled |
Off |
关闭上传进度,切断 109619 的一条触发路径 |
memory_limit |
-1 |
取消 PHP 内存上限,避开 109618 的 OOM 触发点 |
pm.max_requests |
1 |
每个 worker 只服务一个请求,杜绝跨请求状态复用 |
四、验证
4.1 FPM SAPI 层
/data/app/php72/sbin/php-fpm -i -c /data/app/php72/etc/php.ini \
| grep -E '^memory_limit|session.upload_progress.enabled'
memory_limit => -1 => -1
session.upload_progress.enabled => Off => Off
4.2 Web SAPI 层
在站点根目录放临时文件,用真实请求验证:
printf '%s\n' '<?php echo ini_get("session.upload_progress.enabled") ? "ON" : "OFF";' \
> /data/app/tengine/html/_chk.php
curl -s http://127.0.0.1/_chk.php; echo
rm -f /data/app/tengine/html/_chk.php
OFF
printf '%s\n' '<?php echo ini_get("memory_limit"), PHP_EOL;' \
> /data/app/tengine/html/_chk2.php
curl -s http://127.0.0.1/_chk2.php; echo
rm -f /data/app/tengine/html/_chk2.php
-1
4.3 服务状态
curl -sI http://127.0.0.1/ | head -1
# HTTP/1.1 200 OK
worker 回收可以用 PID 对比确认:
pgrep -f 'php-fpm: pool www' | sort > /tmp/fpm_before
for i in $(seq 1 20); do curl -s -o /dev/null http://127.0.0.1/index.php; done
pgrep -f 'php-fpm: pool www' | sort > /tmp/fpm_after
comm -13 /tmp/fpm_before /tmp/fpm_after | wc -l
五、注意事项
这三个配置只是缓解,不是修复。109618 的 OOM 路径和 109619 的其他状态路径仍然存在,官方也明确说没有通用缓解方案。
memory_limit = -1的代价是资源耗尽风险。极端请求可能把内存吃满,触发内核 OOM killer。建议给关键进程加保护:
echo -1000 > /proc/$(pidof mysqld)/oom_score_adj
for p in $(pgrep -f '/data/app/zabbix-4.4.6/sbin/zabbix_server'); do
echo -1000 > /proc/$p/oom_score_adj 2>/dev/null
done
php-fpm.conf和www.conf的注释符是;,不是#。改完先跑php-fpm -t再重启。- CLI 的
php --ini、php -i不代表 FPM 的加载结果。这台机器的 FPM 通过 init 脚本的-c参数读/data/app/php72/etc/php.ini,验证要用php-fpm -i -c或 Web 临时文件。 - 回滚时把备份覆盖回去,再重启 FPM:
cp -a /data/app/php72/etc/php.ini.bak.20261103214011 /data/app/php72/etc/php.ini
cp -a /data/app/php72/etc/php-fpm.d/www.conf.bak.<时间戳> /data/app/php72/etc/php-fpm.d/www.conf
/etc/init.d/php-fpm restart
- 前端目前监听 80,站点根目录直接就是 Zabbix 前端。安全组和防火墙只对运维网段开放,这是成本最低的一道防线。
- 长期方案还是升级。PHP 7.2.34 和 Zabbix 4.4.6 都过了 EOL,最终要迁到 Zabbix 7.x + PHP 8.2.35+ 或更受支持的组合。
六、后续可选:参考第三方补丁重编译
想彻底修掉这两个 CVE,还有一个路:参考 Qsnh/php2shell-fixed,把 patches/php-7.2.34-CVE-2026-109618-109619.patch 打到官方 php-7.2.34 源码上重新编译。仓库里的一键脚本是给宝塔写的,路径写死在 /www/server/php/<版本>,我们这套 /data/app/php72 用不上,只能自己编译。
我试过这个 patch,能干净应用到官方 7.2.34 源码(16 个文件,dry-run 通过)。但风险不低:补丁是第三方从官方 7.4 补丁移植的,不是 PHP 官方产物;要原样复用 2020 年的编译参数,依赖库、扩展、链接库对不上就可能起不来;补丁还改了 OOM 和 multipart 的行为,Zabbix 前端得重新回归。
所以我这次没动,继续用配置缓解顶着,等有维护窗口再评估。