最近想学习一下Java相关的安全知识,就找了几篇文章来学习下,从Java RMI开始吧
什么是 JAVA RMI ?
RMI ( Remote Method Invocation , 远程方法调用 ) 能够让在某个 Java虚拟机 上的对象像调用本地对象一样调用另一个 Java虚拟机 中的对象上的方法 , 这两个 Java虚拟机 可以是运行在同一台计算机上的不同进程, 也可以是运行在网络中不同的计算机上 .
RMI分为三个主体部分:
Client-客户端:客户端调用服务端的方法
Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果。
Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用。
JAVA RMI 的流程 .
如果让客户端直接访问服务端的资源 , 那么有可能出现越权访问的风险 . 在 JAVA RMI 中 , 通过一个 中间人 来解决这类问题 .
服务端( RMIServer ) 会将自己提供的服务的实现类交给这个中间人 , 并公开一个名称 . 任何客户端( RMIClient )都可以通过公开的名称找到这个实现类 , 并调用它 .
这样以来 , 不仅避免了客户端和服务端资源的直接交互 . 也使得客户端能更好的查找要使用的对象( 直接去询问这个中间人 , 若中间人拥有对应实现类 , 那么客户端可以在本地直接调用该类的方法 . 若中间人没有对应的实现类 , 则说明服务端没有提供相应服务 )
这个中间人也被称为 RMIService / RMIRegister .
因此整个 RMI 的流程实际上分为三个部分 , RMIServer , RMIClient , RMIRegister . 其交互的流程如下所示 :
JAVA RMI 的简单例子
1.服务端编写一个远程接口
import java.rmi.Remote;
import java.rmi.RemoteException;
// 定义一个远程接口,继承java.rmi.Remote接口
public interface HelloInterface extends Remote {
String Hello(String age) throws RemoteException;
}
既然 RMIServer 要提供服务 , 那么它一定会准备一个接口 , 让客户端通过这个接口来访问服务
需要说明的是 :
在 Java 中 , 如果一个类继承了 java.rmi.Remote 接口 , 那么该类将成为一个服务端的远程对象 , 供客户端访问并提供一定的服务 .
Remote 接口是一个标识接口 , 本身不包含任何方法 , 该接口用于标识其子类的方法可以被非本地的Java虚拟机调用
由于远程调用的本质依旧是 " 网络通信 " . 而网络通信是经常出现异常的 . 因此 , 继承 Remote 接口的接口的所有方法必须要抛出 RemoteException 异常 . 事实上 , RemoteException 也是继承于 IOException 的 .
2.要想调用远程接口 , 还需要一个实现类
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// 远程接口实现类,继承UnicastRemoteObject类和Hello接口
public class HelloImp extends UnicastRemoteObject implements HelloInterface {
private static final long serialVersionUID = 1L;
protected HelloImp() throws RemoteException {
super(); // 调用父类的构造函数
}
@Override
public String Hello(String age) throws RemoteException {
return "Hello" + age; // 改写Hello方法
}
}
远程接口实现类必须继承UnicastRemoteObject类,用于生成 Stub(存根)和 Skeleton(骨架)。
Stub可以看作远程对象在本地的一个代理,囊括了远程对象的具体信息,客户端可以通过这个代理和服务端进行交互。
Skeleton可以看作为服务端的一个代理,用来处理Stub发送过来的请求,然后去调用客户端需要的请求方法,最终将方法执行结果返回给Stub。
其实 , 与其说是客户端和服务端进行交互 , 不如说是 客户端代理( Stub ) 和 服务端代理( Skeleton ) 在进行交互 .
同时跟进UnicastRemoteObject类源代码我们可以发现,其构造函数抛出了RemoteException异常。但这种写法是十分不好的,所以我们通过super()关键词调用父类的构造函数。
3.RMI服务器端
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
// 服务端
public class RMIServer {
public static void main(String[] args) {
try {
HelloInterface h = new HelloImp(); // 创建远程对象HelloImp对象实例
LocateRegistry.createRegistry(1099); // 获取RMI服务注册器
Naming.rebind("rmi://localhost:1099/hello",h); // 绑定远程对象HelloImp到RMI服务注册器
System.out.println("RMIServer start successful");
} catch (Exception e) {
e.printStackTrace();
}
}
}
LocateRegistry.createRegistry(1099); : 即在本地创建并启动 RMIService , 被创建的 RMIService 服务将会在指定的端口上监听请求 .
RMIService ( RMIRegister ) 服务的默认端口为 : 1099
java.rmi.Naming 类提供在对象注册表中存储和获得远程对远程对象引用的方法 . 这里将远程对象 " h " 绑定到 rmi://localhost:1099/hello 这个 URL 上 . 客户端可以通过这个 URL 直接访问远程对象 .
这里涉及到了另一个问题 : 即 " 开发人员不知道远程实例对象的名称是什么 ." 而通过这种绑定机制 , 开发人员仅需要知道一个公开的路径(URL) , 就可以直接访问到对应的远程对象了 .
4.RMI客户端配置
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
// 客户端
public class RMIClient {
public static void main(String[] args){
try {
HelloInterface h = (HelloInterface) Naming.lookup("rmi://localhost:1099/hello"); // 寻找RMI实例远程对象
System.out.println(h.Hello("run......"));
}catch (MalformedURLException e) {
System.out.println("url格式异常");
} catch (RemoteException e) {
System.out.println("创建对象异常");
} catch (NotBoundException e) {
System.out.println("对象未绑定");
}
}
}
客户端只需要调用 java.rmi.Naming.lookup 函数,通过公开的路径从RMIService服务器上拿到对应接口的实现类, 之后通过本地接口即可调用远程对象的方法 .
因此 , 只需要一个接口 , 一个客户端连接程序 , 即可实现 JAVA 远程调用 .
5.运行结果
通过结果也可以知道,这个方法的调用,实际上也还是在服务端,而不是客户端。
JAVA RMI的一些攻击
因为在整个RMI机制过程中,都是进行反序列化传输,我们可以利用这个特性使用RMI机制来对RMI远程服务器进行反序列化攻击。
但实现RMI利用反序列化攻击,需要满足两个条件:
1、接收Object类型参数的远程方法
2、RMI的服务端存在执行pop利用链的jar包
后续的实例就不试了,因为这篇文章就是自己的一个入门学习,暂时了解了相关原理和知道反序列化传输就行,想查看实例以及相关的通信原理的,可以查看下面的参考文章
https://www.guildhab.top/2020/03/java-rmi-ldap-%e6%b5%81%e7%a8%8b%e5%88%86%e6%9e%90/
https://xz.aliyun.com/t/9261
https://xz.aliyun.com/t/6660#toc-5
|