跳到主内容
帖子

PHP2Shell 和 eMudhra 证书漏洞:怎么回事,怎么修

摘要

2026 年 10 月的两个安全事件:PHP 解释器的 PHP2Shell 远程代码执行漏洞,和证书机构 eMudhra 的中间 CA 私钥泄露。包括发现过程、影响和处理步骤。

2026 年 10 月,PHP 和 HTTPS 证书各出了一件事。

PHP 的事影响网站服务器,攻击者可能不用登录就执行命令。证书的事影响 HTTPS 的信任,攻击者可能伪造网站证书。

1. PHP2Shell 是什么

PHP 是跑网站程序的语言。WordPress、Drupal、Laravel 这些系统都靠它处理请求。

PHP2Shell 是两个漏洞的合称:

  • CVE-2026-109618:PHP 在内存耗尽时没有把内部数据处理完整,可能把无效数据当成有效数据。
  • CVE-2026-109619:一个请求结束后,PHP 没有完全清掉内部状态,下一个请求可能用到上一个请求留下的内存。

这两个问题都在 PHP 解释器本身,不在某个插件或框架里。攻击者不需要先登录,也不需要网站代码有漏洞。只要请求能进到 PHP,就可能触发。

官方给这两个漏洞的评分是 CVSS 9.2,属于严重级别。

2. PHP2Shell 是谁发现的

不是一个人发现的。

PHP 官方在 2026 年 10 月 21 日的说明里写了过程:2024 年 PHP 做外部安全审计时,核心开发者已经注意到内存耗尽相关的异常;2026 年 8 月 11 日,安全公司 Calif 的研究员 Ryan A 报告了一条可以利用的路径,OpenAI 在同一天独立报告了同一个根因,之后 Wiz 又报告了更多攻击路径。

所以准确地说,是 PHP 官方、Calif、OpenAI、Wiz 等几方先后发现并确认了这个问题。不是先有大规模攻击才被知道。

PHP 官方的安全公告 记录了修复版本,发现和修复过程写在 PHP Foundation 的说明 里。

3. PHP2Shell 会造成什么

最严重的结果是服务器被接管。

攻击者能执行命令后,网站配置、数据库密码都可能被读走;网站文件可能被改出后门;这台机器还可能被拿去攻击别人。用户数据也在风险里。

这不是“页面被改”那么简单。PHP 进程通常有权限读网站目录,有些部署还直接跑在 root 或高权限用户下,一旦被利用,影响会扩大到整台机器。

4. PHP2Shell 怎么修

4.1 先查实际版本

不要只看面板显示。先登录服务器,查正在跑的 PHP:

php -v
php-fpm -v

如果网站跑在 Docker 里:

docker ps --format '{{.Names}}\t{{.Image}}'
docker exec <PHP 容器名> php -v
docker exec <PHP 容器名> php-fpm -v

如果用的是 1Panel、宝塔这类面板,面板里显示的版本和容器里实际跑的版本可能不是一回事,以容器里的输出为准。

看到 8.3.35 这个版本号,判断就简单了:它比修复线低一个修订号,不用再猜有没有受影响。

4.2 对照修复版本

PHP 官方给出的修复版本:

  • PHP 8.5.12
  • PHP 8.4.27
  • PHP 8.3.36
  • PHP 8.2.35

低于这些版本的,都在受影响范围。PHP 8.1 及更早版本已经停止维护,官方不再提供修复,只能升级到受支持的分支。

4.3 升级

用系统包管理器的:

Debian/Ubuntu:

sudo apt update
apt-cache policy php8.3-fpm
sudo apt install --only-upgrade php8.3-fpm

RHEL/TencentOS 这类:

sudo dnf check-update php-fpm
sudo dnf upgrade php-fpm

装完先看包版本,确认到了修复版本。发行版仓库有时会比官方慢,慢的话看下一节。

用 Docker 的:

把镜像 tag 改成修复版本,例如:

services:
  php:
    image: php:8.3.36-fpm

然后:

docker compose pull php
docker compose up -d php

不要只执行 docker restart。重启只是把旧镜像里的进程重新拉起来,PHP 版本没变。

用 1Panel 这类面板的:

在面板的 PHP 运行环境里升级到 8.3.36。如果面板商店还没推新版本,先做 4.5 的临时缓解,等镜像更新;不要直接把 8.3.35 的容器重启了事。

4.4 确认进程真的换了版本

升级完要查正在提供服务的进程,不是只查命令行:

php-fpm -v
systemctl status php8.3-fpm

Docker:

docker exec <PHP 容器名> php-fpm -v
docker inspect <PHP 容器名> --format '{{.Config.Image}}'

再访问一次网站:

curl -I https://你的站点

返回 200 或 301 都算正常,500 说明扩展或配置出了问题,看 PHP-FPM 日志。

4.5 升不上去时,先做临时缓解

临时缓解不能代替升级,只能在补丁到位前降低风险。

在 PHP 的配置目录新建一个文件。系统安装一般在 /etc/php/8.3/fpm/conf.d/,1Panel 一般在 /opt/1panel/runtime/php/php83/conf/conf.d/:

cat > /etc/php/8.3/fpm/conf.d/99-php2shell-mitigation.ini <<'EOF'
memory_limit = -1
post_max_size = 8M
upload_max_filesize = 8M
session.upload_progress.enabled = 0
EOF

四个设置的作用:

  • memory_limit = -1:去掉单请求内存上限,避开内存耗尽那条利用路径。
  • post_max_size = 8M:限制 POST 请求体大小。
  • upload_max_filesize = 8M:限制单个上传文件大小。
  • session.upload_progress.enabled = 0:关掉上传进度回调。

重启 PHP-FPM:

sudo systemctl restart php8.3-fpm

Docker:

docker restart <PHP 容器名>

这些设置会改变网站行为。上传大文件的站点可能开始失败,所以补丁装好后要删掉这个文件,再重启一次。

4.6 官方镜像还没发布时

有些时候包管理器和 Docker 都还没有修复版本。2026 年 10 月 21 日这天,php:8.3.36-fpm 和 1panel/php:8.3.36-fpm 都还不存在,php:8.4-fpm 也还是 8.4.26。

如果业务不能等,只能用旧镜像加源码编译一个新镜像。先看原镜像的编译参数:

docker exec <PHP 容器名> php -i \
  | sed -n 's/^Configure Command => //p' > configure-args.txt

把文件开头的 './configure' 删掉,只留下后面的参数。然后下载源码:

curl -fsSLO https://www.php.net/distributions/php-8.3.36.tar.xz
sha256sum php-8.3.36.tar.xz

Dockerfile 的关键部分:

FROM <原来的 PHP 镜像>

RUN apt-get update && apt-get install -y --no-install-recommends \
      build-essential autoconf bison re2c pkg-config xz-utils \
      libxml2-dev libsqlite3-dev libssl-dev libcurl4-openssl-dev \
      libonig-dev libsodium-dev libargon2-dev libreadline-dev \
      zlib1g-dev libicu-dev \
    && rm -rf /var/lib/apt/lists/*

COPY php-8.3.36.tar.xz configure-args.txt /tmp/

RUN cd /tmp \
    && tar -xJf php-8.3.36.tar.xz \
    && cd php-8.3.36 \
    && ./configure $(cat /tmp/configure-args.txt) \
    && make -j"$(nproc)" \
    && make install

构建并起临时容器验证:

docker build -t php-8.3.36-local .

docker run --rm --entrypoint php php-8.3.36-local -v
docker run --rm --entrypoint php php-8.3.36-local -m

原镜像的扩展如果是挂载进去的,临时容器也要按原样挂载,不然 -m 只看到编译进镜像的那部分。确认版本和扩展都对了,再换生产容器。

确认版本是 8.3.36,pdo_mysql、intl、bcmath 这些扩展都在。再用原来的配置和挂载起临时容器,确认站点能跑,最后按 4.3 的方式把容器镜像换成 php-8.3.36-local。

这一步比升级包麻烦,也更容易漏扩展。能等官方镜像就等,不能等再做。

5. eMudhra 证书是什么

浏览器地址栏的小锁,靠的是 HTTPS 证书。

证书由证书颁发机构签发。浏览器和操作系统内置了一批信任的根证书,根证书再授权中间证书,中间证书给网站签证书。

可以把它想成盖章:你信任总章,总章授权分章,分章给文件盖章。看到分章的印,就认这份文件。

eMudhra 是印度的一家证书机构。这次出问题的是它的 8 张中间证书,不是最顶上的根证书。

6. eMudhra 是谁发现的

安全研究员 Wayne 把问题报告给了 Mozilla 和 Chrome 的根证书项目。

Mozilla 当天独立复现,确认这些中间证书的私钥生成过程有缺陷,可以很快恢复出来。Google、Mozilla、苹果、微软随后封禁了相关证书,Cloudflare 也做了处理。CSO Online 的报道 里记录了 Mozilla 的报告和恢复过程。

eMudhra 自己的说法是,这些中间 CA 没有签发过终端证书,根证书和其他中间 CA 没有受影响。但私钥泄露意味着别人可以自己签证书,不需要找 eMudhra 申请。

7. eMudhra 会造成什么

风险是伪造 HTTPS 证书。

攻击者可以给假网站签一张看起来合法的证书,或者做中间人攻击。如果用户的设备还信任 eMudhra 的根证书,浏览器可能不报警。

这和 PHP 那个漏洞不一样:它不是直接接管服务器,而是破坏“这个网站确实是它”的验证。对服务器来说,出站访问 HTTPS 时也可能被伪造证书骗过。

浏览器和手机一般会自动封禁,服务器不会。服务器的系统 CA、Java、Python、容器都要单独检查。

8. eMudhra 怎么修

要清四层:系统 CA、Java truststore、Python certifi、容器内 CA bundle。

8.1 先找系统 CA 里的 eMudhra

awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' \
  /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem \
  | openssl crl2pkcs7 -nocrl -certfile /dev/stdin \
  | openssl pkcs7 -print_certs -noout \
  | grep -iE 'emudhra|emsign'

有输出就说明系统信任库还信 eMudhra。不同发行版路径不同,Debian/Ubuntu 常见的是 /etc/ssl/certs/ca-certificates.crt。

8.2 从系统 CA 里排除

RHEL/TencentOS 这类用 update-ca-trust 的系统:

csplit -z -q -f /tmp/ca- -b '%03d.pem' \
  /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem '/BEGIN CERTIFICATE/' '{*}'

mkdir -p /etc/pki/ca-trust/source/blocklist
for f in /tmp/ca-*.pem; do
  openssl x509 -in "$f" -noout -subject | grep -qiE 'emudhra|emsign' \
    && cp "$f" /etc/pki/ca-trust/source/blocklist/
done

update-ca-trust extract

Debian/Ubuntu 没有 blocklist 目录,可以从 bundle 里删掉对应证书后重新生成,或者把过滤后的 bundle 指给应用。不同发行版机制不一样,先确认自己的系统用哪套。

验证:

awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' \
  /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem \
  | openssl crl2pkcs7 -nocrl -certfile /dev/stdin \
  | openssl pkcs7 -print_certs -noout \
  | grep -icE 'emudhra|emsign'

输出 0 才算清干净。

8.3 清 Java 的 truststore

Java 应用不一定用系统 CA。Halo、Keycloak 这类 Java 服务用的是 JDK 自带的 cacerts。

先找路径:

docker exec <Java 容器名> sh -c 'echo $JAVA_HOME'
docker exec <Java 容器名> sh -c '"$JAVA_HOME/bin/keytool" -list -cacerts -storepass changeit' \
  | grep -iE 'emudhra|emsign'

有输出就删。别名可能是 emsign...,也可能是很长的 CN 串,按输出里的别名填:

docker exec <Java 容器名> sh -c \
  '"$JAVA_HOME/bin/keytool" -delete -cacerts -storepass changeit -alias "<别名>"'

删完重启容器:

docker restart <Java 容器名>

注意:直接在容器里删,容器重建后会回到镜像里的版本。要长期保持,把 cacerts 拷出来处理,再用 compose 挂载:

services:
  halo:
    volumes:
      - ./trust/halo-cacerts:/opt/java/openjdk/lib/security/cacerts:ro

路径按 $JAVA_HOME 的实际值改。

8.4 清 Python 的 certifi

先确认 certifi 文件在哪:

python3 -c 'import certifi; print(certifi.where())'

服务器上可能有多个 venv,每个都带一份 cacert.pem,都要查:

find /opt /srv -path '*/certifi/cacert.pem' 2>/dev/null

替换前先备份。把上一步查到的路径填进来:

CERTIFI=<certifi 的 cacert.pem 路径>
cp "$CERTIFI" "$CERTIFI.bak-emudhra"

过滤脚本:

python3 - "$CERTIFI.bak-emudhra" "$CERTIFI" <<'EOF'
import re, subprocess, sys, pathlib

src, dst = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
blocks = re.findall(
    r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----',
    src.read_text(), re.S)

kept = []
for b in blocks:
    subject = subprocess.run(
        ['openssl', 'x509', '-noout', '-subject'],
        input=b.encode(), capture_output=True).stdout.decode().lower()
    if 'emudhra' in subject or 'emsign' in subject:
        continue
    kept.append(b)

dst.write_text('\n'.join(kept) + '\n')
print(f'{len(blocks)} -> {len(kept)}')
EOF

直接替换会立刻生效,但重装依赖、重建 venv 会把文件覆盖回去。更稳的方法是把过滤后的文件挂进容器,或者用 REQUESTS_CA_BUNDLE、SSL_CERT_FILE 指过去。

8.5 查容器内 CA bundle

容器有自己的 CA 文件,系统更新不会自动同步:

docker exec <容器名> sh -c \
  'grep -ciE "emudhra|emsign" /etc/ssl/certs/ca-certificates.crt'

返回大于 0 就要换。把过滤后的 bundle 拷进容器,或者用 compose 挂载。直接在容器里改,容器重建后会丢。

9. 修完怎么验证

PHP:

php -v
php-fpm -v
curl -I https://你的站点

Docker:

docker exec <PHP 容器名> php -v
docker exec <PHP 容器名> php-fpm -v

证书:

trust list | grep -iE 'emudhra|emsign'
docker exec <Java 容器名> sh -c \
  '"$JAVA_HOME/bin/keytool" -list -cacerts -storepass changeit' \
  | grep -icE 'emudhra|emsign'

PHP 版本到修复线,证书查询输出 0,站点返回 200,才算处理完。

10. 普通用户要做什么

  • 浏览器和手机系统保持更新
  • 看到证书警告不要点“继续访问”
  • 不要在警告页面输入账号、密码、验证码

11. 容易踩的坑

Q1:升级完 php -v 是新的,网站还是旧版本?

php -v 查的是命令行,不一定是正在处理网站请求的 PHP-FPM。再查:

php-fpm -v
docker exec <PHP 容器名> php-fpm -v

Docker 里还要看容器用的镜像 tag。只 docker restart 不会换镜像,进程起来还是旧版本。

Q2:为什么不能只重启容器?

重启只是把容器里的旧进程重新拉起来,镜像里的 PHP 还是旧版本。要换镜像,再重建容器。

Q3:为什么 php:8.3.36-fpm 拉不到?

先查 Docker Hub 的 tag 是否存在:

curl -s -o /dev/null -w '%{http_code}\n' \
  https://hub.docker.com/v2/repositories/library/php/tags/8.3.36-fpm/

返回 404 就是 tag 不存在,不是网络问题。换镜像源也可能一样。

php:8.4-fpm 如果还是 8.4.26,别用它顶替 8.4.27。版本号要看修复线,不是看大版本。

Q4:临时缓解能长期用吗?

不能。memory_limit=-1、post_max_size=8M、关掉 session.upload_progress 都会改变网站行为,上传大文件可能直接失败。补丁装好后要删掉 mitigation 文件,再重启 PHP-FPM。

Q5:系统 CA 清完了,为什么 Java 和容器里还能查到?

系统 CA 和 Java truststore 不是同一份文件。Halo、Keycloak 这类 Java 应用用 JDK 自带的 cacerts;容器里还有自己的 CA bundle。系统更新不会自动同步到这些地方,要一层一层查。

Q6:容器重建后 eMudhra 又回来了?

如果只是 docker exec 进去删,改的是容器临时层,重建后回到镜像里的版本。把 cacerts 或 CA bundle 拷到宿主机处理,再用 compose 只读挂载,或者重新构建镜像。

Q7:certifi 替换完,重装依赖又出现了?

直接替换 venv 里的 cacert.pem 会被 pip install、重建 venv 覆盖。要么把过滤后的文件挂进容器,要么用 REQUESTS_CA_BUNDLE、SSL_CERT_FILE 指向它,并在重装依赖后复查。

Q8:怎么确认不是只改了命令行?

查三处:php-fpm -v 的版本、容器实际使用的镜像 tag、网站返回状态。三个都对上,才算处理完。

· 2 次查看

更多相关内容

2026年10月22日
在绿联 NAS 上部署 DSH,手机可访问
4 次查看
2026年10月19日
API 站被彻底关闭之后
24 次查看
2026年10月19日
GitHub 能做什么:从建仓库到 PR、Pages 和 Actions
0 次查看
2026年10月19日
网站访问速度优化:从排查到分类的实操指南
1 次查看
豫ICP备20014910号-7