0x00 前言
浅学一下
0x01 环境配置
Maven
使用Maven导入依赖,如下:
0x02 FastJson的序列化
序列化
FastJson中序列化函数有toJSONString和toJSONBytes,不过这都8重要。在序列化时,会调用对应类的getter。编写一个测试类Tester(注意要有一个默认构造函数):
然后我们进行一个测试:
输出结果如下:
反序列化
在FastJson中,反序列化,主要有两个函数:
- parse
- parseObject
这两者的区别在于,parseObject会在调用完parse后,将其转化为JSONObject,源码如下:
如果没有指定类型,默认都是JSONObject类,只有在序列化内容中使用@type(autoType)或者在parseObject参数中指定对应的类,才能够将其转换为对应的类,测试代码如下:
注意上面,parseObject在以@type反序列化时,需要指定一个Object.class,不然会默认转换为JSONObject,感兴趣可以追踪源码。上文的输出如下图所示:
观察上面的输出,可以发现,parse在反序列化时会调用对应类(需要指定)的setter(还有特殊情况的getter,见下文),而parseObject因为多了一步toJSON,所以除了调用setter外还会调用getter(没指定Object.class的情况下)。
总的来说就是在反序列化过程中,会调用对应类的setter或者getter,这有什么风险呢?很容易想到,当某个类的setter和getter中存在危险函数时,就可以借助反序列化来执行,这就是FastJson反序列化漏洞的原理。
parse调用getter
根据各位师傅(图片选自@…师傅)对fastjson反序列化源代码分析:
可以知道当我们的getter方法满足下面条件时,就会被parse调用:
- 函数名长度大于等于
4
- 非静态方法
- 以
get
开头且第4
个字母为大写 - 无参数
- 返回值类型继承自
Collection
或Map
或AtomicBoolean
或AtomicInteger
或AtomicLong
0x03 TemplatesImpl链
回顾
Template链算是我们的老朋友了,在Java反序列化之CC2 一文中首次登场,不熟悉的可以回过头再看一下(好吧,我自己都忘记了,哈哈
FastJson中的调用
上文说到过FastJson反序列化漏洞,是通过调用类的getter和setter,但是在我们的Template链中我们只分析了入口为newTransform。是的,TemplatesImpl里面恰好就有这么一个getter,他不仅调用了我们的newTransfomer,并且满足上文说的parse需要的getter条件——”getOutputProperties“:
这里的返回类型是Properties,他继承了Hashtable,Hashtable实现了Map接口,所以满足条件。
几处小细节
a)反序列化不存在setter方法的private变量时,需要加上参数Feature.SupportNonPublicField,测试代码如下:
调试后,发现并没有成功赋值:
加上Feature.SupportNonPublicField后,成功赋值,这里就不再展示了。为什么要强调这一点呢,因为我们的Templat链里,好多变量并不存在setter,这也是这条链的缺陷。
b)getOutputProperties对应的成员变量是什么?
你可能会觉得是outputProperties,但是TemplatesImpl里面并没有这个,而只有_outputProperties
。那是他吗?是的,因为我们知道,在变量面前加_
一般是表示其私有的意思,FastJson在处理时,会自动帮我们去除,并找到对应的getter。感兴趣的话,可以自己在FastJson的源码搜索smartMatch
, 还有个属性,可以关闭他:Feature.DisableFieldSmartMatch
。
c)_bytecodes[][]的处理
FastJson在处理byte[]数组时,会进行Base64解码的操作,如下:
所以我们的Payload中就需要Base64编码(其实如果不编码,我都还不知道怎么把字节类型的Payload塞进去ORZ
payload的构造
第一步,构造出恶意类模板,别忘了CC2中提到的点:
- 需要继承AbstractTranslet
第二步,使用Javassist插入恶意方法,然后Base64编码:
第三步,构造恶意JSON数据:
这里要注意和CC链中构造不同的是,对象要写成{}
,数组要写成[]
,所以我们payload中_tfactory(要是一个对象,但是这里是直接置为空就行,我怀疑我之前构造CC2的时候也可以一个基类)和_bytecodes(是一个数组)都有相应的变换。
最后一步,测试,成功弹出熟悉的计算器!
0x04 JdbcRowSetImpl链
分析
这条链非常滴简单,首先定位到com.sun.rowset.JdbcRowSetImpl的setAutoCommit函数:
当this.conn为空时,会调用this.connect(),跟进:
当this.getDataSourceName()不为空时,使用了JNDI的lookup,所以我们可以借此实现JNDI注入,不记得的话,请看:JNDI注入の拙见、高版本JDK下的JNDI注入, getDataSourceName()如下:
所以实际上,只要控制dataSource的值就够了,因为我们的conn默认就是null,并且这里dataSource虽然是private,但是还存在setter(setDataSourceName),所以比template链限制更小,只受制于JNDI。而且这个链的触发点是setAutoCommit,不同于getter,setter在反序列化时无论怎样都是会调用的。
一处小细节
上文说到过,FastJson在反序列化时,实际上时通过调用setter来给成员赋值。我们这里的dataSource存在setter,但是名称为setDataSourceName:
那我们传参是用dataSource,还是dataSourceName呢?答案是,dataSourceName,这样fastjson才能调用到setter,从而实现datasource成员的赋值。(这部分代码貌似都是ASM操作字节码实现的,不知道师傅们是怎么调试的)
同时还要注意autoCommit要是布尔类型。
payload的构造
JNDI的打法,在上文两篇文章都有给出,不想再赘述了(实际上懒),这里仅以VPS的nc监听端口(你也可以用dnslog平台)来判断是否成功:
成功:
0x04 BasicDataSource链(BCEL)
前言
这个链条利用条件比前两个都要低,他不需要开启额外的选项,也不像JNDI一样需要目标出网,但是很可惜的是,在JDK8u251后,其ClassLoader就找不到了。
具体可看:P牛的博客
依赖导入
BasicDataSource在Tomcat8之前,类路径为org.apache.tomcat.dbcp.dbcp.BasicDataSource,Maven导入配置如下:
Tomcat8.0之后,路径为org.apache.tomcat.dbcp.dbcp2.BasicDataSource,Maven的配置如下:
Sink分析
在其 getConnection()中会调用createDataSource:
跟进createDataSource又会调用createConnectionFactory:
继续跟进createConnectionFactory可以看到调用了Class.forName来加载类:
我们在之前的反序列化学习中,似乎已经见过了ClassLoader加载类,而在这里和JDBC中是使用的Class.forName,两者有什么区别呢?其实Class.forName底层也是用过调用ClassLoader来实现,但是ClassLoader只会把类装入JVM内存,而Class.forName不仅会把类装入内存,还会去初始化,此时如果类中存在static静态代码块,则会被触发执行。
这里的参数都是可控的,所以如果有一个类中存在静态的代码块,就能够与之结合来利用……但是师傅们可能会觉得,这不是没用吗,哪里会有这样的类。确实,除非自己构造一个,不然这样的类确实不太可能有。但是我们注意到这里除了可以传入driverClassName外,还可以传入driverClassLoader,而恰好就有这么一个ClassLoader,给了我们加载任意恶意类的机会。
BCEL部分分析
(这里记得换低版本的JDK)在com.sun.org.apache.bcel.internal.util.ClassLoader的loadClass之中,我们可以看到,他会先将BCEL编码解码,并在解码后直接获取字节码,然后将其加载:
因此,如果把BasicDataSource中的this.driverClassLoader指定为com.sun.org.apache.bcel.internal.util.ClassLoader,this.driverClassName指定为恶意类的BCEL码,我们即可实现攻击。其编码解码操作方法如下:
其中字节码的获取可以使用BCEL库的Repository.lookupClass().getBytes(),也可以使用之前CC链之中的Javassist来创建。
小细节
从上文,我们似乎已经可以构造出Payload了,但是我们不要忘了,我们的触发点是getConnection(),很显然,这是一个getter。而我们在上文说过,ParseObject会调用getter(因为他有一个toJson),这自然是没有问题的。但是在Parse下,要触发getter是存在条件的,即:
- 函数名长度大于等于
4
- 非静态方法
- 以
get
开头且第4
个字母为大写 - 无参数
- 返回值类型继承自
Collection
或Map
或AtomicBoolean
或AtomicInteger
或AtomicLong
这里getConnection的返回类型为Connection类型,继承的是Wrapper和AutoCloseable,显然是不符合要求的。那这就意味着Parse下无法利用吗?并不是的,我们将在下文进行讲解,看看大师傅们是如何巧妙地构造出可用Payload。
ParseObject下的Payload
如上文所说,在ParseObject下,我们完全不用担心什么。首先创建一个恶意类:
然后利用Javassist生成字节码并进行BCEL编码:
之后将Payload拼接:
Parse下Payload
如上文所说,这里的getConnection不符合parse调用getter的规则,但是大师傅们给出了巧妙的解决办法:
我们将其抽象的表达为如下的形式:
- 首先是将原payload包上{},放在value的位置,这样反序列化后,就会获得一个JSONObject对象
- 之后再将其包上{},放在key的位置,在Parse中,Key为Json,则会调用其toString方法,而toString会遍历getter,从而实现触发。
0x05 绕过导论
autoTypeSupport选项
在1.2.25后,FastJson针对反序列化进行了修复,添加了autoTypeSupport选项并增加了一个checkAutoType方法。autoTypeSupport默认为false,此时不再支持@type;当为true时,checkAutoType方法会进行黑白名单的校验。使用如下语句可以开启autoTypeSupport:
下面为在DefaultJsonParser(调用栈中的一环)增加的调用ParserConfig类CheckAutoType方法的逻辑:
checkAutoType方法
当为true时,有以下处理流程(ParseConfig类):
可以看到,如果在白名单内,则会直接调用TypeUtils.loadClass加载类,不在白名单则继续黑名单校验,如果在黑名单中,则会抛出异常。查看this.denyList,得到黑名单(V 1.2.25)如下(可以看到com.sun在黑名单里):
如果也通过了黑名单校验,则会使用TypeUtils.loadClass进行类的加载:
TypeUtils.loadClass方法
可以看到,在loadClass中会对[xxx
和Lxxx;
的格式进行截取,然后进行类的加载。那这里会有个什么问题呢?我们注意到,在上文黑名单的校验中,使用的是startsWith()。那么这里很显然就存在一个逻辑问题,如果我们以提到的这两种形式去构造Payload,就可以轻易的绕过黑名单校验。
绕过研究方向
对于此次的修复,有两类绕过的研究方向:
- 绕过autoTypeSupport
- 开启autoTypeSupport选项时,根据loadClass和黑名单校验的startsWith间的逻辑问题,或是黑名单之外的链,来绕过checkAutoType的黑名单
这里,我更加欣赏对autoTypeSupport的绕过,因为这样的Payload限制更少,不需要目标开启autoTypeSupport选项。
0x06 绕过详解
1.2.25-1.2.41
如上文所提到的,在这些版本中,直接使用[xxx
或是Lxxxx;
即可绕过:
这里[xxx
形式的payload可以根据报错信息进行构造:
可以知道,要把逗号换成[
,然后又有新的报错如下:
增加一个{
,成功执行:
所以下次直接构造["xxxx"[{
即可。
1.2.42
这个版本进行了新的修复,将黑名单换为了hash值(这些hash值对应的类已经有人碰撞出来了,可自己搜索一下)的比较:
并且在进入黑白检查前之前增加了一段代码:
这里实际就是对上一个版本的修复,对类名的开头和结尾进行截取,去除L
和;
。但是因为没有对[xxxx
的形式进行修复,所以这里依然可行。并且我们在上文提到过,在TypeUtils.loadClass中同样会进行L
和;
的截取,所以当我双写L;
时,去除一层L;
后hash值依然匹配不上黑名单,从而导致绕过,并且最后在TypeUtils.loadClass再去除一次时,成功加载恶意类。因此,可用的Payload如下:
1.2.43
在1.2.43中针对1.2.42中的双写绕过进行了修复,修复方法也是很粗暴,如果类以LL开头,则直接抛出异常,但还是没有针对[xxx
进行修复 :
双写不能用,但是如上所说,还能使用[xx
,所以此版本还是可被攻击。
1.2.44-1.2.45
在这个版本之中,终于修复了[xxx
:(并且连Lxxx;
这种都不行了)
可以看到[
开头和Lxxx;
都不再能使用,会直接抛出异常。但是又有一条新的链可以实现攻击(类名不在黑名单中)——mybatis链。
mybatis链
mybatis链需要对方存在mybatis的jar。我们在pom.xml中添加:
在org.apache.ibatis.datasource.jndi.JndiDataSourceFactory包中存在setProperties方法,其中会调用JNDI:
当properties参数存在“data_source”时,会调用JNDI,并传入data_source的值。因此,可以构造payload如下:
但是,在1.2.46版本中,把mybatis链加入到了黑名单。
1.2.25-1.2.47
在上文说过,针对1.2.25添加的autoTypeSupport,有两种方式,一种是绕过其黑名单,还有一种是从根本上绕过这个选项的设置。本小节就是利用缓存来从根本上绕过autoTypeSupport,从而做到了通杀,直到在1.2.48中被修复。
在没开启autoTypeSupport时,会跳过上文检查分支,并经过如下代码的处理:
第一个getClassFromMapping从 TypeUtils的Mapping里获取类,第二个deserializers在ParserConfig类的构造函数中会调用initDeserialzers来初始化,可以浅看一下:
留意这里的MiscCodec。在DefaultJsonParser类中,我们可以清晰的看到,在完成ParserConfig获取后,会先获取他的deserialzer,并最后会调用他的deserialze方法
这里我们跟进上面留意的MiscCodec,在其deserial方法中:
可以看到,当类为Class类时,会调用TypeUtile.loadClass,但是这又不是一个恶意类,有什么用呢?我们注意到这里传入的参数为strVal,strVal来自于ObejectVal (”strVal = (String)objVal“),而ObjectVal获取如下:
此处实际上就是获取其val参数的值(其实我自己也有点没看懂这里代码逻辑,以后再详细阐述一下),所以上文的TypeUtile.loadClass最终是加载了Class类中val参数的值,这有什么用呢?在TypeUtile.loadClass中我们可以看到,其在每次加载完后,都会把其加入到mapping之中,类似于缓存一样
这里的cache,默认为true:
所以这就意味着,我们可以利用Class类的val参数给TypeUtile的mapping缓存添加上任意类,而在上文我们说过,当autoTypeSupport未开启时,会跳过检查分支,尝试从mapping获取。因此,我们可以先通过Class类的val参数给mapping加上恶意类,之后就能成功的从mapping中获取到恶意类并直接return。
于是,我们可以构造出如下的Payload:
此payload可以通杀1.2.25-1.2.47下autoTypeSupport为false的情况,但是当此参数为True时,在低版本(本人测试的1.2.25-1.2.32)中会抛出异常,在高版本1.2.47中无论true还是false都可以成功,原因如下:
在高版本中,抛出异常不仅需要在黑名单之中,还需要不在mapping里面,因为我们已经加入到了mapping,所以在高版本中,即使autoTypeSupport为true,依然可以成功利用。
通过阅读源码,最后发现这个逻辑是在1.2.33中开始加入的,也就是说,缓存链(暂时就叫他这个名字吧)在1.2.25-1.2.32中,需要autoTypeSupport为False,在1.2.33-1.2.47中无论autoTypeSupport是什么值,都可以成功利用。
1.2.48-1.2.67
在1.2.48版本中,修改了cache的默认值为false。所以在1.2.48-1.2.67的版本中,有的都是针对黑名单的绕过,有着许多新的链条。
具体可看:mi1k7ea师傅的博客
1.2.68-
在此版本中, fastJson引入了safeMod功能:
当开启safeMod选项时,会直接抛出异常:
所以,这意味着,之前的Payload在safeMode开启时都会失效。目前好像没有披露出什么办法可以做到绕过此选项。
expectClass绕过AutoTypeSupport(<=1.2.68且未开启safeMode——AutoCloseable)
这里提出了一种不同于利用缓存绕过AutoTypeSupport的思路,此处代码以1.2.25为例子,在其他版本中,代码逻辑变化也不大。当我们的@type为AutoCloseable时,因为他就在Mappings中,所以会返回对应类:
当回到DefaultJsonParser时,获取到的是JavaBeanDeserializer:
最后调用JavaBeanDeserializer的deserialze方法,跟进,有如下的代码逻辑:
从代码可以看到,在JavaBeanDeserializer的deserialze中,会去获取第二个key,并key为@type时,会去获取其值,最后尝试将其反序列化。这里的checkAutoType中的typeName即为第二个@type的值,expectClass为AutoCloseable。继续跟进,回到我们的checkAutoType:
因为expectClass不为空,所以会进入这里的检查,这就以为着我们的类不能在黑名单之中,师傅们可能会觉得,说了半天,这不还是没用吗,但是我们先接着往下看:
从代码上可以看出,因为我们的expectClass不为空,所以会调用loadClass直接加载,之后如果被加载类是expectClass(即AutoCloseable)的子类或是存在继承关系就会成功return。若其中有恶意的setter或者getter就可以用来攻击了。
因此,expectClass绕过AutoTypeSupport的关键点如下:
- 第一个@type要指定为AutoCloseable(本菜狗有遍历一下Mappings中的其他类,他们貌似都获取不到JavaBeanDeserializer)
- Payload中有第二个@type
- 指定的类不能在黑名单之中
- 指定的类与AutoCloseable存在关系
- 指定的类中有可以利用的setter或是getter
POC代码如下,恶意类实现了AutoCloseable接口,并在setter里面有恶意操作:
Payload如下:
不过在真实环境中是不存在这样的类的,考虑到文章篇幅已经很长了,这里就先给出有实际Gadget的链接,之后本人准备将再写一篇《高版本FastJson下的利用链分析》,来将一些链条汇总分析。
具体可看:mi1k7ea师傅的博客,AutoCloseable在1.2.69中被加入了黑名单,所以在之后的版本无法实现绕过。
expectClass绕过AutoTypeSupport(<=1.2.82且未开启safeMode——Exception/Error)
在上文我们讲解了利用AutoCloseable类来进行expectClass的绕过,其实在源码中通过正则checkAutoType\((.+?), (?!null).+,
可以看到,还有还有另外一个Deserializer存在往checkAutoType传exceptClass的情况:
如上图所示,在ThrowableDeserializer也同样存在类似代码,同样的是有获取第二个键值的操作,其中传入的exceptClass是Throwable,并且在最后会调用其setter:
所以我们只要在Mappings中找到谁会调用ThrowableDeserializer就行了,在ParserConfig的getDeserializer可以看到有如下的逻辑:
可以看到,只要是Throwable的子类就行,在Mappings里面我们虽然没有直接看到直接继承Throwable的,但是我们发现Exception和Error是继承的Throwable:
而在mappings里,我们可以看到有一大堆的异常类,他们都直接或是间接的继承了Exception和Error,从而继承了Throwable:
所以POC可以如下构造:
Payload如下:
同样的,这里的实际利用链将在之后的文章进行分析。值得一提的是,利用异常类来绕过AutoType的方法,直到1.2.83才被修复。
0x07 参考
- P牛知识星球里大师傅分享的PDF (大家有兴趣也可以加入P牛的知识星球,一次加入,永久免费:https://t.zsxq.com/04bUVbeuf)
- 跳跳糖@Da22le师傅的文章
- @mi1k7ea师傅的博客
- @kingx师傅的博客