先说结果:
- 10 月 21 日 PHP 官方公布了 CVE-2026-109618 和 CVE-2026-109619,外号 PHP2Shell,CVSS 都是 9.2,攻击者不用登录就可能在服务器上执行代码。宝塔用的 PHP-FPM 也在受影响范围里。
- 我手上两台宝塔服务器都是 PHP 8.3(一台 8.3.30,一台 8.3.27),都升到了 8.3.36。
- 第一台在应用商店点「更新」就完了,编译用了十几分钟,网站没停。
- 第二台卡在下载源码包:宝塔节点每秒只有几 KB,停在 16% 不动。换成 php.net 的官方包(校验过 SHA256)以后,宝塔自己接着编译装完了。
- 第二台升完以后每次运行 php 都提示
Module "mbstring" is already loaded,注释掉配置里多出来的一行就好了。 - 两台的 PHP-FPM 日志和网站文件都查过,没有被入侵的迹象。
一、要升到哪个版本
PHP 官方公告给的修复版本:
| PHP 分支 | 修复版本 |
|---|---|
| 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 的源码补丁。宝塔 Linux 面板打包了修复版:7.4.34、8.0.27、8.1.35 |
还在用 7.4、8.0、8.1 的,宝塔里也能直接更新到上面这几个版本。它们是宝塔自己打包的,不是 PHP 官方发布的,修完以后还是要尽快迁到 8.3 或 8.4。PHP 官方的源码补丁最老只到 7.4。Windows 版宝塔的 8.1 及更老版本,目前只能改配置临时缓解。
官方点名的受影响方式有 PHP-FPM、Apache mod_php、php-cgi、FrankenPHP、LiteSpeed。风险最高的是 mod_php,官方演示过只发一个请求、在网站代码运行之前就能触发。PHP-FPM 上已确认的利用方式要借助文件上传,还要网站代码配合,门槛高一些,但同样不用登录。漏洞出在 PHP 本身,WordPress 的安全插件管不到这一层,只能靠升级。
AWS 说 PHP2Shell 已经在被广泛利用:它的蜜罐 10 月 17 日就收到了攻击请求,比漏洞公开还早 4 天;21 日公开当天,网上又出现了好几份概念验证代码。目前还没有公开确认的成功入侵案例,但不该等到有了再升级。
PHP 8.2 在 2026 年 12 月 31 日停止安全支持,还在用 8.2 的,这次修完以后,年底前最好迁到 8.3 或 8.4。
二、先看自己是什么版本
SSH 登录后执行(宝塔的 PHP 装在 /www/server/php/版本号/,8.3 就是 83):
php -v | head -1
/www/server/php/83/sbin/php-fpm -v | head -1
两条都要看。第一条是命令行版,第二条才是网站真正在用的 PHP-FPM,两个用的配置文件也不一样(后面第五节会碰到)。
我这两台当时是:
PHP 8.3.30 (cli) (built: Feb 28 2026 16:01:45) (NTS)
PHP 8.3.27 (cli) (built: Nov 3 2025 14:38:48) (NTS)
注意:「宝塔里已经是最新版」不等于「已经修好」。宝塔的软件源比官方慢,有人那边 PHP 8.2 在宝塔里显示的最新版一度还是 8.2.34,而 8.2 的修复版本是 8.2.35。要拿版本号和上面的表逐个对。
如果 PHP 是用 apt 装的(比如 Ubuntu 自带的 php8.3-fpm),发行版会把补丁打进旧版本号里,不能只看版本号,要查更新日志里有没有这两个编号:
apt changelog php8.3-fpm 2>/dev/null | grep -E 'CVE-2026-10961(8|9)'
能搜到就是打过补丁了。
三、在宝塔里升级
左侧菜单「应用商店」(老版本叫「软件商店」)→ 找到 PHP-8.3 → 点这一行的「更新」。列表里看不到「更新」,或者版本还是旧的,先点应用商店右上角的「更新软件列表」再看。

弹窗里会写「最新版本」,先看清楚是不是 8.3.36,是了再点「立即更新」。

宝塔是从源码重新编译 PHP。两台都是 2 核 2G,编译各用了十几分钟。编译期间旧的 PHP 照常在跑,网站不受影响,最后重启 PHP-FPM 那一下只有几秒。
第一台就是这样一路走完的。第二台没这么顺。
四、宝塔下载卡住:换成 php.net 的官方源码包
第二台点了更新以后,过了二十分钟,PHP 还是 8.3.27。看宝塔的执行日志:
root@…:~# tail -3 /tmp/panelExec.log
3050K .......... .......... .......... .......... .......... 15% 6.53K 42m30s
3100K .......... .......... .......... .......... .......... 16% 5.38K 42m31s
3150K .......... .......... .......... .......... .........
宝塔在从自己的下载节点拉 php-8.3.36.tar.gz,速度只有每秒 5 KB 上下,下到 3.2 MB(大约 16%)以后就不动了,日志十分钟没有更新。同一台机器从 php.net 下同一个包,20 MB 不到一秒就下完了。
卡住的下载不会自己报错退出,只能手动处理。思路是:从 php.net 下载官方包,校验没问题以后放到宝塔正在下载的那个位置,再结束卡住的下载进程,让宝塔接着往下走。
1. 确认是真卡住了,找到下载进程:
cd /www/server/php/83
ls -l src.tar.gz; sleep 60; ls -l src.tar.gz # 一分钟大小没变就是卡住了
pgrep -af 'wget -O src.tar.gz' # 记下最前面的进程号
2. 从 php.net 下载,并核对官方公布的 SHA256:
curl -s 'https://www.php.net/releases/index.php?json&version=8.3.36' \
| python3 -c 'import json,sys; [print(s["filename"], s["sha256"]) for s in json.load(sys.stdin)["source"]]'
curl -fL -o /root/php-8.3.36.tar.gz https://www.php.net/distributions/php-8.3.36.tar.gz
echo "c93f3441f87f6ab156bef4432db215d7e61ca2c0183bb3a2aa28d9886ab59f53 /root/php-8.3.36.tar.gz" | sha256sum -c -
最后一条输出 OK 才能继续。上面的 SHA256 是 8.3.36 的 .tar.gz,别的版本用第一条命令查到的值替换。
3. 顺手确认宝塔下的和官方的是同一个文件。把宝塔已经下好的那一截和官方包的开头逐字节比一下:
cmp -n $(stat -c %s src.tar.gz) /root/php-8.3.36.tar.gz src.tar.gz && echo 一致
我这台输出了「一致」,前 3276332 字节完全一样。
4. 先替换文件,再结束下载进程(进程号换成第 1 步查到的):
cp /root/php-8.3.36.tar.gz src.tar.gz.new && mv -f src.tar.gz.new src.tar.gz
kill 进程号
tail -f /tmp/panelExec.log
顺序不能反。先 mv 是让 src.tar.gz 这个名字指向完整的包;之后再结束下载,宝塔的脚本接着解压的就是完整的包。如果先结束下载再替换,脚本可能在这几秒里拿着不完整的包往下走。
我这台结束下载以后,日志马上开始刷 checking build system type...,编译接着跑,十几分钟后出现 Command executed!,PHP-FPM 也自动重启了。
如果你那边宝塔因为下载进程被结束而直接报错退出:这时候还没开始编译,原来的 PHP 没被动过。回应用商店再点一次「更新」就行,节点慢的话重复上面的步骤。
五、升级后提示 mbstring already loaded
第二台升完以后,每次运行 php 都多一行:
PHP Warning: Module "mbstring" is already loaded in Unknown on line 0
原因是这次重新编译时 mbstring 直接编进了 PHP,但配置文件里还留着一行再加载一次 mbstring.so。查一下是哪几行:
grep -n 'mbstring.so' /www/server/php/83/etc/php.ini /www/server/php/83/etc/php-cli.ini
宝塔给网站用的 PHP-FPM 读 php.ini,命令行的 php 读 php-cli.ini,两个文件里各有一行。先备份,再把这两行注释掉:
cp /www/server/php/83/etc/php.ini /root/php.ini.bak
cp /www/server/php/83/etc/php-cli.ini /root/php-cli.ini.bak
sed -i 's|^extension = .*/mbstring\.so$|;&|' /www/server/php/83/etc/php.ini /www/server/php/83/etc/php-cli.ini
/etc/init.d/php-fpm-83 reload
php -v | head -1
最后一条不再出现 Warning 就好了。这个提示不影响网站运行,但每次 PHP 进程启动都会往日志里写,还是清掉省心。第一台没有这个问题,不是每台都会遇到。
六、确认新版本真的在跑
程序文件换成了新的,不代表正在跑的进程也换了。要看 PHP-FPM 主进程是不是在升级之后启动的:
/www/server/php/83/sbin/php-fpm -v | head -1
ps -eo lstart,cmd | grep '[p]hp-fpm: master'
curl -s -o /dev/null -w '%{http_code}\n' https://你的域名/
第一台的结果:
PHP 8.3.36 (fpm-fcgi) (built: Oct 22 2026 03:52:20)
Thu Oct 22 03:53:11 2026 php-fpm: master process (/www/server/php/83/etc/php-fpm.conf)
200
03:52 编译完,03:53 主进程启动,跑的就是新版本。主进程启动时间早于编译时间的话,去宝塔里把 PHP 重启一次。
有一个容易看错的地方:宝塔默认 pm = ondemand,没人访问的时候一个 worker 进程都没有,pgrep -fc 'php-fpm: pool' 显示 0 是正常的,不是 PHP 挂了。用下面这条确认自己是不是这种模式:
grep -E '^pm *=' /www/server/php/83/etc/php-fpm.conf
另外,首页返回 200 也可能是缓存在顶着。最好再打开一个肯定要走 PHP 的地址,比如 /wp-json/。
七、暂时升不了的话,先做临时缓解
宝塔还没上修复版本、又不方便自己编译的话,先按官方公告做下面几条。官方说得很清楚:这些只能降低风险,升级才是完整的修复。
宝塔里改:应用商店 → PHP → 设置 → 配置修改。
1. 关掉上传进度(针对 CVE-2026-109619):
session.upload_progress.enabled = 0
必须写在 php.ini 或 FPM 的 pool 配置里。写在 .user.ini 或者用 ini_set() 都不管用,因为攻击发生在这些设置生效之前。改完要把所有 worker 换掉:宝塔的「重载配置」会给 PHP-FPM 发 USR2 信号,所有 worker 都会重新启动;点「重启」也行。
2. 调低请求体上限(针对 CVE-2026-109618)。官方建议:不需要上传文件的站 post_max_size = 8M;要上传图片的 WordPress 站 post_max_size = 32M,而且 upload_max_filesize 要比它小:
post_max_size = 32M
upload_max_filesize = 30M
宝塔默认是 50M,Nginx 那边的 client_max_body_size 也一起改成 32m。
3. 官方还建议把 memory_limit 设成 -1,但同时要有容器或 cgroup 的内存上限兜底。2G 内存的小机器没有兜底就这样改,一个失控的请求就可能把整台机器的内存吃光。没把握的话这条先不做。另外不要用 RLIMIT_AS 或 RLIMIT_DATA 来限制内存,官方说这反而会触发漏洞。
用的是 PHP 8.5 的,再加一条 fatal_error_backtraces = 0。
八、Node.js 也要升
Node.js 在同一天发了安全更新,一共修了 10 个漏洞。最严重的是 Brotli 解压的越界写入(CVE-2026-111497),Node 程序只要解压了攻击者发来的 Brotli 数据,就可能被远程执行代码。
| Node.js 分支 | 修复版本 |
|---|---|
| 22.x | 22.23.4 |
| 24.x | 24.22.1 |
| 26.x | 26.12.1 |
我的网站服务器上没装 Node。另一台用 nvm 装着 22.23.3,升级是一条命令,原来的全局包也一起装过去:
nvm install 22.23.4 --reinstall-packages-from=22.23.3
nvm alias default 22
node -v
升完要把用 Node 跑着的程序重启一遍,不重启跑的还是旧版本。用宝塔「Node.js 版本管理器」装的,在应用商店里打开它,点「更新版本列表」,装上新版本,再把项目切过去并重启。
还在用 Node 20 的,它在 2026 年 4 月就停止维护了,这次的补丁不会有,直接换到 22 或 24。
九、升级前有没有被打
升级只能挡住以后的攻击。如果升级前已经被放了后门,后门还在。下面这几条都是只读的,不会改任何东西。
1. 查 PHP-FPM 日志里有没有大量内存报错和进程崩溃:
grep -hE 'Allowed memory size|SIGSEGV|exited on signal' /www/server/php/83/var/log/php-fpm.log | tail -20
两台都查了。内存报错(Allowed memory size)和段错误(SIGSEGV)一条都没有,查到的全是 worker 被结束的记录:
- 第一台日志 3840 行,只有 4 条,都是 9 月 23 日的
SIGKILL,和 slow.log 最后一条记录在同一分钟,是跑太久的请求被结束了。 - 第二台日志 2519 行,有 20 条,从 5 月到 10 月零零散散。最后两条在 10 月 17 日凌晨 3 点 25 分前后,那正是这台硬盘写满、服务停止响应的时候(过程见 KiwiVM 救回全过程),不是这次的漏洞。
这类记录平时就会有。要警惕的是短时间里大量出现,或者和内存报错、段错误一起出现。
2. 查最近新增或改过的 PHP 文件。有报道说 10 月 17 日就出现了攻击尝试,所以从 17 日查起:
find /www/wwwroot -name '*.php' -newermt '2026-10-17' -not -path '*/cache/*' -ls
find /www/wwwroot -path '*uploads*' \( -name '*.php*' -o -name '*.phtml' -o -name '.user.ini' \) -ls
第一条列出来的,多半是这几天自己更新的插件、主题和语言包,要逐个看路径和时间,对得上自己的操作就没事。第二条正常情况下应该是空的,uploads 目录里不该有 PHP 文件。我这两台第二条都是空的。
3. 看 PHP-FPM 的 worker 有没有带出子进程:
for p in $(pgrep -f 'php-fpm: pool'); do ps --no-headers -o pid,user,lstart,cmd --ppid "$p"; done
worker 底下出现 sh、bash、curl、wget,就要马上查。前面说过 ondemand 模式下没人访问时没有 worker,这条要在有访问的时候跑才有意义。
查出可疑文件先别急着删,先连同时间一起备份留证据,再按 WordPress 挂马排查实录 里的步骤处理。真被放了后门的话,数据库密码、服务器上存的各种 API 密钥都要换掉,因为攻击者拿到的是网站进程的权限,网站能读到的东西他都能读到。
十、源站只放行 Cloudflare:有用,但不能代替升级
我两台的网站都在 Cloudflare 后面,源站 Nginx 只放行 Cloudflare 的 IP。拿服务器 IP 直接访问,会被 Nginx 直接返回 403,请求到不了 PHP:
root@…:~# curl -sk -o /dev/null -w '%{http_code}\n' --resolve 你的域名:443:服务器IP https://你的域名/
403
这挡住了满网扫 IP 的那一类攻击。但正常走 Cloudflare 的访问照样会到 PHP,攻击请求也可以这样走,所以它只能算多一层,不能代替升级。Cloudflare 的 WAF 规则怎么配,可以看 Cloudflare WAF 规则实战。
记下来的几件事
- 「宝塔最新版」不等于「已修复」,版本号要和官方的修复版本逐个对。
- 宝塔下载卡住,可以换成 php.net 的官方包,但要先校验 SHA256;先替换文件,再结束下载。
- 升完以后看
php-fpm -v和主进程的启动时间,命令行的php -v不算数。 - 升级只防以后,升级前的这几天要回头查一遍日志和文件。
© 2026 本文由 Charlie 原创撰写,发布于 sodebug.com。未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。