跳转到帖子

背景

最近帮一位朋友复盘一台业务服务器的异常告警,现象并不复杂: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 事件的发生概率和排查成本。

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…