Java RMI学习与解读(一)

x33g5p2x  于2021-11-20 转载在 Java  
字(15.3k)|赞(0)|评价(0)|浏览(282)

Java RMI学习与解读(一)

写在前面

本文记录在心情美丽的一个晚上。

嗯。就是心情很美丽。

那为什么晚上还要学习呢?

emm... 卷... 卷起来。

全文基本都是根据su18师傅和其他师傅的文章学习的,本文也只是做一个学习的记录,建议大家最好也是去学习这些师傅们的文章,写的真的很棒。

About RMI

首先,关于RMI介绍,建议看这篇文章,里面对不少RMI相关概念解释的都很清晰。

RMI(Remote Method Invocation) 远程方法调用协议,实现了Java程序之间跨JVM通信,可以远程调用其他虚拟机中的对象来执行方法。也就是获取远程对象的引用,通过远程对象的引用调用远程对象的某个方法。
它让我们获得对远程主机上对象的引用,并像在我们自己的虚拟机中一样使用它。RMI 允许我们调用远程对象上的方法,将真实的 Java 对象作为参数传递并获取真实的 Java 对象作为返回值。

无论在何处使用引用,方法调用都发生在原始对象上,该对象仍位于其原始主机上。如果远程主机向您返回对其对象之一的引用,您可以调用该对象的方法;实际的方法调用将发生在对象所在的远程主机上。

关于远程调用(Remote Invocation)在C语言中的RPC(Remote Procedure Calls远程过程调用)就已经实现了可以在远程主机上执行C语言函数并返回结果。而C中的RPC与Java中的RMI最大的区别在于,C中主要关注的是数据结构,在进行RPC远程过程调用的时候打包传输的数据时相对简单,而Java中,例如序列化通常是将一整个类直接序列化之后进行传输,而类中就需要包含该类的属性以及方法。那么RMI相较于RPC而言就不仅仅是传输数据结构了,Java需要将整个类(属性、方法)进行传输并且在落地后是要可以调用该类中的方法的。

RMI进行传输时使用了序列化与反序列化机制,必要时会利用动态类加载和安全管理机制(CC5提到的概念)来安全的传输Java类,个人感觉RMI相较于RPC真正的突破在于可以在网络上传输数据(对象的属性)和行为(对象的方法)。
It should be no surprise that RMI uses object serialization, which allows us to send graphs of objects (objects and all of the connected objects that they reference). When necessary, RMI can also use dynamic class loading and the security manager to transport Java classes safely. Thus, the real breakthrough of RMI is that it’s possible to ship both data and behavior (code) around the Net.

远程与非远程对象

远程对象:RMI中的远程对象首先需要可以序列化;并且需要实现特殊远程接口的对象,该接口指定可以远程调用对象的哪些方法(这个后面会详细提到);其次该对象是通过一种可以通过网络传递的特殊对象引用来使用的。和普通的 Java 对象一样,远程对象是通过引用传递。也就是在调用远程对象的方法时是通过该对象的引用完成的。

非远程对象:非远程对象与远程对象相比只是可被序列化而已,并不会像远程对象那样通过调用远程对象的引用来完成调用方法的操作,而是将非远程对象做一个简单地拷贝(simply copied),也就是说非远程对象是通过拷贝进行传递。

Stubs and skeletons

RMI的实现用到了存根Stubs(client端)和骨架Skeletons(server端)

存根Stubs: 什么是Stubs?之前也说到了:当客户端在调用远程对象上的方法时,是通过远程对象的引用调用远程对象的方法,而这个所谓的"远程对象的引用"实际上是充当该对象代理的本地代码,这段代码就是存根Stub。

骨架Skeletons:而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是骨架 Skeleton,它从 Stubs 中接收远程方法调用并传递给真实的目标类。
Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的。

那么现在可以小结通过RMI进行远程方法调用时有如下这么一个简单的流程:

Client端 ==> 存根Stubs ==> 骨架Skeletons ==> Server端

Remote Interface

上面我们提到了远程对象需要实现特殊的远程接口,下面会涉及三个概念:

  1. 远程对象
  2. 远程对象所实现的特殊的远程接口
  3. java.rmi.Remote接口

在使用RMI进行远程方法调用时,首先需要定义这个特殊的远程接口;而在java.rmi包中有一个接口Remote,实际中远程对象实现的远程接口需要extend这个Remote接口,后续远程对象的创建就实现我们定义的特殊的远程接口即可。且同时生成的存根Stubs也是如此。

大概是这样的流程:
java.rmi.Remote ==> 特殊的远程接口 extends Remote ==> 远程对象类 implements 特殊的远程接口

并且在这个特殊的接口中声明的方法都需要抛出java.rmi.RemoteException 异常,例如:

import java.rmi.*;

public interface RemoteObject extends Remote{

    String doSomething(String thing) throws RemoteException;

    String say() throws RemoteException;

    String sayGoodbye() throws RemoteException;
}

Remote Object

而远程对象类通常还需要继承 java.rmi.server.UnicastRemoteObject 类,在RMI中 UnicastRemoteObject类是与Object超类等效的,该类提供了equals( ) , hashcode( ), toString( )方法;并且在RMI运行时,继承UnicastRemoteObject类的子类会被exports 出去,绑定随机端口,开始监听来自客户端(Stubs)的请求。
About Export of Remote Object

在 export 时,会随机绑定一个端口,监听客户端的请求,所以即使不注册,直接请求这个端口也可以通信,这部分在后面学习与解读RMI攻击时会详细展开。如果不想让远程对象成为 UnicastRemoteObject 的子类,后面就需要主动的使用其静态方法 exportObject 来手动 export 对象。

同时创建远程对象类需要显示定义构造方法并抛出RemoteException,即使是个无参构造也需要如此,不然会报错。

import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObjectImpl extends UnicastRemoteObject implements RemoteObject {

    protected RemoteObjectImpl() throws RemoteException {
    }

    @Override
    public String doSomething(String thing) throws RemoteException {
        return String.format("Doing ", thing);
    }

    @Override
    public String say() throws RemoteException {
        return "This is the say Method";
    }

    @Override
    public String sayGoodbye() throws RemoteException {
        return "GoodBye RMI";
    }
}

关于远程对象以及远程对象所需要implements的'特殊的远程接口'的编写就大致如上所述。

下面学习一下如何通过RMI进行对远程对象上某个方法的调用,在此之前还需要了解一个概念 RMI registry。

RMI registry

About registry

这个概念很好理解,它类似一个电话薄或者路由表,可以通过注册表(RMI registry)来查找对另一台主机上已注册远程对象的引用

好比通过电话薄根据姓名查找到某人电话号码然后通话或者说查找路由表中某ip的路由,通过那个gateway发送就能找到该ip的主机。

而在RMI中的注册表(registry)就是类似于这种机制,当我们想要调用某个远程对象的方法时,通过该远程对象在注册时提供在注册表(registry)中的别名(Name),来让注册表(registry)返回该远程对象的引用,后续通过该引用实现远程方法调用。

注册表(registry)由java.rmi.Namingjava.rmi.registry.Registry实现。

Naming类提供了进行存储及获取远程对象等操作注册表(registry)的相关方法,如bind()实现远程对象别名与远程对象之间的绑定。其他的还有如:

查询(lookup)、重新绑定(rebind)、接触绑定(unbind)、list(列表)
而这些方法的具体实现,其实是调用 LocateRegistry.getRegistry 方法获取了 Registry 接口的实现类,并调用其相关方法进行实现的

比如bind方法的源码

/**
     * Binds the specified <code>name</code> to a remote object.
     *
     * @param name a name in URL format (without the scheme component)
     * @param obj a reference for the remote object (usually a stub)
     * @exception AlreadyBoundException if name is already bound
     * @exception MalformedURLException if the name is not an appropriately
     *  formatted URL
     * @exception RemoteException if registry could not be contacted
     * @exception AccessException if this operation is not permitted (if
     * originating from a non-local host, for example)
     * @since JDK1.1
     */
    public static void bind(String name, Remote obj)
        throws AlreadyBoundException,
            java.net.MalformedURLException,
            RemoteException
    {
        ParsedNamingURL parsed = parseURL(name);
        Registry registry = getRegistry(parsed);

        if (obj == null)
            throw new NullPointerException("cannot bind to null");

        registry.bind(parsed.name, obj);
    }

这个类提供的每个方法都有一个 URL 格式的参数,格式如下://host:port/name

  • host 表示注册表所在的主机
  • port 表示注册表接受调用的端口号,默认为 1099
  • name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键字

java.rmi.registry.Registry接口在RMI中有两个实现类RegistryImpl 以及 RegistryImpl_Stub

创建注册中心(registry)

一般通过LocateRegistry#createRegistry()方法创建

import java.rmi.*;
import java.rmi.registry.LocateRegistry;

public class Registry {
    public static void main(String[] args) {
        try {
          //默认绑定1099端口
            LocateRegistry.createRegistry(1099);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

创建Server端

通过Server端将需要调用的类(远程对象类)进行别名与远程对象的绑定

import java.net.MalformedURLException;
import java.rmi.*;

public class RemoteServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
        //实例化远程对象类,创建远程对象
        RemoteObject remoteObject = new RemoteObject();
        //通过Naming.bind()方法绑定别名与 RemoteObject
        Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven", remoteObject);
    }
}

Client端调用

创建Client端,通过远程对象引用实现对远程方法的调用

import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        //创建注册中心对象
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        //打印注册中心中的远程对象别名list
        System.out.println(Arrays.toString(registry.list()));

        //通过别名获取远程对象存根stub并调用远程对象的方法
        RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven");
        System.out.println(stub.say());
        System.out.println(stub.doSomething("Sing Song"));
        System.out.println(stub.sayGoodbye());

    }
}

这里用一张Longofo师傅的图加深下理解

RMI Demo

上面大概将RMI整个过程中的三个角色Client、RMI Registry、Server端简单的代码demo放了出来,下面我们把它揉到一起实现一次简单的RMI过程。

那么一般RMI Registry和Server端是在同一端的,我们就把它们放在同一个类中

RMI Registry&Server

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RegistryServer {
    public static void main(String[] args) {

        try {
            //创建Registry
            Registry registry = LocateRegistry.createRegistry(1099);

            //实例化远程对象类,创建远程对象
            RemoteObject remoteObject = new RemoteObject();
            //通过Naming类绑定别名与 RemoteObject
            Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven", remoteObject);
            System.out.println("Registry&Server Start");
            //打印别名
            System.out.println("Registry List: " + Arrays.toString(registry.list()));

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

RMI Client

package Rmi;

import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        //获取注册中心对象
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        //打印注册中心中的远程对象别名list
        System.out.println(Arrays.toString(registry.list()));

        //通过别名获取远程对象存根stub并调用远程对象的方法
        RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven");
        System.out.println(stub.say());
        System.out.println(stub.doSomething("Sing Song"));
        System.out.println(stub.sayGoodbye());

    }
}

先运行RMI Registry&Server端

之后启动RMI Client端

成功调用了远程对象的方法。

如果运行过程抛出了如下图的异常,一般是端口占用的问题。

建议:

  1. 排查是否1099端口起了别的服务
  2. 因为RemoteInterface是存在于RegistryServer和Client两端的项目中,那么这个接口代码是需要一致的;且在实例化RemoteObject时,远程对象的类型应为RemoteInterface。例如:

RegistryServer:

RemoteInterface remoteObject = new RemoteObject();

Client:

RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven");

还有一个点就是关于RemoteInterface接口应该在Registry/Server/Client端都存在,否则在registry.lookup之后拿到stub但是无法通过 . 调用远程对象的相关方法。

那么接下来是当传递的参数不是String而是一个对象时需要注意的点,涉及到两个概念

  1. RMI的动态加载类,java.rmi.server.codebase
  2. Java SecurityManager安全管理机制

RMI 流程

小结一下如何从0实现1次RMI:

  1. 创建远程对象接口(RemoteInterface)
  2. 创建远程对象类(RemoteObject)实现远程对象接口(RemoteInterface)并继承UnicastRemoteObject类
  • 创建Registry&Server端,一般Registry和Server都在同一端。

  • 创建注册中心(Registry)LocateRegistry.getRegistry("ip", port);

  • 创建Server端:主要是实例化远程对象

  • 注册远程对象:通过Naming.bind(rmi://ip:port/name ,RemoteObject) 将name与远程对象(RemoteObject)进行绑定

  • 远程对象接口(RemoteInterface)应在Client/Registry/Server三个角色中都存在

  • 创建Client端

  • 获取注册中心LocateRegistry.getRegistry('ip', prot)

  • 通过registry.lookup(name) 方法,依据别名查找远程对象的引用并返回存根(Stub)

  • 通过存根(Stub)实现RMI(Remote Method Invocation)

RMI 动态加载类

在RMI过程中Client端和Server端的数据传输有如下特点:

RMI的Client和Server&Registry进行通信时是将数据进行序列化传输的,所以当我们传递一个可序列化的对象作为参数进行传输时,在Server端肯定会对其进行反序列化。

关于RMI的动态加载类机制:

如果RMI需要用到某个类但当前JVM中没有这个类,它可以通过远程URL去下载这个类。那么这个URL可以是http、ftp协议,加载时可以加载某个第三方类库jar包下的类,或者在指定URL时在最后以\结束来指定目录,从而通过类名加载该目录下的指定类。

动态加载时用到的是java.rmi.server.codebase属性,需要将URL赋值给该属性

一般是通过System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8080/");设置

或以java -Djava.rmi.server.codebase="http://myserver/foo/"的方式指定URL

还有就是Java SecurityManager机制,这个在CC5中也提到了:
当运行未知的Java程序的时候,该程序可能有恶意代码(删除系统文件、重启系统等),为了防止运行恶意代码对系统产生影响,需要对运行的代码的权限进行控制,这时候就要启用Java安全管理器。该管理器默认是关闭的。

而在RMI中进行动态加载类时有一个**限制[1]**为:

需要设置RMISecurityManager作为安全管理器(SecurityManager),这样RMI时才会动态加载类。

System.setSecurityManager(new RMISecurityManager());

同时需要给定一个管理策略文件,该文件以.policy结尾,内容如下可以给定全部权限

grant {
    permission java.security.AllPermission;
};

之后可通过读取静态资源文件的方式加载该管理策略

System.setProperty("java.security.policy", RemoteServer.class.getClassLoader().getResource("rmi.policy").toString());

那么还有一个**限制[2]**为:

属性 java.rmi.server.useCodebaseOnly 的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

动态加载类主要是分为两个场景,角色分别为Client和Server

  1. Client端接受通过RMI远程调用Server端某个方法产生的返回值,但是该返回值是个对象且Client端并没有该对象的类,那么就可以通过Server端提供的URL去动态加载类。
  2. Server端在RMI过程中收到Client端传来的参数,该参数可能是个对象,如果该对象对应的类在Server端并不存在,那么就可以通过Client端提供的URL去动态加载类

场景1:Client端动态加载Server端

测试环境均为JDK7u17

RemoteInterface

import java.rmi.*;

public interface RemoteInterface extends Remote{

    String doSomething(String thing) throws RemoteException;

    String say() throws RemoteException;

    String sayGoodbye() throws RemoteException;

    String sayServerLoadClient(Object name) throws RemoteException;

    Object sayClientLoadServer() throws RemoteException;
}

RegistryServer端的RemoteObject(implements RemoteInterface)

import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {

    protected RemoteObject() throws RemoteException {
    }

    @Override
    public String doSomething(String thing) throws RemoteException {
        return String.format("Doing ", thing);
    }

    @Override
    public String say() throws RemoteException {
        return "This is the say Method";
    }

    @Override
    public String sayGoodbye() throws RemoteException {
        return "GoodBye RMI";
    }

    @Override
    public String sayServerLoadClient(Object name) throws RemoteException {
        return name.getClass().getName();
    }

    @Override
    public Object sayClientLoadServer() throws RemoteException {
        return new ServerObject();
    }
}

Server端待动态加载的类

import java.io.Serializable;

public class ServerObject implements Serializable {
    private static final long serialVersionUID = 3274289574195395731L;
}

RegistryServer2

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RegistryServer2 {
    public static void main(String[] args) {

        try {
            System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8080/");
            //创建Registry
            Registry registry = LocateRegistry.createRegistry(1099);

            //实例化远程对象类,创建远程对象
            RemoteInterface remoteObject = new RemoteObject();
            //通过Naming类绑定别名与 RemoteObject
            Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven2", remoteObject);
            System.out.println("Registry&Server Start");
            //打印别名
            System.out.println("Registry List: " + Arrays.toString(registry.list()));

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

RMIClient2

import java.rmi.NotBoundException;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RMIClient2 {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        //设置java.security.policy属性值与RMISecurityManager
        System.setProperty("java.security.policy", RMIClient2.class.getClassLoader().getResource("rmi.policy").getFile());
        System.setSecurityManager(new RMISecurityManager());
        //获取注册中心对象
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        //打印注册中心中的远程对象别名list
        System.out.println(Arrays.toString(registry.list()));

        //通过别名获取远程对象存根stub并调用远程对象的方法
        RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven2");
        System.out.println(stub);
        System.out.println(stub.say());
        System.out.println(stub.doSomething("Sing Song"));
        System.out.println(stub.sayGoodbye());

        System.out.println("The Class Name: " + stub.sayClientLoadServer().getClass().getName());

    }
}

测试结果

场景2:Server端动态加载Client端

RMIClient

import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        //将指定URL赋值给codebase
        System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8080/");
        //创建注册中心对象
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        //打印注册中心中的远程对象别名list
        System.out.println(Arrays.toString(registry.list()));

        //通过别名获取远程对象存根stub并调用远程对象的方法
        RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven");

        System.out.println(stub.say());
        System.out.println(stub.doSomething("Sing Song"));
        System.out.println(stub.sayGoodbye());
        System.out.println("The Class Name: " + stub.sayServerLoadClient(new ClientObject()));

    }
}

Client端待动态加载的类

这个类限制不多,主要是注意serialVersionUID需要设置一下,以免反序列化时出问题。

import java.io.Serializable;

public class ClientObject implements Serializable {
      private static final long serialVersionUID = 3274289574195395731L;

}

Registry&Server端

import java.rmi.Naming;
import java.rmi.RMISecurityManager;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RegistryServer {
    public static void main(String[] args) {

        try {
            System.setProperty("java.security.policy", RegistryServer.class.getClassLoader().getResource("rmi.policy").getFile());
            System.setSecurityManager(new RMISecurityManager());

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

            //实例化远程对象类,创建远程对象
            RemoteObject remoteObject = new RemoteObject();
            //通过Naming类绑定别名与 RemoteObject
            Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven", remoteObject);
            System.out.println("Registry&Server Start");
            //打印别名
            System.out.println("Registry List: " + Arrays.toString(registry.list()));

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

rmi.policy

grant {
    permission java.security.AllPermission;
};

测试结果

END

本来记录的时候心情很美丽,结果学起来真的很吃力。

最近有点懒忙,后续关于RMI攻击的深入解读还不知道何时能搞定。

测试代码后续会贴到Github

如有错误还烦请各位师傅不吝赐教。

Reference

https://www.oreilly.com/library/view/learning-java/1565927184/ch11s04.html

https://su18.org/post/rmi-attack/

https://paper.seebug.org/1091/

https://github.com/longofo/rmi-jndi-ldap-jrmp-jmx-jms

所有内容仅限于维护网络安全学习参考

相关文章