0x00 底层协议
RPC
Java RMI
最基础的解释,是实现了RPC
的Java API
。RPC
(Remote Procedure Call
,远程过程调用)是一种常见的分布式系统通信协议,它允许一个程序(客户端)通过网络向另一个程序(服务器)发送请求,以执行服务器上的某个过程或方法。这种通信方式可以使得客户端像调用本地过程一样调用服务器上的过程,从而实现了不同系统之间的远程方法调用。
RPC需要解决的问题
- 解决分布式系统中,服务之间的调用
- 远程调用像本地调用一样方便
基本过程
A
调用远程B
- 将调用的信息以序列化(将
Java
对象转换为二进制数据流的过程)的方式进行发送 B
接受信息,然后反序列化(将二进制数据流转换为Java
对象的过程)- 将内容执行,然后再以序列化的方式返回结果
A
反序列化拿到结果
RMI
Java RMI
(Remote Method Invocation
)是RPC
的Java
实现,它提供了一组API
和工具,使得Java
程序能够方便地实现RPC
通信。具体来说,Java RMI
允许一个Java
虚拟机(JVM
)上的对象调用另一个JVM
上的对象的方法,实现了分布式系统中的远程方法调用, 只不过RMI
是Java
独 有的⼀种机制。它利用Java
的序列化和反序列化技术,将对象序列化为字节流,通过网络传输到另一个JVM
,再反序列化为对象,从而实现跨JVM
的方法调用。
JMX与JMS
JMX
:是一个为应用程序、设备、系统等植入管理功能的框架,可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。它是一套标准的代理和服务,允许用户在任何Java
应用程序中使用这些代理和服务实现管理。
JMS
:是访问企业消息系统的标准API
,用于在Java
应用程序中进行消息交换,并且通过提供标准的产生、发送、接收消息的接口简化企业应用的开发。JMS
既支持点对点(point-to-point
)的域,又支持发布/订阅(publish/subscribe
)类型的域,并且提供对经认可的消息传递、事务型消息的传递、一致性消息和具有持久性的订阅者支持。
总结,JMX
主要应用于管理和监控Java
应用程序、设备、系统等,而JMS则主要应用于Java
应用程序之间的消息交换,以支持企业应用的开发。
RPC、RMI、JMS和JMX
区别
RPC
是一种平台中立的应用程序间通信方式,支持多种语言;RMI
是Java
专用的远程对象调用技术;JMS
主要用于Java
应用程序之间的消息交换;RPC
、RMI
和JMS
都是用于不同目的的通信协议,而JMX
则是一种用于管理和监控Java
应用程序的工具。
传输方式
JMS
:对象在物理上被异步从网络的某个JVM
上直接移动到另一个JVM
上RMI
:对象是绑定在本地JVM
中,只有函数参数和返回值是通过网络传送
方法调用
- 通信方式:
RMI
基于请求/应答的同步通信模式,而JMS
支持同步和异步消息处理模式。 - 耦合度:
RMI
是紧耦合结构,而JMS
建立了一个松散耦合的通信框架。 - 需求对象获得方式:在
RMI
中,对象是绑定在本地JVM
中,只有参数和返回值是通过网络传送。而在JMS
中,对象是在物理上被异步从网络的某个节点移动到另一个节点。
总结来说,RMI
基于请求/应答的同步通信模式,采用紧耦合结构,并且对象是绑定在本地JVM
中;JMS
支持同步和异步消息处理模式,建立了松散耦合的通信框架,并且对象是在物理上被异步从网络的某个节点移动到另一个节点。
此外,RMI
是Java
专用的远程对象调用技术,只适用于Java
编写的应用程序,而JMS
则是一种允许应用程序创建、发送、接受和读取消息的Java API
,主要用于在Java
应用程序之间进行消息交换,以支持企业应用的开发。
0x01 RMI概述
Java RMI(Java Remote Method Invocation)
即Java
远程方法调用。RMI
用于构建分布式应用程序,RMI
实现了Java
程序之间跨JVM
的远程通信。RMI
就是Java
中对RPC
的一种实现,对其进行了封装。
- 基于
RMI
利用的JDK
版本<=
6u141
、7u131
、8u121
- 基于
LDAP
利用的JDK
版本<=6u211
、7u201
、8u191
RMI架构
RMI
分为三部分
Registry
存放着远程对象的注册表(IP,port,标识符)Server
提供远程的对象Client
调用远程的对象
- 实现
RMI
所需的API
几乎都在:
java.rmi
:提供客户端需要的类、接口和异常;java.rmi.server
:提供服务端需要的类、接口和异常;java.rmi.registry
:提供注册表的创建以及查找和命名远程对象的类、接口和异常;
RMI通信模型
RMI
底层通讯采用了Stub
(运行在客户端)和Skeleton
(运行在服务端)机制,RMI
调用远程方法的大致如下:
RMI
客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)
。
先跟进getRegistry
方法,然后接着去调用getRegistry
方法,如果传参为LocateRegistry.getRegistry("127.0.0.1", 8989, null);
,则会直接调用144
行的getRegistry
方法
返回一个RegistryImpl_Stub
Stub
会将Remote
对象传递给远程引用层(java.rmi.server.RemoteRef)
并创建java.rmi.server.RemoteCall
(远程调用)对象。RemoteCall
序列化RMI
服务名称、Remote
对象。RMI
客户端的远程引用层传输RemoteCall
序列化后的请求信息通过Socket
连接的方式传输到RMI
服务端的远程引用层。RMI
服务端的远程引用层(sun.rmi.server.UnicastServerRef)
收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
。Skeleton
调用RemoteCall
反序列化RMI
客户端传过来的序列化。Skeleton
处理客户端请求:bind
、list
、lookup
、rebind
、unbind
,如果是lookup
则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall
传输到客户端。RMI
客户端反序列化服务端结果,获取远程对象的引用。RMI
客户端调用远程方法,RMI
服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。RMI
客户端反序列化RMI
远程方法调用结果。
0x02 RMI方法
- 实现
RMI Server
分为三部分:
- 继承
java.rmi.Remote
接口,其中定义需要远程调用的函数hello()
- 实现此接口的类
new
一个主类,用来创建Registry
,并将上面的类实例化后绑定到一个地址
RMI Client
- 可以使用
registry.rebind
或Naming.lookup
- 然后就可以正常调用
registry.rebind
通过注册表绑定远程对象:
registry.rebind
:这个方法用于将远程对象绑定到注册表中。当一个服务端创建一个对象时,它会使用bind()
或rebind()
方法将该对象注册到注册表中。客户端可以使用该对象的引用(例如,通过HelloRegistryFacade
)从注册表中获取对象,例如通过lookup()
方法。
Server
- 定义接口名为
RMIInterface
的远程接口
定义接口继承自java.rmi.Remote
接口,定义一个hello()
方法作为接口,修饰符为public
。并且定义的方法需要抛出RemoteException
的异常。
package server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RMIInterface extends Remote {
public String hello() throws RemoteException;
}
- 编写远程接口的实现类
RMIImpl
- 继承
UnicastRemoteObject
类(提供了很多支持RMI
的方法),这些方法可以通过JRMP
协议导出一个远程对象的引用,并通过动态代理构建一个可以和远程对象交互的Stub
对象。 - 重写
RMIInterface
接口中的hello()
方法。
package server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RMIImpl extends UnicastRemoteObject implements RMIInterface {
protected RMIImpl() throws RemoteException {
System.out.println("构造方法");
}
@Override
public String hello() {
System.out.println("call hello()");
return "this is hello()";
}
}
- 创建
RMIServer
实例,创建一个注册表,将需要提供给客户端的对象注册到注册表中
端口8989
开启RMI
服务,以键值对的形式存储RMI_PATH
和rmiInterface
的对应关系
rmi://127.0.0.1:8989/hello
<==>RMIImpl类实例
然后通过registry.rebind(RMI_NAME, RMIImpl)
绑定对应关系,然后通过RMIImpl
类去实现rmiInterface
接口中的方法
package server;
import java.net.MalformedURLException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, MalformedURLException {
// 创建远程对象
RMIInterface hello = new RMIImpl();
// 创建注册表
Registry registry = LocateRegistry.createRegistry(1099);
// 将远程对象注册到注册表里面,并且设置值为hello
registry.rebind("hello", hello);
}
}
Client
- 创建
RMIClient
实例,远程调用对象
package client;
import server.RMIInterface;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
// 获取远程主机对象
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 利用注册表的代理去查询远程注册表中名为hello的对象
RMIInterface hello = (RMIInterface) registry.lookup("hello");
// 调用远程方法
System.out.println(hello.hello());
}
}
- 运行结果如下
Naming.lookup
- 直接通过创建远程对象使用
Naming.lookup
绑定:
Naming.lookup
:这个方法允许客户端通过完整的URL
查找已注册的服务名,也就是通过RMI
的“活化”模式,将Remote Service
的真实提供者移植到RMI Registry
注册表所在的JVM
上。
Server
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", test);
创建并运行RMI Registry
,将test
对象绑定到Hello
名字上,test
对象就是实现了RMIInterface
接口的类
Naming.bind
参数格式:rmi://host:port/name
,host
和port
就是RMI Registry
的地址和端口,name
是远程对象的名字。如果在本地运行,host和port可以省略不写,Naming.rebind("Hello", test);
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 sRmi {
public interface RMIInterface extends Remote {
public String hello() throws RemoteException;
}
public class test extends UnicastRemoteObject implements RMIInterface {
protected test() throws RemoteException {
System.out.println("构造方法");
}
@Override
public String hello() throws RemoteException {
System.out.println("call from");
return "Hello world";
}
}
private void start() throws Exception {
test test = new test();
// 创建并运行RMI Registry
LocateRegistry.createRegistry(1099);
// 将test对象绑定到Hello名字上
// test对象就是实现了RMIInterface接口的类
Naming.rebind("rmi://127.0.0.1:1099/Hello", test);
}
public static void main(String[] args) throws Exception {
new sRmi().start();
}
}
Client
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class cRMI {
public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
sRmi.RMIInterface haha = (sRmi.RMIInterface) Naming.lookup("rmi://127.0.0.1:1099/Hello");
String hello = haha.hello();
System.out.println(hello);
}
}
分析RMI通信过程
目的是在远程服务器上执行代码,所以需要知道有哪些方法,前面服务端通过RMIInterface
继承了Remote
,并将需要调用的方法写在RMIInterface
接口中,额客服端刚好可以利用RMIInterface
接口
- 为什么端口是
50639
查看ReturnData
数据包,返回目标IP
:192.168.124.18
,后面4
位十六进制字节\x00\x00\xc5\xcf
转换成十进制也就是端口50639
整个过程如下:
- 客户端连接
Registry
,寻找Name
(Hello的对象
),对应数据流中Call
消息; - 然后
Registry
返回一个序列化的数据(Name=Hello的对象
),对应ReturnData
消息; - 客户端反序列化该对象,发现该对象是一个远程对象,地址:
192.168.124.18:50639
,然后再与此地址建立TCP
连接; - 最后在新的连接中,执行真正的远程方法调用,
hello()
方法。
引用p神的图
RMI Registry
相当于网关,RMI Server
可以在RMI Registry
网关上注册一个Name=>对象
的绑定关系。RMI Client
通过Name
像RMI Registry
网关查询,得到对应绑定的关系,然后再连接RMI Server
。最后在RMI Server
上执行方法。
0x03 思考
能否攻击RMI Registry
RMi Registry
只有在来源地址是localhost
的时候,才能调用rebind
、bind
、unbind
等方法。这是Java
对远程访问RMI Registry
的限制。
bind
方法:当客户端想要和远程RMI服务器绑定时,会调用RegistryImpl_Stub
的bind
方法。这个方法的具体调用方式是通过Util
中stubClassExists
方法将RegistryImpl_Stub
反射进行加载,然后对数据进行writeObject
序列化操作。rebind
方法:当客户端想要更新已存在的注册表项时,会使用rebind
方法。这个方法会将参数对象进行序列化发送至RMI Registry
,然后对RMI Registry
的返回数据进行了反序列化。unbind
方法:当客户端想要取消和远程RMI
服务器的绑定时,会使用unbind
方法。这个方法会将参数对象进行序列化发送至RMI Registry
,然后对RMI Registry
的返回数据进行反序列化。
RMI
利用codebase
执行任意代码
更详细内容参考p神Java
安全漫谈
使用限制:
- 安装并配置了
SecurityManager
Java
版本低于7u21
、6u45
,或者设置了java.rmi.server.useCodebaseOnly=false
java.rmi.server.useCodebaseOnly=true
时,Java
虚拟机将只信任预先配置好的codebase
,不再支持从RMI
请求中获取。
利用条件苛刻,有时间再补充复现过程。
0x04 参考链接
https://xz.aliyun.com/t/4711#toc-8
https://www.anquanke.com/post/id/204740