一文吃透文件上传漏洞!路径遍历、后缀绕过、ZIP炸弹全解析
文件上传漏洞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
"上传成功"
;
}
2. 问题点分析:
| 点位 | 风险 |
|---|---|
fileName | 用户可控,未清洗,可能包含 ../ |
| 拼接路径 | 易导致目录穿越 |
| 文件后缀 | 未校验,可能上传 .jsp 等恶意脚本 |
| 随机性 | 无随机命名,可能覆盖旧文件 |
| 文件大小 | 未限制,可能被恶意上传大文件、Zip炸弹等 |
3. 使用 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);
}
}
4. 风险分析与绕过技巧:
item.getName()可被伪造,返回值可能为:../../webapps/ROOT/shell.jsp。- 若直接写入本地文件系统,无适当处理,可能形成 RCE。
- Commons FileUpload 不自带后缀检查,全部靠开发人员自己处理。
二、核心攻击点分析
1. 路径遍历(Path Traversal)
不安全代码:
String fileName = request.getParameter(
"fileName"
);
File file =
new
File(
"/upload/"
+ fileName);
2. 攻击示例:
fileName=../../../../webapps/ROOT/shell.jsp
效果: 上传的文件可能被写入 Web项目根目录 下,造成远程代码执行。
3. 安全建议:
- 禁止文件名中包含
../、\、空格、%编码字符等。 - 使用
file.getCanonicalPath()与上传目录前缀比对。
File dest =
new
File(uploadDir, fileName);
String canonicalPath = dest.getCanonicalPath();
if
(!canonicalPath.startsWith(uploadDir.getCanonicalPath())) {
throw
new
SecurityException(
"路径非法"
);
//会判断上传文件的目录是否在合法目录
}
4. 文件大小未限制(DoS 风险)
未设置上传大小限制,攻击者可构造超大文件导致内存/磁盘耗尽。
Spring Boot 配置:
spring:
servlet:
multipart:
max-file-size:
10MB
max-request-size:
20MB
5. ZIP 炸弹攻击分析
ZIP 炸弹是一种特制的压缩文件:
- 文件体积非常小(几十 KB)
- 解压后数据极大(几个 GB 到 TB)
- 常用于攻击文件上传和解压服务,使 CPU、内存或磁盘瞬间耗尽
📉 攻击形式举例:
| 压缩前 | 压缩后 | 压缩比 |
|---|---|---|
| 10 GB | 20 KB | 500,000:1 |
攻击者可能上传 20KB.zip,但解压后文件达到 10GB,导致:
- 磁盘被写满
- 内存耗尽或服务器卡死
- 服务完全崩溃
6. ZIP 炸弹防护建议
服务端代码解压前,务必加上以下限制:
- 限制解压后文件总大小
- 限制单个文件大小
- 限制文件数量
- 检测压缩比(压缩比过高直接拒绝)
7. Java 安全解压 ZIP 示例(带限制)
import
java.io.*;
import
java.util.zip.ZipEntry;
import
java.util.zip.ZipInputStream;
public
class
SafeZipExtractor
{
private
static
final
long
MAX_TOTAL_UNZIPPED_SIZE =
100
*
1024
*
1024
;
// 100MB
private
static
final
long
MAX_SINGLE_FILE_SIZE =
50
*
1024
*
1024
;
// 50MB
private
static
final
int
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) {
throw
new
SecurityException(
"解压文件数过多,疑似 ZIP 炸弹"
);
}
File newFile =
new
File(targetDir, entry.getName()).getCanonicalFile();
// 防止路径穿越
if
(!newFile.getPath().startsWith(targetDir.getCanonicalPath())) {
throw
new
SecurityException(
"非法路径,疑似穿越攻击: "
+ entry.getName());
}
// 逐步写出解压文件
try
(FileOutputStream fos =
new
FileOutputStream(newFile)) {
byte
[] buffer =
new
byte
[
4096
];
int
len;
long
singleFileSize =
0
;
while
((len = zis.read(buffer)) >
0
) {
singleFileSize += len;
totalUnzippedSize += len;
if
(singleFileSize > MAX_SINGLE_FILE_SIZE) {
throw
new
SecurityException(
"单个文件过大,疑似 ZIP 炸弹"
);
}
if
(totalUnzippedSize > MAX_TOTAL_UNZIPPED_SIZE) {
throw
new
SecurityException(
"解压内容总体积过大,疑似 ZIP 炸弹"
);
}
fos.write(buffer,
0
, len);
}
}
}
}
}
}
8. 上面防护总结:
| 检查项 | 防护内容 |
|---|---|
| 路径合法性检查 | 防止 ZIP 路径穿越攻击 |
| 文件数上限 | 防止上传含成千上万小文件的压缩包 |
| 单文件大小限制 | 防止解压出超大单个文件 |
| 总解压体积限制 | 阻止总体积膨胀的 ZIP 炸弹 |
9. 其他方式:
- 使用像 Zip4j 或 Apache Commons Compress 这样的库能获得更多安全控制。
后缀名校验缺失或被绕过
错误做法:黑名单
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(
"非法文件类型"
);
}
10. 文件名中“点”的位置及隐藏文件攻击
攻击者可上传如下文件名:
.htaccess.env.ssh/authorized_keysabc.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)) {
throw
new
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())) {
throw
new
SecurityException(
"非法路径"
);
}
file.transferTo(dest);
return
"上传成功"
;
}
审计 Checklist
| 检查项 | 是否必做 | 建议 |
|---|---|---|
| 文件名是否随机生成? | ✅ | UUID 或雪花算法 |
| 后缀名是否白名单? | ✅ | 严格控制 |
| 是否防路径穿越? | ✅ | canonicalPath 比对 |
| 是否限制大小? | ✅ | 配置或代码判断 |
| 是否隔离存储目录? | ✅ | 不与 webroot 混用 |
| 是否拦截隐藏文件? | ✅ | .xxx 拒绝上传 |
文件上传总结
| 🧨 攻击点 | 🎯 攻击方式 | 🛡 防御建议 |
|---|---|---|
| 路径控制 | - 路径穿越 ../ - 绝对路径注入 - 上传符号链接 | - 使用 getCanonicalPath() 规范化路径 - 校验目标路径是否在允许目录内 - 禁止软链接上传 |
| 文件类型绕过 | - 双扩展 .php.jpg - 特殊字符 .php%00.jpg - 控制字符/Unicode 后缀绕过 - 魔术头伪装 | - 使用魔术头/MIME 检测文件内容 - 白名单方式验证扩展名 - 禁止解析上传目录(如配置 nginx/php) |
| ZIP炸弹 | - 高压缩比 ZIP - 多层嵌套压缩包 - 超大文件数 | - 限制最大解压大小、文件数、嵌套层数 - 使用 Zip4j/Apache Commons 解压时封装限制 - 拒绝可疑压缩比(如 >1000:1) |
| 图片马 | - 上传带 PHP 代码的图片 - 利用图片解析漏洞 | - 不允许上传至可执行目录 - 拒绝上传含脚本内容的图像(验证内容头) |
| WAF绕过/编码绕过 | - 双写编码绕过 - Content-Type 伪造 - 分块传输绕过检测 | - 上传前验证 MIME 与扩展是否匹配 - 服务端统一校验,不信任前端类型信息 - 使用 RASP 或反向代理层识别异常流量 |
| 大文件/并发DoS | - 上传超大文件压垮服务器 - 并发上传大量小文件耗尽 inode | - 限制 max-file-size / max-request-size - 控制上传频率(限流) - 后台作业处理上传文件 |
| 上传后可访问 | - 上传后直接访问执行 shell - 前后端路径绕过 | - 上传文件重命名为随机 UUID - 上传目录配置为不可执行 - 存储层与访问层解耦 |
题外话
黑客&网络安全如何学习
网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我们和网安大厂360共同研发的的网安视频教程,内容涵盖了入门必备的操作系统、计算机网络和编程语言等初级知识,而且包含了中级的各种渗透技术,并且还有后期的CTF对抗、区块链安全等高阶技术。总共200多节视频,100多本网安电子书,最新学习路线图和工具安装包都有,不用担心学不全。

文件上传漏洞深度解析

8332

被折叠的 条评论
为什么被折叠?



