背景
这篇文章整理的是一次比较典型的 WebShell 入侵排查过程。场景并不复杂:一台对公网开放的业务站点出现异常外连,随后在 Web 目录中发现可疑脚本文件。这里不讨论攻击利用细节,也不提供可直接用于入侵的操作,只记录防守侧如何从日志、文件、进程和代码审计几个角度把事件闭环。
类似事件在中小型业务系统里很常见,尤其是历史包袱较重的 PHP、Java Web 项目:上传目录权限过大、组件版本长期不更新、日志留存不足、应用和系统权限混用,都会放大排查难度。
初始现象
最早的告警来自出口流量监控:业务服务器在非业务时间段向多个陌生 IP 发起 HTTP 请求,频率不高,但持续时间较长。同时,Nginx access.log 中出现一些访问冷门路径的请求,状态码集中在 200 和 404。
现场的几个现象比较有价值:
- Web 根目录下出现近期修改的脚本文件,文件名伪装成缓存文件或图片处理文件。
- 上传目录中混有非图片后缀文件,并且存在部分双后缀文件。
- access.log 中有少量 POST 请求的响应体大小异常稳定。
- PHP-FPM 进程用户具备较大的目录写权限。
这类线索不能单独定性,但可以帮助缩小范围:先确认是否存在 WebShell,再倒查入口点。
排查思路
我通常会把这类事件拆成四条线并行推进:
- 文件线:查找近期新增、修改、权限异常、内容混淆的文件。
- 日志线:从可疑文件访问记录向前追溯上传、编辑、解压、插件安装等行为。
- 进程线:确认 Web 进程是否启动异常子进程或访问异常网络目标。
- 代码线:审计入口功能,包括上传、模板编辑、文件包含、反序列化、后台插件等。
这样做的好处是避免只删除 WebShell 就结束。WebShell 是结果,不是根因;如果入口漏洞还在,后续大概率会再次出现。
文件侧检查
首先保全现场,不建议直接删除文件。至少先记录路径、哈希、时间戳、权限、属主和访问日志片段。可以在只读备份后再分析。
find /var/www/html -type f -mtime -7 -printf '%TY-%Tm-%Td %TT %u %g %m %p\n' | sort
find /var/www/html -type f \( -name '*.php' -o -name '*.jsp' -o -name '*.asp' -o -name '*.aspx' \) \
-printf '%s %TY-%Tm-%Td %TT %p\n' | sort -n对 PHP 项目,可以重点关注以下特征,但不要只依赖关键字命中。现在很多样本会做简单混淆,或者藏在正常业务文件里。
- 高风险函数:eval、assert、system、exec、shell_exec、passthru、proc_open、popen。
- 编码混淆:base64_decode、gzinflate、str_rot13、chr 拼接、变量函数调用。
- 异常入口:读取请求参数后直接进入动态执行、文件写入或包含。
- 文件伪装:图片目录中的脚本文件、缓存目录中的可执行文件、异常长文件名。
grep -RIn --include='*.php' -E "eval\s*\(|assert\s*\(|base64_decode\s*\(|gzinflate\s*\(|shell_exec\s*\(|passthru\s*\(" /var/www/html实际排查时,grep 只能作为第一轮筛选。很多框架、插件也会合法使用部分函数,所以需要结合文件路径、修改时间、代码上下文和访问日志判断。
日志侧关联
确认可疑文件后,下一步是看它是否被访问,以及首次访问时间。以 Nginx 为例,可以按文件名检索:
grep 'suspect.php' /var/log/nginx/access.log*
grep 'POST' /var/log/nginx/access.log | awk '{print $1,$4,$6,$7,$9,$10}' | sort | uniq -c | sort -nr | head如果可疑文件位于上传目录,需要继续向前查同一 IP、同一会话、同一用户在首次访问前的行为。重点看这些功能:
- 文件上传接口:头像、附件、富文本编辑器、导入导出功能。
- 后台模板编辑:主题、插件、页面片段可编辑功能。
- 压缩包处理:上传 zip 后自动解压,可能带来目录穿越或脚本落地风险。
- 远程图片抓取:服务端根据用户提供 URL 拉取资源,可能导致任意文件写入或 SSRF 相关问题。
在一次真实排查中,可疑脚本首次访问之前,同一源 IP 对富文本编辑器上传接口进行了多次探测,请求路径变化不大,但文件名和 Content-Type 不断变化。虽然攻击链细节不展开,但这足以说明入口很可能在上传校验链路。
进程与网络检查
如果 WebShell 已被执行,可能会留下子进程、临时文件、异常外连。这里的目标不是“反制”,而是确认影响范围。
ps -ef --forest | grep -E 'nginx|php-fpm|apache|httpd|java'
ss -antp | grep -E 'php-fpm|nginx|apache|httpd|java'
lsof -u www-data 2>/dev/null | head -100关注点包括:
- Web 进程是否启动 shell、下载工具、解释器、压缩工具等异常子进程。
- 是否存在由 Web 用户创建的计划任务。
- /tmp、/var/tmp、上传目录中是否存在近期生成的二进制或脚本文件。
- 外连目标是否与业务域名、对象存储、消息队列等合法依赖无关。
crontab -u www-data -l 2>/dev/null
find /tmp /var/tmp -type f -mtime -7 -ls 2>/dev/null代码审计重点:上传链路
这次事件中,根因最终落在文件上传校验不足。很多上传漏洞并不是单点错误,而是多个“小问题”叠加:
- 只校验前端后缀,后端信任前端传来的文件名。
- 后端只检查 Content-Type,没有基于文件内容做二次判断。
- 上传目录位于 Web 可访问路径下,并允许脚本执行。
- 文件名未重命名,或保留用户可控后缀。
- Nginx、Apache 或 PHP-FPM 对特殊后缀解析规则配置不严谨。
一个更稳妥的上传处理方式应满足几个原则:白名单后缀、随机文件名、内容识别、存储隔离、禁止执行、权限最小化。示例配置如下,核心是上传目录不应执行脚本:
location ^~ /uploads/ {
autoindex off;
types { }
default_type application/octet-stream;
location ~* \.(php|php5|phtml|jsp|jspx|asp|aspx|sh|pl|py)$ {
return 403;
}
}如果是 PHP-FPM,还要确认不会把非 PHP 文件错误转交给解释器。常见建议是限制 SCRIPT_FILENAME 映射,并关闭不必要的路径信息解析:
cgi.fix_pathinfo=0代码侧也要避免直接使用用户文件名。可以按业务类型生成不可预测文件名,并将原始文件名只作为元数据保存。
// 示例:上传后生成随机文件名,避免沿用用户传入名称
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$allow = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
if (!in_array($ext, $allow, true)) {
throw new RuntimeException('unsupported file type');
}
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$target = $uploadDir . '/' . $newName;处置步骤
建议按“止血、取证、清理、修复、监控”的顺序处理,避免一上来就删文件导致证据断裂。
- 隔离:必要时先下线受影响实例,或在负载均衡层摘除节点。
- 保全:打包 Web 目录、日志、进程列表、网络连接、计划任务、关键配置。
- 确认:对可疑文件计算哈希,分析调用关系和访问记录。
- 清理:删除 WebShell、临时文件、异常账号、异常计划任务。
- 修复:修补上传、文件写入、模板编辑等入口漏洞,升级存在风险的组件。
- 加固:收敛目录权限,上传目录禁止执行,Web 用户禁止登录,敏感配置移出 Web 根目录。
- 监控:增加文件完整性监控、异常 POST 监控、出口访问监控。
权限方面,比较推荐让 Web 进程只拥有必要目录的写权限,而不是整个站点目录可写。配置文件、代码目录、模板目录默认只读,上传和缓存目录单独授权。
chown -R root:root /var/www/html
chown -R www-data:www-data /var/www/html/uploads /var/www/html/runtime
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;常见误区
- 只删除 WebShell,不查入口。这样通常只是把再次入侵的时间推迟几天。
- 只看可疑关键字,不看业务上下文。误报会很多,也容易漏掉混淆样本。
- 忽略日志留存。很多站点 access.log 只保留一两天,真正排查时已经断链。
- 让上传目录可执行。上传目录只负责存储,不应该承担脚本运行能力。
- 把系统用户、数据库账号、对象存储密钥长期放在 Web 可读配置中。
长期建议
如果团队人手有限,优先做几件收益高的事情:
- 保留至少 30 天 Web 访问日志,并统一到日志平台检索。
- 对 Web 根目录做文件完整性监控,重点关注新增脚本文件。
- 上传目录、缓存目录禁止脚本执行。
- 应用依赖纳入版本管理,定期清理历史插件和废弃接口。
- 生产环境关闭调试模式,后台入口增加访问控制和强认证。
- 出口流量做基础白名单或异常告警,至少能发现非业务外连。
总结
WebShell 事件排查的关键不在于识别某一个样本,而在于把“文件落地、访问执行、入口漏洞、权限边界、后续动作”串起来。文件、日志、进程、代码四条线能互相印证,最终才能判断影响范围并完成修复。
从经验看,上传目录可执行、组件长期不更新、权限过大,是这类事件最常见的基础问题。把这些基础面收紧,往往比堆更多工具更有效。
推荐意见