php2shell 漏洞游戏服务器防护:6 小时补丁行动手册
概要
立即按这份6小时行动手册处理php2shell游戏服务器漏洞(CVE-2026-109618/CVE-2026-109619,CVSS 9.2):盘点所有PHP运行时、升级到8.2.35+、配置WAF与Worker回收,防止未授权远程代码执行、数据泄露和拒绝服务,保护游戏后端安全。
你游戏的 PHP 层很可能是技术栈里没人关注的部分:卖装饰品的网页商店、社区运营用的管理后台、你在 2022 年写完就再没碰过的旧排行榜端点。这正是 php2shell 攻击的攻击面。
2026 年 10 月 21 日,Calif.io、Wiz、OpenAI 和 Quarkslab 披露了两个 PHP 运行时漏洞,编号为 CVE-2026-109618 和 CVE-2026-109619,统称为 php2shell。CVSS v4.0 评分为 9.2。它们影响从 5.4 开始的所有 PHP 安装,允许未认证的远程代码执行、数据暴露和拒绝服务;而且 Cloudflare 在公开披露尚未落地前,就观察到攻击者针对真实客户实时变异 payload 的攻击流量。
这不是 WordPress 插件问题,而是 PHP 运行时本身的内存安全缺陷。更新框架、CMS 或 Composer 依赖不会起任何作用。只要你的游戏后端里有任何 PHP,这份行动手册就是为你准备的。
php2shell 实际破坏了什么
这两个 CVE 的根源都在于 PHP 在请求处理出现异常时如何处理内存。通常,PHP 为请求分配内存、使用内存,并在请求结束时释放。当清理路径针对一开始就无效的状态执行时,缺陷就会出现。
CVE-2026-109618:内存耗尽(OOM)损坏
PHP 在请求处理中途触达内存上限,在部分内部结构完成初始化之前中止。清理例程随后把这种半成品数据当作合法数据。能够稳定制造 OOM 状态的攻击者——通过超大请求体、深层嵌套输入、超大上传——就有机会控制那些未完成内存中的内容。
对游戏后端来说,实际触发条件很明显:任何不限制请求体大小的端点。比赛结果提交、截图上传、Mod 元数据、遥测批量上报。
CVE-2026-109619:跨请求的过期全局状态
致命错误中断请求后,PHP 进程仍持有会在请求结束时被释放的内存的引用。同一个 worker 进程处理下一个请求时,可能会跟随这些悬空引用。这就是数据暴露——一个玩家的会话数据或数据库凭据泄漏到另一个玩家的响应中——在合适条件下还能导致代码执行。
对游戏服务器来说,这个缺陷更隐蔽,因为 PHP-FPM worker 在设计上就是长期存活的。单个被污染的 worker 在被回收之前可以处理数百个后续请求。如果你运行 pm.max_requests = 0(默认值,即永不回收),损坏的 worker 可以在容器整个生命周期内持续存在。
步骤 1:盘点你拥有的每一个 PHP 运行时
你无法修补你尚未发现的东西。大多数工作室在事故进行到一半时才发现:自己的三个服务跑着 PHP,其中一个是两年前构建的基础镜像里的 8.1。在做其他任何事情之前,先运行这个。
#!/usr/bin/env bash
# php2shell exposure inventory (CVE-2026-109618 / CVE-2026-109619)
set -euo pipefail
declare -A FIXED=( [8.5]="8.5.12" [8.4]="8.4.27" [8.3]="8.3.36" [8.2]="8.2.35" )
SERVICES=("game-api" "web-store" "admin-panel" "forums")
for svc in "${SERVICES[@]}"; do
ver=$(docker compose exec -T "$svc" php -r 'echo PHP_VERSION;' 2>/dev/null | tr -d '[:space:]') || {
echo "SKIP $svc (no php binary)"; continue; }
branch="${ver%.*}" # "8.3.12" -> "8.3"
min="${FIXED[$branch]:-}"
if [[ -z "$min" ]]; then
echo "EOL $svc on $ver -> no upstream patch, upgrade required"
elif php -r 'exit(version_compare($argv[1], $argv[2], ">=") ? 0 : 1);' "$ver" "$min"; then
echo "OK $svc on $ver"
else
echo "VULN $svc on $ver (fixed in $min)"
fi
done
这个脚本刻意做了两件事:单独标记 EOL 分支,并且绝不假设宿主机上的 php -v 能反映容器内的真实运行时。如果你在使用 Kubernetes,把 docker compose exec 换成 kubectl exec,并遍历 Deployment 而不是服务名。
上游修复版本是 8.5.12、8.4.27、8.3.36 和 8.2.35。任何运行在 8.1、8.0、7.x 或 5.x 上的版本都已生命周期结束,永远不会收到修复。你是在升级,不是打补丁。
步骤 2:修补运行时,而不是修补应用
这类事件发生后的第一个小时里,最常见的失败模式是:团队更新了应用代码、重新部署、看到 CI 变绿,就以为自己完成了。其实并没有。
- PHP-FPM 和 mod_php:重建基础镜像或升级软件包。Debian/Ubuntu:
apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common。Alpine:apk upgrade php83。在运行中的容器内部用php -r 'echo PHP_VERSION;'验证,而不是在构建宿主机上。 - FrankenPHP / php-cgi:它们内嵌了运行时,所以你需要新的发布二进制或基础镜像,而不是升级软件包。
- 托管主机:如果 PHP 由服务商控制,立即开工单并索要安全公告编号。不要把“我们已知悉”当作解决方案。
- Docker 基础镜像:按 digest 锁定,而不是按 tag。
php:8.3-fpm在下次拉取时可能解析到不同的构建;这对打补丁很好,但对可复现性很糟。验证版本后锁定 digest。
打补丁后,杀掉所有现有 worker。如果滚动重启让旧进程继续存活在共享 socket 上,脆弱的 worker 就还在服务流量。执行 docker compose restart 或 kubectl rollout restart——确认进程启动时间已经变化。
步骤 3:等待期间缩小爆炸半径
打补丁需要时间。从“我们知道了”到“所有环境都重建好”之间是一个窗口期。好好利用它。
在边缘限制请求体大小
# Shrink the php2shell trigger surface
client_max_body_size 2m; # game APIs rarely need more than this
client_body_timeout 10s;
client_header_timeout 10s;
large_client_header_buffers 4 8k;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php-fpm:9000;
fastcgi_read_timeout 15s;
fastcgi_buffering on;
}
2 MB 的上限挡不住执着的攻击者——他们可以用大量小请求触发 OOM——但它消除了最无脑的单请求触发方式,而且对你不产生任何成本。
激进地回收 Worker
; /usr/local/etc/php-fpm.d/zz-php2shell.conf
php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 20
pm.max_requests = 500
pm.max_children = 24
request_terminate_timeout = 20s
我们来算一下取舍。24 个 worker 每秒处理 200 个请求,每个 worker 大约处理 8.3 req/s。设置 pm.max_requests = 500 后,每个 worker 大约每 60 秒被回收一次。这限制了被 CVE-2026-109619 污染的进程可以服务流量的时间,但也意味着每分钟要创建 24 个进程——对冷启动开销大的框架来说,这是可测量的额外负担。
把 memory_limit 从 128M 提升到 256M,会减少 OOM 命中次数(也就少了 CVE-2026-109618 的触发机会),但最坏情况内存翻倍:24 个子进程 × 256M = 节点现在需要 6 GB 余量。如果节点只有 4 GB,就降低 pm.max_children,而不是假装内存存在。
这些都不是修复。 它们只是为你争取几个小时。把它当作应急分诊。
步骤 4:检测你是否已经被攻击
披露不等于首次被利用。在官方发布日期之前就已经观察到攻击流量。请假设你已经被人探测过。
# 1. OOM aborts — strongest single signal for CVE-2026-109618
grep -c "Allowed memory size" /var/log/php-fpm/error.log
# 2. Worker crashes — signal for both CVEs
journalctl -u php8.3-fpm --since "48 hours ago" \
| grep -Ei "segfault|SIGSEGV|exited on signal 11"
# 3. Which paths are generating 500s
awk '$9 == 500 {print $7}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20
# 4. Same source IP hitting the same 500-ing path repeatedly
awk '$9 == 500 {print $1, $7}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20
先建立基线。如果你的 PHP-FPM 日志因为某个内存占用很高的导出端点每天已经有 40 次 OOM 中止,那个数字只是噪声。你要找的是速率上的变化——signal 11 退出激增、500 列表里出现新路径、或某个单一 IP 对同一个端点产生几十次失败。
如果你发现成功利用的证据,请按完整失陷事件处理:轮换数据库凭据、会话签名密钥、支付服务商密钥,以及 PHP 进程可触达的任何 API key。这套处置方法并不只针对 PHP——让工作室能够设计出能在入侵中生存下来的游戏后端的纪律在这里同样适用;它始于假设攻击者已经能执行代码,而不只是读取数据。
如果你的后端已经在大面积丢弃请求,而你不确定是负载还是攻击,请遵循这篇服务器冻结诊断协议中的崩溃分诊顺序:在改动配置之前,先隔离故障层。
为什么仅基于请求的 WAF 规则会失效
Cloudflare 的报告中包含整个事件中最值得迁移的工程教训。他们最初的防护是基于请求的,攻击者则通过变异 payload 来应对——探测哪些变体会命中、哪些会漏过,把匹配/不匹配的信号当作预言机。
仅靠请求数据无法把利用流量和合法流量区分开,因为带嵌套数组的大 POST 同样是普通游戏客户端在提交比赛总结时发送的内容。消除歧义的信号是响应:源站是否返回了带 PHP fatal error 追踪信息的 500?这种组合——可疑请求加诊断性响应——才是高置信度。仅凭请求本身不够。
Cloudflare 将此实现为“全事务检测”:在同一个 HTTP 事务内关联请求和响应,并在响应到达请求方之前阻断。如果你自己运营边缘或反向代理,这个模式值得照抄:
def is_probable_php2shell(resp) -> bool:
"""Correlate request shape with origin response, not request alone."""
body = resp.body[:4096]
return (
resp.status == 500
and b"Fatal error" in body
and resp.origin_latency_ms < 50 # exploit aborts fast
and b"Allowed memory size" in body # OOM path = 109618 signature
)
这个教训可以推广到本 CVE 之外:任何只观察入站流量的检测系统,都会被能够看到出站结果的攻击者绕过。 记录响应。关联它们。这就是“能持续生效的规则”和“变成谜题的规则”之间的区别。
最佳实践
- 按计划打补丁,而不是等漏洞公开。 从“CVE 发布”到“野外利用”的时间差现在按小时计算,有时甚至是负数。即使没有任何公告,也每周重建 PHP 基础镜像,并保留回滚路径。
- 绝不在生产环境运行
pm.max_requests = 0。 无上限的 worker 生命周期会把每个内存安全缺陷变成持久立足点。对游戏 API 来说,500–1000 是合理的默认值。 - 在边缘限制每个请求体的大小。 JSON API 设 2 MB,上传设 10 MB,并拒绝任何 content type 不匹配的请求。这能在请求到达 PHP 之前消灭整类内存压力型利用。
- 对 5xx 错误的响应体做采样日志。 如果源站响应在你的可观测性栈中不可见,你就无法构建全事务检测。采样 10%,保留 7 天。
- 维护你运营的每个运行时的清单。 不只是 PHP——Node、Python、Go,凡是你的 Web 层在用的都要。一小时内响应 php2shell 的工作室都有服务目录;花了一天的那些都在 grep Dockerfile。
更大的教训:你的 Web 层就是攻击面
php2shell 提醒我们,游戏后端会不断积累历史包袱。第一年你为了快速交付上线了一个 PHP 管理后台。第三年它还在那里,还在 8.1 上,还持有一份对玩家表有写权限的数据库凭据——被攻破的是它,而不是你加固过的游戏服务器二进制。
结构性的修复是减少你所拥有的运行时数量。如果你运营 PHP Web 层只是为了处理玩家账号、云存档和排行榜,那这些正是 horizOn 以托管 API 暴露的能力——带账号绑定会话的终端用户认证、由客户端解决冲突的版本感知云存档读写、排行榜,以及远程配置。凌晨三点不需要修补 PHP-FPM 进程池,因为根本没有 PHP-FPM 进程池。
这并非说托管服务对漏洞免疫。而是说,你运营的每一个运行时,都是你必须盘点、修补并监控的运行时——而且大多数工作室实际拥有的运行时比他们以为的更多。
现在就做
如果这篇文章你只执行一个动作,那就执行步骤 1 的盘点脚本。对每个环境运行它——生产、预发布、那个给发行商演示时被遗忘的 demo 服务器。你几乎肯定会发现一个你忘记了的运行时,而且它几乎肯定在 EOL 分支上。
然后打补丁、回收 worker,并保留边缘规则。WAF 规则和请求体上限是纵深防御,不能替代厂商修复——PHP 安全公告才是唯一事实来源,也是真正堵住漏洞的东西。
如果盘点结果让你意识到,自己不想再继续维护这么多 PHP,horizOn 可以把账号、存档和排行榜这一层作为托管服务覆盖掉,这是缩小你需要负责打补丁的攻击面的最快方式。无论如何:先盘点,再修补,并且别再默认技术栈里最无聊的部分就是最安全的部分。