spring-integration-zip-unsafe-unzip分析

漏洞信息

看pivotal发布的漏洞信息如下:

通过关键字信息可以看出来,这个漏洞是因为没有对解压的zip中的文件和目录进行确认,导致在解压zip包时可能会存在任意目录文件写入的漏洞。这个漏洞主要与unzip transformer漏洞相关。漏洞的版本需小于1.0.1版本。

漏洞分析

环境搭建

本实验的代码使用的是chybeta师傅提供的代码,下载地址

根据漏洞存在的版本,修改pom.xml文件中的spring-integration-zip1.0.1版本。

恶意zip文件

恶意的zip文件的结构如下所示:

在当前zip文件中,存在good.txt文件以及../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp目录,tmp目录下面存在evil.txt文件。

前提知识

在漏洞漏洞之前,需要对spring-integration-zip中的ZipInputStreamZipEntry有一个简单的认识。通过zip的结构我们可以知道,需要通过zip中的目录名作为目录穿越的payload。通过以下实例代码来了解ZipEntry的用法。

1
2
3
4
5
6
File file = new File("D:\\zip-malicious-traversal.zip");
ZipInputStream zis = new ZipInputStream(new FileInputStream(file));
ZipEntry entry = null;
while ( (entry = zis.getNextEntry()) != null ) {
System.out.println( entry.getName());
}

通过ZipEntrygetName()输出的是:

1
2
good.txt
../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp/evil.txt

所以ZipEntrygetName()会得到zip包中的目录名以及其中的文件名。

漏洞分析

项目的目录结构如下所示:

我们程序的测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,将解压之后的文件写入到父目录中的hehe目录中。漏洞的关键代码位于unZipTransformer.transform(evilMessage);

跟踪代码unZipTransformer.transform(evilMessage);,进入到org.springframework.integration.zip.transformer.UnZipTransformer:doZipTransform()中。当程序运行至68行,分析此时的参数。

inputStream中含有恶意的zip文件,而ZipEntryCallback()作为回调函数进一步对zip包进行处理。首先分析ZipUtil.iterate()函数,进入到org.zeroturnaround.zip.ZipUtil:iterate()中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void iterate(InputStream is, ZipEntryCallback action, Charset charset) {
try {
ZipInputStream in = null;
if (charset == null) {
in = new ZipInputStream(new BufferedInputStream(is));
} else {
in = ZipFileUtil.createZipInputStream(is, charset);
}

ZipEntry entry;
while((entry = in.getNextEntry()) != null) {
try {
action.process(in, entry);
} catch (IOException var6) {
throw new ZipException("Failed to process zip entry '" + entry.getName() + " with action " + action, var6);
} catch (ZipBreakException var7) {
break;
}
}

} catch (IOException var8) {
throw ZipExceptionUtil.rethrow(var8);
}
}

函数参数InputStream isdoZipTransform中的inputStream,ZipEntryCallback actiondoZipTransform中的ZipEntryCallback回调函数。程序通过while循环读取zip包中的目录和文件,回调执行action.process(in, entry);

回到UnZipTransformer:doZipTransform()中对回调函数进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void process(InputStream zipEntryInputStream, ZipEntry zipEntry) throws IOException {
String zipEntryName = zipEntry.getName();
long zipEntryTime = zipEntry.getTime();
long zipEntryCompressedSize = zipEntry.getCompressedSize();
String type = zipEntry.isDirectory() ? "directory" : "file";
....
if (ZipResultType.FILE.equals(UnZipTransformer.this.zipResultType)) {
File tempDir = new File(UnZipTransformer.this.workDirectory, message.getHeaders().getId().toString());
tempDir.mkdirs();
File destinationFile = new File(tempDir, zipEntryName);
if (zipEntry.isDirectory()) {
destinationFile.mkdirs();
} else {
SpringZipUtils.copy(zipEntryInputStream, destinationFile);
uncompressedData.put(zipEntryName, destinationFile);
}
} ...
}

通过String zipEntryName = zipEntry.getName();得到的结果如下:

当程序运行至SpringZipUtils.copy(zipEntryInputStream, destinationFile);,分析此时的参数状态。

此时,tempDir.\here\0365902c-4673-075f-8767-24ec0d67c704\good.txt,zipEntryName../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp/evil.txt,导致通过File destinationFile = new File(tempDir, zipEntryName);得到的destinationFile的值是.\here\0365902c-4673-075f-8767-24ec0d67c704\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\tmp\evil.txt

最后执行SpringZipUtils.copy(zipEntryInputStream, destinationFile);,成功地在根目录tmp下写入evil.txt

漏洞修复

Disallow traversal entity in zip中的修复方案是:

1
2
3
4
5
6
7
8
9
tempDir.mkdirs(); //NOSONAR false positive
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);
}

if (zipEntry.isDirectory()) {
destinationFile.mkdirs(); //NOSONAR false positive
}

在回调函数中,增加了对zipEntryNamedestinationFile的判断。如果在zipEntryName含有..并且通过destinationFile.getCanonicalPath()得到destinationFile的标准化路径,在本例中destinationFile最终的标准化路径是C:\tmp\evil.txtworkDirectory的标准化目录不一致,则认为是目录穿越的漏洞。

CVE-2018-1263

漏洞信息

其中红色部分说明的是,虽然1261的补丁能够很好地防御目录穿越写文件的漏洞,但是这个补丁仅仅只能防御框架本身,如果有用户自己使用了destinationFile并且没有采用补丁的方式进行校验,那么同样会存在目录穿越漏洞。所以这个漏洞的本质原因还是在于生成destinationFile的方式存在问题。

修复commit

UnZipTransformer.java对生成的destinationFile进行校验。如下:

采用了checkPath(message, zipEntryName)的方式生成destinationFile

在生成destinationFile进行判断,如果确认没有问题返回destinationFile,否则认为是目录穿越的漏洞。通过这种方式就能够保证生成的destinationFile是不存在目录穿越的问题的。

参考

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