PHP2Shell 双漏洞自查:检测脚本返回 UNKNOWN 之后,我是怎么确认服务器安全的
10 月 21 日 PHP 官方发了一条罕见的「Critical」安全公告:两个漏洞(CVE-2026-109618 / CVE-2026-109619)可以让未登录的请求直接远程执行代码,影响所有处理 Web 请求的 PHP,包括 PHP-FPM、Apache mod_php、php-cgi、FrankenPHP 和 LiteSpeed。
这几天 GitHub 上同题材的仓库很热,OpenAI 也放了一个官方的版本检测脚本 php2shell-checker。我拿它测了自己的本机和线上站点,结果一个红、一个 UNKNOWN。这篇把这两个结果背后的意思讲清楚,再把我在服务器上真正确认修复的步骤、以及在 CentOS 7 + 宝塔上升级踩到的坑记下来。
先说结论:30 秒自查
官方修复版本(10 月 21 日发布):
| 分支 | 修复版本 |
|---|---|
| 8.5 | 8.5.12 |
| 8.4 | 8.4.27 |
| 8.3 | 8.3.36 |
| 8.2 | 8.2.35 |
| 8.1 及更早 | 官方不再更新,只提供了 7.4 / 8.0 / 8.1 的最小源码补丁 |
三步判断:
- 找到真正在跑网站的那个 PHP(命令行的
php -v不一定是 FPM 用的那个); - 按来源对版本:官方源码编译的对上表;发行版的包(Debian / Ubuntu / RHEL)要对发行版自己的公告,版本号低不一定没修;
- 确认升级后 worker 重启过:包装好了但 FPM 没重启,跑的还是旧代码。
漏洞是什么(一句话版)
按官方公告的描述:
- CVE-2026-109618:内存耗尽(OOM)的处理会在请求内部数据只初始化了一半时打断 PHP,之后的清理流程把这些无效数据当成有效数据用;
- CVE-2026-109619:本该属于单个请求的内部状态,在内存释放后还残留着,同一个 worker 处理的下一个请求可能在正常初始化前就用上了它。
官方认为 mod_php 风险最高,已经有演示:一个未认证请求在任何应用代码运行之前就能触发攻击。也就是说,这跟你的业务代码写得好不好没关系。
用官方检测脚本实测:一个红,一个 UNKNOWN
脚本只有不到 100 行,我先读了一遍源码再跑:它只发 HEAD(遇到 405/501 才退回 GET),只读响应头里的 Server 和 X-Powered-By,不跟跳转,也不发任何攻击探测,可以放心用。
本机:Windows 上装的是 PHP 8.4.21,起一个内置服务器让它测:
$ php -v
PHP 8.4.21 (cli) (built: May 6 2026 09:30:51) (NTS Visual C++ 2022 x64)
$ node phpcheck.js http://127.0.0.1:18099/
POTENTIALLY VULNERABLE: PHP 8.4.21: Potentially vulnerable. HTTP headers cannot tell whether this version contains a backported fix.
$ echo $?
1
8.4.21 < 8.4.27,落在受影响区间,退出码 1。
线上站点:我们的服务器 10 月 22 日已经升到了 8.3.36,但测出来是这样:
$ node phpcheck.js https://claudeops.wbdao.cn/
UNKNOWN: PHP version not found
$ curl -sI https://claudeops.wbdao.cn/api/ | grep -iE "^(server|x-powered-by)"
server: nginx
响应头里只有一个 server: nginx,根本没有 PHP 版本号,脚本只能返回 UNKNOWN(退出码 3)。
这里有两个容易想歪的地方:
- UNKNOWN 不等于安全。隐藏版本号只是让外人看不到,漏洞本身还在不在,取决于服务器上实际跑的是什么。
- OK 也不等于安全。脚本 README 原话是「Even OK only describes the advertised version — it doesn't verify the installation」。响应头可能是别的机器(比如 CDN、反代后面的另一台)给的,也可能是改过的。
脚本里判断版本的核心逻辑就这几行,看一眼就知道它的边界:
const FIXED_IN = { '8.2': 35, '8.3': 36, '8.4': 27, '8.5': 12 }
const OLDER_BRANCHES = new Set(['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'])
// 8.1 及以下一律报 potentially_vulnerable,哪怕补丁号是 8.1.35
// 带 dev/alpha/beta/rc 的预发布版本一律 unknown
所以 URL 检测只适合快速筛一遍你手上一堆站点里有没有明显的老版本,真正下结论得上服务器。

UNKNOWN 之后:上服务器确认的三件事
同一个仓库里还有一份给 Agent 用的 phpcheck-server 说明,思路和我自己排查时一致,按顺序是这三件事。
1. 找到真正在服务网站的那个 PHP
很多服务器上装着不止一个 PHP,命令行默认的那个不一定是 FPM 在用的。直接从正在跑的进程反查:
# 看有哪些 php-fpm master 在跑
ps -eo pid,lstart,cmd | grep "php-fpm: master" | grep -v grep
# 从进程找到实际的二进制,再问它的版本
ls -l /proc/<master_pid>/exe
/proc/<master_pid>/exe -v
如果用的是 Docker,要进容器里查;Apache mod_php 则看加载的 libphp*.so 属于哪个包。
2. 按来源对版本:发行版的包要看发行版公告
这是最容易误判的一步。发行版会把补丁回移(backport)到老版本号上,比如 phpcheck-server 里列的 Ubuntu 24.04 LTS 的修复版本是 8.3.6-0ubuntu0.24.04.13:上游版本号 8.3.6 远低于 8.3.36,但它是已修复的。
- Debian / Ubuntu:
dpkg-query -W php8.3-fpm读完整包版本,用dpkg --compare-versions和 Debian Security Tracker / Ubuntu USN 的修复版本比; - RHEL 系:
rpm -qf <二进制路径>找到所属包,rpm -q --queryformat '%{NAME} %{EPOCHNUM}:%{VERSION}-%{RELEASE}\n' <包名>读完整版本,再对 Red Hat 的勘误; - 自己编译的、宝塔这类面板编译的:没有发行版回移,直接按上游版本号对前面那张表。
反过来,别拿 Ubuntu 的阈值去套 RHEL 的包,也别拿上游版本号去套发行版的包。
3. 确认 worker 重启过
装好新包不等于生效。对比 FPM master 的启动时间和包的安装时间:
ps -o lstart= -p <master_pid> # 进程启动时间
rpm -q --last php-fpm | head -1 # RHEL 系包安装时间
ls -l --time-style=full-iso /var/lib/dpkg/info/php8.3-fpm.list # Debian 系
进程比包早,就说明还在跑旧代码,systemctl restart php-fpm(或对应服务名)之后再查一次。
我们的升级实录:CentOS 7 + 宝塔,只能升到 8.3.36
我们那台机器是 CentOS 7、宝塔面板,原来站点跑在 8.1.27 和 7.3 上,两个都在受影响范围里。实际升级过程踩了三个坑,记下来给同样环境的人省点时间。(这次是让 SpectrAI 里的 Agent 连 SSH 逐条执行的,每条命令和报错都留在会话记录里,下面的报错原文就是从记录里翻出来的。)
坑 1:8.4 / 8.5 装不了。 8.4 起要求 libxml2 ≥ 2.9.4,而 CentOS 7 自带的是 2.9.1,宝塔装 8.4 / 8.5 都过不去。CentOS 7 早已停止维护,不想折腾升级系统库的话,8.3.36 是这台机器上唯一能走的修复版本。
坑 2:编译 8.3 链到了系统老 OpenSSL。 编到 ext/openssl/xp_ssl.c 时报一串:
xp_ssl.c:144:10: error: subscripted value is neither array nor pointer nor vector
xp_ssl.c:145:10: error: subscripted value is neither array nor pointer nor vector
原因是 configure 找到的是系统的 OpenSSL 1.0.2,而不是宝塔自己装的 1.1.1。编译前指定 pkg-config 路径就好:
export PKG_CONFIG_PATH=/usr/local/openssl111/lib/pkgconfig:/usr/local/curl_2/lib/pkgconfig
(另外几个缺依赖的报错,比如 BROTLI 找不到,用 CentOS 7 的 vault 归档源和 EPEL 7 归档源把依赖装上即可。)
坑 3:面板里切了版本,站点其实没切。 宝塔切 PHP 版本改的是 vhost 里的 include enable-php-81.conf,但我们有个站点的配置里还有一行手写的 fastcgi_pass unix:/tmp/php-cgi-81.sock,这一行不改,请求照样走老的 8.1。改完配置后一定要用上面第 1 步的方法,从进程反查一遍。
最后的做法是:所有站点切到 8.3.36 → 逐个站点确认请求落在新 FPM 上 → 停掉 8.1 / 7.3 的 FPM 并关闭开机自启,但先不卸载,留着回滚用。
暂时升不了怎么办
官方给了一些缓解措施,但明确说了只能缩小攻击面,不能让安装变安全,升级是唯一完整的方案:
- 针对 CVE-2026-109618:
memory_limit=-1(用php_admin_value这类应用改不回去的方式设),同时调低post_max_size(不需要上传的应用设 8M,需要上传的设 32M),并在 Web 服务器或代理层限制请求体大小、字段数、multipart 分段数、头数量;用容器或 cgroup 做内存边界,不要用RLIMIT_AS/RLIMIT_DATA替代,反而会触发漏洞路径; - 针对 CVE-2026-109619:在 php.ini 或 FPM pool 里设
session.upload_progress.enabled=0并重启所有 worker,不能靠ini_set()或.user.ini;PHP 8.5 还要设fatal_error_backtraces=0。
10 月 23 日官方又补了一条说明:session.upload_progress.enabled=0 还能顺带防住一个新发现的、触发条件很苛刻的 RCE 向量(CVE-2026-115560),之前按公告设过的保持不动即可。
自查清单
参考
- PHP 官方安全公告(2026-10-21):https://www.php.net/archive/2026.php
- OpenAI php2shell-checker:https://github.com/openai/php2shell-checker
浙公网安备 33010602011771号