禅道 15.5 不升级也能修 PHP2Shell:Docker + Ubuntu Pro ESM 实战
记录一次禅道 15.5 的 PHP2Shell 漏洞修复:保留 ionCube 商业插件,使用 Ubuntu Pro ESM 回移 PHP 7.2 补丁,完成 Docker 镜像构建、排障与生产验收。
2026 年 10 月 21 日,PHP 官方披露了两项可能导致未授权远程代码执行的严重漏洞,社区称为 PHP2Shell。我手上恰好有一套长期运行的 禅道开源版 15.5,内置 PHP 7.2,使用 Apache mod_php。按正常思路,应该升级整套应用;但实际遇到的问题是:购买过的插件依赖 ionCube,直接升级禅道或者 PHP 主版本可能让插件无法运行。
最后选择了一条影响范围更小的路线:禅道 15.5 不升级、插件不变,只给容器内 PHP 7.2 安装 Ubuntu 官方回移的安全补丁。 测试、生产两个环境已经实际完成镜像切换,PHP 包版本、Apache 模块、ionCube、HTTP 和远程数据库连接均通过检查。
下面记录完整处理过程,包括踩过的坑。命令中的镜像名、域名和路径均已脱敏,应用前需替换成自己的配置。
一、这次漏洞是什么
官方披露的两个编号是:
| CVE | 问题 | 可能影响 |
|---|---|---|
| CVE-2026-109618 | PHP 处理内存不足时,内部请求数据状态可能异常 | 远程代码执行 |
| CVE-2026-109619 | 请求内部状态在工作进程复用时管理不当 | 远程代码执行 |
官方认为 Apache mod_php 是需要优先处理的高风险场景之一:攻击可能在应用代码正式执行之前触发。因此,即使业务系统有登录认证,也不能把登录页当成补丁的替代方案。
权威来源:PHP 官方安全公告、Ubuntu 安全公告 USN-8948-2。
二、实际运行环境
开始排查时,我还以为宿主机是 CentOS 7;运行 cat /etc/os-release 后才确认,真正的宿主机是 Ubuntu 22.04.5,而禅道容器内部是 Ubuntu 18.04.5。
| 组件 | 修复前 |
|---|---|
| 宿主机 | Ubuntu 22.04.5 LTS |
| Docker Engine | 29.1.5 |
| 禅道 | 开源版 15.5 |
| 容器基础系统 | Ubuntu 18.04.5 LTS |
| PHP | 7.2.24-0ubuntu0.18.04.9 |
| Web 服务 | Apache 2 + mpm_prefork + php7_module |
| 商业插件环境 | ionCube Loader 10.4.1、mcrypt |
| 数据库 | 远程 MySQL 5.7,业务库与测试库分离 |
检查命令:
docker exec zentao-test php -v
docker exec zentao-test apache2ctl -M 2>/dev/null \
| grep -E 'php7_module|mpm_prefork_module'
docker exec zentao-test php -m
docker exec zentao-test dpkg -s libapache2-mod-php7.2 \
| grep '^Version:'
这里有两个很容易误判的地方:
php -v检查的是 CLI,还需要检查 Apache 加载的 PHP 模块包。- Docker 宿主机升级 PHP 不会改变容器里的 PHP。安全补丁必须进入实际承载 Web 请求的镜像。
三、为什么不升级禅道也能修复
Ubuntu 为 Ubuntu 18.04 的 PHP 7.2 发布了安全回移包:
修复前:7.2.24-0ubuntu0.18.04.9
修复后:7.2.24-0ubuntu0.18.04.17+esm14
PHP 主版本仍是 7.2.24,但 Debian/Ubuntu 软件包的修订版本发生了变化。官方 USN-8948-2 确认该版本包含两个漏洞的修复。
Ubuntu 18.04 的这批补丁通过 Ubuntu Pro 的 ESM(扩展安全维护) 提供。Ubuntu Pro 有符合资格时可用的免费订阅额度(通常为最多 5 台物理机);是否适用,要以账户类型和官方条款为准。本次实际使用的是免费订阅,没有为获取补丁额外支付费用。
这个办法的边界也要说清楚:它解决的是这两个 PHP 漏洞,不等于修复禅道 15.5、旧版 Apache、MariaDB 或其他组件的全部历史漏洞。
四、操作前先备份:镜像、数据库、附件分开处理
这次没有在运行中的生产容器里直接执行 apt upgrade,而是基于旧镜像重新构建,保留回滚路径。
镜像备份:
docker save your-registry.example.com/common/zentao:15.5 \
| gzip > zentao-15.5-original.tar.gz
gzip -t zentao-15.5-original.tar.gz
sha256sum zentao-15.5-original.tar.gz
同时分别备份:
- 禅道生产、测试各自的 MySQL 业务数据库,用一致性逻辑导出,不能把另一个业务库的备份当成禅道备份。
/www/zentaopms对应的宿主机持久化目录,包括附件、配置和已购买插件。- 原 Compose 文件、镜像摘要和容器实际挂载;如旧镜像还声明了
/var/lib/mysql卷,也要确认它在重建时不会被误删。
尤其注意:docker save 只保存镜像,不包含绑定挂载的业务数据。 数据库即使走远程,也必须单独备份。备份文件通过完整性检查,不代表一定可以成功恢复;有条件还应做一次隔离恢复演练。
关于异机备份和保留历史版本,我此前也写过一篇:Ubuntu 服务器互备实战。
五、Ubuntu Pro 准备:不要把订阅 Token 打进镜像
在构建宿主机安装并关联 Ubuntu Pro:
sudo apt update
sudo apt install -y ubuntu-pro-client
# 浏览器授权关联;不要自动打开宿主机上的额外服务
sudo pro attach --no-auto-enable
pro status
宿主机不需要因此切换到 Ubuntu 18.04。 需要修补的是 Docker 镜像内的包。
Ubuntu 官方推荐使用 BuildKit Secret + 临时 Guest Token。不要在 Dockerfile 写 ARG TOKEN=...,也不要把长期 Token 放在 Git 仓库里。
具体原理参见 Ubuntu Pro 官方 Dockerfile 指南。
六、基于原始禅道 15.5 镜像制作补丁版本
先给已经存在的原镜像加一个本地标签,不覆盖原始镜像:
docker tag your-registry.example.com/common/zentao:15.5 \
zentao-php72-base:15.5
然后在独立构建目录创建下面的 Dockerfile。本次安装过程中实际检查了 14 个 PHP 7.2 软件包,不仅是 CLI:
FROM zentao-php72-base:15.5
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN --mount=type=secret,id=pro-attach-config,required=true \
set -euo pipefail; \
export DEBIAN_FRONTEND=noninteractive; \
apt-get update; \
apt-get install -y --no-install-recommends \
ubuntu-pro-client ca-certificates; \
pro attach --attach-config /run/secrets/pro-attach-config; \
apt-get update; \
mapfile -t pkgs < <(dpkg-query -W -f='${binary:Package}\n' \
| grep -E '^(php7\.2($|-)|libapache2-mod-php7\.2$)'); \
test "${#pkgs[@]}" -ge 14; \
apt-get install -y --only-upgrade "${pkgs[@]}"; \
target='7.2.24-0ubuntu0.18.04.17+esm14'; \
for p in "${pkgs[@]}"; do \
v=$(dpkg-query -W -f='${Version}' "$p"); \
dpkg --compare-versions "$v" ge "$target" \
|| { echo "PHP package not patched: $p $v"; exit 1; }; \
done; \
php -m | grep -Fq 'ionCube'; \
php -m | grep -Fq 'mcrypt'; \
pro detach --assume-yes; \
apt-get purge -y ubuntu-pro-client; \
rm -rf /var/lib/apt/lists/*
这个 Dockerfile 是针对 Ubuntu 18.04 + PHP 7.2 + 当时已经安装了这 14 个包 的场景写的,不是适用于所有禅道镜像的通用升级脚本。换镜像后,应先核对包名和补丁版本。
还要创建 .dockerignore:
pro-attach-config.yaml
*.tar.gz
把授权、补丁安装和清理放在同一个 RUN 里,是为了降低凭据在镜像层残留的风险。构建过程中也不输出 Token。
七、生成临时授权并正式构建
以下脚本在已成功关联 Ubuntu Pro 的构建机上运行;不会主动停止旧容器:
#!/usr/bin/env bash
set -euo pipefail
umask 077
SECRET_FILE="$PWD/pro-attach-config.yaml"
trap 'rm -f "$SECRET_FILE"' EXIT
sudo pro api u.pro.attach.guest.get_guest_token.v1 \
| python3 -c '
import json, sys
data = json.load(sys.stdin)
token = data["data"]["attributes"]["guest_token"]
if not token:
raise RuntimeError("empty guest token")
print("token: " + json.dumps(token))
print("enable_services:")
print(" - esm-infra")
print(" - esm-apps")
' > "$SECRET_FILE"
docker buildx build \
--platform linux/amd64 \
--progress=plain \
--provenance=false \
--no-cache \
--load \
--secret id=pro-attach-config,src="$SECRET_FILE" \
-t zentao:15.5-php72-esm14 \
.
Guest Token 是短时凭据;构建失败后需要重新申请,不要重复使用旧文件。构建成功后,再把新镜像发布到自己的镜像仓库,或按实际环境使用本地镜像。
构建完成后的 php -v 输出:
PHP 7.2.24-0ubuntu0.18.04.17+esm14 (cli)
with the ionCube PHP Loader + ionCube24 v10.4.1
with Zend OPcache v7.2.24-0ubuntu0.18.04.17+esm14
这正是我们要的效果:PHP 7.2 保留,官方补丁生效,ionCube 仍可加载。
八、实操中踩到的三个坑
1. Dockerfile 首行拉取构建前端报 403
一开始 Dockerfile 顶部有:
# syntax=docker/dockerfile:1.7
这会要求 BuildKit 额外拉取 Dockerfile frontend。我们的镜像加速源返回了 403 Forbidden,并非 Ubuntu Pro 授权失败。
在确认当前 Docker/BuildKit 的内置 frontend 支持 Secret 挂载后,删除该行即可继续。不是所有环境都应无条件删除这一行;要先验证功能。
2. BuildKit 提示 blob not found
第一次做 Secret 探针时,RUN 只执行了 test,没有写入新文件;执行步骤成功,却在镜像导入时出现 blob not found。改为创建一个实际文件的探针,并关闭 provenance 后,构建与导入均通过:
FROM zentao-php72-base:15.5
RUN --mount=type=secret,id=probe,required=true \
test -s /run/secrets/probe && \
printf 'BuildKit Secret OK\n' > /tmp/buildkit-probe-ok
这只能说明本次导入问题已绕过并通过复测,不能据此断言所有同类错误都由空层导致。在承载多个业务容器的宿主机,不应为排障贸然执行 docker system prune -a 或删除 containerd 数据目录。
3. 数据库日志里出现 ERROR 1045
镜像启动日志里一度有本地 MariaDB 的 root@localhost 免密码连接失败。检查应用实际配置后发现,禅道业务连接的是远程 MySQL。分别对生产库和测试库做只读连接验证,结果均为成功。
结论不是“这条错误一定无害”,而是:不能只看容器内数据库日志就判断禅道业务数据库故障,必须核对应用实际连接目标。 本地数据库的启动脚本仍应后续单独排查。
九、先切测试容器,再切生产容器
修改 Compose 的 image 指向新镜像,保留原端口、网络、环境变量和业务目录挂载。例如:
services:
zentao-test:
image: zentao:15.5-php72-esm14
pull_policy: never
container_name: zentao-test
restart: always
ports:
- "9200:80"
volumes:
- /srv/zentao-test/data:/www/zentaopms
这里的路径仅为示例。旧镜像如果声明了匿名卷,需要确认重建时是否复用;这次实际还保留了原有的 /var/lib/mysql 挂载,即便业务库使用远程 MySQL,也没有直接删除旧卷。
在已核对备份、数据库隔离和 Compose 配置后,才执行:
# 在测试 Compose 目录执行
docker compose config -q
docker compose up -d --no-deps --force-recreate --pull never zentao-test
通过测试环境后,按相同原则在维护窗口切换生产服务。注意:docker restart 不会更换镜像,必须重建容器。 切换后也要实际操作登录、附件、项目和购买的插件功能。
十、最终如何判断补丁真正生效
不要只凭“容器 Up”或者“HTTP 200”就宣布升级完成。我实际核验的是:
# 新镜像是否被生产容器使用
docker inspect zentao \
--format 'Image={{.Config.Image}} ImageID={{.Image}}'
# CLI PHP
docker exec zentao php -v
# Apache 加载的 PHP 包
docker exec zentao dpkg -s libapache2-mod-php7.2 \
| grep '^Version:'
# Apache 运行模式
docker exec zentao apache2ctl -M 2>/dev/null \
| grep -E 'php7_module|mpm_prefork_module'
# 业务入口
curl -sS -o /dev/null -w 'HTTP: %{http_code}\n' \
http://127.0.0.1:9100/
最终得到的证据:
| 验收项 | 结果 |
|---|---|
| 生产镜像 | 已切换至 15.5-php72-esm14 |
| PHP CLI | 7.2.24-0ubuntu0.18.04.17+esm14 |
| Apache 的 PHP 模块包 | 7.2.24-0ubuntu0.18.04.17+esm14 |
| 已安装的相关 PHP 包 | 14/14 达到目标版本 |
| ionCube Loader | v10.4.1,正常加载 |
| PHP 扩展 | 升级前后清单无差异 |
| HTTP | 200 |
| 生产远程 MySQL | Remote DB: OK,业务库为 zentao |
| 禅道社区版 | 15.5,未升级 |
以上足以说明这两个 CVE 对应的 Ubuntu 官方 PHP 修复包已经部署到生产 Web 运行环境。但它不能证明系统升级前没有被攻击,也不代表未执行过的插件功能测试自动通过。
十一、这次升级的结论与后续事项
这次最有价值的经验并不是一条 Docker 命令,而是先找到合适的修复层级:
- 应用层被商业插件的兼容性限制住时,可以调查发行版是否提供安全补丁回移,而不是第一反应就强行升级 PHP 大版本。
- 镜像构建和数据迁移应分开;通过测试镜像 → 测试业务 → 生产重建 → 生产验收逐步缩小故障范围。
- 生产验证必须同时覆盖 CLI、Apache 模块、扩展、真实业务入口和数据库连接。
- 安全补丁不等于全面安全:旧版禅道、容器基础系统以及其他组件仍需继续评估;公网访问控制、日志审计和备份恢复演练不能省略。
如果你也在维护较老的 PHP Docker 项目,先确认实际运行的 PHP 包版本、官方修复来源,以及旧插件是否依赖 ionCube,再决定是升级应用、替换镜像,还是只打发行版补丁。
相关阅读:Ubuntu 24.04 安装与维护 Docker、MySQL 连接问题排查、PwnKit 历史漏洞修复记录。
官方资料:PHP 安全公告 · Ubuntu USN-8948-2 · Ubuntu Pro Dockerfile 指南。
常见问题
禅道 15.5 不升级应用版本,可以修复这次 PHP2Shell 漏洞吗?
可以,但取决于底层 PHP 发行版是否提供回移补丁。本次 Ubuntu 18.04 的 PHP 7.2 通过 Ubuntu Pro ESM 更新到 7.2.24-0ubuntu0.18.04.17+esm14,解决对应两个 CVE;不能因此认为旧版禅道的其他漏洞也被修复。
PHP 7.2.24 的主版本号没有变化,如何判断补丁装好了?
要检查发行版完整包版本,尤其是 libapache2-mod-php7.2 和 php7.2-cli。仅看 PHP 7.2.24 不够,本次验收要求版本包含 +esm14,并且确认 Apache 正在使用修复后的 PHP 模块。
相关文章
这篇文章有帮助吗?
感谢反馈。