JavaWeb解压缩漏洞之ZipSlip与Zip炸弹

简介: JavaWeb解压缩漏洞之ZipSlip与Zip炸弹

前言
前面一篇博文《Android Zip解压缩目录穿越导致文件覆盖漏洞》介绍过 Android 系统 Zip 文件解压缩场景下的目录穿越漏洞,近期在学习 JavaWeb 代码审计的时候从 github 看到《OpenHarmony-Java-secure-coding-guide.md》中“从 ZipInputStream 中解压文件必须进行安全检查”章节提及 JavaWeb 系统同样涉及此类目录穿越漏洞,同时还涉及 Zip 炸弹的攻击场景,故在此学习记录下。

Zip Slip
此类漏洞指的是解压 zip 文件时没有校验各解压文件的名字,如果文件名包含 ../ 会导致解压文件被释放到目标目录之外的目录。

在《Android Zip解压缩目录穿越导致文件覆盖漏洞》一文已经讲过原理,不再过多赘述。直接沿用原来的恶意 Zip 生成代码和 Zip 解压缩代码即可:

import zipfile

def zip_slip_file(output_path):
try:
with open("source/test.txt", "r") as f:
binary = f.read()
zipFile = zipfile.ZipFile(output_path, "a", zipfile.ZIP_DEFLATED)
zipFile.writestr("../../test.txt", binary)
zipFile.close()
except Exception as e:
print(e)

if name == 'main':
zip_slip_file(r'result/test.zip')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void unzipFile(String zipPtath, String outputDirectory) throws IOException {
File file = new File(outputDirectory);
if (!file.exists()) {
file.mkdirs();
}
InputStream inputStream = new FileInputStream(zipPtath); ;
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
byte[] buffer = new byte[1024 * 1024];
int count;
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null){
if (!zipEntry.isDirectory()) {
String fileName = zipEntry.getName();
System.out.println("解压文件的名字: " + fileName + ",解压文件的大小: " + zipEntry.getSize());
file = new File(outputDirectory + File.separator + fileName);
file.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file);
while ((count = zipInputStream.read(buffer)) > 0) {
fileOutputStream.write(buffer, 0, count);
}
fileOutputStream.close();
}
}
zipInputStream.close();
System.out.println("解压完成!");
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
运行结果如下,成功进行路径穿越:

在实际漏洞利用中,可借助上述 Zip Slip 漏洞,对系统重要文件或可执行文件进行被覆盖,从而造成系统故障或任意代码执行的危害。

Zip 炸弹
重点介绍下 Zip 炸弹,先看下 OpenHarmony Java 安全编码指导文档的相关描述:

Zip 炸弹的大致原理是 zip 炸弹文件中有大量刻意重复的数据,这种重复数据在压缩的时候是可以被丢弃的,这也就是压缩后的文件其实并不大的原因。最为典型的 Zip 炸弹就是 42.zip,一个 42KB 的文件,解压完其实是个 4.5 PB(1 PB=1024 TB) 的“炸弹”,详细原理可参见:A better zip bomb。

漏洞演示
Github 有生成 Zip 炸弹的现成项目: CreeperKong/zipbomb-generator。

脚本用法很简单,如下指定生成包含一个 3.9G 左右大小的 test.zip 文件:

python zipbomb.py --mode=quoted_overlap --num-files=1 --compressed-size=3999999 > test.zip
1

修改 --num-files=10 参数则可以令 zip 中包含 10 个重复的上述文件(当然还可以包含更多):

由上面可见,如果 Web 服务器从客户端发送过来的 http 报文中提取 zip 文件并进行解压缩的时候没校验 zip 文件夹内部文件的大小的话,将导致攻击者可以传递 zip 炸弹耗尽服务器资源,形成严重的 Dos 攻击。

历史上知名组件的相关漏洞的话可以参见 ZIP bomb vulnerability in HuTool:

错误修补
上文提到使用 zipEntry.getSize() 函数获取 zip 文件大小是不可取,zipEntry.getSize()是从 zip 文件中的固定字段中读取单个文件压缩前的大小,如何篡改并欺骗服务器?

先模仿一段存在缺陷的修复代码:

public static void unzipFile(String zipPtath, String outputDirectory) throws IOException {
    File file = new File(outputDirectory);
    if (!file.exists()) {
        file.mkdirs();
    }
    InputStream inputStream = new FileInputStream(zipPtath); ;
    ZipInputStream zipInputStream = new ZipInputStream(inputStream);
    byte[] buffer = new byte[1024 * 1024];
    int count;
    ZipEntry zipEntry;
    while ((zipEntry = zipInputStream.getNextEntry()) != null){
        if (!zipEntry.isDirectory()) {
            String fileName = zipEntry.getName();
            System.out.println("解压文件的名字: " + fileName + ",解压文件的大小: " + zipEntry.getSize());
            // 判断被压缩的文件的大小,单个文件不得大于4Mb
            if(zipEntry.getSize() < 4096){
                file = new File(outputDirectory + File.separator + fileName);
                file.createNewFile();
                FileOutputStream fileOutputStream = new FileOutputStream(file);
                while ((count = zipInputStream.read(buffer)) > 0) {
                    fileOutputStream.write(buffer, 0, count);
                }
                fileOutputStream.close();
            }else {
                System.out.println("文件大小超出限制!");
                return;
            }
        }
    }
    zipInputStream.close();
    System.out.println("解压完成!");
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

上述代码判断被压缩的文件的大小,单个文件不得大于 4Mb,如何绕过?

步骤很简单,首先下载用于修改二进制文件的 010editor 软件,安装后打开上面演示用的 Zip 炸弹 test.zip(包含了一个 3.9G 的大文件):

修改图示 frUncompressedsize 字段的值后,重新运行 Java 程序对其进行解压缩,可成功绕过文件大小限制,解压出目标文件:

用 VSCode 可成功打开上述解压缩出来的文件(文件内容全都是 aaaaa……),意味着文件并未损坏:

可以看到,此时zipEntry.getSize() 函数获取到的压缩文件大小已经变成我们修改完以后的值(10),同时成功解压缩出 3.9G 大小的目标文件,成功绕过了修复代码对于压缩文件的大小限制。

值得注意的是,从上述截图也可以看到修改了 zip 文件的 frUncompressedsize 字段的值以后,解压缩 zip 文件会报错,如果直接使用 7-zip 进行解压缩的话更是直接报错而终止,提取不出任何文件:

java.util.zip.ZipException: invalid entry size (expected 10 but got 396289 bytes)
at java.util.zip.ZipInputStream.readEnd(ZipInputStream.java:384)
at java.util.zip.ZipInputStream.read(ZipInputStream.java:196)
at java.io.FilterInputStream.read(FilterInputStream.java:107)
at Util.Util.unzipFile(Util.java:42)
at Main.main(Main.java:14)
1
2
3
4
5
6

但是通过实践也可以看到,通过上述 Java 代码可成功解压缩出来目标文件,这样子的话就不影响我们通过修改 zip 文件的 frUncompressedsize 字段的值,制作 zip 炸弹绕过服务端的文件大小校验检测,完成攻击利用。

安全编码
最后直接看看《OpenHarmony-Java-secure-coding-guide》提供的 Zip 文件解压缩的安全编码示例:

private static final long MAX_FILE_COUNT = 100L;
private static final long MAX_TOTAL_FILE_SIZE = 1024L * 1024L;

...

public void unzip(FileInputStream zipFileInputStream, String dir) throws IOException {
long fileCount = 0;
long totalFileSize = 0;
try (ZipInputStream zis = new ZipInputStream(zipFileInputStream)) {
ZipEntry entry;
String entryName;
String entryFilePath;
File entryFile;
byte[] buf = new byte[10240];
int length;
while ((entry = zis.getNextEntry()) != null) {
entryName = entry.getName();
//先对文件名的合法性进行校验
entryFilePath = sanitizeFileName(entryName, dir);
entryFile = new File(entryFilePath);
if (entry.isDirectory()) {
creatDir(entryFile);
continue;
}
fileCount++;
//对zip压缩包中的文件数量进行限制,设置了上限阈值
if (fileCount > MAX_FILE_COUNT) {
throw new IOException("The ZIP package contains too many files.");
}
//此处不再同通过zipEntry.getSize()函数获取 zip 文件大小,而是通过文件数据流直接读取整个文件的数据并统计大小
try (FileOutputStream fos = new FileOutputStream(entryFile)) {
while ((length = zis.read(buf)) != -1) {
totalFileSize += length;
zipBombCheck(totalFileSize);
fos.write(buf, 0, length);
}
}
}
}
}

//防止压缩文件名携带../导致的Zip Slip路径穿越漏洞
private String sanitizeFileName(String fileName, String dir) throws IOException {
File file = new File(dir, fileName);
String canonicalPath = file.getCanonicalPath();
if (canonicalPath.startsWith(dir)) {
return canonicalPath;
}
throw new IOException("Path Traversal vulnerability: ...");
}

private void creatDir(File dirPath) throws IOException {
boolean result = dirPath.mkdirs();
if (!result) {
throw new IOException("Create dir failed, path is : " + dirPath.getPath());
}
...
}

//防止zip炸弹
private void zipBombCheck(long totalFileSize) throws IOException {
if (totalFileSize > MAX_TOTAL_FILE_SIZEG) {
throw new IOException("Zip Bomb! The size of the file extracted from the ZIP package is too large.");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
上述示例中,一共做了 3 项目安全检查:

在解压每个文件之前对其文件名进行校验,如果校验失败,整个解压过程会被终止,防止路径穿越漏洞;
解压缩过程中,对每个文件通过文件数据流识别其实际大小,如果达到指定的阈值(MAX_TOTAL_FILE_SIZE),会抛出异常终止解压操作;
同时,程序会统计解压出来的文件的数量,如果达到指定阈值(MAX_FILE_COUNT),会抛出异常终止解压操作。
总结
从上面的安全示例编码可以看到,简简单单的一个常见 Zip 文件解压缩过程,需要做的安全校验却并不少。总的来说,研发人员在编写对用户可见的 zip 文件上传功能时,一定要严格校验好 zip 文件中待解压缩的文件文件名是否包含../非法字符,校验带解压的文件大小,同时禁止通过 zipEntry.getSize() 函数获取 zip 文件大小,最后也需要校验下解压缩出来的文件总数(设置阈值,毕竟积少成多,通过大量中小型文件也可以完成 zip 炸弹攻击)。

本文参考文章:

Java代码审计指南;
OpenHarmony-Java-secure-coding-guide;
压缩炸弹(zipbomb)制作(附演示);
一个42KB的文件,是如何解压完变成一个4.5PB的数据;
https://github.com/CreeperKong/zipbomb-generator;
————————————————

                        版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/weixin_39190897/article/details/137090300

目录
相关文章
|
29天前
|
存储 Java API
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
37 4
|
3月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
440 37
|
2月前
|
前端开发 Java 应用服务中间件
Javaweb学习
【10月更文挑战第1天】Javaweb学习
33 2
|
2月前
|
安全 网络协议 Java
Java反序列化漏洞与URLDNS利用链分析
Java反序列化漏洞与URLDNS利用链分析
53 3
|
2月前
|
SQL 安全 Java
JAVA代码审计SAST工具使用与漏洞特征
JAVA代码审计SAST工具使用与漏洞特征
60 2
|
3月前
|
SQL 安全 Java
JAVA代码审计SAST工具使用与漏洞特征
JAVA代码审计SAST工具使用与漏洞特征
74 1
|
3月前
|
安全 Java Android开发
JavaWeb解压缩漏洞之ZipSlip与Zip炸弹
JavaWeb解压缩漏洞之ZipSlip与Zip炸弹
116 2
|
2月前
|
安全 Java Python
基于python-django的Java网站全站漏洞检测系统
基于python-django的Java网站全站漏洞检测系统
35 0
|
12天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
3天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####