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;//接口里有方法的实现,接口的方法不能有主体 ,修饰符是多余的 }
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
protected RemoteHelloWorld() throws RemoteException {
super();
}
public String hello() throws RemoteException { //接口方法的实现
System.out.println("call from");
return "Hello world";
}
}
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099); //绑定端口号,创建并允许RMI Registry 对外提供服务。
Naming.rebind("rmi://127.0.0.1:1099/Hello", h); //将实例化的h对象绑定到Hello这个name上。
}
public static void main(String[] args) throws Exception {
new RmiServer().start();
}
}
> `Naming.bind`的第一个参数是URL,形如`rmi://host:port/name` ,其中,host与port就是RMI Regsiter的地址和端口,name是远程对象的名字。
<a name="pRzDx"></a>
### RMI Client:
```java
package RMI;
import RMI.RmiServer;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class RmiClient {
public static void main(String[] args) throws Exception {
RmiServer.IRemoteHelloWorld hello = (RmiServer.IRemoteHelloWorld)
Naming.lookup("rmi://127.0.0.1:1099/Hello"); //使用Naming.lookup寻找对应的RMI远程服务
String ret = hello.hello(); // 使⽤ Naming.lookup 在Registry(注册表)中寻找到名字是Hello的对象
System.out.println( ret);
}
}
实际通信过程(进行了两次TCP连接
客户端连接Registry寻找其对应的对象服务,对应数据流中的Call消息
Rsgistry返回序列化数据,则是客户端寻找的对象,对应ReturnDate消息
客户端反序列化,是一个远程对象,再与此地址端口发起TCP连接,开始真正端口调用
RMI Registry就像是一个网关,自己是不会执行远程方法的,但RMI Server可以在上⾯注册⼀个Name 到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。实际上就是将此服务绑定在了RMI Rsgister某个端口上面,作为一个中间人提供服务。
RMI连接流程
首先我们的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注册表启动的两种方法:
- 命令行 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端口,该如何发起攻击?
- 尝试绑定恶意对象 答案是不可以,只有来源地址是localhost的时候,才能调用rebind、 bind、unbind方法,但是我们可以使用list和lookup方法
- 利用RMI服务器上存在的恶意方法进行命令执行 我们可以首先通过list列出所有的对象引用,然后只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用,之前曾经有一个工具,其中一个功能就是进行危险方法的探测 https://github.com/NickstaDB/BaRMIe
若控制了客户端Naming.lookup第一个参数,能不能攻击?
如何攻击RMI Registry?
RMI Registry是一个远程对象管理的地方,可以理解为一个远程对象的“后台”,可以尝试直接访问后台功能,比如修改远程服务器上Hello对应的对象:
尝试将我们的恶意对象绑定在注册表上。
RemoteHelloWorld h = new RemoteHelloWorld();
Naming.rebind("rmi://192.168.135.142:1099/Hello", h);
报错:
这里java对远程访问RMI Registry做限制,只有来源地是localhost的时候,才能调用rebind、bind、unbind等方法。
不过list
和lookup
方法可以远程调用:
String[] s = Naming.list("rmi://192.168.135.142:1099") //list方法可以列出目标所有绑定的对象
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还可以管理程序自身
JMX把所有被管理的资源称为MBean,这些MBean都托管给MBeanServer来进行管理,若要访问MBean,可以通过MBeanServer对外提供的接口,例如RMI、HTTP访问。
JMX标准在各种MBean类型之间有所不同,这里我们仅仅处理标准的MBean,要成为有效的MBean
Java类必须满足:
- 实现一个接口
- 提供一个无参构造函数
- 实现getter/setter方法
定义一个接口HelloMBean.java
package com.MBean.sqy;
public interface HelloMBean {
// getter and setter for the attribute "name"
public String getName();
public void setName(String newName);
// Bean method "sayHello"
public String sayHello();}
接口实现的类Hello.java:
package com.MBean.sqy;
public class Hello implements HelloMBean{
private String name = "Sviivya0";
@Override
public String getName() {
return this.name;
}
@Override
public void setName(String newName) {
this.name = newName;
}
@Override
public String sayHello() {
return "hello: " + name;
}
}
每个java进程都有一个运行的MBean服务器服务。
我们可以使用ManagementFactory.getPlatformMBeanServer();访问该服务。以下示例代码“连接”到当前进程的MBean服务器并打印出所有已注册的MBean:
package com.MBean.sqy;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
public class MBeanClient {
public static void main(String[] args) throws MalformedObjectNameException {
// Connect to the MBean server of the current Java process
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); //访问MBean服务器的服务
System.out.println(mBeanServer.getMBeanCount() );
// Print out each registered MBean
for(Object object : mBeanServer.queryMBeans(new ObjectName("*:*"), null))
{
System.out.println( ((ObjectInstance)object).getObjectName() );
}
}
}
- 每个MBean都需要遵循对象名称约定的唯一名称。该名称分为一个域(通常是包)和一个对象名,比如这里我们看到的,对象名称应包含type属性
如何注册MBean:
package com.MBean.sqy;
import javax.management.MBeanServer;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
public class MBeanExample {
public static void main(String[] args) throws Exception {
// Create a new MBean instance from Hello (HelloMBean interface)
Hello mbean = new Hello();
// Create an object name,注册一个MBean对象提供服务
ObjectName mbeanName = new ObjectName("com.MBean.sqy:type=HelloMBean");
// Connect to the MBean server of the current Java process连接MBean服务的java进程
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
server.registerMBean(mbean, mbeanName);
for(Object object : server.queryMBeans(new ObjectName("*:*"), null))
{
System.out.println( ((ObjectInstance)object).getObjectName() );
}
// Keep the application running until user enters something
System.out.println("Press any key to exit");
System.in.read();
}
}
```
此时可以看大输出中存在了我们对声明的MBean对象啦
如何启动JMX监测管理程序
- 命令行输入jconsole即可,连接到正在运行的java程序即可在MBean选项中检查已注册的MBean,也可以获取调用MBean的属性方法等。
所以只要我们能够登陆jconsole即管理java程序即可调用属性方法进行命令执行
JMX远程连接
若要连接到另一台服务器的远程连接,必须使用JMX连接器,那我们可以将MBeanServer挂载在某个端口上,提供对远程MBean服务器的访问
- Java提供一个基于Java RMI(远程调用方法)的远程JMX连接器,可以通过向java调用添加以下参数来启动JMX
(-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如何写一个通用的数据库密码爆破模块?