Java Zip Slip漏洞案例分析及实战挖掘
2023-1-29 20:32:0 Author: xz.aliyun.com(查看原文) 阅读量:64 收藏

Zip Slip的漏洞成因非常简单,这个漏洞绑定的业务功能点:上传压缩包文件,后端解压压缩包保存其中的文件到服务器本地。

漏洞成因:待上传的压缩包中可以构造条目名,后端保存文件的时候,常常将条目名提取出来并和保存目录拼接作为最后的保存文件路径,但是压缩包是可控的,从而其中保存的原始条目名也是可控的,因此可以在文件名处利用../跳转到任意目录,从而向任意目录写入新文件或者覆盖旧文件。具体案例可见下文。

在Zip Slip公布者文章中,提到,Java中的Zip Slip漏洞尤其普遍:

The vulnerability has been found in multiple ecosystems, including JavaScript, Ruby, .NET and Go, but is especially prevalent in Java, where there is no central library offering high level processing of archive (e.g. zip) files. The lack of such a library led to vulnerable code snippets being hand crafted and shared among developer communities such as StackOverflow.

本文从原生的Java.util.zip->zt-zip->spring integration zip进行Zip Slip漏洞分析,并在最后附上此漏洞的代审案例。

import zipfile

if __name__ == "__main__":
    try:
        zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)  ##生成的zip文件
        info = zipfile.ZipInfo("poc.zip")
        zipFile.write("D:/tgao/pass/1", "../password", zipfile.ZIP_DEFLATED)  ##压缩的文件和在zip中显示的文件名
        zipFile.close()
    except IOError as e:
        raise e

上述生成的恶意zip,在Zip Slip中,会取出../password,并与保存目录拼接,其中获取../password的java方法类似与zipEntry.getName()

漏洞代码:实际场景下的的zip包是可控的,如通过文件上传等功能

package zip;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class Zip1 {
    public static void main(String[] args) throws IOException {
        //解压zip的包
        String fileAddress = "D:/pythonProject/exp/ctf/poc.zip";
        //zip文件解压路径
        String unZipAddress = "D:/tgao/pass/";
        //去目录下寻找文件
        File file = new File(fileAddress);
        ZipFile zipFile = null;
        try {
            zipFile = new ZipFile(file);//设置编码格式
        } catch (IOException exception) {
            exception.printStackTrace();
            System.out.println("解压文件不存在!");
        }
        Enumeration e = zipFile.entries();
        while(e.hasMoreElements()) {
            ZipEntry zipEntry = (ZipEntry)e.nextElement();
            File f = new File(unZipAddress + zipEntry.getName());
            f.getParentFile().mkdirs();
            f.createNewFile();
            InputStream is = zipFile.getInputStream(zipEntry);
            FileOutputStream fos = new FileOutputStream(f);
            int length = 0;
            byte[] b = new byte[1024];
            while((length=is.read(b, 0, 1024))!=-1) {
                fos.write(b, 0, length);
            }
            is.close();
            fos.close();
        }
        if (zipFile != null) {
            zipFile.close();
        }
    }
}

漏洞成因: File f = new File(unZipAddress + zipEntry.getName());zipEntry.getName()的值是可控的,从而造成路径穿越,最终写入任意文件。

引入依赖:

<dependency>
  <groupId>org.zeroturnaround</groupId>
  <artifactId>zt-zip</artifactId>
  <version>1.12</version>xml
</dependency>

zt-zip组件中的解压功能,是在原生的java.util.zip基础上进行的封装。
漏洞代码:实际场景下的的zip包是可控的,如通过文件上传等功能。

package zip;

import org.zeroturnaround.zip.ZipUtil;

import java.io.File;

public class Zip2 {
    public static void main(String[] args) {
        File zip = new File("D:/pythonProject/exp/ctf/poc.zip");
        File dir = new File("D:/tgao/pass");
        ZipUtil.unpack(zip, dir);
    }
}

跟进org.zeroturnaround.zip.ZipUtil#unpack(java.io.File, java.io.File)

继续跟进org.zeroturnaround.zip.ZipUtil#unpack(java.io.File, java.io.File, org.zeroturnaround.zip.NameMapper)

在上述方法中使用new ZipUtil.Unpacker(outputDir, mapper)创建ZipEntryCallback对象(ZipUtil.Unpacker)
可以先看其中的org.zeroturnaround.zip.ZipUtil.Unpacker#process方法

上述代码中的this.mapper在调用org.zeroturnaround.zip.ZipUtil#unpack(java.io.File, java.io.File)方法中传入的

进入org.zeroturnaround.zip.IdentityNameMapper

上述的map方法直接将传入的name参数返回并没有任何的过滤。
因此,再看org.zeroturnaround.zip.ZipUtil.Unpacker#process方法对zipEntry.getName()没有任何的过滤。所以导致了Zip Slip漏洞的产生。

再回来看看org.zeroturnaround.zip.ZipUtil#unpack(java.io.File, java.io.File, org.zeroturnaround.zip.NameMapper)

跟进org.zeroturnaround.zip.ZipUtil#iterate(java.io.File, org.zeroturnaround.zip.ZipEntryCallback)

继续跟进org.zeroturnaround.zip.ZipUtil#iterate(java.io.File, org.zeroturnaround.zip.ZipEntryCallback, java.nio.charset.Charset)

可以看到调用了原生的java.util.zip.ZipFile#ZipFile(java.io.File)等API
此方法中也没有任何的过滤,直接将zip流内容和ZipEntry传入了org.zeroturnaround.zip.ZipUtil.Unpacker#process(Unpacker#process在上文已讲过)。

在zt-zip在1.13版本中进行了修复:https://github.com/zeroturnaround/zt-zip/commit/759b72f33bc8f4d69f84f09fcb7f010ad45d6fff#

CVE-2018-1261

引入依赖

<dependency>
  <groupId>org.springframework.integration</groupId>
  <artifactId>spring-integration-zip</artifactId>
  <version>1.0.0.RELEASE</version>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.30</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <version>1.7.30</version>
  <type>jar</type>
</dependency>

spring-integration-zip依赖于zt-zip

漏洞代码:实际场景下的的zip包是可控的,如通过文件上传等功能

package zip;

import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.zip.transformer.UnZipTransformer;
import org.springframework.messaging.Message;

import java.io.File;
import java.io.InputStream;

public class Zip3 {
    private static ResourceLoader resourceLoader = new DefaultResourceLoader();

    public static void main(String[] args) {
        final Resource evilResource = resourceLoader.getResource("classpath:poc.zip");
        try{
            InputStream evilIS = evilResource.getInputStream();
            Message<InputStream> evilMessage = MessageBuilder.withPayload(evilIS).build();
            UnZipTransformer unZipTransformer = new UnZipTransformer();
            unZipTransformer.transform(evilMessage);
        }catch (Exception e){
            System.out.println(e);
        }
    }
}

跟进org.springframework.integration.zip.transformer.UnZipTransformer#UnZipTransformer构造方法

跟进org.springframework.integration.zip.transformer.AbstractZipTransformer#AbstractZipTransformer构造方法

初始化了zipResultTypeworkDirectory属性,前者为ZipResultType.FILE,后续会用到此值,而workDirectory默认值为new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer")后续也会使用到该值。在我的测试环境下,System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer"如下:

创建完UnZipTransformer后,执行org.springframework.integration.transformer.AbstractTransformer#transform方法

其中message参数值是zip文件读取流,继续跟进org.springframework.integration.zip.transformer.AbstractZipTransformer#doTransform

继续跟进org.springframework.integration.zip.transformer.UnZipTransformer#doZipTransform

继续跟进

调用了zt-zipapi,只不过spring integration zip在这里自己创建了一个ZipEntryCallback匿名对象,最后会调用此匿名对象的process方法

没有任何过滤,导致zip slip发生。
修复方案如下:https://github.com/spring-projects/spring-integration-extensions/commit/a5573eb232ff85199ff9bb28993df715d9a19a25

项目地址:https://gitee.com/RainyGao/DocSys
com.DocSystem.controller.BaseController#unZip方法中存在如下代码片段

其中entry.getName()的值是可控的,通过../可以将恶意jsp文件写到web根目录。
寻找触发点,发现在com.DocSystem.controller.ManageController#upgradeSystem方法中触发了com.DocSystem.controller.BaseController#unZip方法

关键代码如下:

具体漏洞复现可参考:https://gitee.com/RainyGao/DocSys/issues/I65IYU


文章来源: https://xz.aliyun.com/t/12081
如有侵权请联系:admin#unsafe.sh