SpringBoot任意文件写入到RCE分析
前言:
在之前研究契约锁的一个漏洞的时候,那篇文章的作者提到了一个知识点是springboot服务是不能通过写webshell来达到rce的,所以当时也在想如果是一个springboot的项目的话有什么办法可以rce~
springboot不能写webshell->rce原因
springboot项目之所以不能写webshell来rce,主要是因为这种项目一般都是会去打包成一个整体的fatjar,也就是把整个项目所依赖的所有第三方jar包也打包进自身的app.jar 中,最后以 java -jar app.jar 形式来运行整个项目,而运行时候的项目的classpath包括app.jar中的BOOT-INF/classes 目录和 BOOT-INF/lib 目录下的所有 jar,所以你是无法在项目运行的时候在classpath中直接写入文件。除此之外,现在的springboot项目多是以RESTful API 接口向外提供服务,所以他很少会动态解析jsp或者其它外部模板文件(后台有相关功能除外),所以直接写webshell的情况一般不会出现
引出问题:
那这时候就会想,如果我现在有一个springboot项目存在一个任意文件写入漏洞,我该如何去升级为rce漏洞
网上由两种常见的方法就是要么通过写linux计划文件,要么就是替换so/dll系统文件进行劫持一些操作系统层面的东西来实现rce,但实际环境因为项目的网络波动性以及权限等问题都不是很好用
但是看了LandGrey师傅的文章,才知道也是有从java层面的利用方法
类装载和类初始化
在分析这个之前,首先我们需要搞清楚一下类装载和类初始化两个操作,在实际生活中大多人会将其统称为类加载,在大多数情况下也并不需要区别对待,但是在一些漏洞利用过程精细的情况下还是需要分清楚的
类装载:
类装载是由jvm的不同classloader包括Bootstrap Classloder、Extention ClassLoader、App ClassLoader 和用户自定义的 Classloder 完成的
他通常是在一个class字节码中引用另一个class时被动触发的,当然也有通过classloader loaderclass和class forname等方式主动触发的
example:
当在一个java程序运行的时候在命令行输入-XX:+TraceClassLoading,可以看一下日志
[Opened **/jre/lib/rt.jar]
//Opened 操作代表打开指定文件,通常表示第一次读取相关字节码到内存
[Loaded java.lang.Object from **/jre/lib/rt.jar]
//Loaded 操作代表将读取的指定类的字节码进行装载
[Loaded java.io.Serializable from **/jre/lib/rt.jar]
[Loaded java.lang.Comparable from **/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from **/jre/lib/rt.jar]
......
看LandGrey师傅的文章,在其中师傅仔细跟踪了jdk的类装载过程,发现了当经过java.lang.Classloder.defineClass 方法的时候,即读取字节码并定义Class类型后,类装载 Loaded 操作也就是完成了
类初始化:
类初始化是一定发生在类装载之后,当调用类中的静态属性或着静态方法时,就会触发类的初始化
调用 new 关键词、newInstance 方法、Class.forName 等方法时,会主动触发类的初始化(在学反射的时候会了解一些)
可以这样理解就是一个类,在项目运行的时候,必须先获取(也就是装载)后,才能讨论去是否初始化这个类中的属性以及方法,但是一个类被获取之后却不一定会接着就被初始化
漏洞思考:
知道了这两者,我们再来看一下我们今天研究的问题就是写文件rce
- 类初始化是会执行static方法和属性的,同样也可能会执行构造器中的代码
- 而类加载,项目在第一次启动的时候会装载很多类,以及在运行过程中也会因为一些代码第一次执行或者第一次异常报错而触发一些类的装载和初始化操作
- 除此之外就是一些框架和程序员也很喜欢用class.forname(classname)这样的类去初始化类的代码,但是classname也很容易被外部控制
所以说如果能找到一种方法可以控制程序在能写入文件的文件范围内去主动触发初始化恶意的类,就有可能让写文件漏洞变成rce
漏洞分析:
写什么文件?往哪里写?
前言已经讲过了是没办法写文件到classpath目录中去的,那既然写不到应用层的classpath目录,我们是否可以写道系统层的classpath目录,也就是JDK HOME目录下
在LandGrey师傅的文章中,师傅是对类装载的过程进行过一段时间的不断调试分析,发现jvm为了避免一次性加载太多暂时用不到的类,并不会在运行一开始就把所有的JDK HOME目录下的jar包全部装载,它存在"懒加载"的行为,表现在
调试过程中就是相关 jar 文件的 Opened 操作没有在一开始就发生,而是调用到相关类时被触发
这样的话我们就可以想到在jdk home目录下写入jar包,然后主动触发jar文件中的类初始化来达到rce的效果
但是还有一个问题就是我们写入jar又要如何去主动触发,这很难(JDK 在启动后,不会主动寻找 HOME 目录下新增的 jar 文件去尝试加载),但是还有一个方法就是我们去替换一个在jdk home目录中本身就存在的包,但是这个包必须满足
在某一个外部可控的因素下去被程序装载
LandGrey师傅找到了,就是当程序代码中如果没有使用Charset.forName("GBK") 类似的代码,默认就不会加载到 /jre/lib/charsets.jar 文件,反之则会加载
注:
写文件一般会指定文件写入的目录,但是jdk home的目录一般不固定,所以可以提前收集好字典,然后去爆破批量上传
/usr/lib/jvm/java-8-oracle/jre/lib/
/usr/lib/jvm/java-1.8-openjdk/jre/lib/
/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/
...
如何控制类去主动触发类初始化
现在写什么文件?往哪里写?两个问题都解决了,还有一个最难的问题就是该如何去控制能主动初始化恶意方法,这个的话就要取决于项目本身的实际场景了,这里LandGrey师傅总结了六种,我也就跟着学习一下
1.spring原生的场景
在spring-web组件中的org.springframework.web.accept.HeaderContentNegotiationStrategy类中有下面这样一段代码

这个类中只有resolveMediaTypes这一个方法,作用是对于每一次的请求,spring框架都会尝试解析web请求包中的accept头的值,然后设置相应的字符集编码
正常的 Accept 头示例:
Accept: text/plain, */*; q=0.01
可以利用的地方是MediaType.parseMediaTypes(headerValues) 这行代码,他在经过调用之后会到org.springframework.util.MimeTypeUtils 类的 parseMimeTypeInternal 方法中

看一下代码会发现他在解析完accept头格式后,最终用可控的type、subtype 和 parameters 生成 org.springframework.util.MimeType 类型实例
new MimeType(type, subtype, parameters)
跟到有参构造函数中去:

会发现使用checkParameters(attribute, value) 对 parameters 进行解析

然后这时候就看到了Charset.forName(value) 用来主动加载字符集的代码,所以我们只需要替换正常的charsets.jar 后,在请求包的accept添加charset=GBK就可以成功触发恶意代码
2.fastjson(1.2.76)默认配置场景
替换charsets.jar 文件可以绕过 fastjson(1.2.76) autoType 限制
在fastjson(1.2.76)中的com.alibaba.fastjson.parser.ParserConfig类中有一段这样的代码
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
实际的findclass会枚举this.buckets 中保存的 IdentityHashMap<Type, ObjectDeserializer> 类型键值对
public Class findClass(String keyString) {
for(int i = 0; i < this.buckets.length; ++i) {
IdentityHashMap.Entry bucket = this.buckets[i];
if (bucket != null) {
for(IdentityHashMap.Entry entry = bucket; entry != null; entry = entry.next) {
Object key = bucket.key;
if (key instanceof Class) {
Class clazz = (Class)key;
String className = clazz.getName();
if (className.equals(keyString)) {
return clazz;
}
}
}
}
}
return null;
}
巧合的是java.nio.charset.Charset 类名正好在白名单中,所以可以直接返回 clazz
并且更巧的是在 fastjson com.alibaba.fastjson.serializer.MiscCodec 类的后续处理中有以下代码
else if (clazz == Charset.class) {
return Charset.forName(strVal);
}
感慨一下,师傅tql~
使用该方法后最终会调用到jre/lib/rt.jar!/sun/nio/cs/AbstractCharsetProvider.class 类的这段代码、
Class var4 = Class.forName(this.packagePrefix + "." + var9, true, this.getClass().getClassLoader());
Charset var5 = (Charset)var4.newInstance();
this.packagePrefix 值为 sun.nio.cs.ext,而他是charsets.jar 的包名前缀,构造json包去加载可控类
{
"x":{
"@type":"java.nio.charset.Charset",
"val":"GBK"
}
}
这样当你写入文件后,就可以主动触发了
3.jackson 开启 enableDefaultTyping 场景
["sun.nio.cs.ext.IBM33722",{"x":"y"}]
4.jdbc url getConnection 场景
GET /jdbc?url=jdbc:mysql://127.0.0.1:3306/test?statementInterceptors=sun.nio.cs.ext.IBM33722
5.直接 Class forName 场景
6.直接 loadClass newInstance 场景
不足:
但是在分析的过程中我个人认为他还是有一点缺陷的,
- 文件太大,charsets.jar文件的大小大概有3m左右,实际上传环境很难可以正常上传
- 需要针对目标服务jdk版本准备恶意charsets.jar文件,不然在测试过程中很容易影响到正常业务
参考文章:
https://forum.butian.net/share/99

浙公网安备 33010602011771号