安全指南

紧急PHP更新后该检查什么:Web PHP与命令行PHP、set -e与diff退出码,以及入侵复查

紧急PHP补丁之后,Web端PHP与命令行(CLI)PHP可能不一致。说明如何分别检查两者、set -e与diff退出码,以及补丁后的入侵检查。

发布于 2026-11-01 更新于 2026-11-01 最后核实 2026-11-01 4 分钟阅读
本页目录

适用读者:运行PHP站点、正在应用紧急PHP更新,或刚刚完成更新并担心存在遗漏的人员。

2026年10月,两个严重PHP漏洞(CVE-2026-109618和CVE-2026-109619)的修复版本发布。本站运营者管理的一个站点在当晚完成了更新。本页将那里发生的情况、两个最初被证明是错误的假设,以及之后对每台服务器应用的入侵复查,整理为通用步骤。漏洞及修复版本的详情见两个严重PHP漏洞与2026年10月的紧急修复版本。

发生了什么(本站运营者管理的站点)

该站点在多种环境中使用PHP。由于PHP团队已公布修复版本的发布时间,运营者在发布前切换到了维护页面,并在修复版本发布之前保持离线。发布后,他们替换为修复版本并解除了维护页面。

  1. 公告之后

    切换到维护页面,等待修复版本
  2. 发布之后

    替换为修复版本,并解除维护页面
  3. 更新后检查

    Web端已更新,但cron使用的命令行PHP仍指向旧版本,于是重新指向了新版本
  4. 当晚

    用于解除维护模式的临时脚本在写入任何内容之前就停止了。原因:set -e与diff同时使用。没有造成损害,脚本已重新编写
  5. 事后

    对每台服务器应用了相同的入侵检查清单,并通过比较文件检查了一个在更新前到达的可疑请求

美国CISA随后将这两个CVE加入其已知被利用漏洞目录(KEV)(CISA公告)。这支持了紧急打补丁的决定,但补丁只能关闭入口。确认没有人已经进入,是另一项工作。

假设一:“Web端PHP已更新,所以完成了”

第一个假设是:只要站点运行在新版本上,该服务器上的所有PHP都已更新。实际上,cron调用的命令行PHP仍指向旧版本。

PHP根据运行方式可以是不同的程序(SAPI,服务器API)。php.net指出,CLI SAPI在许多方面与其他SAPI不同(PHP:从命令行使用PHP)。在同一台机器上安装了多个版本时,Web可能运行新版本,而命令行却运行旧版本。

Web请求 → PHP-FPM / Apache模块

运行已更新的版本(可在Site Health等页面中查看)

cron和shell脚本 → 命令行PHP

仍指向旧版本,Web端的检查无法显示这一点

在同一台服务器上,Web使用的PHP与命令行使用的PHP可能是版本不同的独立程序。打补丁之后,两者都要检查。

分别检查Web与CLI

1

检查命令行PHP版本

php -v显示该shell调用的PHP版本。php.net指出,php -v还会告诉你PHP是CGI还是CLI版本。which php显示实际被调用的可执行文件的位置。

2

在安装了多个版本的Debian系系统上,检查命令指向何处

update-alternatives --display php显示php命令指向哪个版本(update-alternatives手册)。--config可以更改指向,但这属于服务器配置变更,应先确认操作步骤及如何撤销。

3

在cron任务中写明PHP的完整路径

cron在其自身环境中运行任务,与登录shell相互独立(crontab(5))。如果只写php,实际选用哪个PHP取决于该环境。本站的建议是:写明已打补丁版本的完整路径,使更新后的检查只有一个答案。

4

从Web侧检查Web端PHP版本

在WordPress上,后台的Site Health(站点健康)页面会显示该版本。在代码中,可以使用常量PHP_VERSION(版本)和PHP_SAPI(PHP的运行方式:CLI为cli,PHP-FPM为fpm-fcgi,Apache模块为apache2handler)(php_sapi_name)。

5

检查完成后立即删除phpinfo()页面

调用phpinfo()的公开文件会显示版本信息,但也会暴露配置和服务器内部细节。用完后应立即删除。不留存该页面,是使用此方法的前提条件。

假设二:“diff要么成功,要么失败”

用于解除维护模式的临时脚本,在写入任何内容之前先用diff查看当前内容与新内容的差异。脚本中开启了set -e。

diff发现差异时返回退出状态1。在GNU手册中,0表示无差异,1表示发现差异,2表示出错(GNU diffutils:diff)。发现差异正是脚本所预期的。但set -e(errexit)会在非零状态时停止shell,条件判断等少数位置除外。脚本因为存在差异而停止,从未执行到写入步骤。

因此,解除维护模式的步骤在未写入任何内容的情况下停止了,没有造成损害。它是在安全一侧失败的,但反过来的组合,即该停止时却没有停止,则会很危险。脚本已重新编写。

退出状态1并不代表“错误”的命令

命令1的含义2的含义来源
diff发现差异出错GNU diffutils:diff
cmp发现差异出错GNU diffutils:cmp
grep未选中任何行(无匹配)发生错误GNU grep

安全的写法

GNU Bash手册指出,在set -e下,若命令作为if测试的一部分、在除最后一个命令之外的&&或||列表中,或其状态被!取反,则shell不会因该命令返回非零而退出(GNU Bash:The Set Builtin)。应使用这一特性。

会停止,或掩盖真正的失败

  • 在set -e下直接写diff a b(仅因存在差异就停止)
  • diff a b || true(同时吞掉“有差异”和真正的错误(2))
  • 在grep上使用|| true,使“无匹配”与“无法读取文件”看起来相同

分别处理退出码

  • 将退出状态保存到变量中,并根据0、1和2分支处理(推荐的写法)
  • 如果只需判断是否存在差异,可以使用if diff -q a b; then …,但要注意2(错误)会与“有差异”落在同一分支
  • 如果使用|| true,应先确定漏掉2(真正的错误)是否可以接受

下面的示例区分了0、1和2。||左侧的失败不受errexit影响,因此脚本不会停止,且rc保存了退出状态。

#!/usr/bin/env bash
set -euo pipefail
 
rc=0
diff -rq deployed/ live/ || rc=$?
 
case "$rc" in
  0) echo "no differences" ;;
  1) echo "differences: review before continuing" ;;
  *) echo "diff failed" >&2; exit 2 ;;
esac

set -e并不是完整的安全网。它有许多可能掩盖错误的例外情况,因此对于重要的分支,更好的做法是自己读取退出状态再作决定(本站的建议)。

打补丁之后:对每台服务器运行相同的检查

打补丁关闭了入口,但并不能告诉你,在更新之前是否已有人进入。对于KEV漏洞,CISA要求在更新之前检查是否存在入侵(参见PHP文章中的“已确认的利用与加入CISA KEV”)。为避免各服务器的检查结果不一致,运营者对每台服务器应用了相同的检查清单。

项目应查找的内容示例命令
登录密钥你不记得注册过的公钥打开authorized_keys
用户未知用户,管理员组的变化getent passwd、getent group sudo
被修改的软件包文件软件包安装的、自发布后已发生变化的文件debsums -s(Debian系)、rpm -Va(RHEL系)
不属于任何软件包的可执行文件/usr/local、/opt或PATH中新出现的、没有软件包归属的可执行文件dpkg -S <file>(Debian系)或rpm -qf <file>(RHEL系);查找没有所属软件包的文件
SUID以所有者权限运行的新文件find / -xdev -perm -4000 -type f
cron和systemd未知的计划任务和定时器crontab -l、/etc/crontab、/etc/cron.d、systemctl list-timers
/tmp放置在此处的可执行文件find /tmp /var/tmp -type f -perm /111
出站连接通往你不认识的目的地的连接ss -tunp
容器内部正在运行的进程,以及启动后写入的文件docker top、docker diff

crontab -l只显示当前用户的任务,因此应分别检查/etc/crontab、/etc/cron.d以及各用户的cron目录。ss -tunp需要root权限才能显示每个连接背后的进程。dpkg -S和rpm -qf可报告哪个软件包拥有某个文件;“没有所属软件包”的文件仍可能是你自己放置的,因此应与你记得安装过的内容进行核对。

find -perm -4000匹配设置了所有指定位的文件(find(1))。docker diff列出容器创建后发生变化的文件(A:新增,C:修改,D:删除),docker top显示正在运行的进程(docker container diff、docker container top)。容器通常是重新构建而非原地修改的,因此应将docker diff显示的可执行文件视为异常并进行调查(本站的建议)。

关于如何解读各项内容,以及共享主机、WordPress和VPS的步骤,参见如何检查您的网站或服务器是否被入侵。

通过比较文件确认可疑的日志条目

在此次检查中,团队发现,在更新之前,有一个来自Tor出口节点(隐藏发送者身份的网络出口点)的POST请求到达了联系表单。日志只显示请求已到达,并不能说明是否因此写入了任何内容。

因此,他们将实时公开文件与部署时保留的副本进行了比较。两者完全一致,也没有多余的PHP文件。此次检查没有发现任何文件被改动的迹象。

1

在服务器之外保留一份已部署内容的副本

如果用于比较的唯一副本位于服务器上,那么同时发生的变更将不会被发现。每次部署时,都应将已发布的状态保存在服务器之外。

2

与实时文件进行比较

使用diff -rq查找存在差异的文件,或比较各文件的哈希列表(sha256sum)。如果存在差异,应确认是否为你预期的变更。

3

同时查找新增的文件

即使内容一致,新放置的PHP文件也不会出现在原始副本中,因此必须单独查找。diff -rq同样会报告仅存在于一侧的文件。

4

一致并不等于安全

此方法只能说明你所比较的文件未被改动。它无法检查数据库、上传目录或服务器之外的变化。应结合其他检查综合判断(本站的建议)。

本站观点:在紧急之夜之前确定好检查清单

在仓促的夜晚,人们在哪台服务器上查看了什么,往往会有出入。如果检查项目事先确定,每台服务器都会进行相同的检查,遗漏之处也会显现出来。这次之所以发现了旧的命令行PHP,是因为更新后的检查没有止步于Web端。

今天可以做的事

  1. 如果可以登录服务器,运行php -v和which php,并在cron任务中写明PHP的完整路径
  2. 从Web侧检查Web端PHP版本,例如使用Site Health(用完后立即删除任何phpinfo()页面)
  3. 找出set -e脚本中调用diff、grep或cmp的位置,并确定如何处理退出状态1
  4. 在服务器之外保留一份已发布内容的副本,并在更新后进行比较
  5. 让每台服务器的入侵检查清单保持一致,并逐项检查

来源

延伸阅读

FAQ

Q更新PHP之后,它仍然可能是旧版本吗?
A

是的。处理Web请求的PHP(PHP-FPM或Apache模块)与从shell或cron调用的命令行PHP(CLI)可能是版本不同的独立程序。php.net将PHP的运行方式称为SAPI,并指出CLI SAPI在许多方面与其他SAPI不同。Web端可能已打补丁,而cron使用的PHP仍是旧版本。

Q如何检查实际运行的是哪个PHP版本?
A

在命令行中运行php -v。对于Web端,应从Web侧检查;在WordPress上,可在后台的Site Health(站点健康)页面查看。如果使用了phpinfo()页面,用完后应立即删除。which php可显示所调用的可执行文件;在安装了多个版本的Debian系系统上,update-alternatives --display php可显示php命令指向的位置。在cron任务中,写明PHP的完整路径可以消除运行哪个版本的疑问。

Q为什么使用diff时,带有set -e的shell脚本会停止?
A

diff发现差异时返回退出状态1。GNU手册将0定义为无差异,1为有差异,2为出错。set -e(errexit)会在遇到非零状态时停止脚本,因此仅仅发现差异就会使其停止。可将命令放在if条件中,或将退出状态保存到变量中,并分别处理0、1和2。grep(1表示无匹配)和cmp(1表示不同)的行为相同。

Q当日志条目看起来可疑时,如何判断自己是否遭到入侵?
A

仅靠日志无法作出判断。日志记录的是已到达的尝试,不一定是成功。应将实时公开文件与部署时保留的副本进行比较,确认两者一致且不存在多余的PHP文件。一致说明未发现文件被篡改,但这并不证明系统安全,因此应结合其他检查综合判断。