Introduction Adobe ColdFusion & AMF
Before we go into technical details, I will give you a short intro to Adobe ColdFusion (CF). Adobe ColdFusion is an Application Development Platform like ASP.net, however several years older. Adobe ColdFusion allows a developer to build websites, SOAP and REST web services and interact with Adobe Flash using the Action Message Format (AMF).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Undefined - 0x00 | |
Null - 0x01 | |
Boolean - 0x02 | |
Boolean - 0x03 | |
Integer - 0x04 | |
Double - 0x05 | |
String - 0x06 | |
XML - 0x07 | |
Date - 0x08 | |
Array - 0x09 | |
Object - 0x0A | |
XML End - 0x0B | |
ByteArray - 0x0C |
The BlazeDS AMF serializer can serialize complex object graphs. The serializer starts with the root object and serializes its members recursively.
Two general serialization techniques are supported by BlazeDS to serialize complex objects:
- Serialization of Bean Properties (AMF0 and AMF3)
- Serialization using Java's java.io.Externalizable interface. (AMF3)
Serialization of Bean Properties
This technique requires the object to be serialized to have a public no-arg constructor and for every member public Getter-and Setter-Methods (JavaBeans convention).In order to collect all member values of an object, the AMF serializer invokes all Getter-methods during serialization. The member names and values are put in the Action message body with the class name of the object.
During deserialization, the classname is taken from the Action Message, a new object is constructed and for every member name the corresponding set
readScriptObject()
of class flex.messaging.io.amf.Amf3Input
or
readObjectValue()
of class flex.messaging.io.amf.Amf0Input
.Serialization using Java's java.io.Externalizable interface
BlazeDS further supports serialization of complex objects of classes implementing the java.io.Externalizable interface which inherits from java.io.Serializable.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public abstract interface Externalizable | |
extends Serializable | |
{ | |
public abstract void writeExternal(ObjectOutput paramObjectOutput) | |
throws IOException; | |
public abstract void readExternal(ObjectInput paramObjectInput) | |
throws IOException, ClassNotFoundException; | |
} |
java.io.ObjectInput
-implementation to read serialized
primitive types and Strings (e.g. method read(byte[] paramArrayOfByte)).During deserialization of an object (type 0xa) in AMF3, the method
readScriptObject()
of class flex.messaging.io.amf.Amf3Input
gets called.
In line #759 the method readExternalizable
is invoked which
calls the readExternal()
method on the object to be deserialized.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* */ protected Object readScriptObject() | |
/* */ throws ClassNotFoundException, IOException | |
/* */ { | |
/* 736 */ int ref = readUInt29(); | |
/* */ | |
/* 738 */ if ((ref & 0x1) == 0) { | |
/* 739 */ return getObjectReference(ref >> 1); | |
/* */ } | |
/* 741 */ TraitsInfo ti = readTraits(ref); | |
/* 742 */ String className = ti.getClassName(); | |
/* 743 */ boolean externalizable = ti.isExternalizable(); | |
/* */ | |
/* */ | |
/* */ | |
/* 747 */ Object[] params = { className, null }; | |
/* 748 */ Object object = createObjectInstance(params); | |
/* */ | |
/* */ | |
/* 751 */ className = (String)params[0]; | |
/* 752 */ PropertyProxy proxy = (PropertyProxy)params[1]; | |
/* */ | |
/* */ | |
/* 755 */ int objectId = rememberObject(object); | |
/* */ | |
/* 757 */ if (externalizable) | |
/* */ { | |
/* 759 */ readExternalizable(className, object); //<- call to readExternal | |
/* */ } | |
/* */ //... | |
/* */ } |
Previous work
Chris Gates (@Carnal0wnage) published the paper ColdFusion for Pentesters which is an excellent introduction to Adobe ColdFusion.
Wouter Coekaerts (@WouterCoekaerts) already showed in his blog post that deserializing untrusted AMF data is dangerous.
Looking at the history of Adobe ColdFusion vulnerabilities at Flexera/Secunia's database you can find mostly XSS', XXE's and information disclosures.
The most recent ones are:
- Deserialization of untrusted data over RMI (CVE-2017-11283/4 by @nickstadb)
- XXE (CVE-2017-11286 by Daniel Lawson of @depthsecurity)
- XXE (CVE-2016-4264 by @dawid_golunski)
CVE-2017-3066
In 2017 Moritz Bechler of AgNO3 GmbH and my teammate Markus Wulftange discovered independently the vulnerability CVE-2017-3066 in Apache BlazeDS.The core problem of this vulnerability was that Adobe Coldfusion never did any whitelisting of allowed classes. Thus any class in the classpath of Adobe ColdFusion, which either fulfills the Java Beans Convention or implements java.io.Externalizable could be sent to the server and get deserialized. Both Moritz and Markus found JRE classes (
sun.rmi.server.UnicastRef2
sun.rmi.server.UnicastRef
) which implemented the java.io.Externalizable interface
and triggered an outgoing TCP connection during AMF3 deserialization.
After the connection was made to the attacker's server, its response was deserialized
using Java's native deserialization using
ObjectInputStream.readObject()
. Both found a great "bridge" from AMF
deserialization to Java's native deserialization which offers well known
exploitation primitives using public gadgets.
Details about the vulnerability can also be found in Markus' blog post. Apache introduced validation through the class
flex.messaging.validators.ClassDeserializationValidator
.
It has a default whitelist but can also be configured with a configuration file.
For details see the Apache BlazeDS release notes.Finding exploitation primitives before CVE-2017-3066
As already mentioned in the very beginning my teammate Thomas required an exploit which also works without outgoing connection.I had a quick look into the excellent research paper "Java Unmarshaller Security" of Moritz Bechler where he analysed several "Unmarshallers" including BlazeDS. The exploitation payloads he discovered weren't applicable since the libraries were missing in the classpath.
So I started with my typical approach, fired up my favorite "reverse engineering tool" when it comes to Java, Eclipse. Eclipse together with the powerful decompiler plugin "JD-Eclipse" (https://github.com/java-decompiler/jd-eclipse) is all you need for static and dynamic analysis. As a former Dev I was used to work with IDE's which make your life easier and decompiling and grepping through code is often very inefficient and error prone. So I created a new Java project and added all jar-files of Adobe Coldfusion 12 as external libraries.
The first idea was to look for further calls to Java's
ObjectInputStream.readObject
-method. Using Eclipse this is very easy.
Just open class ObjectInputStream
, right click on the readObject()
method and
click "Open Call Hierarchy". Thanks to JD-Eclipse and its decompiler, Eclipse is
able to construct call graphs based on class information without having any source.
The call graph looks big in the very beginning. But with some experience you
see very quickly which nodes in the graph are interesting.After some hours I found two promising call graphs.
Setter-based Exploit
The first one starts with methodsetState(byte[] new_state)
of class
org.jgroups.blocks.ReplicatedTree
.Looking at the implementation of this method, we already can imagine what is happening in line #605.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* */ public void setState(byte[] new_state) | |
/* */ { | |
/* 597 */ Node new_root = null; | |
/* */ | |
/* */ | |
/* 600 */ if (new_state == null) { | |
/* 601 */ if (log.isInfoEnabled()) log.info("new cache is null"); | |
/* 602 */ return; | |
/* */ } | |
/* */ try { | |
/* 605 */ Object obj = Util.objectFromByteBuffer(new_state); | |
/* 606 */ new_root = (Node)((Node)obj).clone(); | |
/* 607 */ root = new_root; | |
/* 608 */ notifyAllNodesCreated(root); | |
/* */ } | |
/* */ catch (Throwable ex) { | |
/* 611 */ if (log.isErrorEnabled()) { log.error("could not set cache: " + ex); | |
/* */ } | |
/* */ } | |
/* */ } |
ObjectInputStream.readObject()
.The only thing to mention here is that the
byte[]
passed to setState()
needs to have an additional byte 0x2 at offset 0x0 as we can see from line 364
of class org.jgroups.util.Util
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* */ public static Object objectFromByteBuffer(byte[] buffer, int offset, int length) throws Exception | |
/* */ { | |
/* 358 */ if (buffer == null) return null; | |
/* 359 */ if (JGROUPS_COMPAT) | |
/* 360 */ return oldObjectFromByteBuffer(buffer, offset, length); | |
/* 361 */ Object retval = null; | |
/* 362 */ InputStream in = null; | |
/* 363 */ ByteArrayInputStream in_stream = new ByteArrayInputStream(buffer, offset, length); | |
/* 364 */ byte b = (byte)in_stream.read(); | |
/* */ try { | |
/* */ int len; | |
/* 367 */ switch (b) { | |
/* */ case 0: | |
/* 369 */ return null; | |
/* */ case 1: | |
/* 371 */ in = new DataInputStream(in_stream); | |
/* 372 */ retval = readGenericStreamable((DataInputStream)in); | |
/* 373 */ break; | |
/* */ case 2: | |
/* 375 */ in = new ObjectInputStream(in_stream); | |
/* 376 */ retval = ((ObjectInputStream)in).readObject(); | |
/* */ //... | |
/* */ } | |
/* */ } | |
/* */ } |
The exploit works against Adobe ColdFusion 12 only since JGroups is only available in this specific version.
Externalizable-based Exploit
The second call graph starts in classorg.apache.axis2.util.MetaDataEntry
with a call to readExternal
which is what we are looking for.In line #297 we have a call to
SafeObjectInputStream.install(inObject)
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* */ public static SafeObjectInputStream install(ObjectInput in) | |
/* */ { | |
/* 62 */ if ((in instanceof SafeObjectInputStream)) { | |
/* 63 */ return (SafeObjectInputStream)in; | |
/* */ } | |
/* 65 */ return new SafeObjectInputStream(in) ; | |
/* */ } |
AMF3Input
instance gets wrapped by a
org.apache.axis2.context.externalize.SafeObjectInputStream
instance.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* */ private Object readObjectOverride() | |
/* */ throws IOException, ClassNotFoundException | |
/* */ { | |
/* 318 */ boolean isActive = in.readBoolean(); | |
/* 319 */ if (!isActive) { | |
/* 320 */ if (isDebug) { | |
/* 321 */ log.debug("Read object=null"); | |
/* */ } | |
/* 323 */ return null; | |
/* */ } | |
/* 325 */ Object obj = null; | |
/* 326 */ boolean isObjectForm = in.readBoolean(); | |
/* 327 */ if (isObjectForm) | |
/* */ { | |
/* 329 */ if (isDebug) { | |
/* 330 */ log.debug(" reading using object form"); | |
/* */ } | |
/* 332 */ obj = in.readObject(); | |
/* */ } else { | |
/* 334 */ if (isDebug) { | |
/* 335 */ log.debug(" reading using byte form"); | |
/* */ } | |
/* */ | |
/* 338 */ ByteArrayInputStream bais = getByteStream(in); | |
/* */ | |
/* */ | |
/* 341 */ ObjectInputStream tempOIS = createObjectInputStream(bais); | |
/* 342 */ obj = tempOIS.readObject(); | |
/* 343 */ tempOIS.close(); | |
/* 344 */ bais.close(); | |
/* */ } | |
/* */ //... | |
/* */ } |
org.apache.axis2.context.externalize.ObjectInputStreamWithCL
is created.
This class just extends the standard java.io.ObjectInputStream
.
In line #342 we finally have our call to readObject()
.The following image shows the request for the exploit.
The exploit works against Adobe ColdFusion 11 and 12.
ColdFusionPwn
To make your life easier I created the simple tool ColdFusionPwn. It works on the command line and allows you to generate the serialized AMF message. It incorporates Chris Frohoff's ysoserial for gadget generation. It can be found on our github.
Takeaways
Deserializing untrusted input is bad, that's for sure. From an exploiters perspective exploiting deserialization vulnerabilities is a challenging task since you need to find the "right" objects (gadgets) which trigger functionality you can reuse for exploitation. But it's also more fun :-)By the way: If you want to make a deep dive into serverside Java Exploitation and all sorts of deserialization vulnerabilities and how to do proper static and dynamic analysis in Java, you might be interested in our upcoming "Advanced Java Exploitation" course.