漏洞公告

https://pivotal.io/security/cve-2018-1261

关于 CVE-2018-1263 ,见补丁浅析部分。

漏洞分析

从简单的测试代码开始:

public final class Main {

    private static ResourceLoader resourceLoader = new DefaultResourceLoader();
    private static File path =  new File("./here/");
    public static void main(final String... args) {
        final Resource evilResource = resourceLoader.getResource("classpath:zip-malicious-traversal.zip");
        try{
            InputStream evilIS = evilResource.getInputStream();
            Message<InputStream> evilMessage = MessageBuilder.withPayload(evilIS).build();
            UnZipTransformer unZipTransformer = new UnZipTransformer();
            unZipTransformer.setWorkDirectory(path);
            unZipTransformer.afterPropertiesSet();
            unZipTransformer.transform(evilMessage);
        }catch (Exception e){
            System.out.println(e);
        }
    }
}

其中zip-malicious-traversal.zip即恶意的压缩包,结构如下:

unZipTransformer.setWorkDirectory(path);设置了正常情况下解压目录为当前目录下的here文件夹,如上gif所示,在here文件夹中生成了good.txt。而evil.txt却逃逸出了这个限制,在G://tmp下生成了。

环境相关源码见附件。为了复现漏洞,需要在硬盘根目录下先创建一个tmp目录,zip-malicious-traversal.zip在CVE-2018-1261\src\main\resources中。

跟踪代码,在unZipTransformer.transform(evilMessage);处打上断点跟入。当控制流到达 org/springframework/integration/zip/transformer/UnZipTransformer.java:112

ZipUtil.iterate(inputStream, new ZipEntryCallback() { ... });

这里会将inputStream输入,ZipEntryCallback作为回调函数。跟入iterate 至org/zeroturnaround/zip/ZipUtil.java。

public static void iterate(InputStream is, ZipEntryCallback action, Charset charset) {
    try {
      ZipInputStream in = null;
      if (charset == null) {
          in = new ZipInputStream(new BufferedInputStream(is));
      }
      else { ... }
      ZipEntry entry;
      while ((entry = in.getNextEntry()) != null) {
        try {
          action.process(in, entry);
        }
        ...
      }
    }
    ...
  }

在iterate中,通过in = new ZipInputStream(new BufferedInputStream(is));生成了ZipInputStream对象in,此后通过in.getNextEntry()来获取对象in中的一个个条目。对于getNextEntry()而已,它会直接把目录给打印出来,具体可以参见stackoverflow: How does ZipInputStream.getNextEntry() work?。所以对于zip-malicious-traversal.zip而言

回到UnZipTransformer.java:

可以看到entry的值即为../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp/evil.txt

此后调用回调函数process:

public void process(InputStream zipEntryInputStream, ZipEntry zipEntry) throws IOException {

    final String zipEntryName = zipEntry.getName();
    ...
    if (ZipResultType.FILE.equals(zipResultType)) {
        final File tempDir = new File(workDirectory, message.getHeaders().getId().toString());
        tempDir.mkdirs(); //NOSONAR false positive
        final File destinationFile = new File(tempDir, zipEntryName);

        if (zipEntry.isDirectory()) { ...   }
        else {
            SpringZipUtils.copy(zipEntryInputStream, destinationFile);
            uncompressedData.put(zipEntryName, destinationFile);
        }
    }
    ...
}

tempDir是临时生成的文件夹,而zipEntryName通过zipEntry.getName()得到,即为../../../那一串。接着通过final File destinationFile = new File(tempDir, zipEntryName);确定解压目录,也正是这里造成了跨越目录漏洞。接着就是调用copy把数据写到destinationFile处。

究其原因,对于getNextEntry而言,../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp仅仅是目录名字,而对于copy操作而言,../../../等将被解释为目录穿越操作从而造成了任意解压。

补丁浅析

1.0.1.RELEASE中的补丁 Disallow traversal entity in zip,主要是在进行copy操作前,对zipEntryName进行了检查

final File destinationFile = new File(tempDir, zipEntryName);

    if (zipEntryName.contains("..") && !destinationFile.getCanonicalPath().startsWith(workDirectory.getCanonicalPath())) {
        throw new ZipException("The file " + zipEntryName + " is trying to leave the target output directory of " + workDirectory);
    }

对于恶意的压缩包,在生成了destinationFile后,假设值为.\here\e401f4b8-0ecb-3f3a-76ce-5318b14d6000\..\..\tmp\evil.txt时,通过调用destinationFile.getCanonicalPath()`...解析成对应的正确的路径,获得它规范化的绝对路径。之后再与工作目录workDirectory.getCanonicalPath()比较来确定是否存在目录穿越。

之后,2018年5月11日pivotal又再次放出公告:

原因在于:

While the framework itself now does not write such files, it does present the errant path to the user application, which could inadvertently write the file using that path.

也就是说,生成的destinationFile其实是错误的,尽管框架本身不会有问题不会出现目录遍历漏洞,但是对于应用而言,可能之后直接使用了destinationFile这个路径来进行操作从而导致错误。因此在1.0.2.RELEASE版本中的补丁中[Dissallow traversal entry even for byte[],直接在生成destinationFile时做了检查:

final File destinationFile = checkPath(message, zipEntryName);

除此之外,在 Remove unnecessary check for the ..中还将zipEntryName.contains("..")的判断删除,因为认为是不必要的。

漏洞考古

类似的压缩文件目录遍历漏洞以前也出现不少,列举几个。

CVE-2018-1261.rar (0.021 MB) 下载附件
源链接

Hacking more

...