Chybeta

Unsafe Unzip with spring-integration-zip 分析-【CVE-2018-1261 与 CVE-2018-1263】

Unsafe Unzip with spring-integration-zip 分析-【CVE-2018-1261 与 CVE-2018-1263】

漏洞公告

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

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

漏洞分析

从简单的测试代码开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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进行了检查

1
2
3
4
5
6
7
8
9
10
11
12
13
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()`比较来确定是否存在目录穿越。
对于恶意的压缩包,在生成了`destinationFile`后,假设值为`.\here\e401f4b8-0ecb-3f3a-76ce-5318b14d6000\..\..\tmp\evil.txt`时,通过调用`destinationFile.getCanonicalPath()`把``.`和`..`解析成对应的正确的路径,获得它规范化的绝对路径。之后再与工作目录`workDirectory.getCanonicalPath()`比较来确定是否存在目录穿越。
之后,2018511日pivotal又再次放出公告:
![11.png](https://github.com/CHYbeta/chybeta.github.io/blob/master/images/pic/20180512/11.png?raw=true)
原因在于:

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.

1
2
3
4
5
也就是说,生成的`destinationFile`其实是错误的,尽管框架本身不会有问题不会出现目录遍历漏洞,但是对于应用而言,可能之后直接使用了`destinationFile`这个路径来进行操作从而导致错误。因此在1.0.2.RELEASE版本中的补丁中[[Dissallow traversal entry even for byte[]](https://github.com/spring-projects/spring-integration-extensions/commit/d10f537283d90eabd28af57ac97f860a3913bf9b),直接在生成`destinationFile`时做了检查:
```java
final File destinationFile = checkPath(message, zipEntryName);

7.png

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

漏洞考古

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

本文标题:Unsafe Unzip with spring-integration-zip 分析-【CVE-2018-1261 与 CVE-2018-1263】

文章作者:chybeta

发布时间:2018年05月14日 - 07:05

最后更新:2018年05月14日 - 07:05

原始链接:http://chybeta.github.io/2018/05/14/Unsafe-Unzip-with-spring-integration-zip-分析-【CVE-2018-1261-与-CVE-2018-1263】/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!