一文吃透文件上传漏洞!路径遍历、后缀绕过、ZIP炸弹全解析

文件上传漏洞深度解析

一文吃透文件上传漏洞!路径遍历、后缀绕过、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 GB20 KB500,000:1

攻击者可能上传 20KB.zip,但解压后文件达到 10GB,导致:

  • 磁盘被写满
  • 内存耗尽或服务器卡死
  • 服务完全崩溃

6. ZIP 炸弹防护建议
服务端代码解压前,务必加上以下限制:
  1. 限制解压后文件总大小
  2. 限制单个文件大小
  3. 限制文件数量
  4. 检测压缩比(压缩比过高直接拒绝)

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_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)) {


        
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多本网安电子书,最新学习路线图和工具安装包都有,不用担心学不全。
在这里插入图片描述

🐵这些东西我都可以免费分享给大家,需要的可以点这里自取👉:网安入门到进阶资源

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值