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