According to the CVE, Spring Framework has security restrictions as one serialise an object, but does not have any security restrictions as it deserialise objects from untrusted sources. Therefore, remote attackers only need to bypass some security restrictions from his side as he serialise a malicious Proxy object that contains a java.lang.Runtime class that executes shell commands.
This vulnerability exists because JdkDynamicAopProxy class and Proxy classes are allowed to be serialised and then deserialised at the server side. It is an issue because JdkDynamicAopProxy allows setting a TargetSource that the JdkDynamicAopProxy will call, and Proxy can be used to implement any interface that the server might be expecting. Since the server just blindly deserialise anything it sees, as long as the interface matches, there will not be any complains. And, before the server crashes, the shell command will be running in the background already. The TargetSource can be used by the attacker to point to a BeanFactory that contains a BeanDefinition that contain the shell commands.
The security restriction at the serialisation side is such that DefaultListableBeanFactory has a writeReplace method which will replace itself with a SerializedBeanFactoryReference. It is a restriction because the SerializedBeanFactoryReference already exist, meaning that the attacker cannot modify to add in the malicious Runtime class to the BeanFactory. And if the writeReplace method is not bypassed, anything the attacker has modified in the BeanFactory will just be replaced by the SerializedBeanFactoryReference. (Un)fortunately, the writeReplace method can be easily replaced, or renamed.
In this walkthrough, I will be using pwntester’s proof of concept exploit – SpringBreaker – to demonstrate the execution of arbitrary commands by serialising a java.lang.Runtime class into a file named “proxy.ser” and letting the server deserialise “proxy.ser” which will end up executing the commands stored in java.lang.Runtime.
First, a DefaultListableBeanFactory is created and the writeReplace method is renamed to writeReplaceDisabled.
Then a runtime BeanDefinition is created to contain the Runtime class and another BeanDefinition named payload is created to contain the runtime BeanDefinition with MethodInvokingFactory because the Runtime class need arguments to be passed to it. MethodInvokingFactory is used instead of the factoryMethod, getRuntime. This is because constructorArgumentValues is unserialisable. The payload BeanDefinition is configured to have runtime BeanDefinition as its targetObject, exec as its targetMethod and the command as its arguments. Next, the payload BeanDefinition is put into the BeanFactory.
After that, all the unserialisable fields are set to null so that the program will not complain. The list of unserialisable fields are constructorArgumentValues and methodOverrides in payload and runtime BeanDefinition and autowireCandidateResolver in the BeanFactory.
Next, a TargetSource is created to point to the BeanFactory and it is used as the target source for AdvisedSupport which DefaultAopProxyFactory will be using to create a InvocationHandler. The AdvisedSupport is to implement the interface that the server is expecting, and in this case, the interface is Contact class. The InvocationHandler is then used to create a Proxy that is casted to a Contact object because the server is expecting a Contact object as it deserialise.
Finally, you just need to serialise this object to a file and use it to send it to the server for deserialisation.
Now, a simple server will be used to deserialise the file and run a function from the deserialised object.
And when you try running it, you will see this:
You will encounter exceptions. However, the exception came too late and the Runtime class will already be running before the program crash. And in this case, Leafpad is a substitute of a malicious command or program.
There are several fixes to this vulnerability starting from version 3.0.6. First, “acceptProxyClasses” flag has been added to RemoteInvocationSerializingExporter, which its default is true, and can be set as false to disallow accepting Proxy classes.
This will throw an Exception if the flag is false. This gives the flexibility of choosing if you want to use Proxy classes. If the flag is false, this will provide protection against future attacks that are based around serialisable classes.
Second, DefaultListableBeanFactory is modified such that it can only be deserialised through SerializedBeanFactoryReference which is resolved to an existing bean factory instance on the server side. So that even at server side, the deserialisation process is added with some security restriction and do not deserialise untrusted objects blindly.
Additionally, serialisation ID is now customizable via contextId. This is to reduce the chance of the client guessing the serialisation ID.
Third, RemoteExporter now only uses opaque proxy. This is to prevent access to interfaces like org.springframework.aop.framework.Advised. As it is possible to inject the exploit as a substitute target source through org.springframework.aop.framework.Advised interface of an exported remote service as stated in its patch notes.
Therefore, this vulnerability is fixed by adding the “acceptProxyClasses” flag, making DefaultListableBeanFactory be serialisable only through SerializedBeanFactoryReference and using an opaque proxy.