Serialization Must Die
Security issues and problems with serialization of random objects.
Join the DZone community and get the full member experience.
Join For FreeWhen @frohoff, @gebl and @breenmachine all combined to melt Java security (in what I’m hereafter conflating under the term “seriapalooza”), I thought about deserialization alternatives. Where are my customers going next? Is there greener grass? We’re going to find out. If the title of my series wasn’t spoiler enough, let me foreshadow more plainly: the grass is brown and dead, everywhere.
Today, we’re looking at Kryo, one of the “hipper” serialization libraries. We know it’s used in a lot of big software already, but it’s a library that’s probably used downstream by many, many organizations. My customers are certainly among those organizations.
Let’s take a peek.
Many Fish in the Sea
The cool and scary part of Kryo is that you can use it to serialize or deserialize any Java types, not necessarily those that are marked with java.io.Serializable. This makes development a lot easier for many situations, like serializing already existing stuff, or serializing stuff you can’t control.
When you use Kryo to deserialize an object, you must declare which type you're expecting. Here’s an example:
// serializing
Kryo kryo = new Kryo();
AcmeMessage message = buildAcmeMessage(); // some domain object
Output out = new Output(response.getOutputStream())
kryo.writeObject(out, myObject);
// deserializing
Input in = new Input(request.getInputStream());
AcmeMessage message = kryo.readObject(in, AcmeMessage.class);
This API design is, at least, an improvement over the Java object serialization spec, which forces you to deserialize whatever the attacker sent, and then pray it’s what you were looking for afterward -- which is lunacy. This is the first hurdle for an attacker: How can I trick you into deserializing arbitrary types?
Bait and Switch
This one was actually easy to get around. In the real world, any domain object that folks send back and forth will have collections -- maps, lists, arrays, or something like that. Let's consider the following example object:
/* AcmeMessage.java */
private List<AcmeRecipient> recipients;
Everywhere that matters, that <AcmeRecipient> bit of code does not exist. The only place it does matter is compile time. I live in the runtime, where attacks exploits occur. This means for our purposes, that code should be understood as this instead:
/* AcmeMessage.java */
private List<Object> recipients;
So, if our target app is deserializing an AcmeMessage type, we can stuff some unexpected, arbitrary types into the recipients field, because all types extend Object. Now, much like with the seriapalooza attacks, the types the attacker uses must still be on the classpath of the victim application, libraries, or server.
Of course, if the app ever tries to use one of those objects as an AcmeMessage, it will blow up -- but our attack should be reaching fruition before that time. See the test case in action.
Navigating the Type Bazaar
Ok, we can send arbitrary types. Any restrictions? In classic serialization, there aren’t many rules - you needed a class marked as java.io.Serializable, and that’s pretty much it. After looking at the code, we appear to only have one rule: the class must have a no-arg constructor. Senses tingling now.
At a high level, Kryo deserializes with this pattern:
- Get the zero-arg constructor for the given type
- If it’s private, mark it as accessible
- Call the constructor
- For every field in the type:
- Deserialize the field passed in the message (recurse)
- Assign the field to the new type created in #3
There are ways to override this existing behavior with even more abusable behavior, but let’s focus on the default settings. I’ve already mentioned the weakness here -- did you see it?
Seriapalooza, Almost
The seriapalooza authors wanted people to understand this: the problem isn’t these 4 or 5 classes. This pattern is foundationally broken. You know how we knew MD5 was broken, even before we had it totally destroyed? Same concept.
If you let developers specify arbitrary behaviors in their types’ Serializable#readObject() methods, you’ll be able to chain their side effects together so that they eventually add up to disaster. And that’s what happened.
And this isn’t very different.
I can submit an arbitrary class that’s on your classpath, and you (the victim app) will call its constructor. Developers can put whatever they want in their constructors. They’re not guaranteed to avoid having side effects.
Let’s look at a few attack strategies and the gadgets we’ll use to carry them out.
Abusing Static Side Effects in Constructors
Here’s a class in ColdFusion that clobbers a singleton that you can control:
package coldfusion.syndication;
import com.sun.syndication.io.impl.CFDateParser;
public class FeedDateParser implements CFDateParser {
private FeedDateParser() {
DateParser.registerCFDateParser(this);
}
...}
Clearly this method was only intended to be called once. What will happen if I send you a Kryo-serialized version of FeedDateParser that has null fields? Answer: a super efficient application DoS. I will override the singleton with my malicious version, filled with null field members. This will cause NullPointerExceptions to start getting thrown everywhere, as everyone uses it. A single HTTP request could take you down completely.
Notice that the constructor is private. Kryo doesn’t care. This isn’t cool, to me. If I mark a constructor private, I intend for it to be created in only the ways I allow. There may be good reasons for that -- maybe even security reasons! However, Kryo users reported not supporting private constructors as a bug, and the library maintainers added support. Strange that the author cited security concerns for not instantiating objects in safer ways in the seminal feature request. They have the ability to turn on safer object instantiation years after that thread, but it’s still not the default.
There is lots of havoc you can cause with constructors. Although I’m sure one is out there, there’s no killer remote code execution using this strategy that I’ve been able to find.
Abusing Cleanup Utilities in finalize()
Constructors aren’t the only source of side effects we can use to affect change.
If a class has implemented an Object#finalize() method, Java will invoke it before the object gets garbage collected. Developers use this method to clean up any non-JVM resources that may not have been cleaned up correctly. Because it’s called automatically, it’s possible to abuse any side effects that may occur in this method, without the app acting on your malicious object at all! In fact, their throwing it away is a necessary step in the exploitation process.
Here are a few types you can use to do some funny stuff.
Attack #1: Delete Arbitrary Files (org.jpedal.io.ObjectStore)
By far, the most commonly abusable tactic in finalize() is messing with files. It makes sense; a type is somehow “backed” by a temp file, and the finalize() is a clear message that the file can be cleaned up.
The first type I found with this capability also happened to be in ColdFusion 10: org.jpedal.io.ObjectStore. When analyzing classes to determine potential Kryo finalize() exploit gadgets, you only need to look at two things: the zero-argument constructor and the finalize() method. Those are the only things that will happen. You can control the fields, as long as they are also objects that can be created with zero-argument constructors. Here’s the constructor:
public ObjectStore() {
init();
}
Here’s a screenshot showing that indeed, the default constructor is invoked during Kryo’s readObject() call:
Doesn’t do much. Anything it does is undone anyway, because Kryo copies our state over the top of their state, once the constructor is finished executing. And next, the finalize():
protected void finalize() {
...
flush();
...
}
protected void flush() {
...
/**
* flush any image data serialized as bytes
*/ Iterator filesTodelete = imagesOnDiskAsBytes.keySet().iterator();
while(filesTodelete.hasNext()) {
final Object file = filesTodelete.next();
if(file! = null){
final File delete_file = new File((String)imagesOnDiskAsBytes.get(file));
if(delete_file.exists()) {
delete_file.delete();
}
}
}
...
}
See the testCF10_JPedal() test case to verify this gadget.
Attack #2: Memory Corruption (multiple types)
I’m definitely not the best person to chase this lead down, but we can use the following gadgets to create memory corruption bug primitives. They all call free() on a user-controlled memory address:
- com.sun.jna.Memory, packaged with Vert.x
- com.sun.medialib.codec.jpeg.Encoder (no source available), part of ColdFusion 10
- com.sun.medialib.codec.png.Decoder (no source available), part of ColdFusion 10
- com.xuggle.ferry.AtomicInteger, part of Liferay (also the xuggle library)
I’m sure there are more out there, but these few are already available on a lot of popular platforms. Let’s take a look at the com.sun.jna.Memory#finalize() call:
That free() is indeed delegated to the stdlib.h::free() function. For the uninitiated, this is incredibly dangerous. The test case that proves memory corruption is here. The test is disabled, but you can enable it by changing it to public from private. When enabled, the test case crashes the JVM as shown here:
To reiterate, reading Kryo objects from untrusted sources with any of these gadgets available are vulnerable to a single-shot application takedown.
Attack #3: Close Any File Descriptors (java.net.DatagramSocket)
There are a couple of these, and much like the other attack types, I haven’t fully explored them all. However, the most obvious one is in java.net.PlainDatagramSocketImpl, packaged with the JRE.
There is no explicit constructor in the type, nor any superclass up until java.lang.Object. There is, however, a java.io.FileDescriptor field called fd in its grandparent, java.net.DatagramSocketImpl.
The FileDescriptor type has a zero-argument constructor and contains nothing but a simple integer to represent the file descriptor at the OS level. The finalize() kicks off the fun:
protected void finalize() {
close();
}
/**
* Close the socket.
*/
protected void close() {
if (fd != null) {
datagramSocketClose();
…}
This method is native, and as shown in this code from the HotSpot native layer, is just a plain old kill-this-file-descriptor-I-don’t-care-what-it-is close() from unistd.h.
int os::close(int fd) {
return ::close(fd);
}
int os::socket_close(int fd) {
return ::close(fd);
}
See the testDatagramSocket() test case to verify this gadget. An attacker could submit many gadget instances and close every possible file descriptor. This would prevent socket communications with users, file reading or writing, or any IPC.
Summary
It's possible to get a Kryo to call some other methods, like compareTo(), entrySet(), toString(), and others, so there are more interesting gadgets out there to find.
The bottom line appears to be that it is likely that any modern application is likely to have a gadget on its classpath that is capable of deleting a file, closing a file descriptor or crashing the JVM entirely. This means that it should be generally considered unsafe to allow Kryo to operate on untrusted streams.
One could strongly argue that Kryo could be made a lot safer by making the default object instantiation strategy not invoke the type’s constructors. This is possible by using JVM-specific tricks that we’ll discuss in our next serializer breakdown. Using this constructor-less instantiation will prevent side effects from occurring in the constructors, as well as their finalize() methods.
However, if you do this, Kryo doesn’t need a zero-argument constructor to create a new object anymore -- so attackers can instantiate many more types! That trade-off will result in many more gadgets being made available to attackers, but fewer opportunities for executing methods on those gadgets -- no more constructors and no more finalizing methods.
Next time, we’ll take a look at someone who tried the safe approach and still got into trouble -- XStream!
Vendor Response
This is definitely a “will it blend?” situation, and thus I expected little-to-no action from the gadget authors and the Kryo library itself. The only person I attempted to contact was the Kryo project lead, and no response was received.
Published at DZone with permission of Arshan Dabirsiaghi, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments