漏洞信息
看pivotal发布的漏洞信息如下:
通过关键字信息可以看出来,这个漏洞是因为没有对解压的zip中的文件和目录进行确认,导致在解压zip包时可能会存在任意目录文件写入的漏洞。这个漏洞主要与unzip transformer
漏洞相关。漏洞的版本需小于1.0.1
版本。
漏洞分析
环境搭建
本实验的代码使用的是chybeta师傅提供的代码,下载地址
根据漏洞存在的版本,修改pom.xml文件中的spring-integration-zip
为1.0.1
版本。
恶意zip文件
恶意的zip文件的结构如下所示:
在当前zip文件中,存在good.txt
文件以及../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp
目录,tmp目录下面存在evil.txt
文件。
前提知识
在漏洞漏洞之前,需要对spring-integration-zip
中的ZipInputStream
和ZipEntry
有一个简单的认识。通过zip的结构我们可以知道,需要通过zip中的目录名作为目录穿越的payload。通过以下实例代码来了解ZipEntry
的用法。1
2
3
4
5
6File 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());
}
通过ZipEntry
的getName()
输出的是:1
2good.txt
../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp/evil.txt
所以ZipEntry
的getName()
会得到zip包中的目录名以及其中的文件名。
漏洞分析
项目的目录结构如下所示:
我们程序的测试代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15private 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
24public 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 is
是doZipTransform
中的inputStream
,ZipEntryCallback action
是doZipTransform
中的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
18public 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
9tempDir.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
}
在回调函数中,增加了对zipEntryName
和destinationFile
的判断。如果在zipEntryName
含有..
并且通过destinationFile.getCanonicalPath()
得到destinationFile
的标准化路径,在本例中destinationFile
最终的标准化路径是C:\tmp\evil.txt
与workDirectory
的标准化目录不一致,则认为是目录穿越的漏洞。
CVE-2018-1263
漏洞信息
其中红色部分说明的是,虽然1261的补丁能够很好地防御目录穿越写文件的漏洞,但是这个补丁仅仅只能防御框架本身,如果有用户自己使用了destinationFile
并且没有采用补丁的方式进行校验,那么同样会存在目录穿越漏洞。所以这个漏洞的本质原因还是在于生成destinationFile
的方式存在问题。
在UnZipTransformer.java
对生成的destinationFile
进行校验。如下:
采用了checkPath(message, zipEntryName)
的方式生成destinationFile
。
在生成destinationFile
进行判断,如果确认没有问题返回destinationFile
,否则认为是目录穿越的漏洞。通过这种方式就能够保证生成的destinationFile
是不存在目录穿越的问题的。