RMI

Java RMI即Java远程方法调用,用于实现远程过程调用的应用程序编程接口,使用JRMP协议实现(远程消息交换协议),使得客户端运行的程序可以调用远程服务器上的对象。RMI的本质是TCP通信,内部封装了序列化和通信过程

RMI Server

  • ⼀个继承了 java.rmi.Remote 的接⼝,其中定义我们要远程调⽤的函数,⽐如这⾥的 hello()
  • ⼀个实现了此接⼝的类
  • ⼀个主类,⽤来创建Registry,并将上⾯的类实例化后绑定到⼀个地址。这就是我们所谓的Server 了。 ```java package RMI; 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 RmiServer { public interface IRemoteHelloWorld extends Remote { String hello() throws RemoteException;//接口里有方法的实现,接口的方法不能有主体 ,修饰符是多余的 }

  1. public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
  2. protected RemoteHelloWorld() throws RemoteException {
  3. super();
  4. }
  5. public String hello() throws RemoteException { //接口方法的实现
  6. System.out.println("call from");
  7. return "Hello world";
  8. }
  9. }
  10. private void start() throws Exception {
  11. RemoteHelloWorld h = new RemoteHelloWorld();
  12. LocateRegistry.createRegistry(1099); //绑定端口号,创建并允许RMI Registry 对外提供服务。
  13. Naming.rebind("rmi://127.0.0.1:1099/Hello", h); //将实例化的h对象绑定到Hello这个name上。
  14. }
  15. public static void main(String[] args) throws Exception {
  16. new RmiServer().start();
  17. }

}

  1. > `Naming.bind`的第一个参数是URL,形如`rmi://host:port/name` ,其中,hostport就是RMI Regsiter的地址和端口,name是远程对象的名字。
  2. <a name="pRzDx"></a>
  3. ### RMI Client:
  4. ```java
  5. package RMI;
  6. import RMI.RmiServer;
  7. import java.rmi.Naming;
  8. import java.rmi.NotBoundException;
  9. import java.rmi.RemoteException;
  10. public class RmiClient {
  11. public static void main(String[] args) throws Exception {
  12. RmiServer.IRemoteHelloWorld hello = (RmiServer.IRemoteHelloWorld)
  13. Naming.lookup("rmi://127.0.0.1:1099/Hello"); //使用Naming.lookup寻找对应的RMI远程服务
  14. String ret = hello.hello(); // 使⽤ Naming.lookup 在Registry(注册表)中寻找到名字是Hello的对象
  15. System.out.println( ret);
  16. }
  17. }

image.png
实际通信过程(进行了两次TCP连接

  • 客户端连接Registry寻找其对应的对象服务,对应数据流中的Call消息

  • Rsgistry返回序列化数据,则是客户端寻找的对象,对应ReturnDate消息

  • 客户端反序列化,是一个远程对象,再与此地址端口发起TCP连接,开始真正端口调用

RMI Registry就像是一个网关,自己是不会执行远程方法的,但RMI Server可以在上⾯注册⼀个Name 到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。实际上就是将此服务绑定在了RMI Rsgister某个端口上面,作为一个中间人提供服务。


RMI连接流程

image.png

首先我们的RMI Client 会远程连接RMI Registry(默认端口1099),然后会在Registry 寻找名字为Test的对象 (对象都绑定在某个端口)(假设此时客户端要调用Test对象中的某个方法),Registry会寻找对应名字的远程对象引用,并且序列化后进行返回(数据内容就是远程对象的地址,这里返回的对象就是前文提到的存根stub),客户端在接受到之后首先会在本机中的classpath进行查找,如果没有找到则说明是远程对象客户端就会与远程地址进行tcp连接。


RMI注册表(Registey

  • 为了使客户端能够查找到服务端对外提供的远程对象,RMI需要维护一个RMI注册表,该注册表维护了对于客户端而言的远程对象位置,对外提供服务,服务端将要对外提供的服务的对象的代理绑定到注册表上。
  • RMI服务器和注册表可以不在同一个主机上??

    在低版本的jdk中,RMI Server与RMI Regsiter可以不在一个同一台服务器的,而在高版本JDK中,Server和Resgiter必须在同一个服务器上,不然无法注册成功。(会有一个checkAccess方法为了检查绑定时是否在同一服务器。

  • RMI注册表启动的两种方法:

  1. 命令行 rmiregistry $port ②LocateRegistry 类的 createRegistry(int port) 方法启动

客户端查找远程对象

  • LocateRegistry 类的对象的 rebind() 和 lookup() 来实现绑定注册和查找远程对象的
  • 利用命名服务 java.rmi.Naming 类的 rebind() 和 lookup() 来实现绑定注册和查找远程对象的
  • 利用JNDI(Java Naming and Directory Interface,Java命名和目录接口) java.naming.InitialContext 类来 rebind() 和 lookup() 来实现绑定注册和查找远程对象的

RMI带来的安全问题

  • 若能访问到RMI Regsitry1099端口,该如何发起攻击?

    1. 尝试绑定恶意对象 答案是不可以,只有来源地址是localhost的时候,才能调用rebind、 bind、unbind方法,但是我们可以使用list和lookup方法
    2. 利用RMI服务器上存在的恶意方法进行命令执行 我们可以首先通过list列出所有的对象引用,然后只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用,之前曾经有一个工具,其中一个功能就是进行危险方法的探测 https://github.com/NickstaDB/BaRMIe
  • 若控制了客户端Naming.lookup第一个参数,能不能攻击?

如何攻击RMI Registry?

RMI Registry是一个远程对象管理的地方,可以理解为一个远程对象的“后台”,可以尝试直接访问后台功能,比如修改远程服务器上Hello对应的对象:

尝试将我们的恶意对象绑定在注册表上。

  1. RemoteHelloWorld h = new RemoteHelloWorld();
  2. Naming.rebind("rmi://192.168.135.142:1099/Hello", h);


报错:

这里java对远程访问RMI Registry做限制,只有来源地是localhost的时候,才能调用rebind、bind、unbind等方法。

image.png
不过listlookup方法可以远程调用:

  1. String[] s = Naming.list("rmi://192.168.135.142:1099") //list方法可以列出目标所有绑定的对象
  2. lookup作用就是获得某个远程对象。若存在一些危险方法则进行调用。

探测RMI 注册表上绑定的危险方法:https://github.com/NickstaDB/BaRMIe

RMI利用codebase执行任意代码(利用苛刻

RMI涉及到远程加载的场景,也会涉及到codebase。
codebase是一个地址,告诉java虚拟机我们应该从哪个地方去搜索类,类似于日常的CLASSPATH,但CLASSPATH是本地路径,而codebase通过是远程url,比如http、ftp。

若我们指定codebase=http://example.com/然后加载org.vulhub.example.Example类,则Java虚拟机会下载这个文件http://example.com/org/vulhub/example/Explame.class,并作为Example类的字节码。

RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻 找类。如果某一端反序列化时发现一个对象,那么就会去自己CLASSPATH下寻找想对应的类;如果在 本地没有找到这个类,就会去远程加载codebase中的类。
满足以下条件才会被攻击:

  • 安装配置SecurityManager
  • Java版本低于7u21、6u45或者设置了java.rmi.server.useCodebaseOnly=false

RMI反序列化 攻击

参考:https://paper.seebug.org/1194/
RMI反序列化必须包含的条件:

  • 能够进行RMI通信
  • 目标服务器使用了第三方存在反序列化漏洞的包:JDK8 121版本以下。

根据ysoserial工具给出的payload即可。


RMI详解


攻击基于RMI的JMX服务

JMX是java平台的管理和监控接口,任何程序只要按照JMX的规范 访问这个接口,就能够获取所有管理与监控信息。
因为是一个标准接口,所以不仅可以管理JMV还可以管理程序自身

RMI-JMX - 图4

JMX把所有被管理的资源称为MBean,这些MBean都托管给MBeanServer来进行管理,若要访问MBean,可以通过MBeanServer对外提供的接口,例如RMI、HTTP访问。

JMX标准在各种MBean类型之间有所不同,这里我们仅仅处理标准的MBean,要成为有效的MBean
Java类必须满足:

  • 实现一个接口
  • 提供一个无参构造函数
  • 实现getter/setter方法

定义一个接口HelloMBean.java

  1. package com.MBean.sqy;
  2. public interface HelloMBean {
  3. // getter and setter for the attribute "name"
  4. public String getName();
  5. public void setName(String newName);
  6. // Bean method "sayHello"
  7. public String sayHello();}

接口实现的类Hello.java:

  1. package com.MBean.sqy;
  2. public class Hello implements HelloMBean{
  3. private String name = "Sviivya0";
  4. @Override
  5. public String getName() {
  6. return this.name;
  7. }
  8. @Override
  9. public void setName(String newName) {
  10. this.name = newName;
  11. }
  12. @Override
  13. public String sayHello() {
  14. return "hello: " + name;
  15. }
  16. }

每个java进程都有一个运行的MBean服务器服务。
我们可以使用ManagementFactory.getPlatformMBeanServer();访问该服务。以下示例代码“连接”到当前进程的MBean服务器并打印出所有已注册的MBean:

  1. package com.MBean.sqy;
  2. import javax.management.MBeanServer;
  3. import javax.management.MalformedObjectNameException;
  4. import javax.management.ObjectInstance;
  5. import javax.management.ObjectName;
  6. import java.lang.management.ManagementFactory;
  7. public class MBeanClient {
  8. public static void main(String[] args) throws MalformedObjectNameException {
  9. // Connect to the MBean server of the current Java process
  10. MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); //访问MBean服务器的服务
  11. System.out.println(mBeanServer.getMBeanCount() );
  12. // Print out each registered MBean
  13. for(Object object : mBeanServer.queryMBeans(new ObjectName("*:*"), null))
  14. {
  15. System.out.println( ((ObjectInstance)object).getObjectName() );
  16. }
  17. }
  18. }
  • 每个MBean都需要遵循对象名称约定的唯一名称。该名称分为一个域(通常是包)和一个对象名,比如这里我们看到的,对象名称应包含type属性
  • RMI-JMX - 图5

如何注册MBean:

  1. package com.MBean.sqy;
  2. import javax.management.MBeanServer;
  3. import javax.management.ObjectInstance;
  4. import javax.management.ObjectName;
  5. import java.lang.management.ManagementFactory;
  6. public class MBeanExample {
  7. public static void main(String[] args) throws Exception {
  8. // Create a new MBean instance from Hello (HelloMBean interface)
  9. Hello mbean = new Hello();
  10. // Create an object name,注册一个MBean对象提供服务
  11. ObjectName mbeanName = new ObjectName("com.MBean.sqy:type=HelloMBean");
  12. // Connect to the MBean server of the current Java process连接MBean服务的java进程
  13. MBeanServer server = ManagementFactory.getPlatformMBeanServer();
  14. server.registerMBean(mbean, mbeanName);
  15. for(Object object : server.queryMBeans(new ObjectName("*:*"), null))
  16. {
  17. System.out.println( ((ObjectInstance)object).getObjectName() );
  18. }
  19. // Keep the application running until user enters something
  20. System.out.println("Press any key to exit");
  21. System.in.read();
  22. }
  23. }

```
此时可以看大输出中存在了我们对声明的MBean对象啦
RMI-JMX - 图6

如何启动JMX监测管理程序

  • 命令行输入jconsole即可,连接到正在运行的java程序即可在MBean选项中检查已注册的MBean,也可以获取调用MBean的属性方法等。

所以只要我们能够登陆jconsole即管理java程序即可调用属性方法进行命令执行

JMX远程连接

若要连接到另一台服务器的远程连接,必须使用JMX连接器,那我们可以将MBeanServer挂载在某个端口上,提供对远程MBean服务器的访问

  • Java提供一个基于Java RMI(远程调用方法)的远程JMX连接器,可以通过向java调用添加以下参数来启动JMX
    1. -Dcom.sun.management.jmxremote.port=2222 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false
    这里没成功,还没找到原因因为jdk包里不存在com.sun.management.jmxremote这个类

这样,在连接的时候就能用JConsole以ip:port的形式进行远程连接到该服务,同时此配置不安全:任何知道(或猜测)您的端口号和主机名的远程用户都将能够监视和控制您的Java应用程序和平台。此外,可能的危害不仅限于您在MBean中定义的操作。远程客户端可以创建一个javax.management.loading.MLet MBean并使用它从任意URL创建新的MBean,至少在没有安全管理器的情况下。换句话说,恶意远程客户端可能使您的Java应用程序执行任意代码。

攻击JMX

三种思路:

  • 攻击JMX-RMI
  • 攻击存在函数参数的对象
  • 利用MLET方式动态加载MBean

参考:
攻击思路分析:
https://mogwailabs.de/en/blog/2019/04/attacking-rmi-based-jmx-services/
https://blog.0kami.cn/2020/03/10/java/java-jmx-rmi/
攻击工具:
https://github.com/wh1t3p1g/ysomap


SPI机制是否有安全性问题?
Java 反射有哪些安全问题?
Java类加载机制是什么?
数据库连接时的密码问题?
使用JDBC如何写一个通用的数据库密码爆破模块?