fastjson到底做錯(cuò)了什么?為什么會(huì)被頻繁爆出漏洞?

作者 l Hollis來(lái)源 l Hollis(ID:hollischuang)
fastjson大家一定都不陌生,這是阿里巴巴的開(kāi)源一個(gè)JSON解析庫(kù),通常被用于將Java Bean和JSON 字符串之間進(jìn)行轉(zhuǎn)換。
前段時(shí)間,fastjson被爆出過(guò)多次存在漏洞,很多文章報(bào)道了這件事兒,并且給出了升級(jí)建議。但是作為一個(gè)開(kāi)發(fā)者,我更關(guān)注的是他為什么會(huì)頻繁被爆漏洞?于是我?guī)е苫螅タ戳讼耭astjson的releaseNote以及部分源代碼。最終發(fā)現(xiàn),這其實(shí)和fastjson中的一個(gè)AutoType特性有關(guān)。從2019年7月份發(fā)布的v1.2.59一直到2020年6月份發(fā)布的 v1.2.71 ,每個(gè)版本的升級(jí)中都有關(guān)于AutoType的升級(jí)。下面是fastjson的官方releaseNotes 中,幾次關(guān)于AutoType的重要升級(jí):
1.2.59發(fā)布,增強(qiáng)AutoType打開(kāi)時(shí)的安全性 fastjson1.2.60發(fā)布,增加了AutoType黑名單,修復(fù)拒絕服務(wù)安全問(wèn)題 fastjson1.2.61發(fā)布,增加AutoType安全黑名單 fastjson1.2.62發(fā)布,增加AutoType黑名單、增強(qiáng)日期反序列化和JSONPath fastjson1.2.66發(fā)布,Bug修復(fù)安全加固,并且做安全加固,補(bǔ)充了AutoType黑名單 fastjson1.2.67發(fā)布,Bug修復(fù)安全加固,補(bǔ)充了AutoType黑名單 fastjson1.2.68發(fā)布,支持GEOJSON,補(bǔ)充了AutoType黑名單。(引入一個(gè)safeMode的配置,配置safeMode后,無(wú)論白名單和黑名單,都不支持autoType。) fastjson1.2.69發(fā)布,修復(fù)新發(fā)現(xiàn)高危AutoType開(kāi)關(guān)繞過(guò)安全漏洞,補(bǔ)充了AutoType黑名單 fastjson1.2.70發(fā)布,提升兼容性,補(bǔ)充了AutoType黑名單甚至在fastjson的開(kāi)源庫(kù)中,有一個(gè)Isuue是建議作者提供不帶autoType的版本:
?那么,什么是AutoType?為什么fastjson要引入AutoType?為什么AutoType會(huì)導(dǎo)致安全漏洞呢?本文就來(lái)深入分析一下。AutoType 何方神圣?fastjson的主要功能就是將Java Bean序列化成JSON字符串,這樣得到字符串之后就可以通過(guò)數(shù)據(jù)庫(kù)等方式進(jìn)行持久化了。但是,fastjson在序列化以及反序列化的過(guò)程中并沒(méi)有使用Java自帶的序列化機(jī)制,而是自定義了一套機(jī)制。其實(shí),對(duì)于JSON框架來(lái)說(shuō),想要把一個(gè)Java對(duì)象轉(zhuǎn)換成字符串,可以有兩種選擇:
- 1、基于屬性
- 2、基于setter/getter
class?Store?{
????private?String?name;
????private?Fruit?fruit;
????public?String?getName()?{
????????return?name;
????}
????public?void?setName(String?name)?{
????????this.name?=?name;
????}
????public?Fruit?getFruit()?{
????????return?fruit;
????}
????public?void?setFruit(Fruit?fruit)?{
????????this.fruit?=?fruit;
????}
}
interface?Fruit?{
}
class?Apple?implements?Fruit?{
????private?BigDecimal?price;
????//省略?setter/getter、toString等
}
Store?store?=?new?Store();
store.setName("Hollis");
Apple?apple?=?new?Apple();
apple.setPrice(new?BigDecimal(0.5));
store.setFruit(apple);
String?jsonString?=?JSON.toJSONString(store);
System.out.println("toJSONString?:?"?+?jsonString);
JSON.toJSONString進(jìn)行序列化,可以得到以下JSON內(nèi)容:toJSONString?:?{"fruit":{"price":0.5},"name":"Hollis"}
那么,這個(gè)fruit的類型到底是什么呢,能否反序列化成Apple呢?我們?cè)賮?lái)執(zhí)行以下代碼:Store?newStore?=?JSON.parseObject(jsonString,?Store.class);
System.out.println("parseObject?:?"?+?newStore);
Apple?newApple?=?(Apple)newStore.getFruit();
System.out.println("getFruit?:?"?+?newApple);
toJSONString?:?{"fruit":{"price":0.5},"name":"Hollis"}
parseObject?:?Store{name='Hollis',?fruit={}}
Exception?in?thread?"main"?java.lang.ClassCastException:?com.hollis.lab.fastjson.test.$Proxy0?cannot?be?cast?to?com.hollis.lab.fastjson.test.Apple
at?com.hollis.lab.fastjson.test.FastJsonTest.main(FastJsonTest.java:26)
Fruit?newFruit?=?newStore.getFruit();
System.out.println("getFruit?:?"?+?newFruit);
SerializerFeature.WriteClassName進(jìn)行標(biāo)記,即將上述代碼中的String?jsonString?=?JSON.toJSONString(store);
修改成:String?jsonString?=?JSON.toJSONString(store,SerializerFeature.WriteClassName);
即可,以上代碼,輸出結(jié)果如下: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進(jìn)行標(biāo)記后,JSON字符串中多出了一個(gè)@type字段,標(biāo)注了類對(duì)應(yīng)的原始類型,方便在反序列化的時(shí)候定位到具體類型如上,將序列化后的字符串在反序列化,既可以順利的拿到一個(gè)Apple類型,整體輸出內(nèi)容: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 何錯(cuò)之有?因?yàn)橛辛薬utoType功能,那么fastjson在對(duì)JSON字符串進(jìn)行反序列化的時(shí)候,就會(huì)讀取
@type到內(nèi)容,試圖把JSON內(nèi)容反序列化成這個(gè)對(duì)象,并且會(huì)調(diào)用這個(gè)類的setter方法。那么就可以利用這個(gè)特性,自己構(gòu)造一個(gè)JSON字符串,并且使用@type指定一個(gè)自己想要使用的攻擊類庫(kù)。舉個(gè)例子,黑客比較常用的攻擊類庫(kù)是com.sun.rowset.JdbcRowSetImpl,這是sun官方提供的一個(gè)類庫(kù),這個(gè)類的dataSourceName支持傳入一個(gè)rmi的源,當(dāng)解析這個(gè)uri的時(shí)候,就會(huì)支持rmi遠(yuǎn)程調(diào)用,去指定的rmi地址中去調(diào)用方法。而fastjson在反序列化時(shí)會(huì)調(diào)用目標(biāo)類的setter方法,那么如果黑客在JdbcRowSetImpl的dataSourceName中設(shè)置了一個(gè)想要執(zhí)行的命令,那么就會(huì)導(dǎo)致很嚴(yán)重的后果。如通過(guò)以下方式定一個(gè)JSON串,即可實(shí)現(xiàn)遠(yuǎn)程命令執(zhí)行(在早期版本中,新版本中JdbcRowSetImpl已經(jīng)被加了黑名單){"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
這就是所謂的遠(yuǎn)程命令執(zhí)行漏洞,即利用漏洞入侵到目標(biāo)服務(wù)器,通過(guò)服務(wù)器執(zhí)行命令。在早期的fastjson版本中(v1.2.25 之前),因?yàn)锳utoType是默認(rèn)開(kāi)啟的,并且也沒(méi)有什么限制,可以說(shuō)是裸著的。從v1.2.25開(kāi)始,fastjson默認(rèn)關(guān)閉了autotype支持,并且加入了checkAutotype,加入了黑名單+白名單來(lái)防御autotype開(kāi)啟的情況。但是,也是從這個(gè)時(shí)候開(kāi)始,黑客和fastjson作者之間的博弈就開(kāi)始了。因?yàn)閒astjson默認(rèn)關(guān)閉了autotype支持,并且做了黑白名單的校驗(yàn),所以攻擊方向就轉(zhuǎn)變成了"如何繞過(guò)checkAutotype"。下面就來(lái)細(xì)數(shù)一下各個(gè)版本的fastjson中存在的漏洞以及攻擊原理,由于篇幅限制,這里并不會(huì)講解的特別細(xì)節(jié),如果大家感興趣我后面可以單獨(dú)寫(xiě)一篇文章講講細(xì)節(jié)。下面的內(nèi)容主要是提供一些思路,目的是說(shuō)明寫(xiě)代碼的時(shí)候注意安全性的重要性。繞過(guò)checkAutotype,黑客與fastjson的博弈
在fastjson v1.2.41 之前,在checkAutotype的代碼中,會(huì)先進(jìn)行黑白名單的過(guò)濾,如果要反序列化的類不在黑白名單中,那么才會(huì)對(duì)目標(biāo)類進(jìn)行反序列化。但是在加載的過(guò)程中,fastjson有一段特殊的處理,那就是在具體加載類的時(shí)候會(huì)去掉className前后的L和;,形如Lcom.lang.Thread;。
?而黑白名單又是通過(guò)startWith檢測(cè)的,那么黑客只要在自己想要使用的攻擊類庫(kù)前后加上L和;就可以繞過(guò)黑白名單的檢查了,也不耽誤被fastjson正常加載。如Lcom.sun.rowset.JdbcRowSetImpl;,會(huì)先通過(guò)白名單校驗(yàn),然后fastjson在加載類的時(shí)候會(huì)去掉前后的L和,變成了com.sun.rowset.JdbcRowSetImpl。為了避免被攻擊,在之后的 v1.2.42版本中,在進(jìn)行黑白名單檢測(cè)的時(shí)候,fastjson先判斷目標(biāo)類的類名的前后是不是L和;,如果是的話,就截取掉前后的L和;再進(jìn)行黑白名單的校驗(yàn)。看似解決了問(wèn)題,但是黑客發(fā)現(xiàn)了這個(gè)規(guī)則之后,就在攻擊時(shí)在目標(biāo)類前后雙寫(xiě)LL和;;,這樣再被截取之后還是可以繞過(guò)檢測(cè)。如LLcom.sun.rowset.JdbcRowSetImpl;;魔高一尺,道高一丈。在 v1.2.43中,fastjson這次在黑白名單判斷之前,增加了一個(gè)是否以LL未開(kāi)頭的判斷,如果目標(biāo)類以LL開(kāi)頭,那么就直接拋異常,于是就又短暫的修復(fù)了這個(gè)漏洞。黑客在L和;這里走不通了,于是想辦法從其他地方下手,因?yàn)閒astjson在加載類的時(shí)候,不只對(duì)L和;這樣的類進(jìn)行特殊處理,還對(duì)[也被特殊處理了。同樣的攻擊手段,在目標(biāo)類前面添加[,v1.2.43以前的所有版本又淪陷了。于是,在 v1.2.44版本中,fastjson的作者做了更加嚴(yán)格的要求,只要目標(biāo)類以[開(kāi)頭或者以;結(jié)尾,都直接拋異常。也就解決了 v1.2.43及歷史版本中發(fā)現(xiàn)的bug。在之后的幾個(gè)版本中,黑客的主要的攻擊方式就是繞過(guò)黑名單了,而fastjson也在不斷的完善自己的黑名單。autoType不開(kāi)啟也能被攻擊?
但是好景不長(zhǎng),在升級(jí)到 v1.2.47 版本時(shí),黑客再次找到了辦法來(lái)攻擊。而且這個(gè)攻擊只有在autoType關(guān)閉的時(shí)候才生效。是不是很奇怪,autoType不開(kāi)啟反而會(huì)被攻擊。因?yàn)?strong style="font-size:inherit;color:inherit;">在fastjson中有一個(gè)全局緩存,在類加載的時(shí)候,如果autotype沒(méi)開(kāi)啟,會(huì)先嘗試從緩存中獲取類,如果緩存中有,則直接返回。黑客正是利用這里機(jī)制進(jìn)行了攻擊。黑客先想辦法把一個(gè)類加到緩存中,然后再次執(zhí)行的時(shí)候就可以繞過(guò)黑白名單檢測(cè)了,多么聰明的手段。首先想要把一個(gè)黑名單中的類加到緩存中,需要使用一個(gè)不在黑名單中的類,這個(gè)類就是java.lang.Classjava.lang.Class類對(duì)應(yīng)的deserializer為MiscCodec,反序列化時(shí)會(huì)取json串中的val值并加載這個(gè)val對(duì)應(yīng)的類。
如果fastjson cache為true,就會(huì)緩存這個(gè)val對(duì)應(yīng)的class到全局緩存中
如果再次加載val名稱的類,并且autotype沒(méi)開(kāi)啟,下一步就是會(huì)嘗試從全局緩存中獲取這個(gè)class,進(jìn)而進(jìn)行攻擊。所以,黑客只需要把攻擊類偽裝一下就行了,如下格式:{"@type":?"java.lang.Class","val":?"com.sun.rowset.JdbcRowSetImpl"}
于是在 v1.2.48中,fastjson修復(fù)了這個(gè)bug,在MiscCodec中,處理Class類的地方,設(shè)置了fastjson cache為false,這樣攻擊類就不會(huì)被緩存了,也就不會(huì)被獲取到了。在之后的多個(gè)版本中,黑客與fastjson又繼續(xù)一直都在繞過(guò)黑名單、添加黑名單中進(jìn)行周旋。直到后來(lái),黑客在 v1.2.68之前的版本中又發(fā)現(xiàn)了一個(gè)新的漏洞利用方式。利用異常進(jìn)行攻擊
在fastjson中, 如果,@type 指定的類為 Throwable 的子類,那對(duì)應(yīng)的反序列化處理類就會(huì)使用到 ThrowableDeserializer而在ThrowableDeserializer#deserialze的方法中,當(dāng)有一個(gè)字段的key也是 @type時(shí),就會(huì)把這個(gè) value 當(dāng)做類名,然后進(jìn)行一次 checkAutoType 檢測(cè)。并且指定了expectClass為T(mén)hrowable.class,但是在checkAutoType中,有這樣一約定,那就是如果指定了expectClass ,那么也會(huì)通過(guò)校驗(yàn)。
?因?yàn)閒astjson在反序列化的時(shí)候會(huì)嘗試執(zhí)行里面的getter方法,而Exception類中都有一個(gè)getMessage方法。黑客只需要自定義一個(gè)異常,并且重寫(xiě)其getMessage就達(dá)到了攻擊的目的。這個(gè)漏洞就是6月份全網(wǎng)瘋傳的那個(gè)"嚴(yán)重漏洞",使得很多開(kāi)發(fā)者不得不升級(jí)到新版本。這個(gè)漏洞在 v1.2.69中被修復(fù),主要修復(fù)方式是對(duì)于需要過(guò)濾掉的expectClass進(jìn)行了修改,新增了4個(gè)新的類,并且將原來(lái)的Class類型的判斷修改為hash的判斷。其實(shí),根據(jù)fastjson的官方文檔介紹,即使不升級(jí)到新版,在v1.2.68中也可以規(guī)避掉這個(gè)問(wèn)題,那就是使用safeModeAutoType 安全模式?可以看到,這些漏洞的利用幾乎都是圍繞AutoType來(lái)的,于是,在 v1.2.68版本中,引入了safeMode,配置safeMode后,無(wú)論白名單和黑名單,都不支持autoType,可一定程度上緩解反序列化Gadgets類變種攻擊。設(shè)置了safeMode后,@type 字段不再生效,即當(dāng)解析形如{"@type": "com.java.class"}的JSON串時(shí),將不再反序列化出對(duì)應(yīng)的類。開(kāi)啟safeMode方式如下:
ParserConfig.getGlobalInstance().setSafeMode(true);
如在本文的最開(kāi)始的代碼示例中,使用以上代碼開(kāi)啟safeMode模式,執(zhí)行代碼,會(huì)得到以下異常:Exception?in?thread?"main"?com.alibaba.fastjson.JSONException:?safeMode?not?support?autoType?:?com.hollis.lab.fastjson.test.Apple
at?com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:1244)

后話目前fastjson已經(jīng)發(fā)布到了 v1.2.72版本,歷史版本中存在的已知問(wèn)題在新版本中均已修復(fù)。開(kāi)發(fā)者可以將自己項(xiàng)目中使用的fastjson升級(jí)到最新版,并且如果代碼中不需要用到AutoType的話,可以考慮使用safeMode,但是要評(píng)估下對(duì)歷史代碼的影響。因?yàn)?strong style="font-size:inherit;color:inherit;">fastjson自己定義了序列化工具類,并且使用asm技術(shù)避免反射、使用緩存、并且做了很多算法優(yōu)化等方式,大大提升了序列化及反序列化的效率。之前有網(wǎng)友對(duì)比過(guò):
當(dāng)然,快的同時(shí)也帶來(lái)了一些安全性問(wèn)題,這是不可否認(rèn)的。最后,其實(shí)我還想說(shuō)幾句,雖然fastjson是阿里巴巴開(kāi)源出來(lái)的,但是據(jù)我所知,這個(gè)項(xiàng)目大部分時(shí)間都是其作者溫少一個(gè)人在靠業(yè)余時(shí)間維護(hù)的。知乎上有網(wǎng)友說(shuō):"溫少幾乎憑一己之力撐起了一個(gè)被廣泛使用JSON庫(kù),而其他庫(kù)幾乎都是靠一整個(gè)團(tuán)隊(duì),就憑這一點(diǎn),溫少作為“初心不改的阿里初代開(kāi)源人”,當(dāng)之無(wú)愧。"其實(shí),關(guān)于fastjson漏洞的問(wèn)題,阿里內(nèi)部也有很多人詬病過(guò),但是詬病之后大家更多的是給予理解和包容。fastjson目前是國(guó)產(chǎn)類庫(kù)中比較出名的一個(gè),可以說(shuō)是倍受關(guān)注,所以漸漸成了安全研究的重點(diǎn),所以會(huì)有一些深度的漏洞被發(fā)現(xiàn)。就像溫少自己說(shuō)的那樣:"和發(fā)現(xiàn)漏洞相比,更糟糕的是有漏洞不知道被人利用。及時(shí)發(fā)現(xiàn)漏洞并升級(jí)版本修復(fù)是安全能力的一個(gè)體現(xiàn)。"就在我寫(xiě)這篇文章的時(shí)候,在釘釘上問(wèn)了溫少一個(gè)問(wèn)題,他竟然秒回,這令我很驚訝。因?yàn)槟翘焓侵苣苣┽斸斂梢宰龅矫牖兀@說(shuō)明了什么?他大概率是在利用自己的業(yè)余維護(hù)fastjson吧…最后,知道了fastjson歷史上很多漏洞產(chǎn)生的原因之后,其實(shí)對(duì)我自己來(lái)說(shuō),我是"更加敢用"fastjson了…致敬fastjson!致敬安全研究者!致敬溫少!參考資料:
https://github.com/alibaba/fastjson/releases
https://github.com/alibaba/fastjson/wiki/security_update_20200601
https://paper.seebug.org/1192/
https://mp.weixin.qq.com/s/EXnXCy5NoGIgpFjRGfL3wQ
http://www.lmxspace.com/2019/06/29/FastJson-反序列化學(xué)習(xí)
推薦閱讀:
喜歡我可以給我設(shè)為星標(biāo)哦
好文章,我“在看”
