前言
RMI,全称Remote Method Invocation,远程方法调用
RMI有客户端和服务端,在Java中客户端可以通过RMI调用来服务端的方法,危险程度无需多言
参考文章:
负数零:https://fushuling.com/index.php/2023/01/30/java%e5%ae%89%e5%85%a8%e7%ac%94%e8%ae%b0/
Boogipop:https://boogipop.com/2023/03/02/%E6%B5%85%E5%AD%A6RMI%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96
以及p神的《java安全漫谈》
概念
流程图如下:
服务端注册了RMI后会在RMI Registry进行注册,之后客户端调用方法都是直接从这个注册中心取出
RMI分为三个主体部分:
Client-客户端:客户端调用服务端的方法
Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果
Registry-注册中心:其实本质就是一个网关/map,相当于是字典一样,用于客户端查询要调用的方法的引用
Registry自己不会执行远程方法,但它可以通过RMI Server在上面注册一个Name到对象的绑定关系,
然后用RMI Client通过Name向RMI Registry查询,得到这个绑定关系,最后再连接RMI Server
因此,远程方法实际上是在RMI Server上调用的
注:在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功
创建
服务端
先简单起个RMI服务
package com.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 {
public 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);
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
这里的Server其实包含了Registry和Server两部分
RMI Registry部分:
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/Hello", new RemoteHelloWorld());
第一行创建并运行RMI Registry,第二行将RemoteHelloWorld对象绑定到Hello这个名字上
注:如果RMI Registry在本地运行,那么host和port是可以省略的,此时host默认是localhost ,port默认是1099
Naming.bind("Hello", new RemoteHelloWorld());
而RMI Server分为三部分:
继承了
java.rmi.Remote
的接口,定义了我们远程调用的函数,如这里的hello()
public interface IRemoteHelloWorld extends Remote { public String hello() throws RemoteException; }
一个实现了此接口的类
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld { protected RemoteHelloWorld() throws RemoteException { super(); }
一个主类,用来创建Registry,并将上面的类实例例化后绑定到一个地址。这就是我们所谓的Server了(这里的代码还包含了RMI Registry部分)
private void start() throws Exception { RemoteHelloWorld h = new RemoteHelloWorld(); LocateRegistry.createRegistry(1099); Naming.rebind("rmi://127.0.0.1:1099/Hello", h); } public static void main(String[] args) throws Exception { new RMIServer().start(); }
客户端
package com.RMI;
import com.RMI.RMIServer;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class TrainMain {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming
.lookup("rmi://127.0.0.1:1099/Hello");
String ret = hello.hello();
System.out.println(ret);
}
}
客户端就比服务端简单多了,使用Naming.lookup
在Registry中寻找到名字是Hello的对象,后面的操作和本地操作一致。
现在我们先运行服务端,再运行客户端
可以看到服务端打印了call from,客户端远程调用了 hello()
方法打印出了Hello world
Attack
攻击RMI Registry
我们已经知道RMI Registry可以管理远程对象,类似于远程对象的“后台”
那么我们能否直接访问这个“后台”,然后修改远程服务器上Hello对应的对象呢?
答案是不行的,Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind、bind、unbind等方法,直接调用的话会报错
不过list和lookup方法可以远程调用,
list方法可以列出目标上所有绑定的对象
String[] s = Naming.list("rmi://127.0.0.1:1099");
System.out.println(s);
而lookup作用就是获得某个远程对象,在这里客户端的代码中一开始就使用了lookup获取了hello对象
那么,只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用
工具:https://github.com/NickstaDB/BaRMIe
其中一个功能就是进行危险方法的探测
RMI利用codebase执行任意代码
这个东西有点古老,可以追溯到Java能运行在浏览器中的时候,暂时先不学这个
调试
先参考pop✌的博客
调试过程中发现RMI发送、接收的数据都是反序列化数据
等完善了反序列化再回来看