0x01 前言

最近这段时间打算入门fastjson反序列化漏洞篇,fastjson反序列化漏洞近些年也比较火,漏洞多,危害大。无论是实际运用场景还是面试问题都是比较热门的,另外在CTF的java题中也常见。因此,这篇文章就针对于fastjson反序列化漏洞的基础以及原理分析。(ps:不得不说,json库的源码真的好复杂好难看,基本都是边调试边问AI,最后终于模模糊糊走了一遍流程)

0x02 fastjson基础

FastJson是阿里巴巴集团开发的一个高性能的开源的Java JSON 库。它提供了一套强大的功能,包括将 Java 对象与 JSON 数据之间的互相转换、高效的序列化和反序列化、灵活的 JSON 解析和生成等。

Maven项目中导入相关依赖:

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

我导入的是1.2.24版本,最先开始发现漏洞的一个版本。

序列化

序列化常用JSON.toJSONString方法,它可以将对象转换为json格式。

序列化代码:

public class fastjsontest {
public static void main(String[] args){
Demo demo = new Demo();
demo.setName("abc");
demo.setAge(19);
String jsonString = JSON.toJSONString(demo,SerializerFeature.WriteClassName);
System.out.println(jsonString);

运行结果如下:

在序列化的时候调用了gettar方法。SerializerFeature.WriteClassName是FastJson 序列化特性的一个选项,用于在序列化过程中包含类名信息。当设置了这个特性的时候,FastJson生成的Json字符串会添加一个@type字段,该字段的值为对象所属类的全限定名。也正是有这个@type,在Fastjson反序列化的时候,可以指定任意反序列化的类,因为用户输入的json串是可控的,有点类似与后门。

反序列化

漏洞代码为:

package fastjson.test;

import java.util.Properties;

public class DemoTest1 {
private String name;
private int age;
private Properties properties;

public DemoTest1() {
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 Properties getProperties() throws Exception{
System.out.println("getProperties");
Runtime.getRuntime().exec("calc");
return properties;
}
}

这是一个javabean格式的类,在getProperties方法里有一个弹计算器的恶意代码。

反序列化代码:

package fastjson.test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class JsonUnserialize {
public static void main(String[] args){
String jsonString = "{\"@type\":\"fastjson.test.DemoTest1\",\"age\":18,\"name\":\"123\",\"address\":\"china\",\"properties\":{}}";
Object obj = JSON.parseObject(jsonString);
}
}

看运行结果:

我们传入的json串在反序列化的时候会自动调用getter和setter方法。

Fastjson反序列化采用JSON.parseObject()JSON.parse()这两个方法,parseObject:返回 fastjson.JSONObject

parse返回我们本身的类。跟进这两个方法,最终都是会走到这里,

只不过JSON.parseObject()封装的多一点,其实都大同小异。

0x03 fastjson反序列化原理

其实在反序列化演示过程中是能够弹出计算器的。既然能够执行恶意代码,那么必然是有漏洞的。其中有两点必然条件:第一,Fastjson规范有@type的键值对,能够反序列化指定的任意的类。第二,在反序列化会自动调用javabean的set和get方法,这就类似于反序列化的入口,我们可以从这个入口进去寻找链子。

那么现在就有个问题,fastjson反序列化的时候为什么会调用getter和setter方法,再通俗一点来说,为什么会弹出计算器,那只能去扒拉json库的源码看看了。

0x04 fastjson反序列化源码分析

在反序列化函数这里下个断点:

调试,跟进parseObject方法,开头都是互相调的过程,省略,到最后都是会走到

实例化一个默认的Json解析器,跟进去这个解析器,走到它的构造方法里,

取解析器所指向的第一个字符(也就是{),进入下面的if判断,将 lexer 的标记(token)设置为 JSONToken.LBRACE,表示当前标记是左大括号。然后调用默认解析器的parse方法,跟进到这个方法里面。

switch语句判断标识符,实例化一个JSONobject,它是一个map。最后调用一个重要的方法,parseObject方法。

获取json字符串第一个键名为@type。后面有一个对key的判断,进入到这个if分支里。

最后来到反序列化的重要漏洞点。

跟进到getDeserializer类的构造方法里,都是一些if条件判断语句,最后调用createJavaBeanDeserializer方法,跟进

这个方法开头设置一个asmEnable布尔型的标识,然后获取给定类型的注解对象,也就是获取用户自定义的反序列化器类,如果存在自定义反序列化器类并创建成功,且该反序列化器实现了 ObjectDeserializer 接口,则将其返回作为结果。这样可以在反序列化过程中根据注解的设定使用自定义的反序列化逻辑,当然这些都不重要,我们调试漏洞肯定用的都是默认的。下面还是一系列的if判断,最终漏洞点在这里

跟进到这个build方法,前面这一段通过反射获取到类的相关信息,方法,字段等等。最后来到第328行,遍历类中的方法,

这段代码逻辑就是筛选出符合条件的setter方法,看代码需要满足以下几个条件。

1. 方法名长度不小于4(满足javabean规范的方法)
2. 不能是静态方法
3. 返回值是void
4. 传入的参数个数为1
5. 方法名为set打头

满足这些条件之后,把可用的 setter 方法放到 fieldList 里面

遍历完setter方法后,就开始遍历getter方法,条件判断大同小异,逻辑大差不差。getter方法需要满足以下几个条件:

1. 方法名长度不小于4
2. 不能是静态方法
3. 方法名要 get 开头同时第四个字符串要大写
4. 方法返回的类型必须继承自 Collection Map AtomicBoolean AtomicInteger AtomicLong
5. 传入的参数个数需要为 0

符合以上条件的方法会被添加到FieldInfo里。

Properties继承了Hashtable,能够满足条件。最后返回JavaBeanInfo对象。

这个FieldInfo里存放了满足两个条件的属性。一路返回,跟进到deserialze方法。

死循环,迭代处理字段,并且调用它的getter(setter)方法,往下看。又是一大堆的if判断,判断字段的类型,当遍历到properties字段的时候,跟进到parseField方法。

它在方法的最后还调用了parseField方法,那么继续跟进,进入到这个setValue方法

又是判断字段的类型,当前properties继承的Map,进入到这个if分支里。

熟悉的代码,不就像动态代理嘛,当前的object为漏洞测试代码,method为getProperties方法,它会动态调用当前漏洞类的getProperties方法,当前方法有弹出计算器的恶意代码。

弹出计算器了。

到此整个流程分析结束。

0x05 结语

fastjson库的代码还是要多看,跟着动态调试的时候很容易就迷了,比较复杂。对于fastjson反序列化漏洞,首先找到反序列化点,也就是反序列化函数(parseObject),传入的poc,也就是要反序列化的json串必须是@type打头的,另外找到反序列化的入口(也就是反序列化的时候会调用getter和seter方法)入门的话不是很难。