0x01 前言
最近摆烂了很久,来学习一下fastjson
0x02 Fastjson 简介
Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。
提供两个主要接口来分别实现序列化和反序列化操作。
JSON.toJSONString 将 Java 对象转换为 json 对象,序列化的过程。
JSON.parseObject/JSON.parse 将 json 对象重新变回 Java 对象;反序列化的过程
- 所以可以简单的把 json 理解成是一个字符串。
0x03 代码 demo
1. 序列化代码实现
这里通过 Demo 了解下如何使用 Fastjson 进行序列化和反序列化,以及其中的一些特性之间的区别等等。
首先,pom.xml 里面导入 Fastjson 的依赖,这里先导入 1.2.24 的。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
定义了一个Student类
public class Student {
private String name;
private int age;
public Student() {
System.out.println("构造函数");
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}
然后写序列化的代码,调用 JSON.toJsonString() 来序列化 Student 类对象 :
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
// 最开始的序列化 demopublic class StudentSerialize {
public static void main(String[] args) {
Student student = new Student();
student.setName("Drunkbaby");
// student.setAge(6);
String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonString);
}
}
这个地方,序列化的逻辑我们可以稍微调试看一下。
首先会进到 JSON 这个类,然后进到它的 toJSONString() 的函数里面,new 了一个 SerializeWriter 对象。我们的序列化这一步在这里就已经是完成了。
在进到 JSON 这个类里面的时候多出了个 static 的变量,写着 “members of JSON”,这里要特别注意一个值 DEFAULT_TYPE_KEY 为 “@type”,这个挺重要的。
里面定义了一些初值,赋值给 out 变量,这个 out 变量后续作为 JSONSerializer 类构造的参数。
继续往下面走,就是显示的部分了,toString() 方法,最后的运行结果。
很明显这句语句是关键的。
String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
我们关注于它的参数
第一个参数是 student,是一个对象,就不多说了;
第二个参数是 SerializerFeature.WriteClassName,是 JSON.toJSONString() 中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type 可以指定反序列化的类,并且调用其 getter/setter/is 方法。
- Fastjson 接受的 JSON 可以通过@type字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作。
输出如下:
// 设置了SerializerFeature.WriteClassName
构造函数
setName
setAge
getAge
getName
{"@type":"org.example.Student","age":6,"name":"John"}
// 未设置SerializerFeature.WriteClassName
构造函数
setName
setAge
getAge
getName
{"age":6,"name":"John"}
2. 反序列化代码实现
调用 JSON.parseObject(),代码如下
FastjsonEzPoc.java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class FastjsonEasyPoc {
public static void main(String[] args){
String jsonString ="{\"@type\":\"Student\",\"age\":18,\"name\":\"ttt\"}";
JSONObject obj = JSON.parseObject(jsonString);
System.out.println(obj);
}
}
运行结果如图
至此,代码 demo 结束
3.调试分析
在JSON.parseObjct(jsontoString)打个断点,我们看看这些方法是怎么调用的
parse分析
getDeserialize分析
进入parseObject,首先通过parse去解析text,返回一个Object的obj,然后返回JSONObject类型的JSON.toJSON(obj)。
这个JSONObject实际上是一个Map
进入parse(text),来到DefaultJSONParser这里,会采用默认的JSONParser解析器来解析text
得到parser后继续通过parse.parse来解析,核心逻辑全在这里
跟进parse.parse,首先会进行lexer的赋值,然后通过switch来判断lexer.token的值
当前token值为12,对应左大括号
所以会进入到case LBRACE这里,new JSONObject首先新建了一个Map赋值为object,然后作为参数进入parseObject
跟进parseObejct,先判断lexer.token的值,都不满足
进入for这里是个死循环,继续走到ParseContext context = this.context这里,进行简单的赋值。AllowArbitraryCommas表示遇到多个逗号会跳过,进行往下走,会判断ch为,或者:之类的
我们的ch为冒号,所以会走到这里。首先通过"为标志取出key为@type,lexer.skipWhitespace()是跳过空格,然后继续取出ch进行判断
接着判断!isObjectKey后进入,寻找下一个元素跳过空格,然后继续取出当前元素。
重点在loadClass这里,先判断key是否等于@type,然后取出要加载的类名typename,接着使用loadClass进行加载
跟进loadClass,先判断className是否为空,然后mappings.get(className)会先从缓存中寻找该类
addBaseClassMappings会预先放一堆类到mappings里面,当然这里肯定是加载不到的
继续往下走,className.charAt(0)从类名中取第一个字符,如果是[的话就使用数组的方式来加载,这里肯定不是了。
第二个if也是进行判断,如果是L字母开头并且是;结尾的,会把L和;去掉然后加载,后面的绕过中有个地方就用到这个特性
走到contextClassLoader处,通过Thread.currentThread().getContextClassLoader()获取AppClassLoader。
接着使用AppClassLoader来加载Student类,将它放到缓存里面,然后返回clazz。
退出loadclass后往下走到,先判断token这类的东西,没有什么影响。走到object.size()这里,每加载完一轮key:value之后都会往object里面放,现在还没有放所以size为0。
接下来这个就比较重要了,上面的过程还是在json字符解析的操作阶段,接下来就要安装java的方法进行反序列化(其实和java那个原生的反序列化也不一样)。通过config.getDeserializer(clazz)获取反序列化器,然后使用这个反序列化器来反序列化
跟进getDeserializer,也是先从缓存中寻找反序列化器,构造方法在构造时会先把系统的一些类放入缓存中,当然这里肯定也是找不到的
继续往下,判断Student类是否为Class的实例
跟进getDeserializer,前面还是从缓存中寻找Student类
走到这里,clazz.getAnnotation(JSONType.class)会clazz中获取注解,类似于自己写个反序列化器,Student类也是没有
走到for这里,有个黑名单java.lang.Thread,设计的目的也许时线程会影响性能,所以className不能为这个
接着如果className是java.awt开头的话,就使用下面的反序列化器
走到这里判断clazz是否是数字、数组之类的,会进行很多类似的判断,我们都不符合
最后系统会默认把你当成一个JavaBean来创建反序列化器
跟进这个createJavaBeanDeserializer,这里有个字段asmEnable,是java底层动态创建类、动态加载的一个记录,默认值为true
前面会经过一些无关紧要的判断就不说了,asmEnable这个开关在某些时候也是会关闭的。比如下面Student的标识符不是public的话就会关闭,也就是asmEnable为flase
如果泛型的长度不为0,也是会关闭的,不过这里是0
直接走到这里,这个函数很重要。在创建对应的反序列化器时,它要把你这个类的东西进行了解,比如setter、getter、filed、构造函数之类的,通过这个build函数组成一个javaBeaninfo
我们看一下这个函数怎么实现的,首先它获取Student的所有字段和所有方法,获取默认的构造方法,实际上就是我们的无参构造
然后进行一些判断,如果没有无参构造函数.........就直接跳过
走到这里,它把这个构造函数设置为能访问
走到三个for这里,比较重要。第一个for是遍历setter、第三个for是遍历getter
第一个methodname是getname,如果长度大于4就跳过,如果getname是修饰符是静态的话也跳过
如果返回类型不是void.type也跳过,setter一般返回类型都是void,我们当前是getname返回类型肯定不是void,所以直接跳过到下一个
下一个是setName,前面说的条件都满足
往下走到这里,如果不是set开头的话也会跳过
上面都满足后就会来到这里,不管你的方法是大写还是小写,都会将它转为小写
接着获取当前field的所有信息,然后一些不重要的判断
一路走到最后add这里
跟进这个FieldInfo,前面是一些赋值、然后进行判断、设置method与field允许访问,重点在getonly这里,默认值是flase
首先判断你的参数类型是否为1,为1则进入if。我们的目的是让它进入else,将getOnly改成true,这个后面再说原因。当然这里也是没成功,进入了if
后面则是一些泛型的判断,也是进不去的,直接返回。这里主要的是这个getOnly,后面会有用
回到外面add会将其放入fieldList表单里面,继续进行for (Method method : methods)的循环,都差不多就不看了,直接进入到下一个for
clazz.getFields()会获取所有public字段,实际上都是private,所以直接进入下一个for遍历getter
它跟前面遍历setter的逻辑有点不一样,前面还是一样的。看看不一样的地方,这个if是要求你的返回值为Collection、Map、AtomicBoolean、AtomicInteger、AtomicLong几种才会进入里面的add
同时要求fieldList这个表单里面没有setter,不然会跳过。也就是只能有getter,不能有setter,而且getter返回值还有符合类型,才能进入add
当前肯定是不满足条件的,直接出来了。最后就是到这进入JavaBeanInfo,可以跟进去看看,没什么特殊的逻辑,就是把该传的传进去
完成JavaBeanInfo.build之后得到beanInfo,在后面还有一些机会把asmEnable这个开关给关了,我们来看看
第一个if如果beanInfo的字段长度大于200,开关会关闭
第二个if如果默认构造函数为空,并且类不是接口,开关会关闭
接着会遍历你的字段,第一个if如果字段中有人是getOnly的话,开关会关闭,后面几个if也是可以关闭开关,不管都进不去了
那么这个开关有什么用呢?一直走到这里看看,如果开关说关闭的,就会new一个系统内置的JavaBeanDeserializer
当前我们是true肯定进入不了,那么就会进入try通过asmFactory创建一个临时的JavaBeanDeserializer
这里已经获取到了,叫做FastjsonASMDerializer
那么这个有什么危害呢?我们上面已经获取到反序列化器了,接下面应该通过该反序列化器进行反序列化
但是这个反序列化器是个临时的,代码不在我们这里面。所以当我们一直F7进入deserializer.deserialze时,代码代码是没有变化的,但下面的值是会改变,已经是调试不了了。所以这就是为什么要关闭开关的原因
那我们要怎么修改呢?我们肯定是要在这里修改,修改某成员的getOnly为true,在ParserConfig.java时就会把开关关闭
那就要进入else。但因为setter的参数值是1,setter肯定进不了的。
而且前面第一个for那里判断时如果把setter参数设为0,也进不来add这一步
所以只能在getter改,但getter返回类型需要满足条件,为其中四种才能进入add
所以直接在Student类那里新建一个getmap就好了,这样就能使用系统默认的反序列化器
我们再来调试一下,当前是getMap
跟进new FieldInfo()
顺利进入else将getOnly设置为true
一直回到ParserConfig这里,当遍历到map的时候,就关掉了开关
后面就能使用javaBeanDeserialize,也就能调试了
一路F8,获取到反序列化器后,就使用该反序列化器来调deserialize方法
deserialize方法
在上面F7一直跟进,前面都是简单的赋值之类的操作,这段代码是遍历Field
后面的一些判断就比较复杂,是根据json字符串走的,我们按照默认的分支看看会发生什么就行了
跟进createInstance,如果是接口的话就先获取默认加载器,然后创建动态代理,这里肯定不是跳过
走到这里,获取默认构造方法
最后在下面的else进行调用,很容易忽略
一路走出来回到这里
上面调用完构造函数,后面就是给它赋值了。赋值的话一般是通过反射或者setter赋值,实际上这里是setter赋值,这里跟进看一下
实际上是通过上面获取setAge方法,然后在这里通过invoke调用
setName也是一样,这里就不细看了,setter方法就是在这里调用的。那么getter方法又在哪里调用呢?
tojson调用
一路F8出来到tojson那里跟进
前面都是一些赋值和对Field的操作,可以自己看看,走到这里
跟进getFieldValuesMap,首先获取第一个getter,就是getAge,然后把它放到map里面
getter方法就是在getPropertyValue调用的
在fieldInfo.get(object)完成调用
还有一种如果符合特殊形式的话,也会调用。可以看到调用了两次getMap,map、properties之类的都行
触发点在这,代码可以自己跟一跟
那么如果某个类的setter或者getter里面有危险方法,不就可以直接完成攻击了吗,类似于我这里改一改
getmap里面有危险方法
0x04 另外一些基础
0x05 fastjson 反序列化漏洞原理
fastjson 在反序列化的时候会去找我们在 @type 中规定的类是哪个类,然后在反序列化的时候会自动调用这些 setter 与 getter 方法的调用,注意!并不是所有的 setter 和 getter 方法。
下面直接引用结论,Fastjson会对满足下列要求的setter/getter方法进行调用:
满足条件的setter:
非静态函数
返回类型为void或当前类
参数个数为1个
满足条件的getter:
非静态方法
无参数
返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
1. 漏洞原理
由前面知道,Fastjson是自己实现的一套序列化和反序列化机制,不是用的Java原生的序列化和反序列化机制。无论是哪个版本,Fastjson反序列化漏洞的原理都是一样的,只不过不同版本是针对不同的黑名单或者利用不同利用链来进行绕过利用而已。
通过Fastjson反序列化漏洞,攻击者可以传入一个恶意构造的JSON内容,程序对其进行反序列化后得到恶意类并执行了恶意类中的恶意函数,进而导致代码执行。
那么如何才能够反序列化出恶意类呢?
由前面demo知道,Fastjson使用parseObject()/parse()进行反序列化的时候可以指定类型。如果指定的类型太大,包含太多子类,就有利用空间了。例如,如果指定类型为Object或JSONObject,则可以反序列化出来任意类。例如代码写Object o = JSON.parseObject(poc,Object.class)就可以反序列化出Object类或其任意子类,而Object又是任意类的父类,所以就可以反序列化出所有类。
接着,如何才能触发反序列化得到的恶意类中的恶意函数呢?
由前面知道,在某些情况下进行反序列化时会将反序列化得到的类的构造函数、getter方法、setter方法执行一遍,如果这三种方法中存在危险操作,则可能导致反序列化漏洞的存在。换句话说,就是攻击者传入要进行反序列化的类中的构造函数、getter方法、setter方法中要存在漏洞才能触发。
我们到DefaultJSONParser.parseObject(Map object, Object fieldName)中看下,JSON中以@type形式传入的类的时候,调用deserializer.deserialize()处理该类,并去调用这个类的setter和getter方法:
public final Object parseObject(final Map object, Object fieldName) {
...
// JSON.DEFAULT_TYPE_KEY即@type
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
...
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
分析在上面调试中了
小结一下
若反序列化指定类型的类如Student obj = JSON.parseObject(text, Student.class);,该类本身的构造函数、setter方法、getter方法存在危险操作,则存在Fastjson反序列化漏洞;
若反序列化未指定类型的类如Object obj = JSON.parseObject(text, Object.class);,该若该类的子类的构造方法、setter方法、getter方法存在危险操作,则存在Fastjson反序列化漏洞;
2. PoC 写法
一般的,Fastjson反序列化漏洞的PoC写法如下,@type指定了反序列化得到的类
{
"@type":"xxx.xxx.xxx",
"xxx":"xxx",
...
}
关键是要找出一个特殊的在目标环境中已存在的类,满足如下两个条件:
1. 该类的构造函数、setter方法、getter方法中的某一个存在危险操作,比如造成命令执行;
2. 可以控制该漏洞函数的变量(一般就是该类的属性);
0x06 小结
总结一下漏洞发生在反序列化的点,也就是 Obj.parse 和 Obj.parseObject 这里。必须的是传参要带入 class 的参数,最好带上 Feature.SupportNonPublicField
PoC 是通过 String 传进去的,要以 @type 打头。
漏洞的原因是反序列化的时候去调用了 getter 和 setter 的方法。其余就没什么了,比较简单。