Scorpion Labs Blog

Java Deserialization Gadget Chains

Written by Andrew Lucia | May 22, 2024 5:06:46 PM

Intro

In my short security testing history, Java deserialization vulnerabilities have been prevalent. Whether it was testing RMI ports in networks or readObject calls in web applications, RCE via Java deserialization is a vulnerability that isn't going away soon. But I always find myself relying on the gadget chains others have built and never fully understanding what is happening in a deserialization payload. This was until I took a (SrcIncite) class in November. We learned about these gadget chains and why they worked, even creating our small custom ones. Now, I wish to share what I've learned and inspire confidence in others to develop their own Java deserialization gadget chains. I will walk through the CommonsCollections1 gadget chain from Ysoserial, probably the most well-known tool for Java deserialization attacks. It's best to read this article with some understanding of what Java reflection is. So, I invite you to do a little research into that if you are unfamiliar. It's also important to note that this is a very old payload, so that I will be referring to the code from JDK 1.8.0_60. You will want to ensure your JDK version is less than or equal to 1.8.0_72, as this is where the patching begins. Let's dive in and start with what we mean when discussing serialization in Java.

 

Java Serialization and Deserialization

Serialization in Java commonly refers to converting a Java object into a stream of bytes. Serialization is usually done so the objects can be sent over a network in a wire protocol such as TCP. The serialization must be done such that the stream of bytes can be deserialized and correctly converted back into a Java object. For an object to be serialized, it must implement the java.io.Serializable interface. The java.io package provides classes that perform serialization and deserialization, including the ObjectOutputStream and ObjectInputStream. Both classes offer default methods to serialize (ObjectOutputStream.writeObject()) and deserialize (ObjectInputStream.readObject()) any serializable objects. Java has a default way of serializing and deserializing objects. However, if a developer needs to implement custom serialization or deserialization functionality for their class, they can do so. Developers can write custom writeObject and readObject methods for their serializable class.

import java.io.*

 

public class Paper implements Serializable {

      public String words;

      public int wordcount;

      private void readObject(ObjectInputStream in) {...}

      private void writeObject(ObjectOutputStream out) {...}

}

During deserialization, the ObjectInputStream.readObject() method will call the readObject method of the object it is trying to deserialize. In the above example, the Paper class's readObject method will be called when an ObjectInputStream tries to deserialize a Paper object. The Paper object's readObject method will then perform the actual deserialization of the Paper object. The same is true for writeObject.

 

Abusing Deserialization for RCE

While ObjectInputStreams are incredibly useful, the default behavior of their readObject method is to deserialize any object whose class implements the java.io.Serializable interface arbitrarily. Because of this, any serialized object can be deserialized by this function, including malicious Java objects. If a Java application doesn't enforce an allowlist of allowed serializable classes, then the application could be vulnerable to deserializing a malicious object that performs unwanted actions. A common way to abuse this is to create objects executing code during the deserialization process. Consider the below class:

import java.io.*

import java.lang.*

 

public class ExecutionObject implements Serializable {

      private String command;

      private void readObject(ObjectInputStream in) {

            in.defaultReadObject();      //populates the command field

 

            Runtime.getRuntime().exec(this.command);

      }

      private void writeObject(ObjectOutputStream out) {...}

}

When an ObjectInputStream is deserializing an object from this class, its readObject method will be called just like any other object. Because a class's readObject method is custom to the class, anything can be done in it. This developer decided to have their class's readObject method execute a command using the java.lang.Runtime class. So, when an object from this class is being deserialized, its readObject method will be called by ObjectInputStream, and it will then execute the command in the command field of the object. The defaultReadObject method is called because the object still needs to be serialized to exist and, like many developers, I don't want to write that code myself. Now, naturally, no developer would do this, hopefully. And if they do, feel free to abuse it. However, there are ways to encapsulate many objects, such that their methods call one after the other, leading to RCE. Think of it as Russian dolls. The outermost object will have its readObject method called. That readObject method will then call another method, a method from one of the object's fields. That method will then call another and another, and so on, until an exec call or similar is made.

import java.io.*

import java.lang.*

 

public class ExecutionObject implements Serializable {

      private DollOne doll1;

      private void readObject(ObjectInputStream in) {

            in.defaultReadObject();

 

            doll1.doSomething();

      }

      private void writeObject(ObjectOutputStream out) {...}

}

 

public class DollOne implements Serializable {

      private DollTwo doll2;

      public void doSomething() {

            doll2.doSomethingElse();

      }

}

 

public class DollTwo implements Serializable {

      private String rce;

      public void doSomethingElse() {

            Runtime.getRuntime().exec(rce);

      }

}

In the above example, ExecutionObject's readObject method would be called during deserialization. That readObject method calls doll1.doSomething(). Doll1.doSomething() calls doll2.doSomethingElse(), which executes a command. This is known as a "Gadget Chain," with each object being a link in a chain leading from deserialization to RCE.

 

The CommonsCollections1 gadget chain

There are many known and popular gadget chains you can use for payloads. Some Java packages are trendy in terms of finding objects for gadget chains. One such package is the commons-collections package. This package contains many powerful Java objects that are used in a variety of Java applications today. The package's unique Java objects and popularity make it perfect for Java deserialization RCE payloads. In this post, we will dissect one of the first gadget chains that used objects from the commons-collections package: CommonsCollections1. The code for this payload can be found on the Ysoserial Github here: https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections1.java.

I've also posted the code below.

 

Ysoserial CommonsCollections1 Payload Creation Code:

At the top of the code, the creator has commented on the method calls in the chain. This post aims to help readers understand what each function call does and why they ultimately lead to remote code execution. The post's conclusion will also discuss how you can make more gadget chains yourself.

 

LazyMap

To best explain this chain, I will break it in half and start with the LazyMap.get() method call. A LazyMap is a commons-collections package class that implements a lazy version of a HashMap. Trying to get a value from a HashMap with a key that doesn't exist in the map will throw an error. The LazyMap class solves this by creating a value for the key using a factory object. The factory object is of a Transformer type. Transformers take an object from one class and transform it into a different object from the same class or a different class. This makes transformers great factories because they can be arbitrarily used to create objects based on your key. The Transformer is an interface, so many different transformers implement this interface to do different transformations. To do this, all transformer classes must implement the method Object transform(Object var1). So, when the LazyMap.get() method is called to try and retrieve a value for a key that doesn't exist; the LazyMap will call its factory's transform method to try and create a value for that key using the transformer it was provisioned with. Below is the code for the LazyMap.get() method.

//LazyMap.get()

 

public Object get(Object key) {

      if (!this.map.containsKey(key)) {                             //if the LazyMap doesn’t contain the key

            Object value = this.factory.transform(key);     // try to create a value for this key

            this.map.put(key, value);

            return value;

      }

      return this.map.get(key);

}

 

ChainedTransformer

So, we know why LazyMap.get() calls its transformer's transform method, but now we have to figure out why the creator chose to use a ChainedTransformer in the LazyMap. A ChainedTransformer is a type of transformer that contains an array of other transformers. Its transform function will pass the input object to the first transformer's transform method in the array. That transformer will process the input and spit out some output object. The ChainedTransformer will then use that output object as input for the next transformer in the array, and so on. So, the ChainedTransformer will go through the array, passing the last Transformer.transform() output as the input to the next Transformer.transform() in the array.

public Object transform(Object object) {

      for (int i = 0; i < this.iTransformers.length; ++i) {

            object = this.iTransformers[i].transform(object);

      }

      return object;

}

The CommonsCollections1 code shows that the ChainedTransformer array is provisioned with a ConstantTransformer and three (3) InvokerTransformers.

 

ConstantTransformer

This one is easy. The ConstantTransformer returns the constant it's provisioned with:

public Object transform(Object input) {

      return this.iConstant;

}

So, when its transform function is called, it will simply return a constant. From the code, we can see that the constant it is given is the Class object Runtime.class. That means the Runtime.class object will be returned and used as the input to the next transformer in the chain, an InvokerTransformer.

 

InvokerTransformer

The InvokerTransformer is a mighty class in the commons-collections package. It is provisioned with a method and arguments to that method, and then, in its transform function, it invokes its method on the given object to the transform call. Let's say we have this class "Example" with a hello method that prints the argument it's given.

public class Example {

      public void hello(String arg) {

            System.out.Println(arg);

      }

}

Then, we create an InvokerTransformer and provision it with the Example class's hello method, the method argument types, and the argument itself. The class array with String.class is an array of the argument types for the method we want to call. We only have one argument for the hello method, and it is a String, so our class array will only have one class, which will be the String class. That class array is used to complete the method signature which determines which method to invoke if there were multiple hello methods in the Example class.

//make a new InvokerTransformer with our method and arg

Transformer it = new InvokerTransformer("hello", Class[]{String.class}, “My argument”);

 

//make a new Example object

Example e = new Example();

 

it.transform(e); // e.hello(“My argument”);

We can then call our InvokerTransformer's transform function on an Example object, which will call the hello method of the Example object we give it, passing in the argument as well.

Now that we know what an InvokerTransformer does, we can figure out what the three InvokerTransformers in the ChainedTransformer are doing.

 

Invoker1

Our first InvokerTransformer is provisioned to invoke the getMethod method and is given the arguments getRuntime and an empty class array. The getMethod method is a part of the Class class and takes in the method you wish to get and the parameter types for that method. It will return a Method object representing the method you tried to get.

new InvokerTransformer(

      "getMethod",

      new Class[] { String.class, Class[].class },

      new Object[] { "getRuntime", new Class[0] }

)

Remember, we are still in the ChainedTransformer, which means that the input to our InvokerTransformer.transform function will come from the output of our last transformer. Our last transformer was the ConstantTransformer, and it simply returned the Runtime.class object, which represents the Runtime class. So, our transform call will look like this:

InvokerTransformer.transform(Runtime.class);

Feel free to go back to the InvokerTransformer section above to follow this. When transform is called, it invokes our InvokerTransformer's method on the object passed to transform. So, it will invoke the getMethod method on our Runtime.class object and pass in the arguments "getRuntime" and an empty class array. We pass in an empty class array because the getRuntime method doesn't take in any arguments:

InvokerTransformer.transform(Runtime.class);        //Runtime.class.getMethod(“getRuntime”, new Class[0]);

The transform call will return the getRuntime method from the Runtime class as a Method object. Conceptually, we're doing this to get a Runtime object to execute our commands eventually. We must use the Runtime class's getRuntime method to get a Runtime object, so here we are. To reiterate, the output of the above transform call is a Method object, an object from the Java “Method” class, representing the getRuntime method from the Runtime class.

 

Invoker2

Our next InvokerTransformer will invoke the invoke method if it wasn't confusing enough already. Its arguments will be null and an empty object array. The invoke method is a part of the Method class and takes in an object to invoke the method from and the arguments to the method.

new InvokerTransformer(

      "invoke",

      new Class[] { Object.class, Object[].class },

      new Object[] { null, new Object[0] }

)

From the last InvokerTransformer, the output was a method object representing the getRuntime method. So, our input to the transform call will be that method object.

InvokerTransformer.transform(Method{GetRuntime});

So, our transform call will invoke the getRuntime method on a null object. This is because getRuntime is a static method, so no object is needed. This will return a Runtime object which will be the output of this transform call.

InvokerTransformer.transform(Method{GetRuntime});         //GetRuntime.invoke(null, Object[]);

 

Invoker3

Our last InvokerTransformer will be used to execute our code finally. It will invoke the exec method and pass in our command as the argument, running the command.

new InvokerTransformer(

      "exec",

      new Class[] { String.class },

      execArgs

)

The input to our transform call will be the output from the last InvokerTransformer, which is the Runtime object:

InvokerTransformer.transform(Runtime{});

So, the exec method gets invoked from the Runtime object, and our command is passed to it, executing our code.

InvokerTransformer.transform(Runtime{}); //Runtime.exec(execArgs);

 

AnnotationInvocationHandler and its Proxy

So now we have code execution. We know that if we can call LazyMap.get(), we can execute code on the system, but how do we get there?

From the Ysoserial Github code, we see LazyMap.get() is called by AnnotationInvocationHandler (AIH) invoke. The AIH class is the invocation handler class for annotation interfaces in Java. This means that it implements the behavior for annotation classes. There are many types of InvocationHandler classes in Java, but they are all used for the same thing: to implement methods for interfaces dynamically. InvocationHandlers are used in conjunction with Proxy classes to do this. Proxy classes add functionality for a method in an interface without changing the existing class that implements that interface. They're able to do this for any interface, thanks to InvocationHandlers. InvocationHandlers are responsible for performing the additional functionality and invoking the method of the underlying class.

To create "proxy" classes, the Java JDK has a Proxy class you can use. This class has a static method called newProxyInstance. You can give this method an interface and an InvocationHandler, and it will return a new object of a "$Proxy0" class that implements the interface you gave it. This newly created $Proxy0 class differs from the Java Proxy class. A new .class file will be written dynamically for this class. This new .class file will specify a class, the $Proxy0 class, that implements the interface you gave it.

However, interfaces only show what the method's signature looks like, i.e., what params it takes, what type it returns, etc. It doesn't know what the methods do. This is where the InvocationHandler comes in. The newly created class will rely on the InvocationHandler you gave it to implement the method logic for all of these methods. The new proxy class calls InvocationHandler.invoke() in all methods.

Below is what the dynamically generated new class will look like if you have it implement the Map interface:

public final class $Proxy0 extends Proxy implements Map {

      private static Method m1;

      private static Method m22;

 

      ...

      public $Proxy0(InvocationHandler var1) throws {

            super(var1);

      }

 

      public final boolean isEmpty() throws {

            try {

                  return (Boolean)super.h.invoke(this, m1, (Object[])null);

            } catch (RuntimeException | Error var2) {

                  throw var2;

            } catch (Throwable var3) {

                  throw new UndeclaredThrowableException(var3);

            }

      }

 

      public final boolean containsKey(Object var1) throws {

            try {

                  return (Boolean)super.h.invoke(this, m22, new Object[]{var1});

            } ...

      }

}

As you can see, it has a method definition for all of Map's methods, and it uses InvocationHandler.invoke() to call the logic of those methods.

This is how our LazyMap.get() gets called. The AnnotationInvocationHandler (AIH) class has a field called "memberValues”, a Map type. In the AIH class' invoke method, it calls memberValues.get().

public Object invoke(Object paramObject, Method paramMethod, Object[] paramArrayOfObject) {

      String str = paramMethod.getName();

      ...

      Object object = this.memberValues.get(str);

      ...

}

So, if we set memberValues as our malicious LazyMap, an AIH.invoke() call will call our LazyMap.get() function. To call our AIH.invoke() method, we will make the InvocationHandler for a proxy class so that when any of the proxy class's methods get called, they will call AIH.invoke(). $Proxy0.method() calls AIH.invoke() which calls LazyMap.get().

We're almost done. So far, we've figured out that if we have a $Proxy0 class that uses an AnnotationInvocationHandler, and one of the $Proxy0 class's methods gets called, the subsequent invoke call will kick off our chain of execution from LazyMap.get(). The last step is to find a way for a readObject call to call any of our $Proxy0 class's methods.

 

Back to the AnnotationInvocationHandler

It turns out that the AIH class works perfectly for this, too. Its readObject method contains a call to memberValues.entrySet(). Remember that memberValues is a Map field in the AIH class. So, if we create a proxy that implements the Map interface, we can provision a second AnnotationInvocationHandler that uses our malicious proxy as its memberValues field. We can do this because our proxy will implement all of Map's methods and can, therefore, be cast to a Map and set as memberValues.

AnnotationInvocationHandler.memberValues = (Map)$Proxy0

So, finally, we can create an AnnotationInvocationHandler with a malicious Map proxy as its memberValues field. When this AIH is being deserialized, its readObject method will be called, which looks like this:

private void readObject(ObjectInputStream paramObjectInputStream) {

      paramObjectInputStream.defaultReadObject();

      ...

      for (Map.Entry entry : this.memberValues.entrySet()) {

            ...

      }

      ...

}

Within this readObject call, the memberValues.entrySet() method is called. Because memberValues is our malicious Map proxy, this call looks like this:

this.memberValues.entrySet() == this.(Map)$Proxy0.entrySet()

 

(Map)$Proxy0.entrySet() == (Map)$Proxy0.AnnotationInvocationHandler.invoke()

 

Therefore:

this.memberValues.entrySet()

==

this.(Map)$Proxy0.AnnotationInvocationHandler.invoke()

And we know that invoke call will call LazyMap.get(). LazyMap.get() will call our ChainedTransformer.transform(). Our ChainedTransformer.transform() will call all the transformers within it. The last will run Runtime.exec() and execute code on the target machine. Congratulations, you've made it.

This is how Java deserialization RCE works. A readObject call for a class will have some issues, such that you can force it to call another object's method and another until the code is executed on the system. In this case, one of the issues is that AnnotationInvocationHandler doesn't strongly type its memberValues map. This lack of type enforcement allows us to use a LazyMap and kick off that chain from LazyMap.get(). The issue was patched in JDK 8u72, and the memberValues field must now be a LinkedHashMap. The cool thing is that LazyMaps and Transformers aren't a part of the standard JDK so they couldn't patch those. They are a part of the commons-collections package and, therefore, were fixed separately by the commons-collections team. But developers may still use an old commons-collections package with the unpatched Maps and Transformers. So, if you can find another way to get to LazyMap.get() from readObject in the new JDKs, the LazyMap gadget chain can still be used. And that's what has been done with the CommonsCollections5 payload.

 

CommonsCollections5

Look at the CommonsCollections5 payload gadget chain. Do you notice anything familiar about it?

It uses the same LazyMap.get() method call chain as CommonsCollections1! All the developer did was find a different way to get there from readObject. This payload, therefore, works on JDK versions greater than 8u72. The point is that developers can't and don't patch every link in these chains. They patched the AnnotationInvocationHandler, so we found a different way to continue the chain. This principal is still true today. If you want to create custom gadget chains, you don't necessarily have to create a new one from scratch. Research which links in the chain still work and are unpatched, then reuse them to find new ways to obtain RCE. I hope this post was helpful. Happy hunting!