RMI
概念
RMI的过程,就是用JRMP协议去组织数据格式,然后通过TCP进行传输,从而达到远程方法调用。
- RMI(Remote Method Invocation):远程方法调用。即让一个JVM中的对象远程调用另一个JVM中的对象的某个方法,简单来说就是跨越JVM,使用Java调用远程Java程序
- Server服务端:提供远程的对象
- Client客户端:调用远程的对象
- Registry注册表:存放着远程对象的位置,用于客户端查询所调用的远程方法的引用
需要注意的是:被调用的方法实际上是在RMI服务端执行
JRMP(Java Remote Message Protocol):Java 远程消息交换协议。运行在TCP/IP之上的线路层协议,该协议要求服务端与客户端都为Java编写。
- Java本身对RMI规范的实现默认使用JRMP协议,而在Weblogic中使用T3协议
JNDI(Java Naming and Directory Interface):Java命名和目录接口。一组在Java应用中访问命名和目录服务的接口,Java中使用最多的基本就是RMI和LDAP的目录服务系统,客户端可以通过名称访问对象,并将其下载下来。
- 命名服务:将名称和对象联系起来,客户端可以使用名称访问对象
- 目录服务:一种命名服务,在命名服务的基础上,增加了属性的概念
RMI-Server
- RMI Server分为三部分:
- 一个远程接口。继承
java.rmi.Remote
,其中定义要远程调用的函数。 - 远程接口的实现类。继承
java.rmi.server.UnicastRemoteObject
,实现远程调用的函数 - 创建实例和
Registry
注册表,然后在注册表中绑定地址和实例
- 一个远程接口。继承
定义远程接口
- 定义一个远程接口,继承
java.rmi.Remote
接口,抛出RemoteException
异常,修饰符需要为public
否则远程调用的时候会报错 ```java package com.naraku.sec.rmidemo;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface IRemoteHelloWorld extends Remote { public String hello() throws RemoteException; }
<a name="Wlrij"></a>
### 远程接口实现
- 远程接口的实现类,继承`java.rmi.server.UnicastRemoteObject`,实现远程调用的函数
```java
package com.naraku.sec.rmidemo;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
protected RemoteHelloWorld() throws RemoteException {
super();
System.out.println("RemoteHelloWorld构造方法");
}
@Override
public String hello() throws RemoteException {
System.out.println("Callback");
return "Hello";
}
}
创建实例和注册表
Naming.bind
和Naming.rebind
的区别:bind
指“绑定”,如果时“绑定”时Registry已经存在对应的Name
,则系统会抛出错误rebind
指“重绑定”,如果“重绑定”时Registry已经存在对应的Name
,则绑定的远程对象将被替换- 除非有特别的业务需求,否则建议使用
rebind
方法进行绑定
Registry.rebind
和Naming.rebind
的区别:Registry.rebind
是使用RMI注册表绑定,所以不需要完整RMI URLNaming.rebind
是通过Java的名称服务进行绑定,由于名称服务不止为RMI提供查询服务,所以绑定时需要填入完整RMI URL
Naming.rebind
- 实现Registry并将上面的类实例化,然后绑定到一个地址
package com.naraku.sec.rmidemo;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class DemoServer {
public static void main(String[] args) throws Exception {
// 实例化远程对象
RemoteHelloWorld hello = new RemoteHelloWorld();
// 实现注册表
LocateRegistry.createRegistry(1099);
// 将远程对象注册到注册表里面, 绑定地址
Naming.rebind("rmi://192.168.111.1:1099/Hello", hello);
// 如果Registry在本地, Host和Port可以省略, 默认 localhost:1099
// Naming.rebind("Hello", hello);
System.out.println("Start Server, Port is 1099");
}
}
Registry.rebind
// 创建远程对象
RemoteHelloWorld hello = new RemoteHelloWorld();
// 创建注册表
Registry registry = LocateRegistry.createRegistry(1099);
// 将远程对象注册到注册表里面,绑定地址
registry.rebind("Hello",hello);
RMI-Client
- 编写RMIClient,并调用远程对象。需要注意的是,如果远程方法有参数,调用方法时所传入的参数必须是可序列化的。在传输中是传输序列化后的数据,服务端会对客户端的输入进行反序列化
package com.naraku.sec.rmidemo;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class DemoClient {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
// 查询 hello 对象
IRemoteHelloWorld hello = (IRemoteHelloWorld) Naming.lookup("rmi://192.168.111.2:1099/Hello");
// 调用远程方法
String ret = hello.hello();
System.out.println(ret);
}
}
- Client也有
Registry.lookup
和Naming.lookup
,但它们是一样的
RMI通信过程
Registry就像⽹关,不会执⾏远程⽅法,但Server可以在上⾯注册⼀个Name到对象的绑定关系。Client通过Name向Registry查询,得到这个绑定关系,然后再连接Server。最后,远程⽅法在Server上调⽤。
发起RMI通信
发起一次RMI通信,Server端192.168.111.1
,Client端192.168.111.2
通信过程中会建立2次TCP连接,数据包附件在文末
第1次连接是Client和Registry的连接,连接到目标IP:1099
- Client发送
Call
消息:Client连接Registry,寻找Name=Hello
的对象
- Registry响应
ReturnData
消息:返回Name=Hello
对象的序列化数据,并包含对象的IP和端口0xACED0005
常见的Java反序列化16进制特征。所以这里从\xAC\xED
开始就是序列化的数据,IP和端口这只是这个对象的一部分
- 返回的端口位于IP地址后一个字节,这里是
\x00\x00\xe0\x77
即57463
,所以后面Client将向Server的该端口发起第2次请求 ```python int(“0x0000e077”, 16)
第2次连接是Client和Server的连接。Client向Server的目标端口发起请求,并正式调用远程方法
<a name="EvY2h"></a>
### 远程调用报错
- Client和Server的`package`路径需要一致
![image.png](https://cdn.nlark.com/yuque/0/2022/png/520228/1648456791641-1c5e9de3-13ef-4073-b826-dbd8d7b89219.png#clientId=u0ba54ed4-a501-4&from=paste&height=136&id=jXhPN&margin=%5Bobject%20Object%5D&name=image.png&originHeight=271&originWidth=1211&originalType=binary&ratio=1&size=64223&status=done&style=none&taskId=u3a094694-8fdc-407e-b020-b98f646b32a&width=605.5)
<a name="E967h"></a>
## 攻击RMI Registry
- 前面是RMI整个的原理与流程,那么RMI会带来哪些安全问题?
- 如果我们能访问RMI Registry服务,如何对其攻击?
- 如果我们控制了目标RMI客户端中`Naming.lookup`的第一个参数(也就是RMI Registry的地址),能不能进行攻击?
- 这里尝试在`192.168.111.2`中调用`192.168.111.1`的Registry服务,发现报错如下图所示。原因是**Java对远程访问RMI Registry做了限制**,只有来源地址是`localhost`的时候,才能调用`rebind/bind/unbind`等方法。
> 官方文档:出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。
![image.png](https://cdn.nlark.com/yuque/0/2022/png/520228/1648525066896-cf539a7a-6438-4bdb-bb69-4fb33589a1d9.png#clientId=u39f2d539-ef97-4&from=paste&height=372&id=u1070f5ac&margin=%5Bobject%20Object%5D&name=image.png&originHeight=743&originWidth=1052&originalType=binary&ratio=1&size=394382&status=done&style=none&taskId=u19b656c0-5ef5-4b80-a8f9-de7e017209d&width=526)
- 但可以远程调用`list/lookup`方法,`list`方法可以列出目标上所有绑定的对象
```java
package com.naraku.sec.rmidemo;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class DemoClient {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
// list方法
String[] objs = Naming.list("rmi://192.168.111.1:1099/");
for (String obj:objs) {
System.out.println(obj);
}
}
}
那么只要目标服务器上存在一些危险方法,通过RMI就可以对其进行调用。之前有一个工具https://github.com/NickstaDB/BaRMIe,其中一个功能点就是进行危险方法的探测。
关于RMI的攻击手法远不止于此,但进一步扩展需要对CC链有所了解,这里先告一段落,后面填坑,也可以根据参考文章进一步学习。
RMI通信数据包:rmi.pcapng.zip
- 参考文章: