Introduction: Exploiting Java Deserialization Vulnerabilities
During a recent application assessment at Rhino we identified a Java deserialization vulnerability which ended up leading to unauthenticated remote code execution. Exploitation of the vulnerability turned out to not be as simple as generating a default payload using Ysoserial. In this blog post we will walk through the process, tools, and techniques of modifying Ysoserial to customize payloads and fix errors which might be encountered during exploitation.
Exploit Troubles: Problem 1 (Compression)
The initial vulnerability was discovered when decoding a base64 encoded parameter returned what looked like a random binary blob. After attempting to decode the binary blob using various encoding and decompression algorithms, it was found that it was actually a serialized Java object compressed with Zlib deflate compression. On the backend, the function responsible for deserializing the object would first decompress the object and then deserialize. If you come across a base64 encoded parameter which does not seem to return anything more than a binary blob, always make sure to try some common encodings and decompression on it to see if there is something interesting actually there. The Burp Suite extension Hackvertor is a great tool to do this with as it has many built-in encoding and compression algorithms.
Hackvertor being used to first base64 decode the parameter value and then decompress it with deflate decompress which reveals a serialized Java object.
The next obvious step was to then compress the Ysoserial payload using Hackvetor before sending it in the request. This did not work, it caused the payload to become corrupted once it was deserialized on the server side and exploitation was not successful.
Problem 2 (Class Version Mismatch)
The second hurdle was the class version which was being serialized by Ysoserial did not match the version of the class on the server side. This will cause a java.io.InvalidClassException to be thrown because the local class used during object serialization does not match the server side class version. This will cause the exploit to fail.
Properly Compressing the Object
Since using Hackvertor did not work when trying to compress the payload, we decided to compress the object directly in Ysoserial before generating the payload. This was done by modifying the Ysoserial code and building a new Java Archive (JAR). It was then possible to generate valid payloads that would properly be deserialized on the server side.
Adding a function to the GeneratePayload class of Ysoserial to compress the object and then return a base64 encoded string of it worked fine. The base64 encoded object was then just printed to the console.
The portion of code which was added to Ysoserial in order to apply deflate compression to the object before generating the payload.
Correcting the SerialVersionUID Mismatch
Now that it was possible to generate a properly compressed payload we still received an error and the exploit did not work. But, this gave enough information to fix the problem.
Here we can see the server side expects serial version UID -3490850999041592962 but the serial version UID we sent was -2044202215314119608.
What even is SerialVersionUID?
SerialVersionUID is an identifier used to identify the version of a class to ensure the version of the class used during serialization is the same as the local class used during deserialization on the server side. If the UID values do not match at the time of deserialization, you will receive an error. If the SerialVersionUID is not explicitly defined in the code, It is typically generated at runtime when the class is serialized using the computeDefaultSUID method.
Methods to Fix SerialVersionUID Mismatch
There are a few methods that can be used to fix this problem.
- Changing the version of the dependency used directly in Ysoserial.
- Setting a breakpoint on the SUID return value from the computeDefaultSUID method at runtime and modifying it to the desired value.
- Modifying the Bytecode of the class to hardcode the SerialVersionUID.
This blog gives a brief overview into the last two methods. We will detail how to modify the value computeDefaultSUID at runtime as well as an additional method which we found to be the easiest, building Ysoserial with the proper class version. Both will work, but it is always nice to have options when you run into problems with one method.
Building Ysoserial With the Proper Version
This was the easiest and fastest method to be able to generate a payload with the proper SerialVersionUID.
Referring to the earlier error message we will need to identify which version of commons-beanutils we need to build Ysoserial with so we get the proper SerialVersionUID for the org.apache.commons.beanutils.BeanComparator class when it is serialized.
Using Bash which will iterate through all the versions of a particular dependency, download each version, and print the SerialVersionUID for the target class. This will allow us to find the version we need.
#!/bin/bash #Example usage: ./getSUIDs.sh https://archive.apache.org/dist/commons/beanutils/binaries/ org.apache.commons.beanutils.BeanComparator #Example2 usage: ./getSUIDs.sh https://archive.apache.org/dist/commons/collections/binaries/ org.apache.commons.collections4.functors.InvokerTransformer url=$1 class=$2 mkdir tmpjars for zip in $(curl -s $url | grep '.zip<' | grep -Eo 'href="[^\"]+"' | cut -d '"' -f 2);do wget -O tmpjars/current.zip -4 $url$zip &>/dev/null unzip tmpjars/current.zip -d tmpjars &>/dev/null echo "Checking file: $zip" for jar in $(find tmpjars/ -name '*.jar');do serialver -classpath $jar $class 2>/dev/null| grep serialVersionUID done rm -rf tmpjars/* done rm -d tmpjars/
From the error message earlier we know we are looking for SUID -3490850999041592962. So using the script:
./getSUIDs.sh https://archive.apache.org/dist/commons/beanutils/binaries/ org.apache.commons.beanutils.BeanComparator
We can see we get a hit for version 1.7.0 having the SerialVersionUID we need.
Now we can grab the source for Ysoserial:
Git clone https://github.com/frohoff/ysoserial.git
Open pom.xml in the Ysoserial directory and modify it, replacing the commons-beanutils version (or whatever dependency is giving you trouble) with the proper version.
Here we went from version 1.9.2 to 1.7.0.
Save it and build Ysoserial.
mvn clean package -DskipTests cd target java -jar ysoserial-0.0.6-SNAPSHOT-all.jar
Now you can use Ysoserial to generate payloads with the proper SerialVersionUID.
Modifying the SerialVersionUID at Runtime
In order to modify the SerialVersionUID at runtime within Ysoserial, we need to use a Java debugger and set a breakpoint at the return value of computeDefaultSUID. This will then pause the application each time the method returns and allow us to find and modify the value as we need. This method is a little more involved, but may be necessary in some situations for debugging or if you cannot locate the proper dependency version for the SerialVersionUID.
You can grab a copy of IntelliJ IDEA here and download Ysoserial here. Open IDEA and create a new empty project using the defaults. Drag the Ysoserial JAR onto your project in the left hand file window (This walkthrough was performed on Kali Linux using OpenJDK 11).
Now we can set up our configuration for running Ysoserial in the debugger. Press ALT+SHIFT+F10 to open up the run menu and select edit.
Set the command arguments to what you need for Ysoserial, in this example we are using:
CommonsBeanutils1 “nslookup example.com”
On the “Logs” tab you can set an output file to make the output easier to work with.
Click Apply and Run to make sure it executes properly.
Now we will set the breakpoint for the SUID return value. The return value comes from the computeDefaultSUID method which is in the java.io.ObjectStreamClass class.
On the left file navigation window expand the following:
- External Libraries
- <Java JDK>
Double click the class and search for “return this.suid” and set a breakpoint at that line (102).
Here you can see the breakpoint set by clicking to the right of the line number for the return value of getSerialVersionUID.
Now, pressing ALT+SHIFT+F9 runs the JAR in the debugger and stops at our breakpoint. We can now continue pressing the green arrow to step through the breakpoints until we reach the class serialVersionUID we need to modify, obtained from the error message we received earlier, in this case BeanComparator. Just right click the suid parameter and use “Set Value” to set the value to -3490850999041592962L.
Modifying the SerialVersionUID in the debugger.
Now just continue the execution and the payload output should now contain the proper SerialVersionUID for that class.
Its All Worth It: Getting a Shell
Now using either method described here we can go ahead and generate a compressed payload with the proper SerialVersionUID and get ourselves a shell.
Even though exploitation of this vulnerability was not obvious at first, it is always worth digging into any base64 encoded value when testing an application. If you run into troubles when trying to exploit a Java deserialization, hopefully some of these steps here will help you out. Although there are two different methods discussed here you may find that one will work better than the other in your particular situation or assist you in debugging to get a working payload.
Thanks for reading, check back frequently for more blog posts, research, and tool releases. In the meantime, follow us on Twitter for news and updates: @RhinoSecurity, @daveysec