SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

PHP2Shell 自查:按来源确认你的 PHP 是否已修复

php -v 显示的版本号不能证明已修复。按 apt、宝塔、1Panel 和 Docker 四种来源,逐个确认每个站点实际调用的 PHP 是否包含 CVE-2026-109618/109619 的补丁,再重启所有 PHP 进程。

先说结论:php -v 的版本号不是判断标准

要确认 PHP 是否已修复 PHP2Shell(CVE-2026-109618 和 CVE-2026-109619),只看 php -v 不够。正确的做法是:先找出每个站点实际调用的那个 PHP,再按它的来源去核对。apt 装的看软件包 changelog 和包版本,宝塔和 1Panel 看面板里的版本,Docker 看镜像标签。核对完还要重启所有 PHP 进程,否则旧代码还留在内存里继续运行。

php -v 不可靠,有两个原因。第一,Ubuntu 打安全补丁时不改上游版本号。Ubuntu 24.04 的修复版是 8.3.6-0ubuntu0.24.04.13,php -v 仍然显示 8.3.6。这个数字比上游修复版 8.3.36 小得多,但它已经修好了。第二,一台 VPS 上经常同时有 apt 装的 PHP、宝塔编译的 PHP 和容器里的 PHP。你在 shell 里敲 php -v,看到的只是 PATH 里排在最前面的那一个,它不一定是正在处理网页请求的那一个。

PHP2Shell 是什么,哪些 PHP 受影响

PHP 官方在 2026 年 10 月 21 日于 php.net 发布安全公告,修复两个可以导致未经认证的远程代码执行(RCE,remote code execution)的严重漏洞。按公告的描述:

  • CVE-2026-109618:内存不足(OOM,out of memory)的处理可能在请求数据只初始化了一部分时打断 PHP,之后的请求清理会把这些无效数据当成有效数据。
  • CVE-2026-109619:属于某个请求的内部状态,在它背后的内存被释放后仍然残留。同一个 worker 处理的下一个请求,可能在正常初始化重置它之前用到这份过期状态。

上游修复版本是 8.5.12、8.4.27、8.3.36、8.2.35,低于这些版本的上游 PHP 都受影响。公告点名的 SAPI(server API,PHP 和 Web 服务器之间的接口)包括 PHP-FPM、Apache mod_php、php-cgi、FrankenPHP 和 LiteSpeed,也就是所有处理网页请求的 PHP。官方认为 mod_php 的风险最高,因为已经有演示表明,一个未认证的请求可以在任何应用代码运行之前触发攻击。所以,你的 WordPress 插件是不是最新版,和这个漏洞没有关系。

PHP 8.1 及更早的版本已经不再由 PHP 项目更新。公告让这些用户联系发行版或 LTS 厂商(有些厂商提供回移补丁),或者升级到仍受支持的版本。如果你打算借这次机会升级,注意 PHP 8.2 会在 2026 年 12 月 31 日结束支持,之后不再有安全修复。

美国 CISA 在 10 月 21 日的通告里说,截至发布时还没有观察到这两个漏洞被利用。同一份通告也建议:无法更新的 PHP 系统应当先下线。没有观察到利用,不是可以拖延的理由。

第一步:找出机器上所有正在运行的 PHP

不要先问"我装了哪个 PHP",要先问"现在跑着哪些 PHP"。下面这条命令列出所有名字里带 php 的进程:

ps -eo pid,user,lstart,args | grep -i '[p]hp'

lstart 是进程的启动时间,第四步判断"更新后有没有重启"要用到它。[p]hp 这种写法让 grep 不会匹配到它自己这一行。PHP-FPM 的输出里通常有一个 master process 和若干 pool 进程,记下每个 master 的 PID。

然后查每个 master 实际执行的是哪个文件,再问 dpkg 这个文件属于哪个软件包:

sudo readlink /proc/<PID>/exe
dpkg -S "$(sudo readlink /proc/<PID>/exe)"

如果 dpkg -S 打印出一个包名,比如 php8.3-fpm,这个 PHP 就是 apt 装的。如果它报 no path found matching pattern,说明这个文件不属于任何 apt 包。它可能是宝塔或 LNMP 一键包编译安装的,也可能来自其他地方。这一点很重要:apt upgrade 只升级 apt 自己管理的文件,所以它永远不会更新这些 PHP。

Docker 容器里的进程也会出现在宿主机的 ps 列表里,但它的文件路径是容器内部的路径,宿主机上的 dpkg 认不出它。先用下面的命令列出所有容器和它们的镜像,容器里的 PHP 留到第三步按 Docker 的方法检查:

docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'

第二步:每个站点调用的是哪一个 PHP

找到所有 PHP 之后,还要知道每个站点用的是哪一个。答案写在 Web 服务器的配置里。

Nginx:看 fastcgi_pass 指向哪里

Nginx 自己不执行 PHP,它用 fastcgi_pass 把请求交给一个 FastCGI 服务器。这个地址可以是 127.0.0.1:9000 这样的 TCP 地址,也可以是 unix:/路径/xxx.sock 这样的 Unix 套接字。nginx -T 会先检查配置,再把所有被加载的配置文件(包括 include 进来的文件)打印出来:

sudo nginx -T 2>/dev/null | grep -E '# configuration file|server_name|fastcgi_pass'

有些面板把 fastcgi_pass 写在单独的文件里,再用 include 引入。nginx -T 会把这些文件一起打印出来,所以这条命令仍然能看到它。如果系统提示找不到 nginx 命令,说明 nginx 不在 PATH 里,可以直接用正在运行的那个程序:

sudo "$(sudo readlink /proc/$(pgrep -o nginx)/exe)" -T 2>/dev/null | grep -E '# configuration file|server_name|fastcgi_pass'

拿到地址后,查是哪个进程在监听它:

sudo ss -xlp | grep -i php
sudo ss -ltnp | grep -i php

第一条列出 Unix 套接字,第二条列出 TCP 端口,两者都会显示监听进程的名字和 PID。把套接字路径或端口和 fastcgi_pass 对上,再拿 PID 回到第一步,就知道这个站点用的是哪个 PHP 了。apt 装的 PHP-FPM,每个池(pool)的监听地址写在 listen 这一行:

grep -R '^listen' /etc/php/*/fpm/pool.d/

Apache:mod_php 还是转发给 FPM

sudo apache2ctl -M 2>/dev/null | grep -Ei 'php|proxy_fcgi'
sudo apache2ctl -S

-M 列出已加载的模块,-S 列出虚拟主机。如果模块列表里有名字带 php 的模块,说明 PHP 以 mod_php 的方式直接跑在 Apache 进程里。这正是公告认为风险最高的方式。如果只有 proxy_fcgi_module,说明 Apache 把 PHP 请求转发给了 PHP-FPM,转发地址写在 SetHandler 里:

sudo grep -Ri 'SetHandler' /etc/apache2/

两种方式的区别在 在 Ubuntu 24.04 上搭建 LAMP 环境 里有完整说明。

容器:看端口和反向代理

如果 Nginx 的 fastcgi_pass 指向 127.0.0.1:9000,而 ss 显示监听这个端口的进程是 docker-proxy,那么 PHP 跑在容器里。第一步 docker ps 输出的 Ports 列会告诉你是哪个容器。也有整个站点(Nginx 加 PHP)都在容器里的情况,这时宿主机的 Nginx 只做反向代理,PHP 同样按 Docker 的方法检查。

第三步:按来源核对是否已修复

apt 安装的 PHP(Ubuntu)

Ubuntu 在 10 月 21 日发布了 USN-8948-1,覆盖 php-fpm、libapache2-mod-php、php-cgi 和 php-cli。ubuntu.com 的 CVE 页面列出的修复版本是:

  • Ubuntu 26.04:php8.5 8.5.4-0ubuntu1.5
  • Ubuntu 24.04:php8.3 8.3.6-0ubuntu0.24.04.13
  • Ubuntu 22.04:php8.1 8.1.2-1ubuntu2.28
  • Ubuntu 26.10:php8.5 8.5.9-0ubuntu2.1

先看装了哪些 PHP 包和它们的版本:

dpkg-query -W -f='${Package} ${Version}\n' 'php*-fpm' 'php*-cgi' 'php*-cli' 'libapache2-mod-php*' 2>/dev/null

再用 dpkg --compare-versions 和修复版比较。它懂 Debian 的版本号规则,所以不会被 8.3.6 小于 8.3.36 这种表面数字误导。下面以 24.04 为例,其他版本把包名和修复版换成上面列表里对应的值:

dpkg --compare-versions "$(dpkg-query -W -f='${Version}' php8.3-fpm)" ge 8.3.6-0ubuntu0.24.04.13 && echo 已修复 || echo 未修复

也可以看软件包的 changelog。apt-get changelog 默认显示已安装版本的 changelog,需要服务器能联网下载:

apt-get changelog php8.3-fpm | grep -E 'CVE-2026-1096(18|19)'

在 24.04 上,8.3.6-0ubuntu0.24.04.13 的 changelog 条目写着 SECURITY UPDATE: Remote RCE via state corruption issue,并列出了这两个 CVE 编号,所以 grep 应该打印两行。其他 Ubuntu 版本也可以这样查。如果 grep 没有输出,以上面的版本比较结果为准。

没修复就更新。USN 给出的更新方法就是普通的系统更新:

sudo apt update && sudo apt upgrade

注意:现在新装的 Ubuntu 24.04 从官方源安装 php8.3-fpm,装到的直接就是修复版。如果你开着自动安全更新,包可能已经装好了,配置方法见 Ubuntu 自动安全更新的配置方法。但包装好了,不等于正在运行的进程已经换成新代码,第四步会解释原因。

Ubuntu 20.04 及更早的版本只能通过 Ubuntu Pro 获得修复。按 CVE-2026-109618 页面:20.04 的 php7.4 修复版是 7.4.3-4ubuntu2.29+esm5,18.04 的 php7.2 是 7.2.24-0ubuntu0.18.04.17+esm14。16.04 的 php7.0 和 14.04 的 php5 还需要 Legacy Support 附加服务。没开 Pro 的话,apt upgrade 拿不到这些包。开启方法见 服务器上的 Ubuntu Pro 说明。

如果你用的是第三方 PPA 而不是 Ubuntu 官方的 PHP 包,版本号规则由 PPA 自己决定,请以该 PPA 的说明为准。

宝塔面板

宝塔在 10 月 21 日发布了紧急通知(docs.bt.cn,10 月 22 日更新)。宝塔的 PHP 由面板自己安装,不是 apt 包,所以只能在面板里升级。按通知的内容:

  • 8.2.35、8.3.36、8.4.27、8.5.12 是官方修复版本,面板里可以直接升级。
  • 7.4.34、8.0.31、8.1.35 由宝塔为 Linux 面板打包,宝塔说明其中包含这两个漏洞的修复。
  • 7.0 到 7.3 由宝塔自行构建为 7.0.34、7.1.34、7.2.35、7.3.34。宝塔说明这是以 PHP 7.4 的官方补丁为基础逐行比对、回移适配的。这是宝塔自己的说法,外部无法独立核实。
  • 通知没有提到 PHP 5.6。在宝塔给出说明之前,把 5.6 当作没有修复,尽快把站点迁到受支持的版本。
  • Windows 面板上 8.1 及更早版本目前没有修复版,宝塔只给出了改配置、禁用相关功能的临时缓解方法。

升级路径:打开 软件商店,选择 已安装,先点右上角的 更新软件列表 / 支付状态 获取最新信息,再对站点正在使用的 PHP 版本点更新,升级完成后确认版本号。宝塔的通知提醒,升级会重启对应的 PHP 服务,站点会短暂不可用,所以建议在业务低峰期操作。

确认版本请看面板 已安装 页面里的版本号,不要拿 shell 里的 php -v 当证据。原因在第一节:shell 里的 php 可能是 apt 装的另一个 PHP。宝塔可以同时安装多个 PHP 版本,不同站点可能用不同的版本,所以每个版本都要检查。

1Panel

1Panel 的 PHP 运行环境一般跑在容器里。截至 10 月 22 日,在 1Panel 的 GitHub issue #14149 里,维护者在 10 月 21 日回复说需要等官方 PHP 镜像更新。同一天另一个 issue(#14150)里有用户反馈应用商店里只有 8.5.11。这些是 issue 里的回复,不是正式的安全公告,情况可能已经变化,请以 1Panel 官方渠道的最新说明为准。

核对方法有两步。先在面板的 PHP 运行环境列表里看每个运行环境的版本,再进容器确认(容器名用第一步 docker ps 的输出):

docker exec <容器名> php -v

对 8.2 及以上的版本,容器里显示的版本号应当不低于上游修复版,也就是 8.2.35、8.3.36、8.4.27 或 8.5.12。8.1 及更早的版本情况不同:Docker 官方的 php 镜像目前只提供 8.2 到 8.5 的标签,没有 8.1、8.0 和 7.4。上游为 7.4、8.0 和 8.1 提供的是最小化的源码回移补丁(php-src 里的单独分支),供自行从源码编译、或者没有 LTS 厂商支持的用户使用,Docker 官方镜像里没有对应的标签。所以这类运行环境里的版本号无论显示多少,都证明不了什么,只能依据 1Panel 自己的说明。如果找不到说明,最稳妥的做法是把站点迁到 8.2 以上。

1Panel 的运行环境也可以选"本地"类型,它使用服务器上已安装的 php-fpm。这种情况回到上面的 apt 一节检查。

Docker 镜像

Docker 官方 php 镜像已经发布了 8.5.12、8.4.27、8.3.36、8.2.35 这些标签。官方镜像的标签就是上游版本号,所以这里 php -v 是有意义的:

docker ps --format 'table {{.Names}}\t{{.Image}}'
docker exec <容器名> php -v

要特别注意 php:8.3-fpm 这类浮动标签。标签只是一个名字,它指向的是你上次拉取时的那个镜像。上游发布新版后,你本地的 php:8.3-fpm 不会自己变,必须重新拉取并重建容器:

docker compose pull
docker compose up -d

如果你的镜像是用自己的 Dockerfile 从 FROM php:... 构建的,pull 不会更新它。要加 --pull 重新构建,这样构建时会先拉取最新的基础镜像:

docker compose build --pull
docker compose up -d

有些镜像不用官方 php 镜像,而是在 Debian 或 Alpine 基础镜像里用系统包管理器装 PHP。这种镜像的版本号遵循发行版自己的规则,要按那个发行版的安全公告核对,不能直接和上游版本号比。

第四步:重启所有 PHP 进程,这一步最常被跳过

软件包更新只替换了磁盘上的文件。已经在运行的进程,用的还是它启动时载入内存的旧代码,直到它重启为止。所以"包已经是修复版"和"服务已经修复"是两件事。USN 只说标准系统更新会完成必要的改动,没有提重启。PHP 公告则在缓解措施里反复要求重启每一个 worker。

还有一个常见误解:更新 WordPress、Laravel 或 Composer 依赖,只更新了用 PHP 写的应用代码,PHP 解释器本身一个字节也没变。

先列出相关的服务:

systemctl list-units --type=service --all 'php*' 'apache2*'

然后重启。下面以 Ubuntu 24.04 为例,服务名用上一条命令的输出:

sudo systemctl restart php8.3-fpm
sudo systemctl restart apache2

用 mod_php 的话,PHP 跑在 Apache 的进程里,所以必须重启 Apache。宝塔的 PHP 在面板里重启对应的 PHP 服务。容器在 docker compose up -d 重建后会用新镜像启动。

长时间运行的 PHP 进程也要处理,比如 Supervisor 管理的队列 worker、Laravel 的 php artisan queue:work 或 Horizon。公告列出的受影响 SAPI 是处理网页请求的那几种,CLI 不在其中。但这些进程同样还在用旧的二进制文件,统一重启最省事。第二条命令要在 Laravel 项目目录里运行:

sudo supervisorctl restart all
php artisan queue:restart

最后验证。重新跑第一步的 ps 命令,每个 PHP 进程的 lstart 都应该晚于包的升级时间。apt 的升级时间可以在 dpkg 日志里查:

grep ' upgrade php' /var/log/dpkg.log | tail -n 5

如果某个进程的启动时间早于升级时间,它就还在跑旧代码,需要再重启一次。

暂时无法更新时,缓解措施能做到什么

PHP 公告说得很清楚:只有更新才是完整的修复,下面这些措施只能减少特定的攻击路径,并不能让系统变安全。宝塔的缓解说明也是同样的立场。它们只适合在更新前的短时间内使用。

针对 CVE-2026-109619:

  • 设置 session.upload_progress.enabled=0,然后重启每一个 worker。这个选项默认是 1(开启)。公告说它能挡住已确认的 session 路径,但挡不住其他变种。
  • PHP 8.5 还要设置 fatal_error_backtraces=0,同样要重启每一个 worker。它只挡住已确认的致命错误回溯路径。

针对 CVE-2026-109618:

  • 设置 memory_limit=-1,最好用应用无法覆盖的方式设置,例如 FPM 池里的 php_admin_value。这和平时"限制内存"的建议正好相反。公告的解释是:它去掉了可以被请求控制的 PHP 内存限制失败,但底层的内存分配失败仍然可能走到有漏洞的代码路径。memory_limit 的默认值是 128M。宝塔的说明在这一项旁边提醒:该配置会取消 PHP 自身的内存限制,不建议普通用户在缺少资源隔离的情况下直接使用。
  • 把 post_max_size 调低:不需要上传文件的应用设为 8M,需要上传的设为 32M,并让 upload_max_filesize 小于 post_max_size。post_max_size 的默认值是 8M。
  • 用容器或 cgroup 的内存限制作为可用性的边界。不要用 RLIMIT_AS 或 RLIMIT_DATA 代替,公告说它们反而可能触发有漏洞的分配失败。

这些设置必须写在 php.ini、FPM 池或服务器配置里,因为它们要在请求解析之前生效。不要依赖 ini_set() 或 FPM 的 .user.ini。FPM 池里的写法如下:

php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[session.upload_progress.enabled] = 0

用 php_admin_value 设置的值,应用无法用 ini_set() 改回去。改完后检查有没有别的池、目录或应用配置把有限的 memory_limit 又设了回去,然后重启 PHP-FPM。

打完补丁后,memory_limit 和 post_max_size 要不要改

有报道说这次修复给 PHP 加了一个会直接结束进程的硬性内存上限。GHSA-w8hw-w9fx-j79w 的原文是这样描述修复的:补丁去掉了 OOM 时使用的 longjmp(),改为在下一个 VM 安全点触发一个延迟的、平稳的软 OOM;如果内存用量达到第二个更高的阈值,就用 _exit() 立即硬性结束。PHP 原有的内存限制机制仍然保留。

公告没有要求打完补丁后重新调整这两个参数。不过有一件事要做:如果你在更新前把 memory_limit 临时设成了 -1,补丁装好、进程重启之后,应该把它改回原来的有限值。-1 只是更新前的权宜之计。它意味着单个请求可以用掉整台机器的内存,在小内存 VPS 上很危险。post_max_size 如果为了缓解调得过低,导致正常的上传失败,也可以恢复成业务需要的值。

另外,如果补丁之后你发现 PHP-FPM 子进程在内存压力大时被直接结束,先检查 memory_limit 和池的大小是否和机器内存匹配,计算方法见 小内存 VPS 上的 PHP-FPM 进程池大小计算。

修完 PHP 之后

这次排查最有价值的产出是一张清单:机器上有几个 PHP,各自来自哪里,哪个站点用哪个。把它写下来,下次公告一出,你只需要几分钟就能确认。想对整台服务器做一次已知漏洞检查,而不只是 PHP,可以接着看 检查服务器上的已知 CVE 漏洞。

FAQ

php -v 显示 8.3.6,比 8.3.36 小,是不是还没修复?

在 Ubuntu 上不一定。Ubuntu 修补安全漏洞时保留上游版本号,只改包版本的后缀。Ubuntu 24.04 的修复版是 8.3.6-0ubuntu0.24.04.13,php -v 仍然显示 8.3.6。用 dpkg --compare-versions 比较完整的包版本,或者用 apt-get changelog php8.3-fpm | grep CVE-2026-109618 查 changelog,才是有效的检查。

宝塔面板里升级了 PHP,还需要做什么?

在 软件商店 的 已安装 页面确认每个 PHP 版本都已经是修复版,不要只看 shell 里的 php -v。然后用 ps -eo pid,lstart,args | grep -i '[p]hp' 确认 PHP 进程的启动时间晚于升级时间。宝塔可以同时安装多个 PHP 版本,不同站点可能用不同版本,每个都要检查。

设置了 session.upload_progress.enabled=0,算不算修复了?

不算。PHP 公告说这个设置只能挡住 CVE-2026-109619 已确认的 session 路径,挡不住其他变种,而且对 CVE-2026-109618 没有作用。它必须写在 php.ini 或 FPM 池配置里,并重启每一个 worker 才生效。完整的修复只有更新 PHP 一条路。

我还在用 PHP 7.4 或 8.1,怎么办?

PHP 项目已经不再更新 8.1 及更早的版本。Ubuntu 22.04 的 php8.1 有官方修复版 8.1.2-1ubuntu2.28,20.04 的 php7.4 要通过 Ubuntu Pro 获取。宝塔为 Linux 面板打包了 7.4.34 和 8.1.35。Docker 官方镜像没有 7.4 和 8.1 的修复版标签。如果你的来源没有给出修复版,就把站点迁到 8.3 或更新的版本,因为 8.2 在 2026 年 12 月 31 日也会结束支持。

更新了 WordPress 或 Laravel,是不是就安全了?

不是。WordPress、Laravel 和 Composer 更新的只是用 PHP 写的应用代码,PHP 解释器本身没有任何变化。PHP 公告指出,针对 mod_php 的攻击可以在任何应用代码运行之前触发。你需要更新的是 PHP 运行环境本身,然后重启 PHP-FPM、Apache 和所有长时间运行的 PHP 进程。

#php#安全#cve#baota#1panel#ubuntu