添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
完美的镜子  ·  RMI [Solved] ...·  2 天前    · 
高大的手术刀  ·  RMI Exception | MiniWiki·  2 月前    · 
一身肌肉的板栗  ·  Caused by: ...·  2 月前    · 
腼腆的绿豆  ·  Error in rconfigure() ...·  2 月前    · 
粗眉毛的八宝粥  ·  France's La Poste ...·  4 周前    · 
愉快的包子  ·  FULL-PACK DELIVERY | ...·  5 月前    · 

RMI概念简述

RMI(Remote Method Invocation)即远程方法调用,是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,比如CORBA、WebService,提到的这两种都是 独立于编程语言 的。

Java RMI是专为Java环境设计的远程方法调用机制,是一种用于实现远程调用(RPC,Remote Procedure Call)的一组Java API,能直接传输序列化后的Java对象和分布式垃圾收集。 它的实现依赖于JVM 支持从一个JVM到另一个JVM的调用

在Java RMI中:

  • 远程服务器实现具体的Java方法并提供接口
  • 客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法
  • 其中对象是通过 序列化 方式进行编码传输的
  • RMI全部的宗旨就是尽可能简化远程接口对象的使用
  • 平时说的反序列化漏洞的利用经常涉及到RMI,就是这个意思

    RMI依赖的通信协议为JRMP(Java Remote Message Protocol,Java远程消息交换协议),该协议是为Java定制的,JRMP协议运行在Java RMI下、TCP/IP层之上的一种协议,要求服务端与客户端都必须是Java编写的。

    Java RMI极大地依赖于接口。在需要创建一个远程对象的时候,程序员通过传递一个接口来隐藏底层的实现细节。客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信。这样一来,程序员只需关心如何通过自己的接口句柄发送消息。

    多说两句:

    Java本身对RMI规范的实现默认使用的是JRMP协议,而Weblogic对RMI规范的实现使用T3协议,Weblogic之所以开发T3协议,是因为他们需要可扩展、高效的协议来使用Java构建企业级的分布式对象系统。

    众所周知,一般情况下Java方法调用指的是同一个JVM内部方法的调用,而RMI与之恰恰相反。

    到这里,做个简短通俗的总结:RMI是一种行为,这种行为指的是Java远程方法调用,调用的是方法。

    RMI能够帮助我们查找并执行远程对象的方法。通俗地说,远程调用就像A主机存放一个Class文件,然后在B机器中调用这个Class的方法。

    Java RMI,跨JVM,是允许运行在JVM内部的一个对象调用运行在另一个JVM内部的对象的方法。 这两个JVM可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

    RMI和序列化的缘分

    RMI的传输是基于序列化机制的。

    如果一个RMI接口的参数类型为一个对象,那么我们客户端就可以发送一个自己构建的对象,来让服务端将这个对象反序列化出来。当然,前提是服务端的classpath里面有合适的类,或者后续恶意类加载也可以。

    RMI代理模式

    设计模式

    RMI的设计模式中,主要包括以下三个部分的角色:

  • Registry:提供服务注册与服务获取。即Server端向Registry注册服务,比如地址、端口等一些信息,Client端从Registry获取远程对象的一些信息,如地址、端口等,然后进行远程调用。
  • Server:服务端,远程方法的提供者,并向Registry注册自身提供的服务
  • Client:客户端,远程方法的消费者,从Registry获取远程方法的相关信息并且调用
  • 交互过程

    一般习惯,注册中心和服务端都在一起,RMI交互过程如图所示:

    RMI由3个部分构成:

  • RMI Registry(JDK提供的一个可以独立运行的程序,在bin目录下)
  • 第二个是Server端的程序,对外提供远程对象
  • 第三个是Client端的程序,想要调用远程对象的方法。
  • 在设计模式中,3个角色的交互过程可简单概述为:

  • 服务端创建远程对象,Skeleton侦听一个随机的端口x,这个端口是用来进行远程通信服务的,以供客户端调用。
  • 启动RMI Registry服务,启动时可以指定服务监听的端口,也可以使用默认的端口(1099)。
  • Server端在本地先实例化一个提供服务的 实现类 ,然后通过RMI提供的Naming/Context/Registry等类的bind或rebind方法将刚才实例化好的实现类注册到RMI Registry上并对外暴露一个名称Name;
  • 客户端对RMI Registry发起请求,根据提供的 Name 从RMI Registry得到 Stub Stub 中包含与 Skeleton 通信的信息(地址,端口等),两者建立通信, Stub 作为客户端代理请求服务端代理 Skeleton 并进行远程方法调用
  • 客户端 Stub 调用远程方法,调用结果先返回给 Skeleton Skeleton 再返回给客户端 Stub Stub 再返回给客户端本身。
  • 多说一句,一般我们知道RMI Registry的端口(1099)就可以了。

    通信端口会包含在Stub中,Stub是RMI Registry给Client分发的。

    此外,我们可以看到,从逻辑上来看数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的。

    方法调用从客户端经存根(Stub)、远程引用层(Remote Reference Layer)和传输层(Transport Layer)向下,传递给主机,然后再次经传输层,向上穿过远程调用层和骨干网(Skeleton),到达服务器对象。

    存根Stub位于客户端,扮演着远程服务器对象的代理,使该对象可被客户激活。远程引用层处理语义、管理单一或多重对象的通信,决定调用是应发往一个服务器还是多个。

    传输层管理实际的连接,并且追踪可以接受方法调用的远程对象。Skeleton完成对服务器对象实际的方法调用,并获取返回值。返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回。最后,存根Stub获得返回值。

    Stub:扮演着远程服务器对象的代理的角色,使该对象可被客户激活。

    远程引用层:处理语义、管理单一或多重对象的通信,决定调用是应发往一个服务器还是多个。

    传输层:管理实际的连接,并且追踪可以接受方法调用的远程对象。

    Skeleton:完成对服务器对象实际的方法调用,并获取返回值。

    返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回。最后,存根获得返回值。

    Stub和Skeleton

    RMI的客户端和服务器并不直接通信,客户与远程对象之间采用的代理方式进行 Socket 通信。为远程对象分别生成了客户端代理和服务端代理,其中位于客户端的代理类称为Stub即存根(包含服务器 Skeleton 信息),位于服务端的代理类称为 Skeleton 即骨干网。

    RMI Registry

    RMI注册表 ,默认监听在 1099 端口上, Client 通过 Name RMI Registry 查询,得到这个绑定关系和对应的 Stub

    远程对象

    在RMI中的核心就是远程对象,一切都是围绕这个东西来进行的。

    顾名思义,远程对象存在于服务端,以供客户端调用。

    任何可以被远程调用的对象都必须实现 java.rmi.Remote 接口,远程对象的实现类必须继承UnicastRemoteObject类。

    如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,并且在远程对象的构造方法中调用UnicastRemoteObject.exportObject()静态方法。

    这个远程对象中可能有很多个函数,但是只有在远程接口中声明的函数才能被远程调用,其他的公共函数只能在本地的JVM中使用。

    序列化传输数据

    客户端远程调用时传递给服务器的参数,服务器执行后传递给客户端的返回值。参数或者返回值,在传输时候会被序列化,在被接受时会被反序列化。

    因此这些传输的对象必须可以被序列化,相应的类必须实现 java.io.Serializable 接口,并且客户端的 serialVersionUID 字段要与服务器端保持一致。

    如图,先假设:

  • 有两个远程服务接口可供Client调用,Factory和Product接口
  • FactoryImpl类实现了Factory接口,ProductImpl类实现了Product接口
  • 工厂模式的处理流程为:

  • FactoryImpl被注册到了RMI Registry中;
  • Client端请求一个Factory的引用;
  • RMI Registry返回Client端一个FactoryImpl的引用;
  • Client端调用FactoryImpl的远程方法请求一个ProductImpl的远程引用;
  • FactoryImpl返回给Client端一个ProductImpl引用;
  • Client通过ProductImpl引用调用远程方法;
  • 可以看到,客户端向 RMI Registry 请求获取到指定的FactoryImpl的引用后,再通过调用FactoryImpl的远程方法请求一个ProductImpl的远程引用,从而调用到ProductImpl引用指向的远程方法。

    上面提到的创建 FactoryImpl对象 ,设置 FactoryImpl对象 指向ProductImp (通过HTTP等协议定位,可以位于其他服务器),具有指向功能的对象也可以叫做 reference对象

    这种RMI+Reference的技术在JNDI注入中是单独作为一种利用方式。

    这里执行远程对象的方法的是RMI通讯的客户端,为攻击客户端的方式,是在具体的代码和利用场景可以参考FastJson中的JNDI注入。

    java.rmi包简介

    Remote

    一个接口interface,这个interface中没有声明任何方法。只有定义在“remote interface”,即继承了Remote的接口中的方法,才可以被远程调用。

    RemoteException

    RemoteException是所有在远程调用中所抛出异常的超类,所有能够被远程调用的方法声明,都需要抛出此异常。

    Naming

    提供向 RMI Registry 保存远程对象引用或者从 RMI Registry 获取远程对象引用的方法。这个类中的方法都是静态方法,每一个方法都包含了一个类型为String的name参数, 这个参数是URL格式,形如://host:port/name。

    Registry

    一个interface, 其功能和Naming类似,每个方法都有一个String类型的name参数,但是这个name不是URL格式,是远程对象的一个命名。Registry的实例可以通过方法LocateRegistry.getRegistry()获得。

    LocateRegistry

    用于获取到 RMI Registry 的一个连接,这个连接可以用于获取一个远程对象的引用。也可以创建一个注册中心。

    RemoteObject

    重新覆写了Object对象中的equals,hashCode,toString方法,从而可以用于远程调用。

    UnicastRemoteObject

    用于获得一个stub。这个stub封装了底层细节,用于和远程对象进行通信。

    Unreferenced

    一个interface, 声明了方法:void unreferenced()。如果一个远程对象实现了此接口,则这个远程对象在没有任何客户端引用的时候,这个方法会被调用。

    RMI动态类加载

    RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的对象class文件可以使用Web服务的方式进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。 对于客户端而言,服务端返回值也可能是一些子类的对象实例,而客户端并没有这些子类的class文件。 如果需要客户端正确调用这些子类中被重写的方法,则同样需要有运行时动态加载额外类的能力。客户端使用了与RMI注册表相同的机制。RMI服务端将URL传递给客户端,客户端通过HTTP请求下载这些类。

    在之后的JNDI注入和反序列化漏洞的利用中,正是涉及到了RMI动态类加载。

    编写RMI的步骤

    定义服务端供远程调用的类

    在此之前先定义一个可序列化的Model层的用户类,其实例可放置于服务端进行远程调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    import java.io.Serializable;

    public class PersonEntity implements Serializable {
    private int id;
    private String name;
    private int age;

    public void setId(int id) {
    this.id = id;
    }

    public int getId() {
    return id;
    }

    public void setName(String name) {
    this.name = name;
    }

    public String getName() {
    return name;
    }

    public void setAge(int age) {
    this.age = age;
    }

    public int getAge() {
    return age;
    }
    }

    定义一个远程接口

    远程接口必须继承java.rmi.Remote接口,且抛出RemoteException错误:

    1
    2
    3
    4
    5
    6
    7
    import java.rmi.Remote;
    import java.rmi.RemoteException;
    import java.util.List;

    public interface PersonService extends Remote {
    public List<PersonEntity> GetList() throws RemoteException;
    }

    开发接口的实现类

    建立PersonServiceImpl实现远程接口,注意此为远程对象实现类,需要继承UnicastRemoteObject(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class PersonServiceImpl extends UnicastRemoteObject implements PersonService {

    protected PersonServiceImpl() throws RemoteException {
    // TODO Auto-generated constructor stub
    super();
    }

    @Override
    public List<PersonEntity> GetList() throws RemoteException {
    // TODO Auto-generated method stub
    System.out.println("Get Person Start!");
    List<PersonEntity> personList = new LinkedList<PersonEntity>();

    PersonEntity person1 = new PersonEntity();
    person1.setAge(3);
    person1.setId(0);
    person1.setName("0range");
    personList.add(person1);

    PersonEntity person2 = new PersonEntity();
    person2.setAge(18);
    person2.setId(1);
    person2.setName("Wind");
    personList.add(person2);
    return personList;
    }
    }

    创建Server和Registry

    其实Server和Registry可以单独运行创建,其中Registry可通过代码启动也可通过rmiregistry命令启动,这里只进行简单的演示,将Server和Registry的创建、对象绑定注册表等都写到一块,且Registry直接代码启动:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import java.rmi.Naming;
    import java.rmi.registry.LocateRegistry;

    public class Program {
    public static void main(String[] args) {
    try {
    PersonService personService=new PersonServiceImpl();
    //注册通讯端口
    LocateRegistry.createRegistry(9898);
    //注册通讯路径
    Naming.rebind("rmi://127.0.0.1:9898/PersonService", personService);
    System.out.println("Service Start!");
    } catch (Exception e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
    }
    }

    创建客户端并查找调用远程方法

    这里我们通过Naming.lookup()来查找RMI Server端的远程对象并获取到本地客户端环境中输出出来:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import java.rmi.Naming;
    import java.util.List;

    public class Client {
    public static void main(String[] args){
    try{
    //调用远程对象,注意RMI路径与接口必须与服务器配置一致
    PersonService personService=(PersonService) Naming.lookup("rmi://127.0.0.1:9898/PersonService");
    List<PersonEntity> personList=personService.GetList();
    for(PersonEntity person:personList){
    System.out.println("ID:"+person.getId()+" Age:"+person.getAge()+" Name:"+person.getName());
    }
    }catch(Exception ex){
    ex.printStackTrace();
    }
    }
    }

    最后,我们看下模拟运行的场景。

    先启动Server和Register,开启成功后显示“Server Start!”,然后运行Client程序,可以看到客户端成功获取到了在Register注册的Server中的远程对象的内容:

    几个函数

    这里小结下几个函数:

  • bind(String name, Object obj):注册对象,把对象和一个名字name绑定,这里的name其实就是URL格式。如果该名字已经与其他对象绑定,则抛出NameAlreadyBoundException错误;
  • rebind(String name, Object obj):注册对象,把对象和一个名字name绑定。如果改名字已经与其他对象绑定,不会抛出NameAlreadyBoundException错误,而是把当前参数obj指定的对象覆盖原先的对象(更暴力);
  • lookup(String name):查找对象,返回与参数name指定的名字所绑定的对象;
  • unbind(String name):注销对象,取消对象与名字的绑定;
  • 经典例子

    这里我再写一个例子,用于强化。

    首先我还是生成一个接口,叫它 IRemoteMath

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import java.rmi.Remote;
    import java.rmi.RemoteException;

    /**
    * 必须继承Remote接口。
    * 所有参数和返回类型必须序列化(因为要网络传输)。
    * 任意远程对象都必须实现此接口。
    * 只有远程接口中指定的方法可以被调用。
    */
    public interface IRemoteMath extends Remote {
    // 所有方法必须抛出RemoteException
    public double add(double a, double b) throws RemoteException;
    public double subtract(double a, double b) throws RemoteException;

    }
  • 这个远程接口必须继承Remote类;
  • 内部抽象方法必须都有throw RemoteException
  • 接下来我写一个接口的实现类,叫做 RemoteMath

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /**
    * 服务器端实现远程接口。
    * 必须继承UnicastRemoteObject,以允许JVM创建远程的存根/代理。
    */
    public class RemoteMath extends UnicastRemoteObject implements IRemoteMath {

    private int numberOfComputations;

    protected RemoteMath() throws RemoteException {
    numberOfComputations = 0;
    }

    @Override
    public double add(double a, double b) throws RemoteException {
    numberOfComputations++;
    System.out.println("Number of computations performed so far = "
    + numberOfComputations);
    return (a+b);
    }

    @Override
    public double subtract(double a, double b) throws RemoteException {
    numberOfComputations++;
    System.out.println("Number of computations performed so far = "
    + numberOfComputations);
    return (a-b);
    }

    }
  • 这个类其实就是接口实现类
  •