背景
最近整理了一次比较典型的 WebShell 告警处置过程。场景并不复杂:某业务系统的运行目录中出现异常脚本文件,安全设备给出了疑似 WebShell 告警。后续排查发现,入口点并不是常见的弱口令后台,而是一个文件上传接口在扩展名校验、存储路径和访问控制上都存在缺陷。
这类问题在企业内网系统里很常见:业务代码年代较久,上传功能为了兼容各种文件类型逐步放宽限制,最后变成“能上传、能访问、能解析”的危险组合。本文不讨论攻击利用细节,重点放在应急排查、代码审计和修复加固上。
问题场景
告警信息中能看到几个关键点:
- 异常文件出现在 Web 根目录下的 upload 目录;
- 文件后缀看起来像图片,但内容中存在脚本特征;
- 访问日志中存在对该文件的直接 HTTP 请求;
- 同一来源 IP 在短时间内多次访问上传接口和异常文件。
从防守角度看,这已经足够判断需要进入应急流程:先止血,再保全证据,最后定位根因并修复。
第一步:现场保护与初步确认
很多人在处理 WebShell 告警时会第一时间删除文件,这个习惯不太好。更稳妥的做法是先隔离访问能力,再保留样本和日志。比如临时在 Web 服务器层面禁止访问上传目录中的可执行脚本,或者将对应目录挂载为不可执行。
Nginx 环境中可以先加一条临时规则,阻断上传目录内脚本解析:
location ~* ^/upload/.*\.(php|jsp|jspx|asp|aspx)$ {
return 403;
}如果是 PHP-FPM,还需要确认 location 匹配顺序,避免被其他规则覆盖。对于 Java 应用,重点看上传目录是否被映射到可直接访问的静态资源路径,是否存在 JSP 被容器解析的可能。
保留证据时,至少建议记录:
- 异常文件路径、文件大小、修改时间、权限;
- 文件 hash;
- 相关访问日志、错误日志、应用日志;
- 上传接口请求时间附近的操作日志;
- 服务器进程、网络连接、计划任务和最近新增文件。
sha256sum /path/to/upload/suspicious_file
stat /path/to/upload/suspicious_file
find /var/www -type f -mtime -3 -ls日志分析:把时间线串起来
应急排查里最重要的是时间线。单独看到一个 WebShell 文件,只能说明“存在异常文件”;只有结合日志,才能判断入口、影响范围和是否发生后续动作。
可以从访问日志里筛选上传接口和异常文件访问记录:
grep -E "POST /.*upload|/upload/" access.log | awk '{print $1,$4,$5,$6,$7,$9,$10}'需要关注以下字段:
- 请求方法:是否存在异常 POST;
- 状态码:上传是否成功,异常文件是否被访问成功;
- User-Agent:是否明显为脚本化工具;
- Referer:是否来自正常业务页面;
- 源 IP:是否与正常用户行为一致;
- 请求时间:是否与文件 mtime 对得上。
如果应用层有审计日志,还要看上传人、业务单号、文件原名、存储名、MIME 类型、文件大小等字段。很多时候,Web 访问日志只能看到结果,应用日志才能定位到具体接口和参数。
样本分析:判断风险而不是炫技
对异常文件的分析,建议遵循“静态优先、隔离环境验证”的原则,不要在生产机上执行。静态分析通常已经足够判断风险,例如:
- 脚本中是否存在命令执行、文件读写、反射调用等高危能力;
- 是否对参数进行加密、编码或混淆;
- 是否具备连接远程地址、下载文件、横向移动的迹象;
- 是否存在固定密钥、特征字符串或行为标识。
常用的基础检查命令如下:
file suspicious_file
strings -a suspicious_file | head -n 50
grep -nE "eval|assert|system|exec|passthru|shell_exec|base64_decode|Runtime|getRuntime|ProcessBuilder" suspicious_file如果文件伪装成图片,可以检查文件头和真实内容是否一致:
xxd -l 64 suspicious_file
file suspicious_file这里的目标不是复现控制过程,而是回答几个防守问题:它是否可执行?是否被访问过?是否有持久化或进一步投递行为?是否可能造成数据泄露?
根因定位:上传功能常见审计点
本次问题的根因在上传接口。代码审计时可以从以下几个点入手。
1. 扩展名校验是否可靠
只依赖前端校验或者简单字符串判断都不可靠。常见错误包括:
- 只检查文件名中是否包含 .jpg、.png;
- 使用黑名单过滤危险后缀;
- 未处理大小写、双后缀、特殊字符;
- 上传后仍保留用户可控文件名。
更合理的方式是使用白名单,并且服务端重新生成文件名。示例思路如下:
String ext = getSafeExtension(originalFilename).toLowerCase(Locale.ROOT);
Set<String> allow = Set.of("jpg", "jpeg", "png", "pdf", "docx");
if (!allow.contains(ext)) {
throw new IllegalArgumentException("unsupported file type");
}
String saveName = UUID.randomUUID().toString().replace("-", "") + "." + ext;注意:白名单不是只看扩展名,还应结合业务场景校验文件内容类型。
2. MIME 类型和文件内容是否校验
Content-Type 来自客户端,不能信任。更稳妥的做法是读取文件头或使用成熟库判断文件类型。例如图片类上传,可以用图片解码库尝试解析,解析失败直接拒绝。
BufferedImage image = ImageIO.read(uploadedFile.getInputStream());
if (image == null) {
throw new IllegalArgumentException("invalid image file");
}对于 PDF、Office 等文件,也建议结合魔数、文件结构解析和杀毒/沙箱扫描结果综合判断。不要把“Content-Type=image/png”当作安全依据。
3. 上传目录是否可执行
这是很多系统被打穿的关键点。即使上传校验有疏漏,只要上传目录不具备脚本执行能力,风险会明显下降。
建议:
- 上传文件放到 Web 根目录之外;
- 通过受控下载接口读取文件,而不是直接暴露真实路径;
- 静态资源服务器禁止脚本解析;
- 对象存储设置合理的 Content-Type 和下载策略;
- 文件名、路径、桶名不要由用户直接控制。
Nginx 可参考如下限制:
location /upload/ {
autoindex off;
add_header X-Content-Type-Options nosniff;
}
location ~* ^/upload/.*\.(php|phtml|jsp|jspx|asp|aspx|sh|pl|py)$ {
return 403;
}4. 下载接口是否存在路径穿越
有些系统虽然把文件放在 Web 根目录外,但下载接口写得不严谨,仍然可能被读取任意文件。审计时重点看是否直接拼接用户传入的 path、filename。
不推荐的模式:
// 风险示例:用户参数直接拼接路径
File file = new File(baseDir + "/" + request.getParameter("file"));更稳妥的方式是只允许通过文件 ID 下载,由服务端查询真实路径,并校验路径规范化结果必须位于允许目录下:
Path base = Paths.get("/data/upload").toRealPath();
Path target = base.resolve(serverSideFileName).normalize();
if (!target.startsWith(base)) {
throw new SecurityException("invalid file path");
}影响面排查
确认入口后,还需要判断是否有后续影响。建议检查以下内容:
- Web 目录近几天新增或修改的脚本文件;
- 系统临时目录中的异常文件;
- crontab、systemd、启动项是否有新增任务;
- 应用账号权限是否过大;
- 数据库账号是否存在异常登录或敏感查询;
- 是否有异常外联、DNS 请求或下载行为。
find /var/www -type f \( -name "*.php" -o -name "*.jsp" -o -name "*.jspx" \) -mtime -7 -ls
crontab -l
systemctl list-timers
ss -antp如果服务器上部署了 EDR 或主机审计,可以结合进程树、文件落地、网络连接做交叉验证。单看 Web 日志容易漏掉后续动作。
修复与加固建议
修复不要只停留在删除 WebShell。建议按优先级处理:
- 立即禁用上传目录脚本执行能力;
- 修复上传接口,改为白名单、重命名、内容校验;
- 上传文件迁移到 Web 根目录外或对象存储;
- 补充上传、下载、预览接口的鉴权;
- 清理异常文件和可疑任务;
- 轮换可能泄露的密钥、数据库密码、后台账号密码;
- 补充日志字段,至少记录上传人、文件 ID、hash、大小、IP;
- 建立定期巡检规则,对 Web 目录新增脚本进行告警。
如果业务允许,上传文件最好统一走异步扫描流程:文件先进入临时区,扫描通过后再入库或进入正式存储。这样即使前端或接口出现疏漏,也能增加一道缓冲。
常见误区
- 只删文件不查入口:很容易反复出现;
- 只看扩展名不看存储路径:上传目录可执行才是高风险组合;
- 只查 Web 日志不查主机行为:可能漏掉后续落地文件;
- 黑名单过滤危险后缀:维护成本高,遗漏概率大;
- 把内网系统当低风险系统:内网系统往往权限更高,日志更弱。
总结
WebShell 告警处置的核心不是“看到异常文件就删除”,而是把入口、样本、访问记录和主机行为串成完整链路。上传功能的安全设计也不应只依赖某一个校验点,而要做到:类型白名单、服务端重命名、内容校验、不可执行存储、受控下载、完整审计。
从长期看,最值得投入的是两件事:一是把上传和下载能力收敛成统一组件,减少各业务重复造轮子;二是建立 Web 目录变更监控和日志关联规则。这样即使问题再次出现,也能更早发现、更快定位,避免从一个小上传点演变成完整入侵事件。
推荐意见