背景
最近帮一位朋友复盘一台业务服务器的异常告警,现象并不复杂:EDR 提示 Web 目录下出现可疑脚本,Nginx 访问日志里也能看到少量异常请求。业务本身是一个常见的 PHP 后台系统,前面有 Nginx,后端 PHP-FPM,服务器上同时跑了定时任务和文件上传功能。
这类事件在社区里经常讨论,但很多排查记录要么只停留在“发现一句话木马”,要么直接跳到“重装系统”。实际工作中,更有价值的是把入口、执行链路、落地点、影响范围和加固动作串起来。本文整理的是一套相对通用的 WebShell 排查与加固流程,偏防守视角,不涉及未授权利用细节。
问题场景
初始告警文件位于 Web 根目录的上传目录下,文件扩展名伪装成图片,但内容里包含 PHP 代码片段。访问日志显示该文件曾被直接请求,且请求参数存在异常长字符串。
环境大致如下:
- 系统:Linux
- Web 服务:Nginx + PHP-FPM
- 业务语言:PHP
- 目录结构:存在用户上传目录、缓存目录、日志目录
- 安全设施:基础主机监控、Nginx 访问日志、应用日志
这里需要注意一点:发现 WebShell 文件,不等于已经完成排查。WebShell 更像是结果,真正要确认的是它如何写入、是否被执行、是否存在横向动作、是否还有其它持久化点。
技术分析思路
一、先保护现场,避免误删关键证据
很多人在看到可疑文件后会第一时间删除,这会影响后续判断。更稳妥的做法是先隔离、备份、记录元信息,再决定处理方式。
mkdir -p /root/incident-backup/$(date +%F)
cp -a /var/www/html/uploads/suspicious.php /root/incident-backup/$(date +%F)/
stat /var/www/html/uploads/suspicious.php
sha256sum /var/www/html/uploads/suspicious.php如果担心文件继续被访问,可以通过 Nginx 规则先临时阻断脚本执行,而不是简单删除。例如上传目录原则上不应执行 PHP:
location ^~ /uploads/ {
location ~ \.php$ {
return 403;
}
}处理后记得重载配置:
nginx -t && systemctl reload nginx二、判断文件是否真的被执行
WebShell 是否产生实际影响,通常要结合 Web 访问日志、PHP-FPM 日志、系统命令历史、进程信息一起看。先从访问日志确认时间线。
grep "suspicious.php" /var/log/nginx/access.log*
grep "uploads" /var/log/nginx/access.log* | grep -E "\.php|%3C|base64|eval|assert"重点关注以下字段:
- 首次访问时间:用于定位入侵窗口
- 来源 IP:用于关联其它请求
- HTTP 方法:是否存在 POST 上传或参数提交
- 状态码:200、500、403 含义不同
- User-Agent:自动化工具通常有特征,但不能完全依赖
- 请求长度与响应大小:异常大请求可能包含代码或数据
如果启用了 PHP-FPM 慢日志或错误日志,也可以按时间段过滤:
grep "2025-" /var/log/php-fpm/error.log
grep -iE "fatal|warning|shell|system|exec|passthru" /var/log/php-fpm/error.log这里不建议只用关键词做结论。很多 WebShell 会混淆变量名,也可能通过正常函数链触发。关键词适合做初筛,最终还是要结合调用路径和业务逻辑。
三、定位写入入口
WebShell 落在上传目录,入口不一定就是上传接口,但上传模块是第一优先级。常见问题包括:
- 只校验前端扩展名,没有服务端校验
- 仅通过 Content-Type 判断文件类型
- 允许上传 .php、.phtml、.phar 等可执行后缀
- 文件名由用户可控,存在双后缀或解析差异
- 上传目录被 Web 服务当作脚本目录解析
- 图片处理库异常,导致原始文件被保留
可以先检索访问日志中同一来源 IP 在告警前后的请求:
zgrep "1.2.3.4" /var/log/nginx/access.log* | less再按时间窗口查看所有上传相关接口:
awk '$4 >= "[10/Oct/2025:13:00:00" && $4 <= "[10/Oct/2025:14:00:00"' /var/log/nginx/access.log | grep -iE "upload|file|image|avatar|attachment"如果应用有自己的操作日志,也要对照用户 ID、接口路径、文件名、保存路径。很多时候入口不是漏洞利用工具,而是后台低权限账号上传附件后触发解析问题。
四、做文件系统范围排查
单个可疑文件往往不是唯一落点。建议围绕 Web 根目录、临时目录、计划任务目录做扩展检查。
# 最近 7 天变更的 PHP 文件
find /var/www -type f -name "*.php" -mtime -7 -ls
# 上传目录中不该出现的脚本类型
find /var/www/html/uploads -type f \( -name "*.php" -o -name "*.phtml" -o -name "*.phar" \) -ls
# 查找可疑高权限文件
find /var/www -type f -perm -002 -ls针对 PHP 文件,可以做一些低成本静态筛查:
grep -RInE "eval\s*\(|assert\s*\(|system\s*\(|exec\s*\(|passthru\s*\(|shell_exec\s*\(|base64_decode\s*\(" /var/www --include="*.php"需要强调的是,命中这些函数不代表一定是 WebShell。比如业务里可能确实用到 base64_decode 处理图片数据,也可能有合法的命令行调用。排查时应结合文件路径、修改时间、代码风格、版本仓库差异一起判断。
如果代码有 Git 管理,直接对比差异通常更高效:
cd /var/www/html
git status
git diff --stat
git diff生产环境不一定有完整仓库,这也是后续建设里值得改进的一点:至少要能确认线上文件与发布包的一致性。
五、检查进程、网络连接和计划任务
WebShell 被执行后,攻击者可能只做了探测,也可能尝试写入持久化。这里建议做基础检查,不要只盯着 Web 目录。
ps aux --sort=start_time | tail -50
ss -antp
lsof -i -P -n | grep -E "php|nginx|www-data|apache"计划任务重点看系统级和业务用户级:
crontab -l
crontab -u www-data -l 2>/dev/null
ls -la /etc/cron.* /var/spool/cron /var/spool/cron/crontabs 2>/dev/null还可以检查启动项和 SSH 相关配置:
systemctl list-unit-files --state=enabled
ls -la ~/.ssh /root/.ssh 2>/dev/null
last -a | head -30这些命令不会直接证明入侵链路,但能帮助判断事件是否从 Web 层扩大到主机层。如果发现异常系统用户、异常服务、未知外连进程,就要提高事件等级,必要时下线取证。
关键步骤整理
| 步骤 | 目标 | 建议动作 |
| 隔离可疑文件 | 防止继续执行 | 备份文件、记录 hash、限制访问 |
| 确认访问痕迹 | 判断是否执行 | 分析 Nginx、PHP-FPM、应用日志 |
| 定位入口 | 找到写入原因 | 回溯上传接口、后台操作、漏洞时间线 |
| 扩展排查 | 确认影响范围 | 扫描近期变更文件、计划任务、进程网络 |
| 修复加固 | 阻断再次发生 | 禁脚本执行、修复上传校验、最小权限 |
| 复盘建设 | 降低后续成本 | 补日志、文件完整性校验、发布一致性检查 |
上传功能的加固建议
从经验看,上传目录执行脚本是最常见也最容易修复的问题。建议至少做到以下几点。
- 上传目录与代码目录分离,最好放到独立存储或对象存储
- 上传文件使用随机文件名,不保留用户可控后缀
- 服务端校验文件类型,不依赖前端和 Content-Type
- 对图片进行重新编码,避免原始内容直接落盘
- Nginx 或 Apache 层明确禁止上传目录执行脚本
- PHP-FPM 运行用户权限最小化,不允许写代码目录
- 限制单文件大小、上传频率和异常扩展名
Nginx 层可以加一条更明确的规则,避免解析差异:
location /uploads/ {
autoindex off;
types { }
default_type application/octet-stream;
}
location ~* ^/uploads/.*\.(php|phtml|phar|php5|php7)$ {
return 403;
}PHP-FPM 侧也建议关闭一些高风险配置或做限制,具体要结合业务评估,不能盲目一刀切:
; php.ini 示例,需结合业务测试
expose_php = Off
allow_url_fopen = Off
allow_url_include = Off
open_basedir = /var/www/html:/tmp注意:disable_functions 在不同 PHP 版本和业务场景下影响较大,配置前要充分测试。安全配置不是越多越好,关键是可运行、可维护、可验证。
代码审计关注点
如果要从代码层面定位入口,建议优先看文件上传、模板渲染、后台插件安装、压缩包解压、文件管理器这几类功能。审计时重点关注:
- 保存路径是否可被用户控制
- 文件后缀是否基于白名单
- 是否存在大小写、双后缀、特殊字符处理问题
- 是否将临时文件移动到 Web 可执行目录
- 是否对压缩包内文件路径做规范化处理,避免路径穿越
- 是否存在后台低权限用户可触发的文件写入点
一个比较稳妥的后缀白名单思路是:业务只允许什么,就只放行什么,而不是维护一个危险后缀黑名单。例如头像上传只允许 jpg、png、webp,并在服务端重新生成图片。
// 简化示例:仅表达校验思路,实际需结合框架处理
$allowedExt = ['jpg', 'jpeg', 'png', 'webp'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExt, true)) {
throw new RuntimeException('invalid file extension');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmpPath);
$allowedMime = ['image/jpeg', 'image/png', 'image/webp'];
if (!in_array($mime, $allowedMime, true)) {
throw new RuntimeException('invalid file type');
}同时,不建议使用用户原始文件名作为最终落盘文件名。文件名可以采用随机值或内容 hash,并由业务数据库维护映射关系。
日志与监控建设
很多事件难复盘,不是因为攻击手法多复杂,而是日志不够。建议至少保留:
- Nginx access/error 日志,保留足够时间
- 应用操作日志,记录用户、接口、关键参数摘要
- 上传文件日志,记录原始文件名、保存路径、大小、hash、用户 ID
- 发布记录,记录版本号、发布人、发布时间
- 主机审计日志,覆盖进程启动、计划任务变更、关键文件变更
如果条件有限,可以先做最基础的文件完整性检查。比如对代码目录生成基线,定期比对新增和变更文件:
find /var/www/html -type f -not -path "*/uploads/*" -exec sha256sum {} \; > /root/web-baseline.sha256
sha256sum -c /root/web-baseline.sha256 | grep -v "OK"这不是完整的安全产品,但对中小型站点很实用,能在没有复杂平台的情况下快速发现线上文件被改动。
处置注意点
- 不要只删除 WebShell,要找到写入入口,否则很快会再次出现
- 不要在未备份的情况下清理现场,后续可能需要证据链
- 不要轻易在生产机上运行来源不明的查杀脚本
- 不要只看 Web 日志,主机层持久化同样重要
- 不要忽略后台账号风险,弱口令和权限过大很常见
- 如果已出现异常外连、提权痕迹或敏感数据访问,应按高等级事件处理
总结
WebShell 排查的核心不是“找到一个可疑文件”,而是建立完整时间线:它什么时候出现、通过什么入口写入、是否被访问执行、执行后做了什么、还有没有其它落点。只有把这些问题回答清楚,修复才不会停留在表面。
从长期治理角度看,最有效的投入通常不是复杂规则,而是几个基础动作:上传目录禁止脚本执行、文件上传严格白名单、代码目录最小写权限、日志可回溯、发布包可校验。这些措施成本不高,但能显著降低 WebShell 事件的发生概率和排查成本。
推荐意见