-
-
[原创]PHP2Shell 复现连载(四):加固、自查与应急响应
-
发表于: 7小时前 64
-
上一篇(第三篇):https://bbs.kanxue.com/thread-293431.htm
PHP2Shell 复现连载(四):加固、自查与应急响应
【测试声明】本篇为防守向内容,所有命令用于自有系统的检查与修复。
0x00 定位
这一篇面向运维和开发:怎么确认自己是否受影响、怎么修、修完怎么验证、怀疑已被打进来之后按什么顺序处理。
前四篇讲原理,这一篇讲动作。如果只看一篇,看这篇。
0x01 判断是否受影响(三件事,按顺序做)
0x01.1 第一步:看版本
php -v
对照下表:
8.5 分支受影响至 8.5.11,修复版本 8.5.12+
8.4 分支受影响至 8.4.26,修复版本 8.4.27+
8.3 分支受影响至 8.3.35,修复版本 8.3.36+
8.2 分支受影响至 8.2.34,修复版本 8.2.35+
8.1 及以下全部受影响,官方已 EOL,没有修复版本
只要版本号低于修复版本,就处于受影响范围。这跟业务代码写得好不好无关,是 PHP 运行时的缺陷。
顺便提醒:8.1 及以下没有修复版本,唯一的出路是升级分支。继续用 EOL 版本,将来还会遇到别的洞。
0x01.2 第二步:看配置(决定攻击面是否存在)
php -i | grep -i "session.upload_progress"
输出:
session.upload_progress.enabled => On => On
session.upload_progress.cleanup => On => On
session.upload_progress.name => PHP_SESSION_UPLOAD_PROGRESS => PHP_SESSION_UPLOAD_PROGRESS
session.upload_progress.freq => 1% => 1%
session.upload_progress.min_freq => 1 => 1
enabled => On 表示攻击面存在。注意这是 PHP 的默认值,也就是说绝大多数安装都是开着的,没有人主动打开过它。
0x01.3 第三步:看暴露面(决定风险等级)
ss -lntp | grep -E "9000|9001|9002|9003"
判读:
输出 127.0.0.1:9000,风险低,仅本机可达;
输出 0.0.0.0:9000 或 [::]:9000,风险高,需立即收敛;
输出 unix socket,风险低;
无输出,正常。
Docker 环境额外查一条:
docker ps --format "table {{.Names}}\t{{.Ports}}"
如果看到 0.0.0.0:9000->9000/tcp,说明 fpm 端口被映射到宿主机所有网卡。这是我在第四篇里反复强调的那个点,也是实际中最常见的失误。
为什么把这一步放在版本检查之后单独强调:因为它决定的是"能不能被打"而不是"有没有洞"。一个版本落后但 fpm 只听本机的站,和一个版本最新但 9000 对着公网的站,后者的即时风险更高。
0x02 修复方案(五个,按优先级排)
0x02.1 方案一:升级 PHP(首选,也是唯一彻底的)
Debian / Ubuntu 用官方源:
apt-get update
apt-get install --only-upgrade php8.4-fpm php8.4-cli php8.4-common
升级后必须重启 FPM,否则旧进程还在跑:
systemctl restart php8.4-fpm
systemctl status php8.4-fpm
Docker 环境注意:只 restart 容器不够,必须重建镜像再重新部署,否则跑的还是旧镜像。
验证:
php -v | head -1
版本号达到修复版本即可。
0x02.2 方案二:关闭 upload_progress(临时缓解)
无法立即升级时的过渡措施。编辑 php.ini:
session.upload_progress.enabled = Off
重启 FPM 生效:
systemctl restart php8.4-fpm
副作用:依赖上传进度条的前端会失效。如果站点确实用到,需要评估业务影响再决定——但注意,这个评估不应该拖太久,因为真正的修复是升级。
验证:
php -i | grep "session.upload_progress.enabled"
session.upload_progress.enabled => Off => Off
0x02.3 方案三:收敛 FPM 监听地址
即使不打补丁,把 fpm 关进本机也能大幅降低风险。改 pool 配置:
; /etc/php/8.4/fpm/pool.d/68cN6%4N6%4i4K6u0W2j5$3!0F1k6R3`.`.
listen = 127.0.0.1:9000
或改用 unix socket,性能更好,且根本不走 TCP:
listen = /run/php/php8.4-fpm.sock
nginx 侧同步改:
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
这一条我认为性价比最高:改动小、无业务影响、当天就能做完,而且顺带解决了一整类"fpm 裸奔"问题,不只是这一个 CVE。
0x02.4 方案四:保持 nginx 请求缓冲(纵深防御)
fastcgi_request_buffering on;
这是 nginx 的默认值。开启后 nginx 会把完整请求体收齐再转发给 FPM,攻击者无法制造"两次数据事件之间的停顿",UAF 的触发条件被破坏(原理见第二篇的 0x05)。
注意:靶场为了复现,特意设成了 off。生产环境不要照抄靶场配置——这一条听起来像废话,但我在真实环境里见过不止一次"从靶场配置直接抄到线上"的情况。
0x02.5 方案五:WAF 规则(补充,不是主力)
在边界拦异常 multipart,可用的判据:
单请求包含 PHP_SESSION_UPLOAD_PROGRESS 字段,且值中含 PHP 开标签;
同一 multipart 请求的分片间隔超过 1 秒;
请求体分片数异常。
这类规则能挡住自动化扫描,挡不住针对性攻击,只能作为补充。别把它当成主要防线。
0x03 修复后怎么验证
0x03.1 用靶场自带的缓解配置验证
靶场提供了 docker-compose.mitigated.yml,加载 php-mitigated.ini(关闭 upload_progress):
cd /root/php2shell/lab-uaf
docker compose -f docker-compose.yml -f docker-compose.mitigated.yml up -d
再跑原语验证:
python3 scripts/php2shell_poc.py --monitor --verify
预期:
upload_progress: DISABLED (mitigated)
Armed: NO - session empty (mitigation active or stall too short)
对照关系:
漏洞配置下 upload_progress 为 ENABLED,Armed 为 YES;
缓解配置下 upload_progress 为 DISABLED,Armed 为 NO。
这个对照很有用:它让你亲手确认"缓解措施确实改变了结果",而不是凭配置文件里那一行来判断。
回到漏洞配置:
docker compose up -d
0x03.2 生产环境验证要点
PHP 版本:php -v,要求不低于修复版本;
配置项:php -i | grep upload_progress,要求 Off,或版本已修复;
FPM 监听:ss -lntp | grep 9000,不允许出现 0.0.0.0;
容器端口:docker ps,不允许出现 0.0.0.0:9000->;
nginx 缓冲:grep fastcgi_request_buffering,要求 on 或未设置。
五条全过,才算闭环。只改配置文件不重启、只重启不验证,都是半成品。
0x04 应急响应流程
如果怀疑已经被打进来,按这个顺序。
0x04.1 第一步:止血(先断,再取证)
不要先取证。先把暴露面关掉:
sed -i 's/^session.upload_progress.enabled./session.upload_progress.enabled = Off/' /etc/php//fpm/php.ini
systemctl restart php*-fpm
并在边界封禁 fpm 端口。
顺序很重要:取证是事后的事,止血是当下的事。反过来做,等于给攻击者留时间。
0x04.2 第二步:排查痕迹
session 目录里的异常文件:
ls -la /var/lib/php/sessions/ | head -50
grep -rl "<?php" /var/lib/php/sessions/ 2>/dev/null
这两条里第二条更关键——session 文件里本来不应该出现 PHP 开标签。有,就说明被投毒过。
Web 访问日志里的异常 multipart:
grep -i "multipart/form-data" /var/log/nginx/access.log | tail -100
PHP 错误日志里的 include 痕迹:
grep -i "auto_prepend_file|Failed opening required" /var/log/php*-fpm.log
第四篇里提到的那行 "Cannot modify header information - headers already sent by (output started at .../sess_xxx:1)",是执行成功的直接物证。看到就按已失陷处理。
0x04.3 第三步:假设已被控制
一旦确认命令执行,应当把主机视为已失陷,而不是"改回来就行":
不要只删 webshell 就恢复服务;
检查 crontab、~/.ssh/authorized_keys、systemd 服务里有没有新增项;
确认数据库连接配置是否被读取(这一步决定要不要轮换凭据);
查横向移动痕迹(内网连接记录、异常的出站流量)。
经验上,只删 webshell 不做这四步的,多数在几周内会二次失陷。
0x05 时间线建议
P0,24 小时内:关闭 upload_progress,或升级 PHP;
P0,24 小时内:把 fpm 端口收敛到 127.0.0.1;
P1,72 小时内:排查历史日志有无利用痕迹;
P1,72 小时内:确认 nginx 缓冲为默认开启;
P2,一周内:建立版本与配置基线巡检。
P0 那两条之所以都是 24 小时,是因为它们互相独立——升级没完成前,关配置就能挡住;配置改不了(业务依赖进度条)的,收敛端口也能挡住。
0x06 一句话总结
这个漏洞的特点不在技术难度,而在覆盖面:PHP 占服务端语言份额极高,session.upload_progress 又是默认开启,所以"存在攻击面"的主体数量非常大。
但它的实际杀伤力高度依赖部署方式。把 fpm 关进本机、保持 nginx 缓冲、及时升级——这三点做到,风险就基本可控。
反过来说,如果评估完得出"我们版本落后、配置默认、端口对着公网"这个结论,那就不要再讨论利用难度了,按已失陷的假设去做排查。
0x07 连载回顾
第一篇:漏洞原理与"能触发不等于能利用"的整体结论;
第二篇:从 session.c 源码定位根因,到 ZendMM 的 320 字节 bin;
第三篇:环境搭建、UAF 验证与两条路径的辨析;
第四篇:加固、自查、应急响应。
四篇里最该被转到群里的是这一篇。
免责声明:本文仅用于安全研究与防御加固讨论。任何测试须确保目标系统为自有资产或已获书面授权,测试环境与生产环境网络隔离。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。