看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
是不存在目录穿越的问题的。