跳转到帖子

背景

最近整理了一起比较典型的 WebShell 入侵事件。事件本身不复杂:业务侧发现服务器上出现异常 PHP 文件,且访问日志中存在可疑请求。比较有价值的是,这类事件在中小型 Web 系统里很常见,攻击入口往往不是高危 0day,而是上传校验不严、历史组件漏洞、后台弱口令、临时调试接口遗留等问题叠加。

本文主要从防守视角做一次完整复盘:如何确认入侵范围、如何从日志中还原关键时间线、如何定位 WebShell 来源,以及如何通过代码审计补上缺口。内容不涉及攻击利用细节,更侧重排查方法和加固思路。

场景概述

业务环境大致如下:

  • Linux 单机部署,Nginx + PHP-FPM。
  • 业务代码为 PHP,存在后台管理功能和文件上传功能。
  • 站点目录包含历史版本代码和若干临时测试文件。
  • 无集中日志平台,只有本机访问日志、错误日志、系统日志。

最初告警来自运维巡检:Web 根目录下发现一个最近修改的 PHP 文件,文件名伪装成图片缓存,例如 cache_img.php。文件内容经过简单混淆,包含可疑的动态执行逻辑。

第一步:先做现场保护

很多事件排查一开始容易直接删除文件、重启服务,这样会破坏时间线和证据。我的习惯是先做最小化隔离和现场留存。

  • 如果业务允许,先将站点切到维护页或限制外部访问。
  • 保留可疑文件副本,记录路径、权限、属主、mtime、ctime。
  • 打包 Web 目录、Nginx 日志、PHP-FPM 日志、系统登录日志。
  • 不要立即清空临时目录,很多落地文件会出现在 /tmp、上传目录或缓存目录。
mkdir -p /root/incident_backup
stat /var/www/html/cache/cache_img.php
sha256sum /var/www/html/cache/cache_img.php
cp -a /var/www/html/cache/cache_img.php /root/incident_backup/
cp -a /var/log/nginx /root/incident_backup/nginx_logs
cp -a /var/log/php-fpm /root/incident_backup/php_fpm_logs 2>/dev/null
last -a > /root/incident_backup/last_login.txt

注意:如果服务器已经存在明显横向移动、挖矿、异常进程等迹象,应优先从主机安全角度处理,包括断网、内存取证、镜像留存。本文只讨论 WebShell 类型事件的常规复盘。

第二步:确认可疑文件特征

对 WebShell 文件不要只看文件名,重点看行为特征。常见可疑点包括:

  • 动态执行:例如 eval、assert、preg_replace /e、create_function、反射调用等。
  • 编码混淆:例如 base64_decode、gzinflate、str_rot13 多层嵌套。
  • 请求参数驱动:通过 POST、COOKIE、HEADER 获取参数后执行。
  • 文件操作:写入、删除、遍历目录、修改权限。
  • 进程操作:调用 system、exec、shell_exec、popen。

可以先用静态 grep 做粗筛,确认是否还有同类文件:

cd /var/www/html
find . -type f \( -name "*.php" -o -name "*.phtml" -o -name "*.inc" \) -mtime -30 -print

grep -RIn --include='*.php' \
  -E 'eval\s*\(|assert\s*\(|base64_decode\s*\(|gzinflate\s*\(|shell_exec\s*\(|passthru\s*\(|system\s*\(' .

这里要避免一个误区:命中危险函数不等于一定是后门。有些框架或旧代码也可能使用动态调用。需要结合文件路径、修改时间、代码风格、访问日志一起判断。

第三步:围绕时间点做日志关联

可疑文件的修改时间是一个非常重要的锚点。假设文件修改时间为 2024-xx-xx 14:32:10,可以向前后各扩展 30 分钟查访问日志。

# 按时间粗筛访问日志,具体格式要根据 access.log 调整
grep '14:2\|14:3\|14:4\|14:5' /var/log/nginx/access.log > /root/incident_backup/access_window.log

# 关注非静态资源请求
awk '$7 !~ /\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|ttf)(\?|$)/ {print}' \
  /root/incident_backup/access_window.log > /root/incident_backup/access_dynamic.log

排查时重点看几类请求:

  • 上传接口:例如 /upload、/file、/ueditor、/editor。
  • 后台接口:例如 /admin、/manage、/cms。
  • 历史组件路径:例如编辑器、旧插件、备份目录。
  • 异常 User-Agent、异常 Referer、短时间高频 404 探测。
  • 访问新生成 PHP 文件的请求。

如果日志里出现先访问上传接口,随后访问上传目录下 PHP 文件,就需要重点核查上传链路。也可以根据可疑 IP 做聚合:

awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -20

grep '可疑IP' /var/log/nginx/access.log > /root/incident_backup/ip_trace.log

第四步:定位可能的入口

本次事件最终定位到上传校验不严。业务代码只校验了前端传入的 MIME 类型和文件后缀,但没有对服务端保存路径、真实文件类型、可执行权限做限制。更关键的是,上传目录位于 Web 可访问路径下,并且 PHP-FPM 会解析其中的 PHP 文件。

类似代码常见问题如下:

// 反例:仅示意问题点,不建议使用
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
if (in_array($ext, ['jpg', 'png', 'gif', 'php'])) {
    move_uploaded_file($_FILES['file']['tmp_name'], '/var/www/html/uploads/' . $_FILES['file']['name']);
}

这段逻辑至少有几个问题:

  • 扩展名白名单错误,甚至包含可执行脚本后缀。
  • 使用原始文件名保存,存在覆盖和特殊字符风险。
  • 上传目录在 Web 根目录内,且没有禁用脚本执行。
  • 未校验真实文件内容,例如图片头、尺寸、MIME 由服务端检测。
  • 未限制文件大小、上传频率、用户权限。

更稳妥的处理方式是:服务端白名单、随机文件名、上传目录不可执行、必要时对象存储隔离。示例配置如下:

# Nginx:禁止 uploads 目录执行 PHP
location ^~ /uploads/ {
    location ~ \.php$ {
        return 403;
    }
}

# 或者更直接:上传目录只作为静态文件目录
location ^~ /uploads/ {
    types {
        image/jpeg jpg jpeg;
        image/png png;
        image/gif gif;
    }
    default_type application/octet-stream;
}

PHP 侧也要避免信任客户端提供的信息:

$allowedExt = ['jpg', 'jpeg', 'png', 'gif'];
$originalName = $_FILES['file']['name'] ?? '';
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));

if (!in_array($ext, $allowedExt, true)) {
    http_response_code(400);
    exit('invalid extension');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['file']['tmp_name']);
$allowedMime = ['image/jpeg', 'image/png', 'image/gif'];

if (!in_array($mime, $allowedMime, true)) {
    http_response_code(400);
    exit('invalid mime');
}

$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$target = '/data/uploads/' . $newName;
move_uploaded_file($_FILES['file']['tmp_name'], $target);

第五步:确认影响范围

找到入口后,还需要判断攻击者做了什么。常见排查方向如下:

  • 是否还有其他 WebShell 或落地文件。
  • 是否被新增系统用户、SSH key、计划任务。
  • 是否存在异常进程、异常外连。
  • 数据库是否出现异常导出、批量查询、后台账号新增。
  • 业务配置文件是否泄露,例如数据库密码、第三方密钥。

一些基础命令可以快速检查:

# 最近修改的 Web 文件
find /var/www/html -type f -mtime -7 -ls | sort -k8,9

# 临时目录异常文件
find /tmp /var/tmp -type f -mtime -7 -ls 2>/dev/null

# 计划任务
crontab -l
ls -la /etc/cron* /var/spool/cron 2>/dev/null

# 异常监听和外连
ss -antup
ps aux --sort=-%cpu | head
ps aux --sort=-%mem | head

# 用户和登录痕迹
cat /etc/passwd
last -a
lastlog

如果 Web 进程用户具备过高权限,影响面会明显扩大。例如 PHP-FPM 以 root 或高权限用户运行,或者 Web 目录对进程用户可写范围过大,都可能导致后续清理困难。

代码审计关注点

这类事件复盘后,建议对代码做一次针对性审计。优先级可以按入口暴露面排序:

模块关注点建议
文件上传扩展名、MIME、保存路径、可执行权限白名单校验,上传目录禁执行
后台登录弱口令、验证码、爆破限制启用 MFA 或至少增加限速和审计
富文本编辑器历史版本漏洞、上传接口暴露升级组件,关闭未使用接口
调试接口phpinfo、测试脚本、备份文件生产环境删除
模板渲染动态 include、模板写入限制路径,避免用户可控模板
命令执行系统命令拼接、参数过滤避免调用 shell,必要时严格参数化

审计时不要只找危险函数,也要看数据流:用户输入从哪里来,经过哪些校验,最终进入文件系统、数据库、模板引擎还是系统命令。很多漏洞不是单点错误,而是多个“看起来问题不大”的逻辑组合起来形成风险。

加固建议

  • Web 目录和上传目录分离,上传目录不解析脚本。
  • PHP-FPM 使用低权限用户运行,按站点隔离用户。
  • 关闭不必要的 PHP 危险函数,结合业务评估影响。
  • 部署文件完整性监控,关注新增 PHP、phtml、inc 文件。
  • 保留足够长的访问日志,建议至少 30 到 90 天。
  • 后台入口增加访问控制、登录限速和操作审计。
  • 及时清理历史备份文件、测试脚本、旧组件目录。
  • 数据库账号最小权限,避免 Web 账号具备高危管理权限。
# PHP disable_functions 示例,需结合业务测试
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,show_source

# 生产环境建议关闭错误回显
display_errors = Off
log_errors = On

注意点

  • 不要只删除 WebShell 就结束。入口不修,后门很快会再次出现。
  • 不要完全依赖文件修改时间,攻击者可能修改时间戳,需要结合日志和备份对比。
  • 访问日志如果经过代理,需要确认真实客户端 IP 字段,避免误判。
  • 如果配置文件疑似泄露,应轮换数据库密码、对象存储密钥、第三方 API Token。
  • 如果无法确认影响范围,重装系统和重新部署可信代码通常比“原地修补”更稳妥。

总结

WebShell 事件排查的核心不是识别某一个后门文件,而是还原链路:入口是什么,文件何时写入,攻击者访问过哪些资源,权限扩展到什么程度,业务数据是否受影响。

从经验看,上传目录可执行、后台弱口令、旧组件遗留、日志保留不足,是最常见的几个问题。建议把这类复盘沉淀成固定检查清单,平时做代码审计和上线检查时就能提前发现不少风险。安全工作里,能稳定执行的基础项,往往比临时补救更有价值。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

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