安全矩阵

 找回密码
 立即注册
搜索
查看: 286|回复: 0

Java安全小白的入门心得 - 初见RMI协议

[复制链接]

139

主题

186

帖子

955

积分

高级会员

Rank: 4

积分
955
发表于 2022-10-3 11:44:46 | 显示全部楼层 |阅读模式

Java安全小白的入门心得 - 初见RMI协议

RMI 协议的全称为 Remote Method Invocation (远程方法调用)协议。

RMI 应用程序通常包含两个独立的程序,一个服务器和一个客户端。典型的服务器程序会创建一些远程对象,使对这些对象的引用可访问,并等待客户端调用这些对象上的方法。典型的客户端程序获取对服务器上一个或多个远程对象的远程引用,然后调用它们上的方法。RMI 提供了服务器和客户端通信和来回传递信息的机制。这样的应用程序有时被称为分布式对象应用程序。

首先,实现一个最基本的RMI服务。

一个RMI由三部分组成:

  1. <code style="outline: 0px; max-width: 1000%; display: flex; position: relative;" liberation="" mono",="" menlo,="" courier,="" monospace;="" visibility:="" visible;="" box-sizing:="" border-box="" !important;"=""><span class="code-snippet_outer" style="outline: 0px; max-width: 1000%; visibility: visible; box-sizing: border-box !important;"><span class="code-snippet__attr" style="outline: 0px; max-width: 1000%; visibility: visible; box-sizing: border-box !important;">RMI</span> <span class="code-snippet__string" style="outline: 0px; max-width: 1000%; color: rgb(221, 17, 68); visibility: visible; box-sizing: border-box !important;">Registry</span></span></code><code style="outline: 0px; max-width: 1000%; display: flex; position: relative;" liberation="" mono",="" menlo,="" courier,="" monospace;="" visibility:="" visible;="" box-sizing:="" border-box="" !important;"=""><span class="code-snippet_outer" style="outline: 0px; max-width: 1000%; visibility: visible; box-sizing: border-box !important;"><span class="code-snippet__attr" style="outline: 0px; max-width: 1000%; visibility: visible; box-sizing: border-box !important;">RMI</span> <span class="code-snippet__string" style="outline: 0px; max-width: 1000%; color: rgb(221, 17, 68); visibility: visible; box-sizing: border-box !important;">Server</span></span></code><code style="outline: 0px; max-width: 1000%; display: flex; position: relative;" liberation="" mono",="" menlo,="" courier,="" monospace;="" visibility:="" visible;="" box-sizing:="" border-box="" !important;"=""><span class="code-snippet_outer" style="outline: 0px; max-width: 1000%; visibility: visible; box-sizing: border-box !important;"><span class="code-snippet__attr" style="outline: 0px; max-width: 1000%; visibility: visible; box-sizing: border-box !important;">RMI</span> <span class="code-snippet__string" style="outline: 0px; max-width: 1000%; color: rgb(221, 17, 68); visibility: visible; box-sizing: border-box !important;">Client</span></span></code>
复制代码

其中,Registry的生成包含于RMI Server中。

RMIServer
  1. import java.rmi.Naming;
  2. import java.rmi.Remote;
  3. import java.rmi.RemoteException;
  4. import java.rmi.registry.LocateRegistry;
  5. import java.rmi.registry.Registry;
  6. import java.rmi.server.UnicastRemoteObject;
  7. public class RMIServer {
  8.     public interface IRemoteHelloWorld extends Remote {
  9.         public String hello() throws RemoteException;
  10.     }
  11.     public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
  12.         protected RemoteHelloWorld() throws RemoteException {
  13.             super();
  14.         }
  15.         public String hello() throws RemoteException {
  16.             System.out.println("call from");
  17.             return "Hello world";
  18.         }
  19.     }
  20.     private void start() throws Exception {
  21.         RemoteHelloWorld h = new RemoteHelloWorld();
  22.         LocateRegistry.createRegistry(1099);
  23.         Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
  24.     }
  25.     public static void main(String[] args) throws Exception {
  26.         new RMIServer().start();
  27.     }
  28. }
复制代码


⼀个RMI Server分为三部分:

① ⼀个继承了 java.rmi.Remote 的接⼝,其中定义我们要远程调⽤的函数,⽐如这⾥的 hello()。

② ⼀个实现了此接⼝的类。

③ ⼀个主类,⽤来创建Registry,并将上⾯的类实例化后绑定到⼀个地址。这就是我们所谓的Server了。

RMIClient
  1. import java.rmi.Naming;
  2. import java.rmi.NotBoundException;
  3. import java.rmi.RemoteException;

  4. public class TrainMain {
  5.     public static void main(String[] args) throws Exception {
  6.         RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
  7.                 Naming.lookup("rmi://10.253.132.182:1099/Hello");
  8.         String ret = hello.hello();
  9.         System.out.println( ret);
  10.     }
  11. }
复制代码


rmi协议,格式为:rmi://host:port/Object
这里的host为127.0.0.1或者cmd命令行ipconfig命令中查询到的本机地址。
这里有一点小疑问,host使用上面两个,即192.168.220.1和192.168.83.1都可以达成同样的效果。这是VMware给本机建立的虚拟网卡,同样能够让rmi协议定位到本机。但是有一个比较奇怪的点,我打开wareshark抓包抓不到Client与Registry和Server的TCP与rmi包。只有在输错host时能够抓到tcp retransmission包。

修改host做了上述实验,先运行RMIServer,再运行RMIClient,这里我运行了3次,效果如下:

远程调用RMI测试

相信刚刚接触到rmi的同学都会有这样一个疑问,既然rmi叫Remote Method Invocation (远程方法调用)协议,那么我们能不能在云服务器上搭建RMIServer再本地远程调用方法呢。这里我就来试试:

在云服务器上编写以上代码。

javac RMIServer.javajava RMIServer
产生报错:

Error: Could not find or load main class RMIServer

搜索得可能是环境变量有问题。
  1. 打开 /etc/profile

  2. #关于java环境变量的配置,网上很多文章都是jdk8及以前。而我所使用的是jdk11,与之前的版本有所不同
  3. #这是我的jar包路径。。这里又有个小问题,jdk11以后不会自带jre文件夹,需要自己生成
  4. # 在JAVA_HOME目录执行以下命令生成jre
  5. # bin\jlink.exe --module-path jmods --add-modules java.desktop --output jre
  6. # 其中并不包含传统的tool.jar, dt.jar包,也不需要配置相关环境,只用像下面这样

  7. #在文件末尾加上
  8. set java environment
  9. JAVA_HOME=/www/wwwroot/http/2022_study/jdk-11.0.15.1
  10. PATH=$PATH:$JAVA_HOME/bin:$JAVA_HOME/jre/bin
  11. CLASSPATH=.:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
  12. export JAVA_HOME CLASSPATH PATH

  13. 保存退出 source更新环境变量
  14. source /etc/profile
复制代码


再次运行RMIServise,运行成功。

没高兴多久,报了下面的错误:

server端


client端

通过报错信息来看,应该是不允许不是本机的机器进行访问。竟然是远程方法调用为什么不能远程调用呢。

在stackoverflow找到了以下解释:
正如错误所示,您无法与远程注册中心绑定、重新绑定或解除绑定。你必须在同一个主机上运行。这是RMI的基本安全措施。

最开始与学长讨论后以为是java版本过高导致的java安全策略过强导致。所以降低了java版本,由java11降为了java8,再由java8新版本jdk1.8.0_341降到了java8老版本jdk1.8.0_71。发现还是报同样的错误。这里卡住了,继续学习p牛的java安全漫谈,看看掌握了RMI的原理以后能不能解决这个问题。

RMI原理概述

接下来就是抓包检验RMI的运行流程,p牛的java安全漫谈中能够抓到rmi协议包,但是我在本机并未抓到,在此留下疑问。
根据整个通信过程可以总结出,整个RMI远程调用方法的流程如下:
⾸先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据流中的Call消息;然后Registry返回⼀个序列化的数据,这个就是找到的Name=Hello的对象,这个对应数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 192.168.135.142:33769 ,于是再与这个地址建⽴TCP连接;在这个新的连接中,才执⾏真正远程⽅法调⽤,也就是 hello()。

用通俗一点的话来说,就是client首先向远端Registry发送一个请求,请求其中的某个对象,Registry收到client的请求后告诉client这个对象,但是这个对象是远程对象(即源代码在远端),然后client再与server建立连接,远端server执行完请求的方法,再把结果告诉client。为了容易理解什么是远端对象,接下来我们利用以下代码打印输出远端对象,看看远端对象到底是什么样的。
  1. public class TrainMain {
  2.     public static void main(String[] args) throws NotBoundException, RemoteException, MalformedURLException {
  3.         RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
  4.                 Naming.lookup("//101.43.122.221:1099/Hello");
  5.         System.out.println(hello);
  6.         String ret = hello.hello();
  7.         System.out.println( ret);

  8.     }
  9. }
复制代码


结果如下:

Proxy[RMIServer$IRemoteHelloWorld,RemoteObjectInvocationHandler[UnicastRef [liveRef: [endpoint:[169.254.218.208:49154](remote),objID:[-259c9b84:183733a63d6:-7fff, -604087336640089995]]]]]
可以看到169.254.218.208就是远程对象的地址,49154就是请求远程对象所需要连接的端口。

远程调用问题的解决
首先要解决的是AccessException的问题,经过多方搜索及尝试,基本能定位到问题的缘由。首先,我改换server中rebind函数中的host来测试rebind到底在干一件什么事。

如果我们在远端运行以下代码:
  1. public class RMIServer {
  2.     public interface IRemoteHelloWorld extends Remote {
  3.         public String hello() throws RemoteException;
  4.     }
  5.     public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
  6.         protected RemoteHelloWorld() throws RemoteException {
  7.             super();
  8.         }
  9.         public String hello() throws RemoteException {
  10.             System.out.println("call from");
  11.             return "Hello world";
  12.         }
  13.     }
  14.     private void start() throws Exception {
  15.         RemoteHelloWorld h = new RemoteHelloWorld();
  16.         LocateRegistry.createRegistry(1099);
  17.         System.out.println("ok1");
  18.         Naming.rebind("rmi://101.43.122.221:1099/Hello", h);//自己云服务器的ip
  19.         System.out.println("ok2");
  20.     }
  21.     public static void main(String[] args) throws Exception {
  22.         System.out.println("start");
  23.         new RMIServer().start();
  24.     }
  25. }
复制代码


运行时,控制台输出start ok1,但没有输出ok2,这意味着rebind命令并未执行完毕,过一段时间后便会报之前展示的AccessException漏洞。这时我把Naming.rebind()语句换为以下语句再进行启动:

  1. Naming.rebind(<span class="code-snippet__string" style="outline: 0px; max-width: 1000%; color: rgb(221, 17, 68); box-sizing: border-box !important;">"rmi://127.0.0.1:1099/Hello"</span>, h);
复制代码


会发现没有产生报错,rebind执行成功。

本机使用以下代码进行访问:
  1. public static void main(String[] args) throws NotBoundException, RemoteException, MalformedURLException {
  2.     RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
  3.         Naming.lookup("//127.0.0.1:1099/Hello");
  4.     System.out.println(hello);
  5.     String ret = hello.hello();
  6.     System.out.println(ret);
  7. }
复制代码


此时会发现控制台输出了hello远端对象,不过还是产生了报错。
  1. Proxy[RMIServer$IRemoteHelloWorld,RemoteObjectInvocationHandler[UnicastRef [liveRef: [endpoint:[127.0.0.1:40509](remote),objID:[-141e8e8c:1837365c720:-7fff, 2932857538888970532]]]]]
  2. Exception in thread "main" java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
  3.     java.net.ConnectException: Connection refused: connect
复制代码


结论:以上实验证明了rebind函数中的host为远端函数的地址,client向server发起请求后,server回复相应的对象的远端地址就为rebind中的host。在以上案例中client接受到的远程对象地址在127.0.0.1,便向本机发起了远程对象访问请求,但这个远程对象明显是在远程服务器注册的,所以产生了报错,在127.0.0.1找不到该远端对象。

一番搜索后终于解决了这个问题,很多网上的资料都是在本地起的server又在本地搭client,像我这样在云服务器起server,在本地用client调用远端方法的较少。这一部分解决了的文章中又有大部分是在云再开一个进程专门rebind,这样就能rebind到服务器ip上。

我在这里终于是找到了一个方法配置,在main函数开始加上如下代码:
  1. System.setProperty("java.rmi.server.hostname","101.43.122.221");
复制代码


这样程序就将本机名与ip绑定了起来,此时再使用。

  1. Naming.rebind(<span class="code-snippet__string" style="outline: 0px; max-width: 1000%; color: rgb(221, 17, 68); box-sizing: border-box !important;">"rmi://loaclhost:1099/Hello"</span>, h);
复制代码


就可以将对象绑定到服务器ip了。

此时再分别运行server和client,client又产生了另一个报错。

  1. <code style="outline: 0px; max-width: 1000%; display: flex; position: relative;" liberation="" mono",="" menlo,="" courier,="" monospace;="" box-sizing:="" border-box="" !important;"=""><span class="code-snippet_outer" style="outline: 0px; max-width: 1000%; box-sizing: border-box !important;"><span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">java</span><span class="code-snippet__selector-class" style="outline: 0px; max-width: 1000%; box-sizing: border-box !important;">.rmi</span><span class="code-snippet__selector-class" style="outline: 0px; max-width: 1000%; box-sizing: border-box !important;">.ConnectException</span>: <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">Connection</span> <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">refused</span> <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">to</span> <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">host</span>: 101<span class="code-snippet__selector-class" style="outline: 0px; max-width: 1000%; box-sizing: border-box !important;">.43</span><span class="code-snippet__selector-class" style="outline: 0px; max-width: 1000%; box-sizing: border-box !important;">.122</span><span class="code-snippet__selector-class" style="outline: 0px; max-width: 1000%; box-sizing: border-box !important;">.221</span>; <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">nested</span> <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">exception</span> <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">is</span>:</span></code><code style="outline: 0px; max-width: 1000%; display: flex; position: relative;" liberation="" mono",="" menlo,="" courier,="" monospace;="" box-sizing:="" border-box="" !important;"=""><span class="code-snippet_outer" style="outline: 0px; max-width: 1000%; box-sizing: border-box !important;">    <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">java</span><span class="code-snippet__selector-class" style="outline: 0px; max-width: 1000%; box-sizing: border-box !important;">.net</span><span class="code-snippet__selector-class" style="outline: 0px; max-width: 1000%; box-sizing: border-box !important;">.ConnectException</span>: <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">Connection</span> <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">timed</span> <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">out</span>: <span class="code-snippet__selector-tag" style="outline: 0px; max-width: 1000%; color: rgb(202, 125, 55); box-sizing: border-box !important;">connect</span></span></code>
复制代码


在Oracle官网社区找到了答案。

(低级错误,服务器端口没开放。)但是这时候问题又诞生了,远程对象的端口是随机给出的,我该如何让他始终使用同一个端口呢。答:写一个类继承Socket生产类,我们定制socket产生方式,使用以下代码:
  1. public class CustomSocket extends RMISocketFactory {
  2.     @Override
  3.     public ServerSocket createServerSocket(int port) throws IOException {
  4.         if (port==0)
  5.         {
  6.             port=33457;
  7.         }
  8.         return new ServerSocket(port);
  9.     }
  10.     @Override
  11.     public Socket createSocket(String host, int port) throws IOException {
  12.         System.out.println("host:"+host+" port:"+port);
  13.         return new Socket(host,port);
  14.     }
  15. }
复制代码


修改server程序如下:
  1. public class RMIServer {
  2.     public interface IRemoteHelloWorld extends Remote {
  3.         public String hello() throws RemoteException;
  4.     }
  5.     public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
  6.         protected RemoteHelloWorld() throws RemoteException {
  7.             super();
  8.         }
  9.         public String hello() throws RemoteException {
  10.             System.out.println("call from");
  11.             return "Hello world";
  12.         }
  13.     }
  14.     private void start() throws Exception {
  15.         RemoteHelloWorld h = new RemoteHelloWorld();
  16.         LocateRegistry.createRegistry(1099);
  17.         System.out.println("ok1");
  18.         Naming.rebind("rmi://loaclhost:1099/Hello", h);
  19.         System.out.println("ok2");
  20.     }
  21.     public static void main(String[] args) throws Exception {
  22.         System.setProperty("java.rmi.server.hostname","101.43.122.221");
  23.         try {
  24.             CustomSocket cs=new CustomSocket();
  25.             try {
  26.                 RMISocketFactory.setSocketFactory(cs);

  27.             } catch (IOException e) {
  28.                 e.printStackTrace();
  29.             }
  30.         }catch (Exception e){
  31.             e.printStackTrace();
  32.         }
  33.         System.out.println("start");
  34.         new RMIServer().start();
  35.     }
  36. }
复制代码


激动人心的测试!
成功!这样我就能在服务器运行server且能用任意一台机器远程调用方法还只占用1099和33457两个端口啦!

以上就是本期的全部内容啦,简简单单的一个远程连接问题居然能花3天时间来解决,RMI这块知识网上确实比较欠缺的,都是上来就讲漏洞讲攻击了,我把我学习RMI原理的细节在这里摊开来讲,也希望其他小白也能借此快速入门!



回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2022-12-4 22:08 , Processed in 0.015080 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表