Java RMI篇

0x00 底层协议

RPC

Java RMI最基础的解释,是实现了RPCJava APIRPCRemote Procedure Call,远程过程调用)是一种常见的分布式系统通信协议,它允许一个程序(客户端)通过网络向另一个程序(服务器)发送请求,以执行服务器上的某个过程或方法。这种通信方式可以使得客户端像调用本地过程一样调用服务器上的过程,从而实现了不同系统之间的远程方法调用。

RPC需要解决的问题

  • 解决分布式系统中,服务之间的调用
  • 远程调用像本地调用一样方便

基本过程

  1. A调用远程B
  2. 将调用的信息以序列化(将Java对象转换为二进制数据流的过程)的方式进行发送
  3. B接受信息,然后反序列化(将二进制数据流转换为Java对象的过程)
  4. 将内容执行,然后再以序列化的方式返回结果
  5. A反序列化拿到结果

RMI

Java RMIRemote Method Invocation)是RPCJava实现,它提供了一组API和工具,使得Java程序能够方便地实现RPC通信。具体来说,Java RMI允许一个Java虚拟机(JVM)上的对象调用另一个JVM上的对象的方法,实现了分布式系统中的远程方法调用, 只不过RMIJava独 有的⼀种机制。它利用Java的序列化和反序列化技术,将对象序列化为字节流,通过网络传输到另一个JVM,再反序列化为对象,从而实现跨JVM的方法调用。

JMX与JMS

JMX:是一个为应用程序、设备、系统等植入管理功能的框架,可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。它是一套标准的代理和服务,允许用户在任何Java应用程序中使用这些代理和服务实现管理。

JMS:是访问企业消息系统的标准API,用于在Java应用程序中进行消息交换,并且通过提供标准的产生、发送、接收消息的接口简化企业应用的开发。JMS既支持点对点(point-to-point)的域,又支持发布/订阅(publish/subscribe)类型的域,并且提供对经认可的消息传递、事务型消息的传递、一致性消息和具有持久性的订阅者支持。

总结,JMX主要应用于管理和监控Java应用程序、设备、系统等,而JMS则主要应用于Java应用程序之间的消息交换,以支持企业应用的开发。

RPC、RMI、JMS和JMX

区别

  • RPC是一种平台中立的应用程序间通信方式,支持多种语言;
  • RMIJava专用的远程对象调用技术;
  • JMS主要用于Java应用程序之间的消息交换;
  • RPCRMIJMS都是用于不同目的的通信协议,而JMX则是一种用于管理和监控Java应用程序的工具。

传输方式

  • JMS:对象在物理上被异步从网络的某个JVM上直接移动到另一个JVM
  • RMI:对象是绑定在本地JVM中,只有函数参数和返回值是通过网络传送

方法调用

  • 通信方式:RMI基于请求/应答的同步通信模式,而JMS支持同步和异步消息处理模式。
  • 耦合度:RMI是紧耦合结构,而JMS建立了一个松散耦合的通信框架。
  • 需求对象获得方式:在RMI中,对象是绑定在本地JVM中,只有参数和返回值是通过网络传送。而在JMS中,对象是在物理上被异步从网络的某个节点移动到另一个节点。

总结来说,RMI基于请求/应答的同步通信模式,采用紧耦合结构,并且对象是绑定在本地JVM中;JMS支持同步和异步消息处理模式,建立了松散耦合的通信框架,并且对象是在物理上被异步从网络的某个节点移动到另一个节点。

此外,RMIJava专用的远程对象调用技术,只适用于Java编写的应用程序,而JMS则是一种允许应用程序创建、发送、接受和读取消息的Java API,主要用于在Java应用程序之间进行消息交换,以支持企业应用的开发。

0x01 RMI概述

Java RMI(Java Remote Method Invocation)Java远程方法调用。RMI用于构建分布式应用程序,RMI实现了Java程序之间跨JVM的远程通信。RMI就是Java中对RPC的一种实现,对其进行了封装。

  • 基于RMI利用的JDK版本<=6u1417u1318u121
  • 基于LDAP利用的JDK版本<=6u2117u2018u191

RMI架构

  1. RMI分为三部分
  • Registry 存放着远程对象的注册表(IP,port,标识符)
  • Server 提供远程的对象
  • Client 调用远程的对象
  1. 实现RMI所需的API几乎都在:
  • java.rmi:提供客户端需要的类、接口和异常;
  • java.rmi.server:提供服务端需要的类、接口和异常;
  • java.rmi.registry:提供注册表的创建以及查找和命名远程对象的类、接口和异常;

RMI通信模型

RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:

  1. RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)

先跟进getRegistry方法,然后接着去调用getRegistry方法,如果传参为LocateRegistry.getRegistry("127.0.0.1", 8989, null);,则会直接调用144行的getRegistry方法

返回一个RegistryImpl_Stub

  1. Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。
  2. RemoteCall序列化RMI服务名称、Remote对象。
  3. RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端的远程引用层。
  4. RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  5. Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。
  6. Skeleton处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
  7. RMI客户端反序列化服务端结果,获取远程对象的引用。
  8. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  9. RMI客户端反序列化RMI远程方法调用结果。

0x02 RMI方法

  1. 实现RMI Server分为三部分:
  • 继承java.rmi.Remote接口,其中定义需要远程调用的函数hello()
  • 实现此接口的类
  • new一个主类,用来创建Registry,并将上面的类实例化后绑定到一个地址
  1. RMI Client
  • 可以使用registry.rebindNaming.lookup
  • 然后就可以正常调用

registry.rebind

通过注册表绑定远程对象:

registry.rebind:这个方法用于将远程对象绑定到注册表中。当一个服务端创建一个对象时,它会使用bind()rebind()方法将该对象注册到注册表中。客户端可以使用该对象的引用(例如,通过HelloRegistryFacade)从注册表中获取对象,例如通过lookup()方法。

Server

  1. 定义接口名为RMIInterface的远程接口

定义接口继承自java.rmi.Remote接口,定义一个hello()方法作为接口,修饰符为public。并且定义的方法需要抛出RemoteException的异常。

package server;

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

public interface RMIInterface extends Remote {
    public String hello() throws RemoteException;
}
  1. 编写远程接口的实现类RMIImpl
  • 继承UnicastRemoteObject类(提供了很多支持RMI的方法),这些方法可以通过JRMP协议导出一个远程对象的引用,并通过动态代理构建一个可以和远程对象交互的Stub对象。
  • 重写RMIInterface接口中的hello()方法。
package server;

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

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

    @Override
    public String hello() {
        System.out.println("call hello()");
        return "this is hello()";
    }
}
  1. 创建RMIServer实例,创建一个注册表,将需要提供给客户端的对象注册到注册表中

端口8989开启RMI服务,以键值对的形式存储RMI_PATHrmiInterface的对应关系

  • rmi://127.0.0.1:8989/hello<==>RMIImpl类实例

然后通过registry.rebind(RMI_NAME, RMIImpl)绑定对应关系,然后通过RMIImpl类去实现rmiInterface接口中的方法

package server;

import java.net.MalformedURLException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, MalformedURLException {
        // 创建远程对象
        RMIInterface hello = new RMIImpl();
        // 创建注册表
        Registry registry = LocateRegistry.createRegistry(1099);
        // 将远程对象注册到注册表里面,并且设置值为hello
        registry.rebind("hello", hello);
    }
}

Client

  1. 创建RMIClient实例,远程调用对象
package client;

import server.RMIInterface;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        // 获取远程主机对象
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);
        // 利用注册表的代理去查询远程注册表中名为hello的对象
        RMIInterface hello = (RMIInterface) registry.lookup("hello");
        // 调用远程方法
        System.out.println(hello.hello());
    }
}
  1. 运行结果如下

Naming.lookup

  1. 直接通过创建远程对象使用Naming.lookup绑定:

Naming.lookup:这个方法允许客户端通过完整的URL查找已注册的服务名,也就是通过RMI的“活化”模式,将Remote Service的真实提供者移植到RMI Registry注册表所在的JVM上。

Server

LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", test);

创建并运行RMI Registry,将test对象绑定到Hello名字上,test对象就是实现了RMIInterface接口的类

Naming.bind参数格式:rmi://host:port/namehostport就是RMI Registry的地址和端口,name是远程对象的名字。如果在本地运行,host和port可以省略不写,Naming.rebind("Hello", test);

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class sRmi {
    public interface RMIInterface extends Remote {
        public String hello() throws RemoteException;
    }

    public class test extends UnicastRemoteObject implements RMIInterface {

        protected test() throws RemoteException {
            System.out.println("构造方法");
        }

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

    private void start() throws Exception {
        test test = new test();
        // 创建并运行RMI Registry
        LocateRegistry.createRegistry(1099);
        // 将test对象绑定到Hello名字上
        // test对象就是实现了RMIInterface接口的类
        Naming.rebind("rmi://127.0.0.1:1099/Hello", test);
    }

    public static void main(String[] args) throws Exception {
        new sRmi().start();
    }
}

Client

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

public class cRMI {
    public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
        sRmi.RMIInterface haha = (sRmi.RMIInterface) Naming.lookup("rmi://127.0.0.1:1099/Hello");
        String hello = haha.hello();
        System.out.println(hello);
    }
}

分析RMI通信过程

目的是在远程服务器上执行代码,所以需要知道有哪些方法,前面服务端通过RMIInterface继承了Remote,并将需要调用的方法写在RMIInterface接口中,额客服端刚好可以利用RMIInterface接口

  1. 为什么端口是50639

查看ReturnData数据包,返回目标IP192.168.124.18,后面4位十六进制字节\x00\x00\xc5\xcf转换成十进制也就是端口50639

整个过程如下:

  1. 客户端连接Registry,寻找NameHello的对象),对应数据流中Call消息;
  2. 然后Registry返回一个序列化的数据(Name=Hello的对象),对应ReturnData消息;
  3. 客户端反序列化该对象,发现该对象是一个远程对象,地址:192.168.124.18:50639,然后再与此地址建立TCP连接;
  4. 最后在新的连接中,执行真正的远程方法调用,hello()方法。

引用p神的图

RMI Registry相当于网关,RMI Server可以在RMI Registry网关上注册一个Name=>对象的绑定关系。RMI Client通过NameRMI Registry网关查询,得到对应绑定的关系,然后再连接RMI Server。最后在RMI Server上执行方法。

0x03 思考

能否攻击RMI Registry

RMi Registry只有在来源地址是localhost的时候,才能调用rebindbindunbind等方法。这是Java对远程访问RMI Registry的限制。

  1. bind方法:当客户端想要和远程RMI服务器绑定时,会调用RegistryImpl_Stubbind方法。这个方法的具体调用方式是通过UtilstubClassExists方法将RegistryImpl_Stub反射进行加载,然后对数据进行writeObject序列化操作。
  2. rebind方法:当客户端想要更新已存在的注册表项时,会使用rebind方法。这个方法会将参数对象进行序列化发送至RMI Registry,然后对RMI Registry的返回数据进行了反序列化。
  3. unbind方法:当客户端想要取消和远程RMI服务器的绑定时,会使用unbind方法。这个方法会将参数对象进行序列化发送至RMI Registry,然后对RMI Registry的返回数据进行反序列化。

RMI利用codebase执行任意代码

更详细内容参考p神Java安全漫谈

使用限制:

  • 安装并配置了SecurityManager
  • Java版本低于7u216u45,或者设置了java.rmi.server.useCodebaseOnly=false
  • java.rmi.server.useCodebaseOnly=true时,Java虚拟机将只信任预先配置好的codebase,不再支持从RMI请求中获取。

利用条件苛刻,有时间再补充复现过程。

0x04 参考链接

https://govuln.com

https://longlone.top

https://xz.aliyun.com/t/4711#toc-8

https://www.anquanke.com/post/id/204740

https://www.cnblogs.com/akka1/p/16110567.html

https://www.cnblogs.com/nice0e3/p/14280278.html

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇