AIGC标识 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 的最小源码补丁

三步判断:

  1. 找到真正在跑网站的那个 PHP(命令行的 php -v 不一定是 FPM 用的那个);
  2. 按来源对版本:官方源码编译的对上表;发行版的包(Debian / Ubuntu / RHEL)要对发行版自己的公告,版本号低不一定没修;
  3. 确认升级后 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),之前按公告设过的保持不动即可。

自查清单

参考

posted @ 2026-10-26 10:37  SpectrAI光谱AI  阅读(3)  评论(0)    收藏  举报