Fastjson 反序列化分析
前言
继续学习Fastjson反序列化
Fastjson的使用
直接上代码 先引入fastjson1.2.24依赖
|
|
创建一个user类
|
|
在name的getter/setter中输出了一个调用方便后续调试
然后写一个测试类test
|
|
看一下test输出的结果
其中JSON.toJSONString(user)
的功能为将类转换为json字符串,并且在转换的同时调用了get方法这是fastjson反序列中一个重要的点,这里先记住后面在解释
接着往下看,看下面三行代码,它们输出结果一致,其功能都为将json字符串转化为一个类,且都会转换为JSONObject
类,但实则他们的具体实现肯定不一样,parse
会转换为@type
指定的类,parseObject
会默认指定JSONObject
类,而在parseObject
参数中加一个类参数则会转换为其指定的类(这里指定Object会自动转化为JSONObject)
|
|
接下来把测试类test中的注释去掉,且将parse和parseObject的参数改为s2,再来看一下运行结果
先来看第一部分,调用了两次get方法这是因为调用了两次toJSONString
,接着看s2的输出结果中带有一个@type
参数,值为user
类,区别在于在toJSONString
中加了一个SerializerFeature.WriteClassName
参数,其会将对象类型一起序列化并且会写入到@type
字段中
第二部分,parse进行反序列化,因此json字符串中有@type
因此会自动执行指定类的set方法,并且会转换为@type
指定类的类型
第三部分,parseObject进行反序列话时会自动执行@type
指定类的get和set方法,并且转换为JSONObject
类
我们来看一下源码就明白了
其实相当于封装了一个parse,先进行了parse然后执行toJSON
并且强制转换为JSONObject
类
其中parse会调用set方法,toJSON会调用get方法
第四部分,虽然我们指定了类为Object
类,但是我们传进去的json字符串中有@type
指定的类导致其会转换为其指定的类,那这样我们指定类岂不是多余?接下来我们直接通过代码调试来看一下这个问题
重新建了一个userTest类,并且将json字符串改为没有加@type
的s1
并且指定类型为我们新建的userTest类,然后输出结果
|
|
可以看到这个正是正常的结果,接下来我们再将s1
改为指定@type
的s2
会抛出异常:类型不匹配,也就是说当传进去带@type
字段的json字符串后并不能够将其转换为指定类
这里为什么会这样?如果有兴趣的师傅可以继续探索
回到一开始的问题,为什么指定了Object
类后输出结果却为@type
指定的类型,直接调试发现了在com.alibaba.fastjson.parser.deserializer.JavaObjectDeserializer#deserialze
中进行了对type的判断也就是一开始传的Object.class,会首先判断是否是类,然后如果是Object.class
和Serializable.class
的话会直接进入到parser.parse(fieldName)
中
继续往下跟进会进入到DefaultJSONParser
中,会提取@type
的值转换为其指定的类,到这里大概就清楚了其原因,这里简单解释一下,有兴趣的师傅可以继续探索
也就是说当我们指定@type
为恶意类时,并且其getter/setter有着一定危害时,就会出现无法预估的危害,重点就在于其会自动执行getter/setter,简单的来解释下原理就是通过反射调用get方法获取值,相应的就是通过反射调用set方法存储值,其中getter自动调用还需要满足以下条件:
- 方法名长度大于4
- 非静态方法
- 以get开头且第四个字母为大写
- 无参数传入
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
setter自动调用需要满足以下条件:
- 方法名长度大于4
- 非静态方法
- 返回值为void或者当前类
- 以set开头且第四个字母为大写
- 参数个数为1个
除此之外Fastjson还有以下功能点:
- 如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用
Feature.SupportNonPublicField
参数 - fastjson 在为类属性寻找getter/setter方法时,调用函数
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略_ -
字符串 - fastjson 在反序列化时,如果Field类型为byte[],将会调用
com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行base64解码,在序列化时也会进行base64编码
漏洞分析
1.2.24
在这个版本中有两条链子:
- com.sun.rowset.JdbcRowSetImpl
- com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
JdbcRowSetImpl
直接来看一下JdbcRowSetImpl
中的setAutoCommit
函数,当this.conn
为null的时候会进入到this.connect()
中,而this.conn
在构造函数中初始为null
继续跟进可以看见var1.lookup()
经典的JNDI注入,且DataSourceName
可控
因此直接构造以下payload
|
|
这里注意一点,jdk版本需要满足 8u161 < jdk < 8u191
TemplatesImpl
这个链子利用条件比较苛刻,因为要用到的变量都是private的需要在反序列化时加上Feature.SupportNonPublicField
参数
先来看一下TemplatesImpl
的getOutputProperties
方法,它是_outputProperties
的getter方法,在前面讲到过Fastjson的一些其它功能点就是在为类属性调用getter/setter时会调用smartMatch()
忽略掉_ -
字符串,这里还用到了另一个功能点就是因为最后payload为byte[]会进行base64编码,继续往下看这里会去调用newTransformer()
继续跟进,在newTransformerImpl
对象时会进入到getTransletInstance()
中
继续跟进,在getTransletInstance()
中,如果在_name
不等于null且_class
等于null时会进入到defineTransletClasses()
中,这里先继续往下看,其中_transletIndex
为-1,也就是说会对_class
数组中的第一个类进行实例化,并且会强制转换为AbstractTranslet
,接下来来看下class是怎么来的
跟进到defineTransletClasses()
中,通过for循环加载_bytecodes[]
来加载类,也就是说_bytecodes[]
就是我们构造注入的点,其中_tfactory
不为null,并且因为加载完类后会强制类型转换为AbstractTranslet
,也就是说加载的类必须为AbstractTranslet
的子类,这样整条链子就齐了
总结一下TemplatesImpl
链子要满足的点:
- fastjson反序列化时需有
Feature.SupportNonPublicField
参数 _bytecodes[]
需进行base64编码_bytecodes[]
中加载的类需为AbstractTranslet
的子类_name
不为null_tfactory
不为null
payload如下:
|
|
在使用
JSON.parseObject
反序列化时看到很多文章里加了一个config
参数,发现删了这个参数并不影响漏洞的触发,然后看了一下代码,发现在JSON
类中已经自动帮我们加上了这个参数
1.2.25-1.2.41
在此版本中,新增了黑名单和白名单功能
在ParserConfig
中,可以看到黑名单的内容,而且设置了一个autoTypeSupport
用来控制是否可以反序列化,autoTypeSupport
默认为false
且禁止反序列化,为true时会使用checkAutoType
来进行安全检测
接着来看一下checkAutoType
怎么进行拦截的,在autoTypeSupport
开启的情况下先通过白名单进行判断,如果符合的话就进入TypeUtils.loadClass
,然后在通过黑名单进行判断,如果在黑名单中就直接抛出异常
接着继续往下看,从Mapping
中寻找类然后继续从deserializers
中寻找类,这里先不做过多解释继续往下看,如果autoTypeSupport
没有开启的情况下,会对指定的@type
类进行黑白名单判断,然后抛出异常,最后如果autoTypeSupport
开启的情况下,会再一次进行判断然后进入到TypeUtils.loadClass
中
在TypeUtils.loadClass
中,可以看到对[ L ;
进行了处理,而其中在处理L ;
的时候存在了逻辑漏洞,可以在@type
的前后分别加上L ;
来进行绕过
因此构造payload如下:
|
|
1.2.42
在此版本中,将黑名单改为了hashcode,但是在com.alibaba.fastjson.util.TypeUtils#fnv1a_64
中有hashcode的计算方法,然后在checkAutoType
中,使用hashcode对L ;
进行了截取,然后进入到TypeUtils.loadClass
中,也就是说对L ;
进行双写即可绕过
payload如下:
|
|
1.2.43
在此版本中,checkAutoType
对LL
进行了判断,如果类以LL
开头,则直接抛出异常
在TypeUtils.loadClass
中,还对[
进行了处理,因此又可以通过[
来进行绕过,具体可以根据报错抛出的异常来进行构造payload
payload如下:
|
|
该payload在前几个版本也可以使用,影响版本
1.2.25 <= fastjson <= 1.2.43
1.2.44
修复了[
的绕过,在checkAutoType
中进行判断如果类名以[
开始则直接抛出异常
1.2.45
增加了黑名单,存在组件漏洞,需要有mybatis
组件
|
|
payload如下:
|
|
1.2.47
在此版本中可以在不开启autoTypeSupport
的情况下,触发漏洞
|
|
payload如下:
|
|
问题还是在checkAutoType
中,在开启autoTypeSupport
的情况下,代码会走到Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null
来进行判断抛出异常,如果不符合的话会继续往下走从Mapping
和deserializers
中寻找类,如果存在则返回clazz
而在ParserConfig
类初始化时会执行initDeserializers
方法,会向deserializers
中添加许多的类,类似一种缓存,其中会添加这么一个类this.deserializers.put(Class.class, MiscCodec.instance);
进入到MiscCodec
类中,有这么一个方法deserialze
,而在进行json反序列化时会调用这个方法,在方法内会对clazz
进行判断,当类为Class.class
也就是java.lang.Class
类时,会进入到TypeUtils.loadClass
中
在TypeUtils.loadClass
中,如果cache
为true则会将className
放到mapping
中,其中cache
默认为true,className
为传进来的strVal
在deserialze
中,strVal
由objVal
强制转换而来
|
|
而objVal
是在parser.parse()
中截取而来,且参数名必须为val
,否则会抛出异常,也就是说可以通过反序列化往mapping
中添加任何类,这样的话添加com.sun.rowset.JdbcRowSetImpl
类,从而绕过autoTypeSupport
的和黑名单的限制,然后再次传递json去触发JdbcRowSetImpl
的JNDI注入
1.2.48
在MiscCodec
中修改了cache
的默认值,修改为false
,并且对TypeUtils.loadClass
中的mapping.put
做了限制
1.2.68
在1.2.48 - 1.2.68
中还出现了一些黑名单的绕过,这里就不细讲了,在此版本中新增了一个safeMode
功能,如果开启的话,将会直接抛出异常,完全杜绝了autoTypeSupport
的绕过,于此同时还曝出了在不开启safeMode
的前提下,对autoTypeSupport
的绕过
通过expectClass
进行绕过,当传入的expectClass
不在黑名单中后,expectClassFlag
的值为true时,会调用TypeUtils.loadClass
加载类,其中clazz
也就是传进去的另一个类名必须为expectClass
的子类
其中java.lang.AutoCloseable
因为在白名单中,因此可以使用其子类来进行绕过autoTypeSupport
这里稍微总结一下恶意类要满足的条件:
- 恶意类不在黑名单内
- 恶意类的父类(例如
AutoCloseable
)不在黑名单内 - 恶意类不能是抽象类
- 恶意类中的
getter/setter/static block/constructor
能触发恶意操作