背景
最近帮一个小型业务站点做应急排查,现象比较典型:服务器 CPU 偶发升高,访问日志里出现了大量非业务路径请求,站点目录下还发现了几个命名很像缓存文件的 PHP 文件。这个案例没有复杂的 0day,更多是常见薄弱点叠加:上传校验不足、历史测试文件未清理、目录权限过大。
这类问题在实际环境里很常见,也适合整理成一套可复用的排查流程。本文重点放在防守视角:如何从日志、文件、进程和代码审计几个维度定位 WebShell,并完成加固闭环。不会展开攻击利用细节。
问题场景
业务架构比较简单:Nginx + PHP-FPM + MySQL,站点部署在单台 Linux 服务器上。管理员最初反馈的问题有三个:
- 某些时间段服务器负载异常,但持续时间不长。
- 访问日志出现大量对 upload、cache、static 等目录下 PHP 文件的请求。
- 站点根目录下存在多个近期修改的陌生文件,文件名类似 image_cache.php、config.bak.php。
初步判断,这不是单纯爬虫扫描,更像是已经落地过可执行脚本,需要确认入口、影响范围和残留。
排查思路
我一般会先避免直接删除文件,而是先取证保存:日志、可疑文件、进程信息、计划任务、Web 目录文件清单。原因很简单,如果直接清理,后面很难判断漏洞入口,也容易留下复发点。
建议顺序如下:
- 先看访问日志,确认可疑请求时间线。
- 再看文件修改时间,找出同一时间窗口内新增或变更的文件。
- 检查 PHP-FPM、Nginx、系统进程是否有异常子进程。
- 审计上传、文件包含、模板渲染、插件入口等高风险代码。
- 最后进行权限和配置加固,避免只清理不修洞。
日志分析
第一步是从 Nginx access.log 里筛选近期访问 PHP 的请求,重点看上传目录、静态目录、缓存目录等理论上不应执行脚本的位置。
grep -E "(upload|uploads|cache|static|assets).+\.php" /var/log/nginx/access.log | tail -n 100如果日志量较大,可以先按状态码和 URI 聚合:
awk '{print $7, $9}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -n 50本次排查里,比较明显的是某个上传目录下的 PHP 文件在短时间内被多次访问,而且 User-Agent 不固定,来源 IP 也比较分散。这里只能说明文件可能已被访问,不能直接说明入口就是上传接口,还需要结合文件时间和业务日志继续确认。
另外要关注 POST 请求。很多 WebShell 的交互会表现为小体积 POST、高频访问、响应码 200,但这不是绝对特征。可以用下面的方式粗筛:
grep 'POST' /var/log/nginx/access.log | awk '{print $1, $4, $7, $9, $10}' | tail -n 100文件排查
Web 目录里最有价值的是文件修改时间。先列出最近三天修改过的 PHP 文件:
find /data/www/site -type f -name "*.php" -mtime -3 -printf "%TY-%Tm-%Td %TH:%TM %p\n" | sort如果服务器支持,也可以用 ctime 辅助判断元数据变化:
find /data/www/site -type f -ctime -3 -printf "%TY-%Tm-%Td %TH:%TM %p\n" | sort这次发现的可疑文件有几个共同点:
- 位于 upload、cache 目录,正常业务不需要执行 PHP。
- 文件名伪装成图片缓存、配置备份或临时文件。
- 代码经过简单混淆,如变量名无语义、字符串拼接、动态函数调用。
- 修改时间与日志中的异常访问时间接近。
对可疑文件不要直接在线打开执行,建议复制到隔离环境后查看。可以先用静态方式检查危险函数和编码特征:
grep -RInE "eval|assert|base64_decode|gzinflate|str_rot13|system|shell_exec|passthru|proc_open|popen|create_function" /data/www/site --include="*.php"需要注意,命中危险函数不代表一定是后门,很多框架、插件也会出现类似调用;反过来,没有命中也不代表安全。这里的关键是结合路径、时间、业务逻辑和代码上下文判断。
可疑 PHP 代码的静态审计要点
WebShell 常见特征不一定是某个固定函数,而是“用户输入可控”加“动态执行或文件写入”。审计时我会重点看几类数据流:
- 来自 $_GET、$_POST、$_COOKIE、$_REQUEST、HTTP Header 的参数是否进入动态函数调用。
- 上传文件是否只校验扩展名,是否允许落到可执行目录。
- 文件包含参数是否可控,是否存在目录穿越风险。
- 模板缓存、日志写入、导入导出功能是否能写入 PHP 代码。
- 插件、富文本编辑器、历史测试接口是否暴露在公网。
例如,下面这种模式在代码审计中需要重点关注。这里用的是简化示例,目的是说明风险点:
// 风险示例:不要在生产环境使用这种写法
$action = $_GET['action'] ?? '';
if (function_exists($action)) {
$action($_POST['data'] ?? '');
}问题不在于 function_exists 本身,而是函数名由用户控制,后续又把用户输入作为参数传入,容易形成不可控的调用链。修复方式应使用白名单映射:
$allowActions = [
'preview' => 'safePreview',
'status' => 'safeStatus',
];
$action = $_GET['action'] ?? '';
if (!isset($allowActions[$action])) {
http_response_code(400);
exit('invalid action');
}
call_user_func($allowActions[$action]);上传目录执行权限控制
本次事件的一个核心问题是上传目录允许 PHP 执行。即使上传校验做得一般,只要上传目录不可执行,也能降低很多风险。Nginx 下可以对上传目录增加限制:
location ~* ^/(upload|uploads|static|assets|cache)/.*\.php$ {
return 403;
}也可以更严格一些,将 PHP 解析只限制在明确的入口目录或入口文件。比如很多项目只有 index.php 需要转发给 PHP-FPM,其它路径不应直接解析 PHP:
location = /index.php {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass 127.0.0.1:9000;
}
location ~ \.php$ {
return 403;
}如果业务确实有多个 PHP 入口,也应显式列出,而不是对整个站点目录开放解析。
权限与进程检查
清理文件前,还要确认是否存在异常进程、计划任务或持久化点。常用检查包括:
ps auxf
crontab -l
ls -al /var/spool/cron/
systemctl list-timers --all
find /tmp /var/tmp -type f -mtime -3 -lsWeb 用户权限也要看。如果 PHP-FPM 运行用户对整个站点目录都有写权限,攻击面会明显放大。较合理的做法是:
- 代码目录由发布用户维护,Web 运行用户只读。
- 上传、缓存、日志目录单独赋写权限。
- 上传目录禁止脚本执行。
- 生产环境关闭不必要的调试接口和测试文件。
可以用下面的命令快速查看 Web 目录权限:
namei -l /data/www/site/uploads
find /data/www/site -type d -perm -002 -ls影响范围确认
发现 WebShell 后,不要只关注单个文件。需要确认是否有横向影响或数据风险。建议至少检查:
- 数据库账号权限是否过大,是否出现异常登录、异常导出。
- 站点配置文件是否被读取或下载过。
- 管理员账号、后台登录日志是否异常。
- 是否有新增后台用户、API Token、SSH key。
- 同服务器上的其它站点是否被同样方式写入文件。
如果 access.log 和业务日志保留时间较短,后续溯源会很困难。生产环境建议至少保留 30 到 90 天日志,并集中存储,避免入侵后本机日志被清理。
处置步骤
比较稳妥的处置流程如下:
- 保存证据:打包可疑文件、访问日志、错误日志、进程列表、计划任务。
- 临时止血:限制可疑路径访问,必要时将站点切到维护页。
- 清理后门:删除确认后的恶意文件,检查同时间窗口新增文件。
- 修复入口:修上传校验、禁执行、删测试文件、升级存在漏洞的组件。
- 重置凭据:数据库、后台账号、服务器账号、对象存储密钥等按风险级别轮换。
- 恢复服务:观察日志和进程,确认没有新的异常写入和访问。
- 复盘沉淀:补充监控规则、发布规范和权限基线。
常见误区
- 只删除 WebShell,不修复入口。这样通常会复发。
- 只看文件名判断恶意。真实环境里伪装文件很多,必须结合内容和日志。
- 把所有危险函数命中都当作后门。容易误删业务代码,造成故障。
- 忽略上传目录执行权限。上传校验再完善,也建议从服务器层做兜底。
- 没有保留现场。后面想查入口、影响范围时会非常被动。
可落地的基线建议
| 项目 | 建议 |
|---|---|
| 上传目录 | 禁止 PHP、JSP、ASP 等脚本执行,文件名随机化,限制 MIME 和扩展名 |
| PHP 配置 | 禁用不必要危险函数,关闭 display_errors,限制 open_basedir |
| 目录权限 | 代码目录只读,写权限仅开放给 uploads、cache、logs 等必要目录 |
| 日志保留 | 访问日志、错误日志、业务审计日志集中存储并保留足够周期 |
| 发布流程 | 禁止临时测试文件进入生产,使用版本管理对比变更 |
| 监控告警 | 监控 Web 目录新增脚本、异常 POST 请求、敏感配置文件访问 |
总结
WebShell 排查不是简单地找几个可疑函数,也不是删掉陌生文件就结束。真正有效的方式是把日志时间线、文件变更、代码入口和服务器配置串起来,形成闭环。
这次事件的根因并不复杂:上传目录可执行、历史接口缺少维护、权限边界不清。类似问题在很多站点都会出现。建议把“上传目录禁执行、代码目录只读、日志集中保留、生产禁测试文件”作为最低基线,成本不高,但能挡住大量低成本入侵和批量扫描。
推荐意见