宝塔PHP高危漏洞修复实录:8.2.35升级与FPM验收
本页记录澄成官网一次实际的宝塔环境PHP安全处置:确认运行中的PHP版本、查站点绑定、备份、校验官方源码、在隔离目录编译、验证后替换、重启全部FPM工作进程,最后分别检查动态业务与公网访问。命令中的目录是本次环境示例,不是所有宝塔服务器的固定配置。
1. 先弄清这次是什么漏洞
2026年10月21日,PHP项目发布两处严重远程代码执行漏洞的修复:CVE-2026-109618和CVE-2026-109619,分别涉及内存耗尽处理时的状态损坏、跨请求的陈旧内部状态。不要把它们与以前的Windows CGI参数注入漏洞混为一谈。
官方明确PHP-FPM也在受影响范围。换成Nginx、前面有CDN,或者首页是静态HTML,都不能单独证明安全;登录、接口、后台等请求仍可能进入PHP。我们没有用攻击脚本尝试让线上进程崩溃,而是根据实际版本、构建来源和运行路径核对修复状态。
修复版本为PHP 8.2.35、8.3.36、8.4.27、8.5.12。发行版可能回移植补丁,因此其他机器不能只按显示版本号判断,应核对应的软件包安全公告。本次是宝塔独立编译版,不属于系统RPM包。
2. 本次环境实际发现了什么
- 操作系统:Alibaba Cloud Linux 3;Web服务器是Nginx,PHP由宝塔目录管理。
- 主站绑定PHP 8.2,命令行与FPM二进制均为8.2.28,构建日期为2025年8月;运行中的主进程指向该目录;宝塔version.pl却记录8.2.31,显示元数据与真实运行时不一致,不能用它作修复依据。
- PHP 5.6.40也在运行并设置了开机启动,但生效Nginx配置没有业务站绑定它,仅有本地状态查看路由。
- Help知识库本身是静态站;这不代表同机主站和其他动态站免受影响。
- FPM加载php.ini,CLI加载php-cli.ini;FPM的memory_limit是128M,post_max_size与upload_max_filesize均50M,session.upload_progress.enabled为On。
因此本次需要升级真正承载业务的PHP 8.2运行时,同时处理无人使用但仍运行的旧实例。只在SSH里看到一个较新php -v,或者只升级系统包,都可能漏掉网站真正使用的解释器。
一行启动工具与适用范围
我们把本次PHP8.2处置做成了可检查的启动工具:先下载到随机临时文件,核固定SHA256,再以root启动;不使用不校验来源的curl管道直接执行。脚本默认--check只读,--apply才会备份、构建和切换;同一次只能运行一个修复任务。
适用边界是Linux、宝塔Nginx、现有源码版/www/server/php/82、相应SysV脚本和已具备的编译依赖。新编译路径要求GCC9+、至少750MiB可用内存;旧CentOS7工具链或低可用内存会停止,不能把它说成任意宝塔都可一键修好。它不会自动把7.4或8.3迁到8.2,不更新所有PHP分支、不自动安装系统依赖;如果另有活动PHP实例,先退出,要求单独核依赖和处置。本文的5.6停用步骤是我们核完站点绑定后的人工操作,不藏在通用一键脚本里。
这是一行“启动完整修复流程”,不是一秒完成。编译时间取决于CPU、内存和业务负载;切换会重启FPM,可能短暂影响动态请求。请先安排维护窗口并保留SSH连接,脚本的运行时验收不替代各业务流程验收。
第二台CentOS 7:为什么不能原样复制第一台的构建过程
第二台服务器使用CentOS 7与glibc 2.17,也运行PHP8.2.28,同时保留PHP7.4.33进程。Nginx配置中的几个站点虽然包含enable-php-74.conf,实际请求走Node反向代理或静态文件;核对应目录没有PHP文件,旧FPM在108天内累计连接仅19次。我们备份后停用7.4并取消自启,原静态/代理入口200或301、实际PHP82管理登录页200均保持。这里不能只看到“绑定7.4”就强行升级应用,也不能只看进程少就贸然停止。
首轮编译出现过高负载、SSH及动态探测超时,最终在OpenSSL扩展处报错退出,未替换生产运行时。排查发现:旧PHP实际链接的是已安装的OpenSSL1.1.1w,而默认pkg-config返回系统1.0.2k,头文件/库选择与原运行时不同。只复用php-config --configure-options会遗漏这类构建环境。
后续明确选择原有OpenSSL1.1.1w的pkg-config目录。该机原FPM没有加载OPcache,也未启用JIT;本次保留字节码缓存模块,但构建时禁用JIT,减少老编译器的内存需求。没有把第一台系统生成的二进制直接复制过来。
export PKG_CONFIG_PATH=/usr/local/openssl111/lib/pkgconfig:/usr/local/onig/lib/pkgconfig:/usr/local/libsodium/lib/pkgconfig:
pkg-config --modversion openssl
# 在复用原configure选项的基础上,仅此台额外加入:
# --disable-opcache-jit为保护正在运行的业务,“仅编译”阶段放入单独资源组,内存上限700MB、最多一个CPU核心、Nice10。CentOS 7的systemd219使用MemoryLimit;不要照抄新系统才支持的属性。原PHP进程和后续服务重启不放进这个临时编译资源组,以免资源组退出时连业务进程也被回收。
本机使用的完整构建脚本如下。先完成下文环境核对、备份和官方源码校验,再按实际路径准备脚本;其中OpenSSL、oniguruma和libsodium路径都来自本机核验,不能盲目移植:
cat > /root/php-security-20261022/build-safe.sh <<'BUILD'
#!/bin/bash
set -eu
TASK=/root/php-security-20261022
cd "$TASK"
mkdir -p source-safe
if test ! -d source-safe/php-8.2.35; then
tar -xf php-8.2.35.tar.xz -C source-safe
fi
cd source-safe/php-8.2.35
export CC=/usr/bin/gcc CXX=/usr/bin/g++
export PKG_CONFIG_PATH=/usr/local/openssl111/lib/pkgconfig:/usr/local/onig/lib/pkgconfig:/usr/local/libsodium/lib/pkgconfig:
pkg-config --modversion openssl
python3 - <<'PY'
import shlex, subprocess
args = shlex.split(subprocess.check_output([
'/www/server/php/82/bin/php-config', '--configure-options'
]).decode())
subprocess.check_call(['./configure'] + args + ['--disable-opcache-jit'])
PY
make -j1
make install INSTALL_ROOT="$TASK/stage"
"$TASK/stage/www/server/php/82/bin/php" -n -v
"$TASK/stage/www/server/php/82/sbin/php-fpm" -v
date -Is > "$TASK/build-success"
BUILD
chmod 700 /root/php-security-20261022/build-safe.sh随后启动“只编译、不切换”的资源组。实际操作中我们把该脚本标准输出与错误输出重定向到私有build-safe.log,便于SSH断开后继续检查;systemd journal也可查看单元状态。下面展示资源控制参数:
systemd-run --unit=php82-security-build47 \
--property=MemoryLimit=700M --property=CPUQuota=100% \
--property=CPUShares=128 --property=Nice=10 \
/bin/bash /root/php-security-20261022/build-safe.sh
systemctl show php82-security-build47 \
-p ActiveState -p MemoryCurrent -p MemoryLimit -p CPUQuotaPerSecUSecbuild-safe.sh只做前文的独立源码配置、make、暂存安装与版本检查,不启动或重启PHP服务。编译资源触顶时应允许构建失败,再调整方法,不提高业务PHP的内存上限来掩盖问题。针对这次经验,公开工具已经加入旧编译器和低可用内存门槛,不再让类似环境直接进入无人值守的在线编译。
实际结果:受限单任务构建完成后,先在暂存目录验证PHP8.2.35和11项基础功能,再通过独立SSH会话运行修复脚本,未在构建单元内启动生产服务。切换后Unix socket实际FPM返回8.2.35,9个业务模块均加载;主进程映射哈希与新FPM一致,php.ini、php-cli.ini、php-fpm.conf与备份逐字节相同。管理登录页返回200且含登录标题;其余Node/静态路由保持原200或301。重复执行工具识别已修补版本,不再次安装或重启。旧7.4实例停用并取消自启,文件保留,不能写成7.4已经升级。没有进行数据库写入、真实客户表单、完整上传或计划任务验收。
3. 第一步:查CLI、FPM、进程与站点绑定
以下命令是只读检查。先确认自己的实际路径,再执行;不要照抄其他机器的版本目录。
command -v php
php -v
/www/server/php/82/bin/php -v
/www/server/php/82/sbin/php-fpm -v
/www/server/php/82/bin/php --ini
/www/server/php/82/sbin/php-fpm -i \
| grep -E '^(PHP Version|Loaded Configuration|memory_limit|post_max_size|upload_max_filesize|session.upload_progress.enabled)'
ps -eo pid,args | grep '[p]hp-fpm: master process'再读取运行中主进程实际使用的二进制。把PID换成刚查出的主进程号:
readlink /proc/PID/exe
sha256sum /proc/PID/exe检查生效配置,不能只看宝塔网站列表里的显示标签:
/www/server/nginx/sbin/nginx -T 2>&1 \
| grep -E 'server_name|enable-php|fastcgi_pass|php-cgi-56.sock|php-cgi-82.sock'
rpm -qf /www/server/php/82/bin/php本次主站与同机其他动态站均指向php-cgi-82.sock。php-cgi-56.sock只出现在状态查看路由,没有业务站绑定。完整Nginx配置可能带私有地址或凭据,不应直接贴到公开工单。
另一个实际陷阱是服务状态:本次systemctl显示生成式服务为inactive,但进程与Unix套接字实际存在。判断运行状态要结合进程、监听和宝塔/SysV服务脚本,不能因此认定PHP已经停了。
4. 第二步:备份运行时与配置,记录回退依据
本次备份放在Web目录外、仅管理员可读的目录。记录原二进制哈希、原配置选项与扩展清单;备份里可能有私有配置,不能放进知识库下载或公开Git仓库。
TASK=/root/php-security-20261022
mkdir -p "$TASK"
chmod 700 "$TASK"
tar -czf "$TASK/php82-before.tar.gz" -C /www/server/php 82
cp -a /www/server/php/56/etc "$TASK/php56-etc-before"
cp -a /etc/init.d/php-fpm-82 /etc/init.d/php-fpm-56 "$TASK/"
cp -a /www/server/panel/vhost/nginx "$TASK/nginx-vhosts-before"
/www/server/php/82/bin/php-config --configure-options \
> "$TASK/configure-options.before.txt"
/www/server/php/82/bin/php -m > "$TASK/modules-before.txt"
sha256sum /www/server/php/82/bin/php \
/www/server/php/82/sbin/php-fpm > "$TASK/binaries-before.sha256"这份备份服务于PHP运行时回退,不是整机灾备。本次不变更业务数据库结构,也不把数据库导出、客户表、证书私钥等加入公开记录。如果后续还要迁移数据库、应用或主版本,应另做对应备份与恢复演练。
5. 第三步:核实无依赖后,停掉未使用的PHP 5.6
本次先查生效站点绑定、实际监听与启动方式,确认没有业务站使用PHP 5.6,再正常停止并关闭其SysV自启:
/etc/init.d/php-fpm-56 stop
chkconfig php-fpm-56 off
chkconfig --list php-fpm-56
ps -eo pid,args | grep '[p]hp-fpm: master process'
ss -xlpn | grep 'php-cgi-56.sock'验收点是旧主进程与监听退出,启动项显示off,而不是只看停止命令返回。旧文件和配置保留,未把PHP 5.6说成“已经打上补丁”。如果你的站点仍依赖5.6,不能直接停;应先安排兼容性迁移或受维护供应商方案。官方此次提供的旧分支最小源码回移植涉及7.4、8.0、8.1,不应据此认为5.6有同等现成修复。
6. 为什么没有直接点宝塔的卸载重装,或运行yum update php
本次/www/server/php/82/bin/php不属于系统RPM包,系统包更新不能保证改到宝塔运行时。为避免升级过程中删掉现有扩展和配置,我们选择在同一8.2分支使用官方8.2.35源码,按原编译参数构建,先暂存再切换。
如果你的宝塔软件商店已经提供包含修复的对应版本,也可以先做好备份,通过面板更新;更新后仍要检查实际FPM版本、旧工作进程和业务。本文记录的是本机执行的源码更新路径,不冒充“本次亲自在面板点过”的步骤,也不保证不同宝塔版本菜单完全相同。
7. 第四步:从官方取源码,并校验SHA256
本次使用php.net发布信息里的8.2.35 tar.xz校验值,下载和校验均成功。其他版本不能套用这个哈希。
TASK=/root/php-security-20261022
cd "$TASK"
curl -fL --connect-timeout 15 --max-time 180 \
https://www.php.net/distributions/php-8.2.35.tar.xz \
-o php-8.2.35.tar.xz
printf '%s\n' \
'5737b98601fe5ab1b8c5db0c82ed3158acb9283ed3081ffe2faffb9ac81455e6 php-8.2.35.tar.xz' \
| sha256sum -c -
tar -xf php-8.2.35.tar.xz必须得到php-8.2.35.tar.xz: OK再继续。不要为了下载成功关闭TLS证书校验,不使用来历不明的“漏洞修复一键脚本”。
8. 第五步:复用原编译选项,在隔离目录构建
本机已有编译器和原依赖,构建使用原php-config输出的选项,保留现有安装前缀;构建本身不会覆盖生产。服务器只有2核、约2GB内存,最初使用make -j1;核可用内存后仅暂停隔离目录的make并以nice10、最多2任务继续,期间观察内存和线上响应,不把本次机器的资源数字推广为PHP通用要求。
TASK=/root/php-security-20261022
cd "$TASK/php-8.2.35"
export PKG_CONFIG_PATH=/usr/local/onig/lib/pkgconfig:/usr/local/libsodium/lib/pkgconfig:
python3 - <<'PY'
import shlex, subprocess
args = shlex.split(subprocess.check_output([
'/www/server/php/82/bin/php-config', '--configure-options'
]).decode())
subprocess.check_call(['./configure'] + args)
PY
nice -n 10 make -j2
make install INSTALL_ROOT="$TASK/stage"INSTALL_ROOT把安装结果先放到私有暂存目录;不要删掉这一项直接安装。若configure缺依赖或make失败,应先停在这里,核错误和依赖,不覆盖生产、不无差别安装整套新环境。原参数里的PKG_CONFIG_PATH是本机依赖位置,别照搬到没有这些目录的机器。
9. 切换前必须检查什么
暂存二进制应能启动,版本正确,依赖不缺失;再用生产的CLI配置检查扩展、用生产FPM配置测试语法。此时生产仍由原二进制提供服务。
TASK=/root/php-security-20261022
STAGE="$TASK/stage/www/server/php/82"
"$STAGE/bin/php" -n -v
"$STAGE/bin/php" -c /www/server/php/82/etc/php-cli.ini -m
"$STAGE/sbin/php-fpm" -t -y /www/server/php/82/etc/php-fpm.conf
ldd "$STAGE/bin/php" | grep 'not found'
ldd "$STAGE/sbin/php-fpm" | grep 'not found'ldd没有not found才是依赖检查通过;grep无匹配会返回1,不能机械当成构建失败。还要比较原扩展清单,重点核mysqli、pdo_mysql、mbstring、curl、openssl、gd、intl、sodium、zip等实际业务依赖。不要因为php -v成功就忽略PHP Startup或无法加载扩展的警告。
10. 生产替换、重启与本次验收结果
本次暂存二进制通过11项基础功能检查,再运行同一份公开工具的--apply路径完成切换;工具复用了本次已经按前文编译并验收的暂存产物。没有在另一台干净服务器重复全流程,也没有对各发行版、扩展组合或其他PHP分支宣称通用验证。
TASK=/root/php-security-20261022
bash "$TASK/baota-php82-security-fix.sh" --apply脚本实际替换的是PHP82的CLI、CGI、FPM二进制、php-config/phpize等元数据工具和对应OPcache模块;不覆盖etc配置目录、不重装外部zip.so、不改站点源码、数据库结构、Nginx站点绑定或原套接字。每个文件先写同目录临时文件再原子替换。切换期间若关键步骤报错或收到退出信号,会尝试恢复私有备份并明确报告“未修复”,不会把回退当成功;本次没有故意在生产制造失败来演练回退。
随后执行:
/www/server/php/82/sbin/php-fpm -t -y /www/server/php/82/etc/php-fpm.conf
/etc/init.d/php-fpm-82 restart
/www/server/php/82/bin/php -v
/www/server/php/82/sbin/php-fpm -v这些配置验证、重启和版本检查由工具完成,不要执行工具后又机械重启一遍。重启PHP-FPM会短暂影响动态请求,本文没有测出或承诺零中断。
本次实际验收结果如下:
- CLI与FPM均返回8.2.35;主进程PID变化,运行中主进程与子进程的/proc/PID/exe哈希均与新版FPM文件一致。
- 通过本机Unix套接字请求一个位于Web目录外的短期诊断脚本,实际返回version8.2.35、sapi=fpm-fcgi、加载php.ini,9项业务扩展均true;测试后删除诊断脚本,没有公开phpinfo页面。
- 新版CLI的JSON中文、mbstring、OpenSSL加解密、PDO MySQL驱动、mysqli、curl、GD、intl、sodium、ZIP读写及SQLite内存查询11项检查均通过。没有写入业务数据库或提交客户表单。
- php.ini、php-cli.ini、php-fpm.conf与修复前备份逐字节一致;原站点绑定及套接字保留。
- 主站源站wp-login.php为200并包含登录表单;实际进入PHP与WordPress的只读REST请求为200且返回内容ID。不是只验静态首页。
- Nginx配置语法检查通过;PHP5.6进程仍停止、自启仍关闭。宝塔version.pl在二进制与进程检查完成后才校正为8.2.35,不是单改标签。
- 再次运行工具的--apply路径,识别版本已达目标,核运行进程后不降级、不重复编译或重启。
同机其他站不能默认都是WordPress:我们尝试过的两个WordPress专用探测路径在其他站返回404,这只说明该路径不能用于其业务验收,不直接当升级故障;主站正确路径单独验收。另一个图片子站源站TLS出现自签证书校验失败,属于单列的证书问题,未通过关闭证书校验把它报成全站验收通过。
构建日志也并非“完全零警告”:原宝塔参数包含已废弃或不识别的选项,编译器亦输出过警告;最终以构建退出状态、实际产物、模块和运行检查为准。不能从旧参数里出现--with-xmlrpc就认定XML-RPC扩展已启用。本次修复不是一份完整的入侵调查,也没有实测所有后台写入、上传、业务计划任务或长时间高负载。
11. 回退不是恢复安全状态
若新运行时出现业务不兼容,应先暂停切换、保留错误证据,使用本机私有备份恢复必要二进制与配置并重新启动服务,再核动态业务。回到8.2.28只是业务应急回退,会重新暴露漏洞,必须配合限制暴露面并继续完成安全升级;不能把回退完成写成漏洞修复完成。
公开工具的restore_previous函数保留了自动恢复逻辑。管理员如需手动应急回退,应先核实私有备份、环境和目标;下面只恢复工具实际替换过的文件,不解压覆盖整个运行目录。仅在确认需要业务应急、已安排隔离或限制暴露面后,核实TASK与PREFIX再以root逐步执行:
TASK=/root/php-security-20261022
PREFIX=/www/server/php/82
test -s "$TASK/php82-before.tar.gz"
test -d "$TASK/replace-before"
/etc/init.d/php-fpm-82 stop
while IFS= read -r -d '' old; do
rel="${old#"$TASK/replace-before/"}"
cp -a "$old" "$PREFIX/$rel.rollback-new"
mv -f "$PREFIX/$rel.rollback-new" "$PREFIX/$rel"
done < <(find "$TASK/replace-before" -type f -print0)
if test -f "$TASK/version.pl.before"; then
cp -a "$TASK/version.pl.before" "$PREFIX/version.pl"
fi
"$PREFIX/sbin/php-fpm" -t -y "$PREFIX/etc/php-fpm.conf"
/etc/init.d/php-fpm-82 start
"$PREFIX/bin/php" -v恢复后还须核真实进程、模块、源站动态请求与业务;步骤失败应停下定位,不能继续覆盖。这段命令解释本次真实备份布局,但没有在已修好的生产环境再次执行降级试验。直接解压覆盖整个运行目录可能覆盖新文件、权限或正在运行的进程;没有业务绑定的5.6也不应因回退82而自动重启。
12. 修复中容易误判的几件事
- 首页200可能只是静态快照或CDN缓存;必须另验登录页、动态接口及实际FPM。
- php -v是CLI结果;FPM、不同站点、容器或旧worker可能仍使用另一个版本。
- 改php.ini但没有重启所有worker,旧配置或旧代码可能仍在处理请求。
- 只关闭session.upload_progress.enabled属于特定攻击路径的缓解,不能代替这两处漏洞的完整补丁。
- 直接把memory_limit改成无限不是无风险修复,可能带来可用性问题;官方也没有把它称为完整修复。本次选择升级运行时,不照搬这个临时方案。
- 没看到错误日志、只扫描一次、WAF显示拦截、系统软件更新成功,都不能证明运行时已修复或服务器没有被入侵。
- PHP 8.2接近维护周期末尾,8.2.35属于本次同分支紧急修补,后续应另排受支持主分支迁移并做应用兼容性测试。
13. 官方依据与后续维护
本次实际运行记录与通用建议分别标记;后续版本的修复状态以PHP官方及所用发行商公告为准。如果存在异常进程、未知PHP文件或账号变动,应另行开展入侵排查,升级PHP不能清除已经留下的后门。