Some time ago I detailed PHP Object Injection vulnerabilities and this post will get into details of Java deserialization vulnerabilities. The concept is simple: developers use a feature of the programming language, serialization, to simplify their job, but they are not aware about the risks.
Java deserialization is a vulnerability similar to deserialization vulnerabilities in other programming languages. This class of vulnerabilities came to life in 2006, it become more common and more exploited and it is now part of the OWASP Top 10 2017.
What is deserialization?
In order to understand deserialization (or unserialization), we need to understand first serialization.
Each application deals with data, such as user information (e.g. username, age) and uses it to do different actions: run SQL queries, log them into files (be careful with GDPR) or just display them. Many programming languages offers the possibility to work with objects so developers can group data and methods together in classes.
Serialization is the process of translating the application data (such as objects) into a binary format that can be stored or sent over the network, in order to be reused by the same or by other application, which will deserialize it as a reverse process.
The basic idea is that it is easy to create and reuse objects.
Serialization example
Let’s take a simple example of code to see how serialization works. We will serialize a simple String object.
import java.io.*; public class Serial { public static void main(String[] args) { String name = "Nytro"; String filename = "file.bin"; try { FileOutputStream file = new FileOutputStream(filename); ObjectOutputStream out = new ObjectOutputStream(file); // Serialization of the "name" (String) object // Will be written to "file.bin" out.writeObject(name); out.close(); file.close(); } catch(Exception e) { System.out.println("Exception: " + e.toString()); } } }
We have the following:
- A String (object) “name”, which we will serialize
- A file name where we will write the serialized data (we will use FileOutputStream)
- We call “writeObject” method to serialize the object (using ObjectOutputStream)
- We cleanup
As you can see, serialization is simple. Below is the content of the serialized data, the content of “file.bin” in hexadecimal format:
AC ED 00 05 74 00 05 4e 79 74 72 6f ....t..Nytro
We can see the following:
- Data starts with the binary “AC ED” – this is the “magic number” that identifies serialized data, so all serialized data will start with this value
- Serialization protocol version “00 05”
- We only have a String identified by “74”
- Followed by the length of the string “00 05”
- And, finally, our string
We can save this object on the file system, we can store it in a database, or we can even send it to another system over the network. To reuse it, we just need to deserialize it later, on the same system or on a different system and we should be able to fully reconstruct it. Of course, being a simple String, it’s not a big deal, but it can be any object.
Let’s see now how easy it is to deserialize it:
String name; String filename = "file.bin"; try { FileInputStream file = new FileInputStream(filename); ObjectInputStream out = new ObjectInputStream(file); // Serialization of the "name" (String) object // Will be written to "file.bin" name = (String)out.readObject(); System.out.println(name); out.close(); file.close(); } catch(Exception e) { System.out.println("Exception: " + e.toString()); }
We need the following:
- An empty string to store the reconstructed – deserialized object (name)
- The file name where we can find the serialized data (using FileInputStream)
- We call “readObject” to deserialize the object (using ObjectInputStream) – and convert the Object returned to String
- We cleanup
By running this, we should be able to reconstruct the serialized object.
What can go wrong?
Let’s see what can happen if we want to do something useful with the serialization.
We can execute different actions as soon as the data is read from the serialized object. Let’s see a few theoretical examples of what developers might do during deserialization:
- if we deserialize an “SQLConnection” object (e.g. with a ConnectionString), we can connect to the database
- if we deserialize an “User” object (e.g. with a Username), we can retrieve user information form the database (by running some SQL queries)
- if we deserialize a “LogFile” object (e.g. with Filename and Filecontent) we can restore the previously saved log data
In order to do something useful after deserialization, we need to implement a “readObject” method in the class we deserialize. Let’s take the “LogFile” example.
// Vulnerable class class LogFile implements Serializable { public String filename; public String filecontent; // Function called during deserialization private void readObject(ObjectInputStream in) { System.out.println("readObject from LogFile"); try { // Unserialize data in.defaultReadObject(); System.out.println("File name: " + filename + ", file content: \n" + filecontent); // Do something useful with the data // Restore LogFile, write file content to file name FileWriter file = new FileWriter(filename); BufferedWriter out = new BufferedWriter(file); System.out.println("Restoring log data to file..."); out.write(filecontent); out.close(); file.close(); } catch (Exception e) { System.out.println("Exception: " + e.toString()); } } }
We can see the following:
- implements Serializable – The class has to implement this interface to be serializable
- filename and filecontent – Class variables, which should contain the “LogFile” data
- readObject – The function that will be called during deserialization
- in.defaultReadObject() – Function that performs the default deserialization -> will read the data from the file and set the values to our filename and filecontent variables
- out.write(filecontent) – Our vulnerable class wants to do something useful, and it will restore the log file data (from filecontent) to a file on the disk (from filename)
So, what’s wrong here? A possible use case for this class is the following:
- A user logs in and execute some actions in the application
- The actions will generate a user-specific log file, using this class
- The user has the possibility to download (serialize LogFile) it’s logged data
- The user has the possibility to upload (deserialize LogFile) it’s previously saved data
In order to work easier with serialization, we can use the following class to serialize and deserialize data from files:
class Utils { // Function to serialize an object and write it to a file public static void SerializeToFile(Object obj, String filename) { try { FileOutputStream file = new FileOutputStream(filename); ObjectOutputStream out = new ObjectOutputStream(file); // Serialization of the object to file System.out.println("Serializing " + obj.toString() + " to " + filename); out.writeObject(obj); out.close(); file.close(); } catch(Exception e) { System.out.println("Exception: " + e.toString()); } } // Function to deserialize an object from a file public static Object DeserializeFromFile(String filename) { Object obj = new Object(); try { FileInputStream file = new FileInputStream(filename); ObjectInputStream in = new ObjectInputStream(file); // Deserialization of the object to file System.out.println("Deserializing from " + filename); obj = in.readObject(); in.close(); file.close(); } catch(Exception e) { System.out.println("Exception: " + e.toString()); } return obj; } }
Let’s see how a serialized object will look like. Below is the serialization of the object:
LogFile ob = new LogFile(); ob.filename = "User_Nytro.log"; ob.filecontent = "No actions logged"; String file = "Log.ser"; Utils.SerializeToFile(ob, file);
Here is the content (hex) of the Log.ser file:
AC ED 00 05 73 72 00 07 4C 6F 67 46 69 6C 65 D7 ¬í..sr..LogFile× 60 3D D7 33 3E BC D1 02 00 02 4C 00 0B 66 69 6C `=×3>¼Ñ...L..fil 65 63 6F 6E 74 65 6E 74 74 00 12 4C 6A 61 76 61 econtentt..Ljava 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 4C 00 08 /lang/String;L.. 66 69 6C 65 6E 61 6D 65 71 00 7E 00 01 78 70 74 filenameq.~..xpt 00 11 4E 6F 20 61 63 74 69 6F 6E 73 20 6C 6F 67 ..No actions log 67 65 64 74 00 0E 55 73 65 72 5F 4E 79 74 72 6F gedt..User_Nytro 2E 6C 6F 67 .log
As you can see, it looks simple. We can see the class name, “LogFile”, “filename” and “filecontent” variable names and we can also see their values. However, it is important to note that there is no code, it is only the data.
Let’s dig into it to see what it contains:
- AC ED -> We already discussed about the magic number
- 00 05 -> And protocol version
- 73 -> We have a new object (TC_OBJECT)
- 72 -> Refers to a class description (TC_CLASSDESC)
- 00 07 -> The length of the class name – 7 characters
- 4C 6F 67 46 69 6C 65 -> Class name – LogFile
- D7 60 3D D7 33 3E BC D1 -> Serial version UID – An identifier of the class. This value can be specified in the class, if not, it is generated automatically
- 02 -> Flag mentioning that the class is serializable (SC_SERIALIZABLE) – a class can also be externalizable
- 00 02 -> Number of variables in the class
- 4C -> Type code/signature – class
- 00 0B -> Length of the class variable – 11
- 66 69 6C 65 63 6F 6E 74 65 6E 74 -> Variable name – filecontent
- 74 -> A string (TC_STRING)
- 00 12 -> Length of the class name
- 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B -> Class name – Ljava/lang/String;
- 4C -> Type code/signature – class
- 00 08 -> Length of the class variable – 8
- 66 69 6C 65 6E 61 6D 65 -> Variable name – filename
- 71 -> It is a reference to a previous object (TC_REFERENCE)
- 00 7E 00 01 -> Object reference ID. Referenced objects start from 0x7E0000
- 78 -> End of block data for this object (TC_ENDBLOCKDATA)
- 70 -> NULL reference, we finished the “class description”, the data will follow
- 74 -> A string (TC_STRING)
- 00 11 -> Length of the string – 17 characters
- 4E 6F 20 61 63 74 69 6F 6E 73 20 6C 6F 67 67 65 64 -> The string – No actions logged
- 74 -> A string (TC_STRING)
- 00 0E -> Length of the string – 14 characters
- 55 73 65 72 5F 4E 79 74 72 6F 2E 6C 6F 67 -> The string – User_Nytro.log
The protocol details are not important, but they might help if manually updating a serialized object is required.
Attack example
As you might expect, the issue happens during the deserialization process. Below is a simple example of deserialization.
LogFile ob = new LogFile(); String file = "Log.ser"; // Deserialization of the object ob = (LogFile)Utils.DeserializeFromFile(file);
And here is the output:
Deserializing from Log.ser readObject from LogFile File name: User_Nytro.log, file content: No actions logged Restoring log data to file...
What happens is pretty straightforward:
- We deserialize the “Log.ser” file (containing a serialized LogFile object)
- This will automatically call “readObject” method of “LogFile” class
- It will print the file name and the file content
- And it will create a file called “User_Nytro.log” containing “No actions logged” text
As you can see, an attacker will be able to write any file (depending on permissions) with any content on the system running the vulnerable application. It is not a directly exploitable Remote Command Execution, but it might be turned into one.
We need to understand a few important things:
- Serialized objects do not contain code, they contain only data
- The serialized object contains the class name of the serialized object
- Attackers control the data, but they do not contain the code, meaning that the attack depends on what the code does with the data
Is is important to note that readObject is not the only affected method. The readResolve, readExternal and readUnshared methods have to be checked as well. Oh, we should not forget XStream. And this is not the full list…
For black-box testing, it might be easy to find serialized objects by looking into the network traffic and trying to find 0xAC 0xED bytes or “ro0” base64 encoded bytes. If we do not have any information about the libraries on the remote system, we can just iterate through all ysoserial payloads and throw them at the application.
But my readObject is safe
This might be the most common problem regarding deserialization vulnerabilities. Any application doing deserialization is vulnerable as long as in the class-path are other vulnerable classes. This happens because, as we already discussed earlier, the serialized object contains a class name. Java will try to find the class specified in the serialized object in the class path and load it.
One of the most important vulnerabilities was discovered in the well-known Apache Commons Collections library. If on the system running the deserialization application a vulnerable version of this library or multiple other vulnerable libraries is present, the deserialization vulnerability can result in remote command execution.
Let’s do an example and completely remove the “readObject” method from our LogFile class. Since it will not do anything, we should be safe, right? However, we should also download commons-collections-3.2.1.jar library and extract it in the class-path (the org directory).
In order to exploit this vulnerability, we can easily use ysoserial tool. The tool has a collection of exploits and it allows us to generate serialized objects that will execute commands during deserialization. We just need to specify the vulnerable library. Below is an example for Windows:
java -jar ysoserial-master.jar CommonsCollections5 calc.exe > Exp.ser
This will generate a serialized object (Exp.ser file) for Apache Commons Collections vulnerable library and the exploit will execute the “calc.exe” command. What happens if our code will read this file and deserialize the data?
LogFile ob = new LogFile(); String file = "Exp.ser"; // Deserialization of the object ob = (LogFile)Utils.DeserializeFromFile(file);
This will be the output:
Deserializing from Exp.ser Exception in thread "main" java.lang.ClassCastException: java.management/javax.management.BadAttributeValueExpException cannot be cast to LogFile at LogFiles.main(LogFiles.java:105)
But this will result as well:
We can see that an exception related to casting the deserialized object was thrown, but this happened after the deserialization process took place. So even if the application is safe, if there are vulnerable classes out there, it is game over. Oh, it is also possible to have issues with deserialization directly on JDK, without any 3rd party libraries.
How to prevent it?
The most common suggestion is to use Look Ahead ObjectInputStream. This method allows to prevent deserialization of untrusted classes by implementing a whitelist or a blacklist of classes that can be deserialized.
However, the only secure way to do serialization is to not do it.
Conclusion
Java deserialization vulnerabilities became more common and dangerous. Public exploits are available and is easy for attackers to exploit these vulnerabilities.
It might be useful to document a bit more about this vulnerability. You can find here a lot of useful resources.
We also have to consider that Oracle plans to dump Java serialization.
However, the important thing to remember is that we should just avoid (de)serialization.
Pingback: 2018-05-31 News Feed – My Crap
Pingback: Understanding Java deserialization – anonymous
Pingback: GitHub, Oracle, & GDPR - Application Security Weekly #18 - Security Weekly
Pingback: GitHub, Oracle, & GDPR – Application Security Weekly #18 – Cyber Sercuirty
thanks
Pingback: BSides RDU EverSec CTF - Challenge Solutions | doyler.net
Thanks for the article. Gave me a better understanding of serialization!
Pingback: Techniques et outils d’attaque sur les moteurs de désérialisation (Java) - RiskInsight