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')
    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("解压完成!");
    }

运行结果如下,成功进行路径穿越:
image.png
在实际漏洞利用中,可借助上述 Zip Slip 漏洞,对系统重要文件或可执行文件进行被覆盖,从而造成系统故障或任意代码执行的危害。

Zip 炸弹

重点介绍下 Zip 炸弹,先看下 OpenHarmony Java 安全编码指导文档的相关描述:
image.png
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

image.png
修改 --num-files=10 参数则可以令 zip 中包含 10 个重复的上述文件(当然还可以包含更多):
image.pngimage.png
由上面可见,如果 Web 服务器从客户端发送过来的 http 报文中提取 zip 文件并进行解压缩的时候没校验 zip 文件夹内部文件的大小的话,将导致攻击者可以传递 zip 炸弹耗尽服务器资源,形成严重的 Dos 攻击。

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

错误修补

上文提到使用 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("解压完成!");
    }

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

步骤很简单,首先下载用于修改二进制文件的 010editor 软件,安装后打开上面演示用的 Zip 炸弹 test.zip(包含了一个 3.9G 的大文件):
image.png
image.png
修改图示 frUncompressedsize 字段的值后,重新运行 Java 程序对其进行解压缩,可成功绕过文件大小限制,解压出目标文件:
image.png
用 VSCode 可成功打开上述解压缩出来的文件(文件内容全都是 aaaaa……),意味着文件并未损坏:
image.png
可以看到,此时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)

image.png
但是通过实践也可以看到,通过上述 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.");
    }
}

上述示例中,一共做了 3 项目安全检查:

  1. 在解压每个文件之前对其文件名进行校验,如果校验失败,整个解压过程会被终止,防止路径穿越漏洞;
  2. 解压缩过程中,对每个文件通过文件数据流识别其实际大小,如果达到指定的阈值(MAX_TOTAL_FILE_SIZE),会抛出异常终止解压操作;
  3. 同时,程序会统计解压出来的文件的数量,如果达到指定阈值(MAX_FILE_COUNT),会抛出异常终止解压操作。

总结

从上面的安全示例编码可以看到,简简单单的一个常见 Zip 文件解压缩过程,需要做的安全校验却并不少。总的来说,研发人员在编写对用户可见的 zip 文件上传功能时,一定要严格校验好 zip 文件中待解压缩的文件文件名是否包含../非法字符,校验带解压的文件大小,同时禁止通过 zipEntry.getSize() 函数获取 zip 文件大小,最后也需要校验下解压缩出来的文件总数(设置阈值,毕竟积少成多,通过大量中小型文件也可以完成 zip 炸弹攻击)。

本文参考文章:

  1. Java代码审计指南;
  2. OpenHarmony-Java-secure-coding-guide;
  3. 压缩炸弹(zipbomb)制作(附演示)
  4. 一个42KB的文件,是如何解压完变成一个4.5PB的数据
  5. https://github.com/CreeperKong/zipbomb-generator
  • 28
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要修复JavaWeb文件上传漏洞,可以采取以下几个步骤: 1. 验证文件类型:在服务器端对上传的文件进行类型验证,比较常见的方法是通过文件后缀名和MIME类型验证。可以根据应用的需求,限定允许上传的文件类型,并拒绝上传不允许的文件类型。 2. 检查文件内容:可以使用文件解析库来解析上传的文件内容,确保其符合预期的格式和结构。例如,对于图像文件,可以使用图像处理库来检查图像的有效性,避免恶意代码注入。 3. 控制文件大小:限制上传文件的最大大小,可以防止攻击者上传过大的文件导致服务器资源耗尽。可以在应用程序中配置上传文件的最大大小,并在上传时检查文件大小是否超出限制。 4. 随机化文件名:为了避免被攻击者猜测到上传文件的真实路径,可以生成随机的文件名,并将上传的文件保存在指定目录中。 5. 安全存储:确保将上传的文件存储在安全的位置,并设置适当的文件权限。避免将上传的文件存储在Web根目录下或其他可以直接通过URL访问的地方。 6. 定期清理:定期清理服务器上存储的上传文件,删除不再需要的文件,以减少攻击面和释放磁盘空间。 除了以上的措施,还应该定期更新和维护服务器和相关库的补丁,以确保应用程序的安全性。另外,进行安全性测试和代码审查也是非常重要的步骤,以发现和修复其他潜在的安全漏洞
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tr0e

分享不易,望多鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值