0x00 前言

本文将缩减调试配图,增加言语概述,对于新手理解本文有一定的难度(新手也不会看本文吧)。

0x01 概述

概念

Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。

提供两个主要接口来分别实现序列化和反序列化操作。

  • JSON.toJSONString 将 Java 对象转换为 json 对象,序列化的过程
  • JSON.parseObject/JSON.parse 将 json 对象重新变回 Java 对象;反序列化的过程

依赖

1
2
3
4
5
<dependency>  
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

0x02 表象

假设服务端要解析一串JSON数据:

image-20250314084705343

对应的Person类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.Map;

public class Person {

public int age;
public String name;

public Person(){
System.out.println("constructor");
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

}

我们会发现执行结果:
image-20250314084844576

显然Fastsjon自动调用了Person类的setter和getter。

只要是动态调用,就可能会存在危害。目前,我们先理一下该反序列化的整个流程,再思考怎么利用这个动态调用。

0x03 调试流程

显而易见,反序列化的过程封装在parseObject方法:

image-20250314085532452

步入,发现parseObject方法本质上还是调用同类的parse方法:

image-20250314085840445

不禁思考,为什么要这么做呢?显而易见,parseobject方法就比parse多了一层壳,那么这层壳又有什么用呢?

强制运行到下一行,发现obj是Person对象,那么这层壳就要求返回值必须是JSONObject对象。

FastJson中的 parse()parseObject() 方法都可以用来将JSON字符串反序列化成Java对象,parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()。所以进行反序列化时的细节区别在于,parse() 会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,而 parseObject() 由于多执行了 JSON.toJSON(obj),所以在处理过程中会调用反序列化目标类的所有 settergetter 方法。

言归正传,步入parse方法,走到这里:

image-20250314090606980

调用默认解析器,显然核心逻辑还在parser.parse方法里。

走到这里:

image-20250314090825013

由于传进来的JSON字符串的第一个字符是左大括号,程序判断解析尚未开始,所以先初始化一个JSONObject对象,再进行解析。

步入parseObject方法:

image-20250314091447642

该for循环需要关注continue、break以及return等关键字。左大括号之后的字符是双引号,下面一行代码直接得到key。

往下面走几步,发现对key进行了判断:

image-20250314092607211

如果key值是@type,则程序会加载对应的类,这里就是加载Person类。

走到这里:

image-20250314092913109

还记得这个Object是什么吗?是JSONObject,其本质上是一个Map,用来存放JSON里的键值对,这里也就是Person类的属性和对应的值。因为本次解析过程,我们尚未解析一个Field,所以该表还是空表。

看看下面两行:

image-20250314093310211

先获取反序列化器,再进行反序列化。先看看该反序列化器的流程:

image-20250314093532897

derializer是一个反序列化器表,在构造方法中,derializer存放了很多类对应的反序列化器。但是由于Person类是我们自己创建的,显然是不在该表的。

一直顺着流程走到了黑名单判断:

image-20250314093941104

该判断与线程有关,与本次调试无关。

然后程序又经过好几次判断,发现怎么都找不到对应的反序列化器,最后判断clazz是JavaBean类型,直接创建JavaBean的反序列化器:

image-20250314094249775

步入,开幕就很值得注意:

image-20250314094428070

asmEnable是什么?先想想asm是什么:

image-20250314094541717

显然asm是一种动态生成和操控class的技术。那么这里的asmEnable又有什么用呢?先留心一下该属性,下面有大用。

接着走到这里:

image-20250314161551886

当前的任务是创建JavaBean的反序列化器,创建该反序列化器需要对应JavaBean的信息,这里把信息封装到JavaBeanInfo类。

步入,注意JavaBeanInfo类的几个属性。

下图是核心流程:
image-20250314162153166

具体流程如何,推荐大家自行读读源码,这段源码不难理解。这里只讲大概流程:

第一个Method的for循环,关注setter方法,并把对应的Field添加到FieldList。

然后是Field的for循环,这里不关注。

第二个Method的for循环,关注getter方法,并把符合如下类型要求的Field添加到FieldList:

image-20250314162703986

这里插一个题外话:

翻开methods数组,里面不仅有Person的方法,还有根对象Object的方法——这些方法是我们所不需要关注的:
image-20250314163000541

所以不妨给断点下一个条件,减轻工作量:

image-20250314163205929

这一段很重要,但是为什么重要现在还不需要知道,先往下走。

最后创建JavaBeanInfo的方法没什么好看的,回到这里:
image-20250314163426861

下面是一系列的判断,涉及到asmEnable的修改。

到这里,稍微留心一下:
image-20250314163545020

如果fieldInfo的getOnly属性为true,则asmEnable为false。

说人话,如果有一个Field只有getter方法,并且属于程序要求的那几个类,则可以让asmEnable=false。

当然不止这一种方法使得asmEnable=false,但是这里特意提到了,显然我们要用到这段逻辑。

下面还是一堆类似的,使得asmEnable=false的判断,但是我们都进入不了。

重点来了:

image-20250314164149676

如果asmEnable=false,则调用JavaBeanDeserializer构造方法,返回的JavaBeanDeserializer对象(也就是JavaBean的反序列化器)有class文件。

但是,但是,如果asmEnable=true,那么会返回一个asmFactory的创建反序列化器的方法,而该反序列化器是没有class文件的,也就是说,该反序列化器是动态生成的。

直白地说,动态的反序列化器没有对应的源码,我们无法进行针对性的调试,更别说攻击了。所以我们需要程序走到调用JavaBeanDeserializer构造方法,也就是asmEanble=false。

上文提及,让asmEnable=false有好几种方法,但是目前可行的,就是getOnly属性。

让我们回到Field.getOnly属性。之前的流程里,似乎没怎么见过该属性。为了遇到该属性,我们可以给Person类加一个private的Map类属性,并如法炮制一个getter方法。

那么在第二个Method的for循环里,add方法就会进行getOnly=true:

image-20250314170628330

image-20250314170659986

这个新的JavaBeanInfo如下:

image-20250314170903842

成功调控asmEnable:

image-20250314171826041

成功返回有class文件的JavaBeanDeserializer对象:

image-20250314171909181

进入反序列化流程:

image-20250314172042862

进来后,一路走到这里:

image-20250314172315650

该对象即是Person对象,但是此时属性尚未赋值。这里进行赋值:
image-20250314172453454

步入,发现了invoke方法:
image-20250314172534765

每一个属性的赋值都会调用setValue方法,然后在setValue方法里进行反射调用。

如果我反序列化一个危险类Test,该类只有一个属于cmd属性的getter方法,该方法有恶意代码,那么invoke就可以执行该恶意代码。例子如下:

image-20250314173002194

image-20250314173249443

小结:

由浅入深,由深归浅。该反序列化的POC实在上就是一段JSON字符串而已。但是现实中显然不可能有这么理想的情况,一个危险类可以直接被反射调用。

今儿只是把这种反序列化的逻辑稍微分析了一下。具体怎么用,待下文讲解。

0x04 参考教程

fastjson反序列化漏洞1-流程分析_哔哩哔哩_bilibili

fastjson - Potat0w0

Java反序列化Fastjson篇01-FastJson基础 | Drunkbaby’s Blog