跳到正文
runningbai
Powered by Pagefind
Docker 与容器

禅道 15.5 不升级也能修 PHP2Shell:Docker + Ubuntu Pro ESM 实战

记录一次禅道 15.5 的 PHP2Shell 漏洞修复:保留 ionCube 商业插件,使用 Ubuntu Pro ESM 回移 PHP 7.2 补丁,完成 Docker 镜像构建、排障与生产验收。

2026/10/22阿白最后更新: 2026/10/2220 分钟阅读

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 模块。

相关文章

这篇文章有帮助吗?