跳转到帖子

背景

最近在做一批老旧 PHP 站点的安全巡检时,遇到一个比较典型的 WebShell 告警案例。站点本身业务不复杂,LNMP 架构,PHP 版本偏旧,目录里存在历史遗留的上传接口和多个第三方插件。告警来自主机侧文件监控,提示某个图片上传目录下出现异常 PHP 文件。

这类事件在实际环境里很常见:单点文件被查杀后删除,但没有继续追溯入口、持久化和横向影响,几天后又会反复出现。本文整理一套偏实战的排查流程,重点放在定位入侵路径、确认影响范围和加固闭环,不涉及攻击利用细节。

场景与初步现象

主机告警文件路径类似:

/data/www/example.com/public/uploads/2024/11/.cache.php

从文件名看有伪装痕迹,位置在上传目录,且以点开头隐藏。进一步查看文件属性:

stat /data/www/example.com/public/uploads/2024/11/.cache.php
ls -la /data/www/example.com/public/uploads/2024/11/
file /data/www/example.com/public/uploads/2024/11/.cache.php

可以关注几个字段:创建/修改时间、属主属组、权限、文件类型。实际排查中,WebShell 文件的 mtime 往往能和访问日志中的某次上传请求、异常 POST 请求或插件接口访问对应上。

排查思路

个人习惯按“三条线”并行看:文件线、日志线、进程线。不要只盯着单个木马文件,因为入口点、二次落点和持久化通常不在同一个位置。

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

先围绕告警时间做目录扫描,优先找最近被修改的 PHP、可执行脚本、异常隐藏文件和大小明显异常的文件。

# 最近 7 天修改过的 PHP 文件
find /data/www/example.com -type f -name "*.php" -mtime -7 -printf "%TY-%Tm-%Td %TH:%TM %p\n" | sort

# 上传目录中不该出现的脚本文件
find /data/www/example.com/public/uploads -type f \( -name "*.php" -o -name "*.phtml" -o -name "*.phar" \) -printf "%TY-%Tm-%Td %TH:%TM %p\n" | sort

# 查找隐藏文件
find /data/www/example.com/public/uploads -type f -name ".*" -printf "%TY-%Tm-%Td %TH:%TM %p\n" | sort

如果站点使用 Composer 或某些 CMS,还要避免误判 vendor、cache、runtime 等目录。但上传目录、主题目录、插件目录、临时目录中出现陌生脚本,优先级应当提高。

对可疑 PHP 文件不建议直接在生产机上执行或访问。可以先做静态查看:

sha256sum suspicious.php
strings -a suspicious.php | head -n 50
sed -n '1,120p' suspicious.php

常见可疑特征包括:大量混淆字符串、动态函数调用、异常编码解码链、调用系统命令函数、远程加载代码、修改文件时间戳等。这里的目的不是复现能力,而是辅助判断文件性质和关联样本。

二、日志线:反推入口请求

从文件 mtime 向前后各扩展一段时间,例如 10 到 30 分钟,查看 Nginx/Apache 访问日志和 PHP-FPM 日志。

# 按时间粗筛访问日志,实际要按日志格式调整
zgrep "24/Nov/2024:10:" /var/log/nginx/access.log* | grep "example.com" > /tmp/access_window.log

# 关注上传、插件、后台、接口路径
cat /tmp/access_window.log | egrep -i "upload|file|plugin|admin|api|editor|ajax|ueditor|kindeditor|tinymce"

# 关注异常状态码和大请求体
cat /tmp/access_window.log | awk '$9 ~ /200|302|500/ {print}' | sort -k10 -nr | head

如果日志中记录了请求体大小,可以重点看上传前后的 POST 请求。很多入侵入口不是直接访问 WebShell,而是先通过上传接口、编辑器组件、插件接口写入文件,然后再访问落点文件确认结果。

另外要看是否存在对告警文件的直接访问:

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

如果有来自固定 IP 的访问,可以继续横向检索该 IP 在同一时间窗口访问了哪些路径:

grep "攻击源IP" /var/log/nginx/access.log* | awk '{print $4,$5,$7,$9,$10}' | head -n 200

这里不建议只凭 IP 做结论。代理、扫描器、云函数出口都很常见,更重要的是请求路径、时间顺序、User-Agent、Referer、状态码和响应大小之间的关联。

三、进程线:确认是否已执行命令或落地后门

如果 WebShell 已被访问,需要确认是否有命令执行、反连、计划任务、异常进程等痕迹。

# 当前 Web 用户进程
ps aux | egrep "www|nginx|apache|php-fpm" | grep -v grep

# 网络连接
ss -antp

# 定时任务
crontab -l
ls -la /var/spool/cron/
ls -la /etc/cron.* /etc/crontab

# 最近登录与提权痕迹
last -a | head
lastlog | head
grep -i "sudo\|session\|authentication" /var/log/secure /var/log/auth.log 2>/dev/null | tail -n 100

如果业务允许,建议先做隔离而不是直接关机:下线公网入口、保留磁盘快照、复制日志和可疑文件。直接删除文件会破坏时间线,给后续复盘造成困难。

入口点定位:几个常见方向

在这个案例里,最终入口来自一个历史图片裁剪插件,上传接口只校验了前端扩展名,后端没有严格校验文件内容和保存路径,导致脚本文件进入了可执行目录。类似问题一般可以从以下方向排查:

  • 上传接口:是否只校验扩展名,是否允许双扩展名,是否将用户可控文件保存在 Web 可执行目录。

  • 富文本编辑器:老版本 UEditor、KindEditor、FCKeditor 等历史组件经常被遗留。

  • 插件和主题:CMS 插件目录权限过宽,后台上传主题或插件后未做完整校验。

  • 后台弱口令:日志中如果有后台登录成功记录,需要结合账号、来源 IP 和操作记录确认。

  • 反序列化或文件包含:如果没有明显上传请求,要关注异常参数、包含路径、缓存写入点。

代码审计时,可以用关键词先做粗筛:

grep -RIn "move_uploaded_file\|copy(\|file_put_contents\|fopen\|rename\|unlink" /data/www/example.com/app /data/www/example.com/plugins

grep -RIn "include\|require\|eval\|assert\|call_user_func" /data/www/example.com/app /data/www/example.com/plugins

重点不是看到危险函数就判漏洞,而是追踪参数是否来自用户输入、是否经过白名单校验、保存路径是否可控、最终目录是否会被 Web 服务器解析执行。

修复与加固建议

处置阶段建议分层做,不要只删除 WebShell。

1. 上传目录禁止脚本执行

Nginx 可以对上传目录增加规则,避免 PHP 被解析:

location ^~ /uploads/ {
    location ~* \.(php|phtml|phar|php5)$ {
        return 403;
    }
}

Apache 可在对应目录使用配置禁止脚本执行,具体方式取决于是否允许 .htaccess:

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

更稳妥的做法是将上传文件存储到 Web 根目录之外,通过受控下载接口或对象存储访问。

2. 后端上传校验使用白名单

上传校验至少包含:扩展名白名单、MIME 校验、文件头检查、随机文件名、固定存储目录、禁止用户控制后缀和路径。图片类文件可考虑重新编码生成,避免原样落盘。

伪代码思路如下:

$allowExt = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowExt, true)) {
    throw new Exception('invalid extension');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmpFile);
$allowMime = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mime, $allowMime, true)) {
    throw new Exception('invalid mime');
}

$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$target = $uploadDir . '/' . $newName;

3. 收紧文件权限

Web 服务用户不应拥有整个站点的写权限。常见原则是:代码目录只读,上传、缓存、日志目录按需可写。

# 示例:代码目录只读,按业务实际调整
chown -R root:www-data /data/www/example.com
find /data/www/example.com -type d -exec chmod 750 {} \;
find /data/www/example.com -type f -exec chmod 640 {} \;

# 上传目录允许 Web 用户写入,但禁止执行由 Web 配置控制
chown -R www-data:www-data /data/www/example.com/public/uploads
chmod -R 750 /data/www/example.com/public/uploads

4. 建立文件完整性基线

对于中小型 PHP 站点,文件完整性监控非常有用。至少对代码目录、插件目录、入口文件做 hash 基线,发布时更新基线,运行时发现异常及时告警。

cd /data/www/example.com
find app config public -type f \( -name "*.php" -o -name "*.js" -o -name "*.html" \) -print0 | sort -z | xargs -0 sha256sum > /var/baseline/example.sha256

后续可以定期比对:

sha256sum -c /var/baseline/example.sha256 | grep -v "OK"

复盘时容易忽略的点

  • 只删除文件不找入口:这是最常见问题,入口不修,WebShell 会反复出现。

  • 忽略日志轮转:很多站点日志保留时间很短,发现告警后应第一时间备份 access、error、php-fpm、系统认证日志。

  • 忽略后台账号:如果攻击者通过后台上传插件或模板落地,单纯修上传接口不够,还要改密钥、重置后台密码、检查管理员列表。

  • 忽略供应链组件:老插件和历史编辑器经常被复制到多个站点,修一个站点不代表同服务器其他站点安全。

  • 忽略可执行目录策略:上传目录禁止执行是成本很低、收益很高的防线,应作为默认配置。

一个简化排查清单

阶段检查项目标
发现确认告警文件路径、hash、mtime、权限确定时间窗口和样本特征
日志检索上传、插件、后台、异常 POST 请求反推入口路径
执行检查 WebShell 访问、异常进程、网络连接确认是否被进一步利用
持久化检查 cron、启动项、SSH key、后台账号排除长期驻留风险
修复修补代码、升级组件、禁止上传目录执行关闭入侵入口
加固权限收敛、完整性监控、日志留存降低复发概率

总结

WebShell 告警本身只是一个结果,真正有价值的是把时间线串起来:文件什么时候出现、由哪个请求触发、入口代码在哪里、是否被执行、是否留下持久化。只要沿着文件、日志、进程三条线推进,大多数 PHP 站点入侵都能还原出比较清楚的路径。

从防守角度看,上传目录禁止脚本执行、后端白名单校验、代码目录只读、组件及时升级、日志留存和文件完整性监控,是几项投入不高但效果明显的措施。老站点不一定能一次性重构,但这些基础面补上后,能挡住相当一部分常见入侵链路。

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

0篇意见

推荐意见

没有意见。

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