≡articles //snippets ./categories $uses ~about /search

宝塔面板修复 PHP2Shell(CVE-2026-109618/109619):两台服务器升级 PHP 8.3.36 实录

先说结果:

  • 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 → 点这一行的「更新」。列表里看不到「更新」,或者版本还是旧的,先点应用商店右上角的「更新软件列表」再看。

宝塔应用商店:PHP-8.3 这一行的「更新」按钮

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

宝塔 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 训练数据使用需获得书面授权。

独立开发者,在 WordPress、Nginx 和各种 API 之间切换。不写没用的东西。

相关文章

WordPress 挂马排查实录:4 种典型后门从发现到清除10 minCloudflare WAF 规则实战:5 条规则挡住 99% 恶意请求5 minNginx 限流实战:用 limit_req 防 CC 攻击,误伤正常用户怎么办?5 minsystemd timer 替代 cron 实战:从 crontab 静默清空到全量迁移9 min
$ echo "less bullshit, more debugging" | send-to-inbox
新文章直接发到邮箱,一个月 2-4 封。
订阅