fastjson 看我一命通关
简介
Fastjson是一款由阿里巴巴开发的高性能、功能丰富的 JSON 处理库,用于实现Java 对象和 JSON 字符串的转换。而Fastjson组件提供的反序列化功能,允许用户通过”@type” 字段来指定任意反序列化类名,并自动调用该类的 setter 方法及部分 getter 方法,这就导致当反序列化不可信数据时便可触发远程代码执行。
漏洞分析
fastjson -1.2.24
影响版本:fastjson <= 1.2.24
主要利用方式为:
- JdbcRowSetImpl 结合JNDI注入
1 | { |
- TemplatesImpl 动态加载字节码
1 | { |
- BasicDataSource 不出网利用
1 | { |
具体利用过程就不在赘述了,主要看下其他版本的绕过方式和利用思路。
fastjson -1.2.25
在1.2.25版本中,官方引入了一个安全机制checkAutoType
,主要进行了黑白名单检测,上述利用方式都被加入到了黑名单中
- 影响版本:1.2.25 <= fastjson <= 1.2.41
- autotype:开启
checkAutoType
其中有这三个属性,分别代表是否开启checkAutoType和黑白名单,autoTypeSupport默认为false
接着就看下checkAutoType
具体做了什么,之前的版本中是通过TypeUtils.loadClass直接加载我们传入的类,并通过deserialze进行反序列化
1.2.25开始加入了checkAutoType进行安全检测
开启AutoType
开启autoType的模式下先进行白名单检测如果有就直接加载,没有在进行黑名单检测,不在黑名单的话会从mapping缓存中查找
最后如果还没有则会直接加载
1 | if (autoTypeSupport || expectClass != null) { |
关闭AutoType
关闭的逻辑是先黑再白,如果还是找不到就抛出异常
利用链
JdbcRowSetImpl、TemplatesImpl、BCEL(sun包下的所有内容)都被加入了黑名单,看似无法再使用,但在loadClass的逻辑中发现这么一段代码:
1 | if (className.startsWith("L") && className.endsWith(";")) { |
如果类名以L 开头; 结尾,则会递归的去除该符号,而这就出现了一个逻辑漏洞,com.sun.*
被加入了黑名单,但是Lcom.sun.*;
并没有,利用这个机制就可以绕过黑名单检测
而这需要开启autotype,在上边的逻辑中有提到,如果黑白名单和mapping缓存中都没有该类则会调用如下代码直接加载
1 | if (autoTypeSupport || expectClass != null) { |
若没有开启autotype黑白名单中没有则会直接抛出异常
最终的 payload 在类名前后加上L
和;
即可:
1 | { |
fastjson -1.2.42
- 影响版本:1.2.25 <= fastjson <= 1.2.42
- autotype:开启
自1.2.42开始,checkAutoType做了两个变化:
1、黑名单采用hash形式,防止安全人员对其研究。不过很多都已经被师傅们撞出来了:LeadroyaL/fastjson-blacklist (github.com)

2、在开头部分加了L和;判断
这种方式只是表面上防止了L开头;结尾的情况,但loadClass中的逻辑是递归解析,所以这里双写绕过即可:
1 | { |
fastjson -1.2.43
- 影响版本:1.2.25 <= fastjson <= 1.2.43
- autotype:开启
自1.2.43开始,又对之前的漏洞进行了修复,这次修复的方式为:如果出现LL则直接抛出异常
但loadClass中还有另一段逻辑:
1 | if(className.charAt(0) == '['){ |
如果以[开头,也会进行截断解析,所以便可构造Payload:
1 | { |
但遇到报错:
畸形payload
上边报错希望42行的位置是[,但却是逗号,所以加上[
1 | { |
运行后报错希望43行为{
1 | { |
现在成功运行了
分析下原因,先看下逗号前添加 “[“ 的逻辑
在deserialze中先获取ComponentType型的数据,这个数据就是截取”[“后的实体类,接着调用parseArray进行解析
先获取token,如果值不是14就会抛出刚才的异常
所以问题在于前边解析lexer中token的时候发生的,逻辑就在DefaultJSONParser中,lexer是fastjson中的一个词法解析器,它希望下一次解析内容为JSONToken.COMMA
也就是逗号前的内容
之后如果逗号前的内容是[,就会返回token值14,便不再会抛出异常
逗号前是”{“同理,逻辑在JavaBeanDeserializer#deserialze,可以自行分析下
所以最终的payload为:
1 | { |
但该漏洞在1.2.44修复了,修复方式仍然是黑名单抛异常
fastjson -1.2.47
在 fastjson 不断迭代到 1.2.47 时,爆出了最为严重的漏洞—Cache通杀,可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。
影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport
影响版本:1.2.33 <= fastjson <= 1.2.47
autotype:1.2.32前关闭,1.2.33—1.2.47均可
到此黑名单已经设置的比较严格了,在 autoTypeSupport 为默认的 false 时,程序直接检查黑名单并抛出异常,没有其它的绕过方式,于是思路就转变到了在判断autoTypeSupport 前找调用链
1.2.47
autoTypeSupport 前有两个可以加载类的部分,分别是从mappings缓存和deserializers中获取
先看下deserializers是否可控,它是一个IdentityHashMap对象,对其进行赋值的函数有
getDeserializer()
:这个类用来加载一些特定类,以及有JSONType
注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。initDeserializers()
:无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。putDeserializer()
:被前两个函数调用,我们无法控制入参。
因此就无法通过修改deserializers的方式获取类了,接着看哪里能赋值mappings
,于是找到了TypeUtils的loadClass,当cache为true时就会将传入的className存储到mappings中
而在MiscCodec的deserialze有对应的调用,会把strVal存入mappings中
所以接下来的任务就是:
- 找到某个类调用MiscCodec#deserialze
- 判断strVal是否可控
MiscCodec#deserialze
在fastjson执行反序列化前,会调用getDeserializer()
寻找适合的反序列化器
而反序列化器是从deserializers获取的,deserializers又在初始化中添加了MiscCodec这个类,所以只需要我们传入的类为Class.class即可,即:{"@type":"java.lang.Class","a":"aaa"}
strVal
strVal的值是objVal赋过去的,而objVal的值则是解析json中的val属性的值
现在这样就能将JdbcRowSetImpl类存到mappings中
1 | { |
由于fastjson是循环解析json数据,所以只需要在构造一段触发恶意类的json数据即可,payload:
1 | { |
1.2.32
1.2.32前需要关闭autotype才能执行,原因在于自1.2.33开始,当开启autotype之后,进行黑名单检测时如果检测到黑名单类并不会直接抛出异常,而是还会从mappings缓存中查找,如果都没有才会抛出异常
而在1.2.32中没有该逻辑,如果匹配到黑名单则直接抛出异常
fastjson -1.2.68
影响版本:fastjson <= 1.2.68
autotype:均可
在1.2.47之后,官方把MiscCodec中调用loadClass的地方cache默认设为了false,并且TypeUtils的loadClass也默认设为了false,cache加载恶意类的方式便不再适用
在1.2.68中又提出了一个新的 autoType 绕过思路:利用 expectClass 绕过 checkAutoType()
。
在 checkAutoType()
函数中有这样的逻辑:如果函数有 expectClass
入参,且我们传入的类名是 expectClass
的子类或实现,并且不在黑名单中,就可以通过 checkAutoType()
的安全检测。
1 | if (clazz != null) { |
所以接下来就需要寻找哪里调用checkAutoType()
,并且调用时有expectClass作为入参,最终找到了这两个类
ThrowableDeserializer#deserialze()
JavaBeanDeserializer#deserialze()
ThrowableDeserializer
expectClass入参为Throwable
如果传入的类是Throwable的子类或者实现类,就可以绕过checkAutoType检测,获取class
并且由于是Throwable的子类,在getDeserializer()
获取反序列化器时,也能拿到ThrowableDeserialize反序列化器,最终出发对应的ThrowableDeserializer#deserialze()
gadget
Exception是Throwable的子类
1 | import java.io.IOException; |
poc
1 | { |
JavaBeanDeserializer
在getDeserializer()
获取反序列化器时,如果找不到合适的反序列化器,会调用最后的createJavaBeanDeserializer()
,创建JavaBeanDeserializer进行反序列化
在TypeUtils#addBaseClassMappings(),发现白名单中有个AutoCloseable,经调试他会获取JavaBeanDeserializer反序列化器
而他的expectClass是可控的,就是在经过的ParserConfig#checkAutoType后得到的clazz值,而如果我们传入
1 | { |
得到的expectClass就是java.lang.AutoCloseable
所以就可以构造,实现java.lang.AutoCloseable接口的子类可以绕过autotype反序列化
1 | import java.io.Closeable; |
poc
1 | { |
fastjson在后续版本中将java.lang.Runnable、java.lang.Readable加入了黑名单,经测试这些类也是可以触发的,不过像这种把命令执行写到异常类的处理方式未免有些鸡肋,在实际开发场景中一般不会碰见,并且自1.2.68开始由于加了这段代码,jndi等利用方式也不再适用
1 | if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger |
于是浅蓝师傅提出了写文件拿shell方式,具体可参考
Fastjson 68 commons-io AutoCloseable
fastjson -1.2.80
影响版本:fastjson <= 1.2.80
autotype:均可
1.2.69开始,将AutoCloseable以hash的形式又又又又加入了黑名单,如果使用AutoCloseable,expectClassFlag的值会变为false,若此时默认关闭autotype功能则无法加载继承AutoCloseable的恶意类,这条路便无法走通了
但这次黑名单中并没有将Throwable加入黑名单,所以expectClass入参为Throwable的方式仍然可用,除此外更具突破性的应该是师傅们挖掘的对于特殊依赖的利用链
gadget:
https://github.com/su18/hack-fastjson-1.2.80
https://github.com/knownsec/KCon/blob/master/2022/Hacking%20JSON%E3%80%90KCon2022%E3%80%91.pdf
1.2.83版本彻底将期望类ban掉了,并且在添加mapping缓存时也加了一层autotype判断
- https://github.com/alibaba/fastjson/commit/35db4adad70c32089542f23c272def1ad920a60d
- https://github.com/alibaba/fastjson/commit/8f3410f81cbd437f7c459f8868445d50ad301f15
通过parseObject()触发利用的方式得到了暂时的修复,但通过fastjson 原生反序列化仍存在相应的利用绕过方式
原生反序列化
fastjson < 1.2.49
fastjson中有两个类实现了Serializable接口,分别是JSONArray
和JSONObject
,并且他们都继承于JSON类
而在JSON类中,有一个toString方法,会调用toJSONString(),toJSONString()又会调用指定类的getter方法
但fastjson组件中没有重写readObject()
,所以就要借助其它类的readObject类进行拼接,调用toString直接可以想到现有的链BadAttributeValueExpException,整条调用链也就出来了
1 | BadAttributeValueExpException.readObject() —> |
POC:
1 | public class NativeDeser { |
fastjson ≥ 1.2.49
修复
自1.2.49开始,fastjson实现了自己的readObject()
类,它定义了SecureObjectInputStream()
方法
封装完secIn对象后,会调用到secIn.defaultReadObject();
,会走到JSON类重写的resolveClass()
方法,调用checkAutoType()
方法进行黑名单检测,所以Templates加载字节码的方式就会被check掉
绕过
但这种方式并不是安全的,因为这里的反序列化过程是:
1 | ObjectInputStream -> readObject -> SecureObjectInputStream -> readObject -> resolveClass |
相当于在ObjectInputStream套了个SecureObjectInputStream进行检测
而正常流程应该是生成一个继承ObjectInputStream的类并重写resolveClass,由它来做反序列化的入口,这样才是安全的
1 | 继承类 -> readObject -> resolveClass |
既然这样,就需要考虑如何绕过resolveClass()
解析,了解反序列化流程的话应该知道
readObject0()
的switch语句中,会走到TC_OBJECT
处理对象,调用readOrdinaryObject()
,其中会调用readClassDesc()
读取类描述符,最后调用readNonProxyDesc()
,调用到resolveClass()
,具体流程可以参考之前文章defaultReadObject分析 | S1nJa’s Blog
所以想要不执行resolveClass
就要考虑如何才能不执行readClassDesc()
,而switch
语句中有这么几个分支不会走到该方法TC_NULL
、TC_REFERENCE
、TC_STRING
、TC_LONGSTRING
、TC_EXCEPTION
其中TC_REFERENCE是引用类型,如果一个对象被多次引用,那么只会在第一次出现时进行完整地反序列化,而后面的引用将使用 TC_REFERENCE 进行标记
因此在调用时,往List、set、map类型中添加同样对象即可成功利用,即:
1 | //List |
POC
1 | public class NativeDeser { |