一文吃透文件上传漏洞!路径遍历、后缀绕过、ZIP炸弹全解析
一文吃透文件上传漏洞!路径遍历、后缀绕过、ZIP炸弹全解析
原创 火力猫 季升安全 2025-04-16 06:45
🔍 文件上传漏洞Java源码审计详解(附代码分析)
文件上传是 Web 应用中极其常见的功能,但一旦实现不当,极易造成严重漏洞
,如:上传 WebShell、任意文件写入、远程命令执行等。本篇将从源码审计角度,深入剖析文件上传中关键风险点,包含路径处理、文件大小限制、后缀校验、绕过技巧、白名单误用
等,并提供典型实现方式与安全建议。
🧱 文件上传的常见实现方式
✅ 1. Spring MultipartFile 实现
@PostMapping("/upload")public String upload(@RequestParam("file") MultipartFile file) throws IOException { String fileName = file.getOriginalFilename(); File dest = new File("/upload/dir/" + fileName); file.transferTo(dest); return "上传成功";}
🔍 问题点分析:
|
|
---|---|
fileName |
../ |
|
|
|
.jsp 等恶意脚本 |
|
|
|
|
⚠️ 2. 使用 Apache Commons FileUpload(ServletFileUpload)
DiskFileItemFactory factory = new DiskFileItemFactory();ServletFileUpload upload = new ServletFileUpload(factory);List<FileItem> items = upload.parseRequest(request);for (FileItem item : items) { if (!item.isFormField()) { String fileName = item.getName(); File file = new File("/upload/" + fileName); item.write(file); }}
🔍 风险分析与绕过技巧:
– item.getName()
可被伪造,返回值可能为:../../webapps/ROOT/shell.jsp
。
-
若直接写入本地文件系统,无适当处理,可能形成 RCE。
-
Commons FileUpload 不自带后缀检查,全部靠开发人员自己处理
。
☠️ 核心攻击点分析
📁 1. 路径遍历(Path Traversal)
❌ 不安全代码:
String fileName = request.getParameter("fileName");File file = new File("/upload/" + fileName);
攻击示例:
fileName=../../../../webapps/ROOT/shell.jsp
🧨 效果:
上传的文件可能被写入 Web项目根目录 下,造成远程代码执行。
✅ 安全建议:
-
禁止文件名中包含 ../
、\
、空格、%编码字符等。 -
使用 file.getCanonicalPath()
与上传目录前缀比对。
File dest = new File(uploadDir, fileName);String canonicalPath = dest.getCanonicalPath();if (!canonicalPath.startsWith(uploadDir.getCanonicalPath())) { throw new SecurityException("路径非法"); //会判断上传文件的目录是否在合法目录}
📏 2. 文件大小未限制(DoS 风险)
未设置上传大小限制,攻击者可构造超大文件导致内存/磁盘耗尽。
✅ Spring Boot 配置:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB
💣 3.ZIP 炸弹攻击分析
ZIP 炸弹是一种特制的压缩文件:
– 文件体积非常小(几十 KB)
-
解压后数据极大(几个 GB 到 TB)
-
常用于攻击文件上传和解压服务,使 CPU、内存或磁盘瞬间耗尽
📉 攻击形式举例:
|
|
|
---|---|---|
|
|
|
攻击者可能上传 20KB.zip
,但解压后文件达到 10GB,导致:
– 磁盘被写满
-
内存耗尽或服务器卡死
-
服务完全崩溃
🔒 ZIP 炸弹防护建议
✅ 服务端代码解压前,务必加上以下限制:
-
限制解压后文件总大小
-
限制单个文件大小
-
限制文件数量
-
检测压缩比(压缩比过高直接拒绝)
🧪 Java 安全解压 ZIP 示例(带限制)
import java.io.*;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream;publicclass SafeZipExtractor { privatestaticfinallong MAX_TOTAL_UNZIPPED_SIZE = 100 * 1024 * 1024; // 100MB privatestaticfinallong MAX_SINGLE_FILE_SIZE = 50 * 1024 * 1024; // 50MB privatestaticfinalint MAX_FILE_COUNT = 100; public static void unzipSafely(File zipFile, File targetDir) throws IOException { long totalUnzippedSize = 0; int fileCount = 0; try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { fileCount++; if (fileCount > MAX_FILE_COUNT) { thrownew SecurityException("解压文件数过多,疑似 ZIP 炸弹"); } File newFile = new File(targetDir, entry.getName()).getCanonicalFile(); // 防止路径穿越 if (!newFile.getPath().startsWith(targetDir.getCanonicalPath())) { thrownew SecurityException("非法路径,疑似穿越攻击: " + entry.getName()); } // 逐步写出解压文件 try (FileOutputStream fos = new FileOutputStream(newFile)) { byte[] buffer = newbyte[4096]; int len; long singleFileSize = 0; while ((len = zis.read(buffer)) > 0) { singleFileSize += len; totalUnzippedSize += len; if (singleFileSize > MAX_SINGLE_FILE_SIZE) { thrownew SecurityException("单个文件过大,疑似 ZIP 炸弹"); } if (totalUnzippedSize > MAX_TOTAL_UNZIPPED_SIZE) { thrownew SecurityException("解压内容总体积过大,疑似 ZIP 炸弹"); } fos.write(buffer, 0, len); } } } } }}
✅ 上面防护总结:
|
|
---|---|
|
|
|
|
|
|
|
|
🚨 其他方式:
- 使用像
Zip4j
或 Apache Commons Compress 这样的库能获得更多安全控制。
📂 4. 后缀名校验缺失或被绕过
❌ 错误做法:黑名单
if (fileName.endsWith(".jsp") || fileName.endsWith(".php")) { throw new SecurityException("禁止上传脚本文件");}
🧨 绕过方式:
|
|
---|---|
|
shell.jpg.jsp |
|
shell.JSP |
|
shell.jsp%00.jpg
|
✅ 正确做法:白名单校验
List<String> allowExt = Arrays.asList(".jpg", ".png", ".pdf", ".docx");String ext = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();//.的定位也很重要,如果用的不是lastIndexOf定位最后一个.大概率存在问题if (!allowExt.contains(ext)) { throw new SecurityException("非法文件类型");}
🎭 5. 文件名中“点”的位置及隐藏文件攻击
攻击者可上传如下文件名:
– .htaccess
-
.env
-
.ssh/authorized_keys
-
abc.jsp.
(带空格)
🧨 某些系统识别后缀可能失误,或将其当作隐藏文件,或绕过后缀判断。
✅ 建议:
-
拒绝以点开头的文件(隐藏文件)
-
拒绝多个点或尾部空格(如 file.jsp
)
if (fileName.startsWith(".") || fileName.contains("..") || fileName.trim().endsWith(".")) { throw new SecurityException("非法文件名");}
🔒 推荐安全上传实现
@PostMapping("/upload")public String secureUpload(@RequestParam("file") MultipartFile file) throws IOException { String originalName = file.getOriginalFilename(); // 后缀白名单 String suffix = originalName.substring(originalName.lastIndexOf(".")).toLowerCase(); List<String> allow = Arrays.asList(".jpg", ".png", ".pdf"); if (!allow.contains(suffix)) { thrownew IllegalArgumentException("不允许的文件类型"); } // 随机命名 + 限定目录 String newName = UUID.randomUUID().toString().replace("-", "") + suffix; File saveDir = new File("/opt/upload/"); if (!saveDir.exists()) saveDir.mkdirs(); File dest = new File(saveDir, newName); // 路径校验 if (!dest.getCanonicalPath().startsWith(saveDir.getCanonicalPath())) { thrownew SecurityException("非法路径"); } file.transferTo(dest); return"上传成功";}
🛡️ 审计 Checklist
|
|
|
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.xxx
|
📌 文件上传总结
|
|
|
---|---|---|
路径控制 |
../ – 绝对路径注入 – 上传符号链接 |
getCanonicalPath() 规范化路径– 校验目标路径是否在允许目录内 – 禁止软链接上传 |
文件类型绕过 |
.php.jpg – 特殊字符 .php%00.jpg – 控制字符/Unicode 后缀绕过 – 魔术头伪装 |
– 白名单方式验证扩展名 – 禁止解析上传目录(如配置 nginx/php) |
ZIP炸弹 |
– 多层嵌套压缩包 – 超大文件数 |
– 使用 Zip4j/Apache Commons 解压时封装限制 – 拒绝可疑压缩比(如 >1000:1) |
图片马 |
– 利用图片解析漏洞 |
– 拒绝上传含脚本内容的图像(验证内容头) |
WAF绕过/编码绕过 |
– Content-Type 伪造 – 分块传输绕过检测 |
– 服务端统一校验,不信任前端类型信息 – 使用 RASP 或反向代理层识别异常流量 |
大文件/并发DoS |
– 并发上传大量小文件耗尽 inode |
max-file-size / max-request-size – 控制上传频率(限流) – 后台作业处理上传文件 |
上传后可访问 |
– 前后端路径绕过 |
– 上传目录配置为不可执行 – 存储层与访问层解耦 |