# FastJSON 反序列化漏洞
首先,在你的项目中引入 fastjson 的依赖。在 Maven 项目中,可以在 pom.xml 文件中添加以下依赖:
<dependency> | |
<groupId>com.alibaba</groupId> | |
<artifactId>fastjson</artifactId> | |
<version>1.2.24</version> | |
</dependency> |
# fastJSON 原理流程
FastJson 对于 JSON 格式字符串、JSON 对象及 JavaBean 之间的相互转换
package Fsat; | |
import com.alibaba.fastjson.JSON; | |
import com.alibaba.fastjson.JSONObject; | |
public class Fastuser { | |
public static void main(String[] args) { | |
// JSON string | |
String jsonStr = "{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}"; | |
JSONObject obj = JSON.parseObject(jsonStr);// 将 JSON 字符串解析为 Java 对象 | |
System.out.println(obj);// 输出 Java 对象 | |
System.out.println(obj.getString("name"));// 输出 name 字段的值 | |
} | |
} |
FsatJSON 还支持将 JSON 字符串解析为 javaBean 就是一种格式就是一个属性有属性具有 getter 和 setter 方法。
JavaBean 在 fastjson 中的使用
fastjson 可以方便地将 JavaBean 序列化为 JSON 字符串,以及将 JSON 字符串反序列化为 JavaBean。
package Fsat; | |
public class User { | |
public String name; | |
public int age; | |
public User() { | |
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; | |
} | |
} | |
package Fsat; | |
import com.alibaba.fastjson.JSON; | |
import com.alibaba.fastjson.JSONObject; | |
public class Fastuser { | |
public static void main(String[] args) { | |
// JSON string | |
String jsonStr = "{\"name\":\"John\",\"age\":30}"; | |
// Java object | |
User user = JSON.parseObject(jsonStr, User.class);// 将 JSON 字符串解析为 User 对象 | |
// 访问 Java 对象的属性 | |
System.out.println(user.getName()); | |
} | |
} | |
结果为: | |
构造函数 | |
我是setName方法 | |
我是setAge方法 | |
我是getName方法 | |
John |
他会去调用 set 给属性赋值,这个写法指定了是 User 类,但是 fastjson 是可以根据你指定的类让他去解析
由于 fastjson 可以根据 JSON 字符串中的 @type 字段来实例化对象
写法就是
package Fsat; | |
import com.alibaba.fastjson.JSON; | |
import com.alibaba.fastjson.JSONObject; | |
public class Fastuser { | |
public static void main(String[] args) { | |
// JSON string | |
String jsonStr = "{\"@type\":\"Fsat.User\",\"name\":\"John\",\"age\":30}"; | |
// Java object | |
JSONObject user = JSON.parseObject(jsonStr);// 将 JSON 字符串解析为 User 对象 | |
// 访问 Java 对象的属性 | |
System.out.println(user); | |
} | |
} | |
/* 结果 | |
构造函数 | |
我是 setName 方法 | |
我是 setAge 方法 | |
我是 getAge 方法 | |
我是 getName 方法 | |
{"name":"John","age":30} | |
他把我们传入的 User 类的构造函数 set,get 方法全部调用了一遍 |
@type 后面 Fsat.User 是我们指定的类,fastjson 就会根据这个类去解析,在这个过程里面他会把 json 字符串按照你传入的类进行解析赋值调用等,因为这个类是客户端可以控制的, fastjson 允许 JSON 数据指定反序列化后的类型,所以我们就可以通过构造包含恶意 @type 字段的 JSON 字符串,来加载并执行任意类中的代码,这个功能就导致的安全问题。
# 调试


这段代码用于将 JSON 字符串解析为 JSONObject 。它首先调用 parse 方法将 JSON 字符串解析为一个 Java 对象,然后检查该对象是否是 JSONObject 的实例,如果是则直接返回,否则调用 toJSON 方法将该对象转换为 JSONObject 并返回。
往下走走到这里他会用他会用 DefaultJSONParser 去对传进来的 JSON 字符串进行解析

再往下走核心代码就在 DefaultJSONParser 里面,解析第一步就是去字符串进行匹配

这段代码是 fastjson 库中用于解析 JSON 数据的一部分。它通过检查 JSONLexer 的当前标记来决定如何处理输入,并将其解析为相应的 Java 对象
当前我们是左大括号标记是 LBRACE (表示对象的左大括号),则创建一个 JSONObject ,解析对象内容并返回。

parseObject,反序列化的核心方法
# 逻辑步骤
- 初始化和验证:
- 获取
lexer(词法解析器)。 - 如果当前的
lexertoken 是JSONToken.NULL,则返回null。 - 如果当前的
lexertoken 是JSONToken.RBRACE,则返回传入的object。
- 获取
- 处理 JSON 对象的开始:
- 如果
lexer的 token 既不是JSONToken.LBRACE也不是JSONToken.COMMA,则抛出异常。 - 初始化
ParseContext,并设置一个标志setContextFlag为false。
- 如果
- 循环解析 JSON 对象中的每个键值对:
- 跳过空白字符。
- 如果启用了
Feature.AllowArbitraryCommas,则跳过连续的逗号。 - 根据当前字符判断并解析键(
key),支持双引号、单引号、无引号、数字、嵌套对象或数组作为键。 - 跳过键后的冒号,并再次跳过空白字符。
- 处理特殊键:
- 如果键为
@type且未禁用Feature.DisableSpecialKeyDetect,则解析类型信息并根据类型创建对象。 - 如果键为
$ref且未禁用Feature.DisableSpecialKeyDetect,则解析引用并返回引用的对象。
- 如果键为
- 设置解析上下文:
- 如果尚未设置上下文,则设置当前解析上下文。
- 解析值:
- 根据键后的字符判断并解析值(
value),支持字符串、数字、数组、嵌套对象等各种类型的值。 - 将解析的键值对添加到
object中。
- 根据键后的字符判断并解析值(
- 处理对象结束:
- 如果遇到右花括号
},则结束对象的解析并返回object。 - 如果遇到逗号
,,则继续解析下一个键值对。 - 如果遇到其他字符,则抛出异常。
- 如果遇到右花括号
走到这个 if 判断

检查键和配置项:
key == JSON.DEFAULT_TYPE_KEY检查当前键是否为@type。!lexer.isEnabled(Feature.DisableSpecialKeyDetect)检查是否启用了特殊键检测功能。如果禁用了,则跳过后续处理。
扫描类型名称:
String typeName = lexer.scanSymbol(symbolTable, '"');从 JSON 中读取@type的值,即类型名称。类型名称是用双引号括起来的字符串。
加载类:
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());根据类型名称加载对应的 Java 类。使用TypeUtils.loadClass方法,通过类加载器config.getDefaultClassLoader()来加载类。
跟到 TypeUtils.loadClass

这段代码定义了一个 loadClass 方法,用于根据类名和类加载器加载类。它首先检查缓存中是否已有该类,如果没有则尝试从给定的类加载器、当前线程的上下文类加载器或系统类加载器中加载类。具体逻辑如下:
-
参数检查:
- 如果类名为空或长度为零,则返回
null。
- 如果类名为空或长度为零,则返回
-
缓存检查:
- 从
mappings缓存中获取类名对应的类,如果存在则直接返回。
- 从
-
处理数组类型:
- 如果类名以
[开头,表示是数组类型,递归调用loadClass方法加载组件类型,并返回该组件类型的数组类。
- 如果类名以
-
处理对象类型:
- 如果类名以
L开头并以;结尾,表示是对象类型,去掉L和;后递归调用loadClass方法加载类。
- 如果类名以
-
尝试使用给定的类加载器加载类:
- 如果提供了类加载器,则尝试使用它加载类,并将加载的类缓存到
mappings中。
- 如果提供了类加载器,则尝试使用它加载类,并将加载的类缓存到
-
尝试使用当前线程的上下文类加载器加载类:
- 如果类加载器加载失败,则尝试使用当前线程的上下文类加载器加载类,并将加载的类缓存到
mappings中。
- 如果类加载器加载失败,则尝试使用当前线程的上下文类加载器加载类,并将加载的类缓存到
-
尝试使用系统类加载器加载类:
- 如果上下文类加载器加载失败,则尝试使用系统类加载器加载类,并将加载的类缓存到
mappings中。
- 如果上下文类加载器加载失败,则尝试使用系统类加载器加载类,并将加载的类缓存到
-
返回加载的类或
null:- 如果所有尝试都失败,则返回
null。
后面的绕过黑名单就因为去除了 L 和;
![image-20240709102540344]()
- 如果所有尝试都失败,则返回
解析器解析完后下一步,走到这里回来后才是我们的 User 类,前面还是对字符串操作

从配置中获取特定类的反序列化器,并使用它进行反序列化。具体逻辑如下:
- 获取反序列化器:
- 调用
config.getDeserializer(clazz)方法,根据类clazz从配置config中获取对应的ObjectDeserializer反序列化器。
- 调用
- 使用反序列化器进行反序列化:
- 调用反序列化器的
deserialze方法,传入当前解析器this、类clazz和字段名fieldName进行反序列化,返回反序列化后的对象。
- 调用反序列化器的
# 1.2.24 利用
FastJSON 和原生反序列化区别
不要需要实现 Serializable,不需要 readObject 而是 setter getter,变量不需要 transient
相同点 sink 反射,动态类加载
