Java Deserialization vulnerability has been a security buzzword for the past couple of years with almost every applications using native Java serialization framework to be vulnerable. Since its inception, there have been many scattered attempts [1][2][3] to come up with a solution to best address this flaw. In this post I'll be reviewing Java deserialization vulnerability, and explain how Oracle provides a mitigation framework in it's newest JDK versions.
Background
Before going further let's start by reviewing the Java deserialization process. Java Serialization Framework is JDK's built-in utility that allows Java objects to be converted into byte representation of the object and vise versa. The process of converting Java objects into their binary form is called serialization, and the process of reading binary data to construct a Java object is called deserialization. In any enterprise environment the ability to save or retrieve the state of the object, is a critical factor in building reliable distributed systems. For instance, a JMS message may be serialized to stream of bytes and is sent over the wire to a JMS destination. A RESTful client application may serialize the OAuth token to a disk for future verification. Java's Remote Method Invocation (RMI) uses serialization under the hood to pass objects between JVMs. These are some of the use cases that Java serialization is used.
Now let's have a closer look at what actually happens during desrialization process within the JVM. When the application code triggers the deserialization process, ObjectInputStream will be initialized to construct the object from the stream of bytes. ObjectInputStream ensures the object graph that has been serialized is recovered, including the reference objects that client might have serialized, either from socket stream, or form marshaling arguments in a remote systems. During this process, ObjectInputStream matches the stream of bytes against the classes present in the JVM.
Deserialization Overview |
Inspecting the flow
Now let's have a closer look at what actually happens during desrialization process within the JVM. When the application code triggers the deserialization process, ObjectInputStream will be initialized to construct the object from the stream of bytes. ObjectInputStream ensures the object graph that has been serialized is recovered, including the reference objects that client might have serialized, either from socket stream, or form marshaling arguments in a remote systems. During this process, ObjectInputStream matches the stream of bytes against the classes present in the JVM.
Deserialization Workflow |
So, what is the problem?
During deserialization process, when readObject() takes the byte stream to reconstructs the object, it looks for the magic bytes relevant to the object type that has been written in to the serialization stream, to determine what object type e.g. enum, array, String, etc. it needs to resolve the byte stream to. If the byte stream can not be resolved to one of these types, then it will be resolved to ordinary object (TC_OBJECT), and finally the local class for that ObjectStreamClass will be retrieved from the JVM's classpath. If the class is not found then an InvalidClassException will be thrown.
The problem arises when readObject() is presented with a byte stream that has been manipulated to leverage from classes that have a high chance of being available in the JVM's classpath also known as gadget classes and are also vulnerable to Remote Code Execution (RCE). So far a number of classes have been identified to be vulnerable to RCE, however research is still ongoing to discover more of such classes. Now you might ask, how these classes can be used for RCE? well, depending on the nature of the class, the attack can be materialized by constructing the state of that particular class with a malicious payload, which is serialized and can be fed at the point in which serialized data is exchanged i.e. Stream Source in the above workflow. This tricks JDK to believe this is the trusted byte stream, and it will be deserialized by initializing the class with the payload. Depending on the payload this can have disastrous consequences.
Exploit JVM vulnerable classes |
Of course the challenge for the adversary is, to be able to access the stream source for this purpose. I won't go into details of how the attack can be executed in this article however, if you are interested I suggest to review ysoserial which is arguably the best tool for generating payloads for an unsafe desrialization.
How to mitigate against deserialization?
Loosely speaking, mitigation against deserialization vulnerability is to implement the LookAheadObjectInputStream strategy. The implementation needs to subclass the existing ObjectInputStream to override the resolveClass() method to verify if the class is allowed to be loaded. This approach appears to be an effective way of hardening against deserialization, and usually consist of two implementation flavors, whitelist or blacklist. In whitelist approach, implementation only includes the acceptable business classes that are allowed to be deserialized and blocks other classes. Blacklist implementation on the other hand holds a set of well-known classes that are vulnerable and blocks them from being serialized.
Both whitelist and blacklist have their own pros and cons. However whitelist-based implementation proves to be a better way to mitigate against deserialization flaw. It effectively follows the principle of checking the input against the good values which has always been a part of security practices. On the other hand, blacklist-based implementation heavily relies on the intelligence gathered around what classes have been vulnerable and gradually include them in the list which is easy enough to be missed or bypassed.
JDK's new deserialization filtering
Although adhoc implementation exists to harden against deserialization flaw however, the official specification on how to deal with this issue is still lacking. To address this issue, Oracle has introduced a serialization filtering to improve the security of deserialization of data which seems to have incorporated both whitelist and blacklist scenarios. The new deserialization filtering is targeted for JDK 9, however it has been backported to some of the older versions of JDK as well. So, if you are on those versions then you should be able to use the new mitigation mechanism.
The core mechanism of deserialization filtering is based on a ObjectInputFilter interface which provides a configuration capabilities in a way that incoming data streams can be validated during deserailzation process. The status check on the incoming stream is determined by Status.ALLOWED, Status.REJECTED, and Status.UNDECIDED arguments of an enum type within ObjectInputFilter interface. These arguments can be configured depending on the deserialization scenarios, for instance, if the intention is to blacklist a class then the argument will return Status.REJECTED for that specific class and allows the rest to be deserialized by returning the Status.UNDECIDED. On the other hand, if the intention of the scenario is to whitelist, then Status.ALLOWED argument can be returned for classes that match the expected business classes. In addition to that, the filter also allows access to some other information for the incoming deserilizing stream, such as the number of array elements when deserializing an array of class (arrayLength), the depth of each nested objects (depth), the current number of object references (references), and the current number of bytes consumed (streamBytes). This information provides more fine-grained assertion points on the incoming stream and return the relevant status that reflects each specific use cases.
Ways to configure the Filter
JDK 9 filtering supports 3 ways of configuring the filter, custom filter, process-wide filter also known as global filter, and built-in filters for the RMI registry and Distributed Garbage Collection (DGC) usage.
Case-based Filters
The configuration scenario for custom filter occurs when deserialization requirement is different from any other deserialization process throughout the application. In this use case, a custom filer can be created by implementing the ObjectInputFilter interface, and overriding the checkInput(FilterInfo filterInfo) method.
JDK 9 has added two methods to ObjectInputStream class allowing the above filter to be set/get for the current ObjectInputStream:
Contrary to JDK 9, latest JDK 8 (1.8.0_144) seems to only allow filter to be set on ObjectInputFilter.Config.setObjectInputFilter(ois, new VehicleFilter()); at the moment.
Process-wide (Global) Filters
Process-wide filter can be configured by setting jdk.serialFilter as either a system property or a security property. If the system property is defined, then it is used to configure the filter otherwise the filter checks for the security property i.e. jdk1.8.0_144/jre/lib/security/java.security to configure the filter.
The value of jdk.serialFilter is configured as a sequence of patterns, either by checking against the class name, or the limits for incoming byte stream properties. Patterns are separated by semi-colon and whitespace, are also considered to be part of a pattern. Limits are checked before classes, regardless of the order in which the pattern sequence is configured. Below are the limit properties which can be used during the configuration:
The value of jdk.serialFilter is configured as a sequence of patterns, either by checking against the class name, or the limits for incoming byte stream properties. Patterns are separated by semi-colon and whitespace, are also considered to be part of a pattern. Limits are checked before classes, regardless of the order in which the pattern sequence is configured. Below are the limit properties which can be used during the configuration:
- maxdepth=value // the maximum depth of a graph
- maxrefs=value // the maximum number of the internal references
- maxbytes=value // the maximum number of bytes in the input stream
- maxarray=value // the maximum array size allowed
Other patterns match the class or package name as returned by Class.getName(). Class/Package patterns accept asterisk (*), double asterisk (**), period (.), forward slash (/) symbols as well. Below are a couple pattern scenarios that could happen:
- "jdk.serialFilter=org.example.Vehicle;*!" // this matches a specific class and rejects the rest
- "jdk.serialFilter=!org.example.**;!*" // this matches all classes in the package and all subpackages and rejects the rest
- "jdk.serialFilter=org.example.*;!*" // this matches all classes in the package and rejects the rest
- "jdk.serialFilter=*; // this matches any class with the pattern as a prefix
if none of the above filters were matched then the filter returns Status.UNDECIDED.
Built-in Filters
JDK 9 has also introduced additional built-in/configurable filters mainly for RMI Registry and Distributed Garbage Collection (DGC) . Built-in filters for RMI Registry and DGC, white-list classes that are expected to be used in either of these services. Below are classes for both RMIRegistryImpl and DGCImp:
In addition to these classes users can also add their own customized filters using sun.rmi.registry.registryFilter and sun.rmi.transport.dgcFilter system or security properties with the property pattern syntax as described in previous section.