跳转到帖子

背景

最近帮一套老旧业务系统做安全排查,起因是主机侧监控发现 Web 目录下出现了异常 PHP 文件,并且访问时间集中在凌晨。业务本身不大,典型的传统 PHP + Nginx + MySQL 架构,后台有文件上传、富文本编辑、插件管理等功能。

这类事件在中小型系统里很常见:入侵点未必复杂,往往是上传校验不严格、历史组件未更新、后台弱口令或配置暴露。本文不讨论攻击利用过程,而是整理一次偏防守视角的排查方法:如何从日志中定位入口,如何确认影响范围,如何回到代码层修复问题,并建立后续检查清单。

问题场景

现场环境大致如下:

  • Web 服务:Nginx 1.20
  • 运行环境:PHP 7.4-FPM
  • 应用类型:自研 CMS,部分历史模块多年未维护
  • 上传目录:/data/www/app/public/uploads/
  • 日志路径:/var/log/nginx/access.log、/var/log/nginx/error.log

异常现象包括:

  • 上传目录出现随机命名的 PHP 文件
  • Nginx access log 中存在访问该文件的记录
  • 业务用户反馈偶发 500 错误
  • Web 目录内部分文件 mtime 与发布记录不一致
处理这类事件时,第一原则不是立刻删除文件,而是先保留证据、复制样本、冻结现场时间线。否则后续很难判断入侵入口和影响范围。

技术分析

排查思路可以分成四层:文件层、日志层、进程层、代码层。不要只盯着 WebShell 文件本身,它通常只是结果,入口可能在上传接口、后台功能、第三方组件或凭据泄露。

一、文件层:确认异常文件和时间线

先在 Web 根目录和可写目录中查找最近变更的脚本文件。这里重点关注上传目录、缓存目录、临时目录、模板编译目录。

find /data/www/app -type f \( -name "*.php" -o -name "*.phtml" -o -name "*.phar" \) -mtime -14 -printf "%TY-%Tm-%Td %TH:%TM:%TS %p\n" | sort

如果系统存在发布记录,可以将文件变更时间与发布窗口对比。非发布期间新增的可执行脚本,应当重点检查。

常见可疑特征包括:

  • 文件名随机,如 a9f3.php、cache_update.php
  • 代码中存在高风险函数调用,如 eval、assert、system、shell_exec、passthru
  • 存在明显混淆,如长 Base64 字符串、字符串拼接、压缩编码
  • mtime 与周边文件明显不一致

可以用只读方式做初筛:

grep -RIn --include="*.php" -E "eval\s*\(|assert\s*\(|shell_exec\s*\(|passthru\s*\(|base64_decode\s*\(" /data/www/app 2>/dev/null

需要注意,存在这些函数不等于一定被入侵,很多老项目会在模板、插件、任务脚本中使用动态执行。这里的价值是缩小人工审计范围。

二、日志层:还原访问链路

确认异常文件后,先查它的访问记录。假设发现文件为 /uploads/2024/11/tmp_a.php,可以搜索对应 URI:

grep "tmp_a.php" /var/log/nginx/access.log*

重点关注字段:

  • 首次访问时间
  • 来源 IP 和 User-Agent
  • 请求方法,是否有 POST
  • 响应状态码和响应大小
  • Referer 是否指向上传接口或后台页面

如果 access log 格式中记录了请求耗时、上游状态、请求体大小,会更容易判断是否存在异常交互。建议生产环境至少保留以下字段:

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent" "$http_x_forwarded_for" '
                'rt=$request_time uct=$upstream_connect_time '
                'uht=$upstream_header_time urt=$upstream_response_time';

接着反查同一 IP 在异常文件出现前后的行为:

grep "1.2.3.4" /var/log/nginx/access.log | awk '{print $4,$5,$6,$7,$9,$10}'

这里不是为了做攻击溯源,而是为了定位入口。比如在异常文件首次访问前,如果同一 IP 请求了 /admin/upload、/ueditor/php/controller.php 或某个插件接口,就要回到对应代码审计。

三、进程层:确认是否有横向影响

WebShell 类事件有时会伴随系统命令执行、计划任务写入或异常进程。排查时建议看以下位置:

ps auxww --sort=start_time | tail -30
crontab -l
ls -la /etc/cron.d /var/spool/cron 2>/dev/null
last -a | head
ss -tunlp

如果业务容器化部署,也要看容器内外两层:

docker ps --no-trunc
docker inspect <container_id>
docker exec -it <container_id> sh -c 'ps aux; crontab -l; find /tmp -type f -mtime -7 -ls'

这里的判断标准是“是否存在与业务无关、无法解释的持久化和网络连接”。不要看到陌生进程就直接 kill,先记录命令行、父进程、启动时间、可执行文件路径。

四、代码层:定位入口并修复

本次事件最终定位到一个历史上传接口。问题点不复杂:后端只根据前端传来的 MIME 类型和文件扩展名判断,没有重新解析文件内容,也没有禁止上传目录执行脚本。

典型问题代码类似下面这样:

if ($_FILES['file']['type'] === 'image/jpeg') {
    $ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
    $target = __DIR__ . '/uploads/' . date('Ymd') . '/' . uniqid() . '.' . $ext;
    move_uploaded_file($_FILES['file']['tmp_name'], $target);
}

这里至少有三个问题:

  • $_FILES['file']['type'] 来自客户端,不可信
  • 扩展名只取原始文件名,未做白名单归一化
  • 上传目录允许 PHP 解析执行

修复建议不是简单加黑名单,而是采用白名单、内容校验、随机文件名、权限隔离和 Web 服务器层禁止执行。示例:

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

if (!in_array($ext, $allowExt, true)) {
    throw new RuntimeException('invalid extension');
}

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

if (!in_array($mime, $allowMime, true)) {
    throw new RuntimeException('invalid mime');
}

$dir = __DIR__ . '/uploads/' . date('Ymd');
if (!is_dir($dir)) {
    mkdir($dir, 0750, true);
}

$name = bin2hex(random_bytes(16)) . '.' . $ext;
$target = $dir . '/' . $name;

if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) {
    throw new RuntimeException('upload failed');
}

Nginx 层也要限制上传目录执行脚本。即使应用层漏掉一次,Web 服务器层也应提供兜底:

location ^~ /uploads/ {
    types {
        image/jpeg jpg jpeg;
        image/png png;
        image/gif gif;
    }
    default_type application/octet-stream;

    location ~* \.(php|phtml|phar|shtml|cgi|pl)$ {
        return 403;
    }
}

如果是 Apache,可以通过目录配置禁用脚本执行:

<Directory "/data/www/app/public/uploads">
    php_admin_flag engine off
    Options -ExecCGI
    RemoveHandler .php .phtml .phar
</Directory>

关键步骤整理

阶段目标建议动作
发现确认异常文件查找近期新增脚本、记录路径和时间
保全避免证据丢失复制样本、保存日志、记录 hash
定位找到入口围绕首次访问时间反查上传、后台、组件接口
处置清除影响移除异常文件、重置凭据、检查计划任务和进程
修复关闭漏洞代码白名单校验、目录禁执行、权限最小化
复盘降低复发补日志字段、加文件完整性监控、建立发布基线

注意点

  • 不要只删除 WebShell 文件。删除结果不修入口,后续大概率还会出现。
  • 不要完全依赖文件扩展名和客户端 MIME。上传校验必须在服务端完成。
  • 上传目录、缓存目录、日志目录原则上不应具备脚本执行能力。
  • 业务账号、数据库账号、后台账号都应在事件后轮换,尤其是配置文件可能被读取的情况。
  • 日志保留时间要覆盖排查窗口。很多现场最大的问题不是漏洞复杂,而是日志只有一天。
  • 对老项目做代码审计时,优先看文件上传、模板渲染、插件安装、压缩包解压、远程图片抓取等高风险功能。

后续加固建议

从长期维护角度看,建议增加几项低成本措施:

  • 对 Web 根目录做文件完整性基线,发布时更新基线,非发布变更告警。
  • 将上传文件放到独立存储域名或对象存储,避免与应用代码同目录。
  • PHP-FPM 运行用户与部署用户分离,Web 进程只拥有必要写权限。
  • 后台入口增加多因素认证或至少限制来源 IP。
  • 对历史第三方组件建立版本清单,定期核对公开漏洞。

一个简单的完整性检查可以先从 hash 基线开始,不一定一开始就上复杂平台:

cd /data/www/app
find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.css" \) -print0 | sort -z | xargs -0 sha256sum > /opt/baseline/app.sha256

后续比对:

cd /data/www/app
sha256sum -c /opt/baseline/app.sha256 | grep -v "OK$"

总结

WebShell 事件的处理重点不在“识别一个恶意文件”,而在还原链路和修复入口。比较稳妥的顺序是:先保留现场,再建立时间线,然后通过访问日志定位入口,最后回到代码和配置层做闭环。

对大多数传统 Web 系统来说,上传目录禁执行、服务端白名单校验、权限最小化、日志字段补全、文件完整性监控这几项投入不高,但收益很明显。真正能降低复发率的,通常不是某个单点工具,而是把排查经验沉淀成可重复执行的检查清单。

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

0篇意见

推荐意见

没有意见。

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