背景
WebShell 排查是很多应急场景里绕不开的一环。相比“发现一个可疑文件就结束”,更有价值的是把入口、落点、执行链路和处置措施串起来,否则很容易出现删了文件但攻击者很快又回来。
这篇文章整理一次常见 Web 站点被写入脚本后门的排查思路,重点放在日志关联、文件取证、代码特征识别和加固复盘。内容只讨论防御和分析方法,不涉及攻击利用细节。
问题场景
某业务反馈站点偶发异常,表现为部分接口响应慢,且服务器 CPU 在短时间内有异常抖动。初步查看进程没有发现明显异常服务,但 Web 目录下存在近期新增的 PHP 文件,命名类似正常资源文件。
环境大致如下:
- Nginx + PHP-FPM
- 业务代码部署在 /data/www/app
- 访问日志位于 /var/log/nginx/access.log
- 应用存在文件上传功能
这种场景下,不建议直接删除可疑文件后收工。更合理的做法是先保留证据,确认文件来源和触发路径,再做清理和加固。
技术分析
WebShell 排查通常围绕三个问题展开:
- 文件是什么时候出现的?
- 是谁通过什么入口写入的?
- 是否已经被访问或执行过?
如果只看文件内容,很容易漏掉入口点。例如后门可能由上传漏洞写入,也可能由后台弱口令、插件漏洞、反序列化漏洞、定时任务或供应链文件覆盖引入。日志和时间线会比单点判断更可靠。
关键步骤
1. 保护现场并建立时间线
先对可疑文件和相关日志做只读备份,避免后续操作覆盖访问时间或被日志轮转清理。
mkdir -p /root/incident-$(date +%F)
cp -a /data/www/app /root/incident-$(date +%F)/app-snapshot
cp -a /var/log/nginx /root/incident-$(date +%F)/nginx-logs
stat /data/www/app/path/to/suspect.php
sha256sum /data/www/app/path/to/suspect.php重点关注文件的 mtime、ctime、属主属组和权限。如果 Web 目录大量文件在同一时间被修改,要考虑批量写入或部署流程异常。
2. 查找近期新增和高风险脚本文件
先按时间过滤,再结合文件类型和路径判断。上传目录、缓存目录、临时目录、静态资源目录里出现可执行脚本都值得重点看。
find /data/www/app -type f -mtime -7 \
\( -name "*.php" -o -name "*.phtml" -o -name "*.phar" -o -name "*.jsp" -o -name "*.asp" \) \
-printf "%TY-%Tm-%Td %TH:%TM:%TS %u %g %m %p\n" | sort如果目录较大,可以先看上传目录和缓存目录:
find /data/www/app/uploads /data/www/app/cache -type f \
-printf "%TY-%Tm-%Td %TH:%TM:%TS %m %p\n" | sort | tail -1003. 静态识别可疑代码特征
WebShell 不一定都有明显特征,但以下模式经常出现在后门、混淆代码或异常执行链中:
- 动态函数调用,如变量函数、拼接函数名
- 高危执行函数,如 system、exec、shell_exec、passthru、proc_open
- 编码和压缩组合,如 base64_decode、gzinflate、str_rot13
- 异常入口变量,如直接处理请求参数并执行
- 长字符串、高熵内容、不可读混淆块
可以先用 grep 做粗筛:
grep -RIn --include="*.php" \
-E "eval\s*\(|assert\s*\(|system\s*\(|exec\s*\(|shell_exec\s*\(|passthru\s*\(|proc_open\s*\(|popen\s*\(|base64_decode\s*\(|gzinflate\s*\(" \
/data/www/app需要注意,命中这些函数不代表一定是后门。有些框架、模板引擎或运维脚本也可能使用动态执行。判断时要结合上下文:函数参数是否来自用户输入,是否存在鉴权,是否有业务合理性。
4. 关联访问日志
找到可疑文件后,先查它是否被访问过,以及访问来源、User-Agent、请求方法、状态码和响应大小。
grep "suspect.php" /var/log/nginx/access.log* | tail -50如果 access.log 已轮转压缩,可以使用 zgrep:
zgrep "suspect.php" /var/log/nginx/access.log*.gz接着按文件修改时间前后扩展排查。例如可疑文件 mtime 是 2024-05-10 03:14,可以查看前后 30 分钟内的上传接口、后台接口、插件接口访问情况。
awk '$4 ~ /10\/May\/2024:03:/ {print}' /var/log/nginx/access.log \
| egrep "upload|admin|plugin|file|editor|api"这里的重点不是找“看起来最像攻击”的请求,而是还原链路:哪个请求导致文件写入,哪个请求触发了执行。
5. 检查 Web 服务和 PHP-FPM 日志
如果开启了 PHP-FPM slowlog 或 error log,经常能看到异常脚本执行路径、报错堆栈或超时位置。
grep -RIn "suspect.php\|Fatal error\|Warning\|shell_exec\|proc_open" \
/var/log/php* /var/log/php-fpm* 2>/dev/null同时检查 Nginx error.log,部分上传绕过、路径解析异常或权限错误会留下痕迹:
grep -RIn "suspect.php\|permission denied\|upstream timed out\|FastCGI" \
/var/log/nginx/error.log*6. 确认可疑文件是否具备执行条件
有些文件虽然落地,但不一定能执行。应检查 Web 服务器配置,确认上传目录、缓存目录是否允许 PHP 解析。
nginx -T | grep -nE "location|php|fastcgi|root|alias|uploads"比较稳妥的配置原则是:上传目录只作为静态文件目录,禁止脚本解析。例如:
location ^~ /uploads/ {
types { }
default_type application/octet-stream;
location ~* \.php$ {
return 403;
}
}如果业务允许上传图片,还应配合服务端 MIME 检查、文件头校验、随机文件名、扩展名白名单和对象存储隔离。
7. 排查持久化和横向影响
WebShell 只是表象,后续要确认是否存在持久化手段:
- crontab 是否有异常任务
- systemd 是否有新增服务
- Web 目录是否存在批量新增文件
- 用户 SSH authorized_keys 是否被写入异常公钥
- 数据库是否出现异常管理员账号
- 业务后台账号是否有异常登录
常用检查命令如下:
crontab -l
ls -al /etc/cron* /var/spool/cron 2>/dev/null
systemctl list-unit-files | grep enabled
find /data/www/app -type f -mtime -7 -printf "%TY-%Tm-%Td %TH:%TM:%TS %p\n" | sort
find /home /root -name authorized_keys -type f -exec ls -al {} \;如果服务器上存在多个站点,还要检查其他站点目录是否有同类文件。攻击者经常会在同权限可写目录中放多个备份入口。
代码审计关注点
如果日志显示入口来自上传功能,代码审计时建议重点看以下位置:
- 是否只在前端限制扩展名
- 服务端是否使用黑名单而不是白名单
- 是否信任 Content-Type
- 文件名是否直接使用用户输入
- 是否允许双扩展名、大小写混淆或特殊字符
- 上传目录是否位于 Web 可执行路径
一个更稳妥的服务端校验思路是:扩展名白名单、内容类型辅助判断、真实文件头校验、重命名存储、目录不可执行。
// 示例:只表达防御思路,需结合实际框架处理
$allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExt, true)) {
throw new RuntimeException('invalid extension');
}
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$target = $uploadDir . '/' . $newName;
if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) {
throw new RuntimeException('upload failed');
}这里不要把“保存成功”当作安全闭环。真正关键的是上传后的文件不能被解释为脚本执行。
处置建议
- 先隔离主机或限制外部访问,避免继续被触发
- 备份可疑文件、日志和关键配置,保留时间线
- 清除确认的后门文件和异常持久化项
- 修复入口漏洞,例如上传校验、后台弱口令、组件漏洞
- 轮换相关凭据,包括后台账号、数据库账号、部署密钥
- 对 Web 目录做权限收敛,运行用户只保留必要写权限
- 补充监控规则,对新增脚本、异常请求、执行函数告警
权限上建议遵循最小化原则。很多站点被写入后门,是因为整个项目目录都给了 Web 用户写权限。通常只需要 uploads、cache、runtime 等目录可写,代码目录不应由运行用户修改。
chown -R deploy:deploy /data/www/app
chown -R www-data:www-data /data/www/app/uploads /data/www/app/cache /data/www/app/runtime
find /data/www/app -type d -exec chmod 755 {} \;
find /data/www/app -type f -exec chmod 644 {} \;注意点
- 不要只依赖特征库。简单混淆、分段拼接、异步加载都可能绕过粗糙规则。
- 不要忽略正常文件被插入少量恶意代码的情况,尤其是入口文件、配置文件、模板文件。
- 不要在未备份前直接删除文件,后续很难还原入侵路径。
- 不要只修复后门文件,不修复入口漏洞等于等待再次写入。
- 日志时间要注意时区、容器时间和宿主机时间是否一致。
总结
一次有效的 WebShell 排查,不是“grep 到一个 eval”就结束,而是要完成从文件落点、访问日志、入口漏洞到持久化检查的闭环。实际工作里,时间线和权限边界往往比单个样本更重要。
建议平时就做好三件事:上传目录禁止脚本执行,代码目录权限收敛,关键日志集中留存。等到事件发生时,这些基础措施会直接决定排查效率和影响范围。
推荐意见