RMI

概念

RMI的过程,就是用JRMP协议去组织数据格式,然后通过TCP进行传输,从而达到远程方法调用。
  • RMI(Remote Method Invocation):远程方法调用。即让一个JVM中的对象远程调用另一个JVM中的对象的某个方法,简单来说就是跨越JVM,使用Java调用远程Java程序

    • Server服务端:提供远程的对象
    • Client客户端:调用远程的对象
    • Registry注册表:存放着远程对象的位置,用于客户端查询所调用的远程方法的引用

需要注意的是:被调用的方法实际上是在RMI服务端执行

  • JRMP(Java Remote Message Protocol):Java 远程消息交换协议。运行在TCP/IP之上的线路层协议,该协议要求服务端与客户端都为Java编写。

    • Java本身对RMI规范的实现默认使用JRMP协议,而在Weblogic中使用T3协议
  • JNDI(Java Naming and Directory Interface):Java命名和目录接口。一组在Java应用中访问命名和目录服务的接口,Java中使用最多的基本就是RMI和LDAP的目录服务系统,客户端可以通过名称访问对象,并将其下载下来。

    • 命名服务:将名称和对象联系起来,客户端可以使用名称访问对象
    • 目录服务:一种命名服务,在命名服务的基础上,增加了属性的概念

RMI-Server

  • RMI Server分为三部分:

    • 一个远程接口。继承java.rmi.Remote,其中定义要远程调用的函数。
    • 远程接口的实现类。继承java.rmi.server.UnicastRemoteObject,实现远程调用的函数
    • 创建实例和Registry注册表,然后在注册表中绑定地址和实例

定义远程接口

  • 定义一个远程接口,继承java.rmi.Remote接口,抛出RemoteException异常,修饰符需要为public否则远程调用的时候会报错
package com.naraku.sec.rmidemo;

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

public interface IRemoteHelloWorld extends Remote {
    public String hello() throws RemoteException;
}

远程接口实现

  • 远程接口的实现类,继承java.rmi.server.UnicastRemoteObject,实现远程调用的函数
package com.naraku.sec.rmidemo;

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

public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
    protected RemoteHelloWorld() throws RemoteException {
        super();
        System.out.println("RemoteHelloWorld构造方法");
    }

    @Override
    public String hello() throws RemoteException {
        System.out.println("Callback");
        return "Hello";
    }
}

创建实例和注册表

  • Naming.bindNaming.rebind的区别:

    • bind指“绑定”,如果时“绑定”时Registry已经存在对应的Name,则系统会抛出错误
    • rebind指“重绑定”,如果“重绑定”时Registry已经存在对应的Name,则绑定的远程对象将被替换
    • 除非有特别的业务需求,否则建议使用rebind方法进行绑定
  • Registry.rebindNaming.rebind的区别:

    • Registry.rebind是使用RMI注册表绑定,所以不需要完整RMI URL
    • Naming.rebind是通过Java的名称服务进行绑定,由于名称服务不止为RMI提供查询服务,所以绑定时需要填入完整RMI URL

Naming.rebind

  • 实现Registry并将上面的类实例化,然后绑定到一个地址
package com.naraku.sec.rmidemo;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class DemoServer {
    public static void main(String[] args) throws Exception {
        // 实例化远程对象
        RemoteHelloWorld hello = new RemoteHelloWorld();

        // 实现注册表
        LocateRegistry.createRegistry(1099);

        // 将远程对象注册到注册表里面, 绑定地址
        Naming.rebind("rmi://192.168.111.1:1099/Hello", hello);
        
        // 如果Registry在本地, Host和Port可以省略, 默认 localhost:1099
        //  Naming.rebind("Hello", hello);
        
        System.out.println("Start Server, Port is 1099");
    }
}

Registry.rebind

// 创建远程对象
RemoteHelloWorld hello = new RemoteHelloWorld();

// 创建注册表
Registry registry = LocateRegistry.createRegistry(1099);

// 将远程对象注册到注册表里面,绑定地址
registry.rebind("Hello",hello);

RMI-Client

  • 编写RMIClient,并调用远程对象。需要注意的是,如果远程方法有参数,调用方法时所传入的参数必须是可序列化的。在传输中是传输序列化后的数据,服务端会对客户端的输入进行反序列化
package com.naraku.sec.rmidemo;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class DemoClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
        // 查询 hello 对象
        IRemoteHelloWorld hello = (IRemoteHelloWorld) Naming.lookup("rmi://192.168.111.2:1099/Hello");

        // 调用远程方法
        String ret = hello.hello();
        System.out.println(ret);
    }
}
  • Client也有Registry.lookupNaming.lookup,但它们是一样的

RMI通信过程

Registry就像⽹关,不会执⾏远程⽅法,但Server可以在上⾯注册⼀个Name到对象的绑定关系。Client通过Name向Registry查询,得到这个绑定关系,然后再连接Server。最后,远程⽅法在Server上调⽤。

发起RMI通信

发起一次RMI通信,Server端192.168.111.1,Client端192.168.111.2

Java-Sec-RMI-1

通信过程中会建立2次TCP连接,数据包附件在文末

Java-Sec-RMI-2

第1次连接是Client和Registry的连接,连接到目标IP:1099

  • Client发送Call消息:Client连接Registry,寻找Name=Hello的对象

Java-Sec-RMI-3

  • Registry响应ReturnData消息:返回Name=Hello对象的序列化数据,并包含对象的IP和端口

    • 0xACED0005常见的Java反序列化16进制特征。所以这里从\xAC\xED开始就是序列化的数据,IP和端口这只是这个对象的一部分

Java-Sec-RMI-4

  • 返回的端口位于IP地址后一个字节,这里是\x00\x00\xe0\x7757463,所以后面Client将向Server的该端口发起第2次请求
int("0x0000e077", 16)

第2次连接是Client和Server的连接。Client向Server的目标端口发起请求,并正式调用远程方法

远程调用报错

  • Client和Server的package路径需要一致

Java-Sec-RMI-5

攻击RMI Registry

  • 前面是RMI整个的原理与流程,那么RMI会带来哪些安全问题?

    • 如果我们能访问RMI Registry服务,如何对其攻击?
    • 如果我们控制了目标RMI客户端中Naming.lookup的第一个参数(也就是RMI Registry的地址),能不能进行攻击?
  • 这里尝试在192.168.111.2中调用192.168.111.1的Registry服务,发现报错如下图所示。原因是Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind/bind/unbind等方法。
官方文档:出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。

Java-Sec-RMI-6

  • 但可以远程调用list/lookup方法,list方法可以列出目标上所有绑定的对象
package com.naraku.sec.rmidemo;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class DemoClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
        // list方法
        String[] objs = Naming.list("rmi://192.168.111.1:1099/");
        for (String obj:objs) {
            System.out.println(obj);
        }
    }
}

Java-Sec-RMI-7

如果觉得我的文章对你有帮助,请我吃颗糖吧~