点击上方蓝字关注我们
0x00 前言
0x01 RMI是什么
RMI(Remote Method Invocation,远程方法调用)是Java的一组拥护开发分布式应用程序的API。RMI使用Java语言接口定义了远程对象,它集合了Java序列化和Java远程方法协议(Java Remote Method Protocol)。
RMI依赖的通信协议是JRMP。JRMP: Java远程方法协议(Java Remote Method Protocol,JRMP),是特定于Java技术的、用于查找和引用远程对象的协议。RMI对象是通过序列化方式进行传输的。
简单理解:原本的java程序只能在同一操作系统的方法调用,通过RMI就可以变成在不同操作系统之间对程序中方法的调用。也可以说是一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法。
0x02 rmi的三大角色
RMI中涉及到三个角色,它们分别为服务端(Server),注册中心(Registry)和客户端(Client)。
我整理成一幅图,流程大概是这样子的:
1.服务端(Server): 负责将远程对象绑定(rebind/bind)至注册中心。
2.注册中心(Registry): 服务端会将远程对象绑定至此。客户端会向注册中心查询(lookup)绑定的远程对象。
3.客户端(Client): 与注册中心和服务端交互。
存根/桩(Stub):客户端侧的代理,每个远程对象都包含一个代理对象stub,当运行在本地Java虚拟机上的程序调用运行在远程Java虚拟机上的对象方法时,它首先在本地创建该对象的代理对象stub, 然后调用代理对象上匹配的方法。
骨架(Skeleton):服务端侧的代理,用于读取stub传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值。
0x03 代码实现
分四步来创建:
1、创建远程接口
2、实现远程接口(服务端逻辑)
3、服务端(启动服务并注册)
4、客户端(调用远程服务)
下面是每个步骤的代码demo:
1️⃣创建远程接口:
package javaSec;import java.rmi.Remote;import java.rmi.RemoteException;//创建一个接口,必须继承Remote类。public interface IRemote extends Remote { public String hack() throws RemoteException;}
2️⃣实现远程接口:
package javaSec;import java.io.BufferedReader;import java.io.InputStreamReader;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;//远程对象的实现类必须要继承UnicastRemoteObject,否则我们就需要手动调用类中的exportObject静态方法public class IRemoteImpl extends UnicastRemoteObject implements IRemote { public IRemoteImpl() throws RemoteException { } @Override public String hack() throws RemoteException { StringBuilder output = new StringBuilder(); try { //执行uname命令或whoami命令都可以 Process process = Runtime.getRuntime().exec("uname"); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { output.append(line).append("n"); } process.waitFor(); } catch (Exception e) { output.append("Error: ").append(e.getMessage()); } return output.toString(); }}
最终这个“恶意”的hack方法是在服务端执行,然后将命令执行的结果返回给客户端。
3️⃣服务端:
package javaSec;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 { IRemote remoteObj = new IRemoteImpl(); Registry registry = LocateRegistry.createRegistry(1099); registry.rebind("shell", remoteObj); System.out.println("Server ready........here we go!!!"); }}
4️⃣客户端代码:
package javaSec;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 { //默认端口是1099,改成其他端口也可以,自定义。 Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); IRemote remoteObj = (IRemote) registry.lookup("shell"); System.out.println(remoteObj.hack()); }}
这里因为我在本地调试,所以我新建了两个不同的项目来代表服务端和客户端。
服务端3个代码(接口/实现类/服务端创建注册中心):
客户端2个代码(接口/客户端类-调用远程方法):
上面是本地调试的例子,下面是更真实的远程方法调用例子,为什么我这里要说更真实呢?因为目前网上关于rmi的分析都是基于本地调试,初学者和小白可能不能理解rmi的精髓——“remote(远程)”,所以我在2024年的12月14日开了一次eureka群的直播技术分享,特地演示了一遍把服务端代码真正的挪到一台vps上面,自己的个人主机电脑是客户端,通过本地调远程的方法来更直观的去看到rmi攻击的流程。
可能你会觉得这只是改个注册中心ip的事,但是并没有那么简单,实践中还是会存在很多坑的。我们首先需要将服务端的3个代码用idea打包成一个jar包,我打包成了Servlet.jar,然后放到vps上面启动,如果直接用-jar运行会报错,这时候需要用这条命令启动才能生效:
java -Djava.rmi.server.hostname=1xx.xx.xx.31 -jar Servlet.jar
为什么要加-Djava.rmi.server.hostname 这个启动参数呢?
-Djava.rmi.server.hostname 参数的作用是 指定 RMI 注册表的绑定主机名或 IP 地址。默认情况下,RMI 会使用本地机器的 localhost 或 127.0.0.1 和本机内网网段的地址作为远程对象的主机名,这就导致客户端无法从远程访问服务端。除此之外,也可以通过 System.setProperty()
在代码中设置这个主机名,这样就可以在运行jar包的时候不加-Djava.rmi.server.hostname参数启动了:
System.setProperty("java.rmi.server.hostname", "vps的公网ip地址");
下面将通过底层代码的逻辑来简单分析下rmi客户端和服务端的之间的通信过程,也顺便引出我认为初学者会疑惑的1个问题:为什么服务端的接口实现类要继承UnicastRemoteObject?
0x04 代码分析
4.1 服务端创建远程服务
通过底层代码的分析,我个人觉得整个rmi的调用其实是蛮复杂的,所以希望大家最好跟着我手敲一遍代码,然后跟我一起把代码流程走一遍。
调试之前请务必设置好idea的配置,这里有一个小坑点,也是其他文章中没提过的,那就是File > Project Structure > SDKs > Sourcepath 不能是oracle jdk的src.zip,因为oracle jdk没有sun包源码。为了避免debug过程中只能step into进.class文件的问题,一定要在File > Project Structure > SDKs > Sourcepath中使用openjdk而不是oracle jdk。
openjdk的下载地址(选择Binary格式,解压把src.zip拷贝出来):
https://adoptium.net/zh-CN/temurin/archive/?version=8
前置准备完毕之后,我们首先从创建远程对象和创建注册中心这两个关于“创建”部分的代码讲起,先看创建远程对象。主要是分析怎么把这个对象发到网络上去的过程。
打上断点,跟进后来到父类的构造函数:
java.rmi.server.UnicastRemoteObject#UnicastRemoteObject()
这里给这个远程的对象开了个端口为0,其实是随机的意思,注意这里的端口并不是注册中心的默认1099端口,然后来到java.rmi.server.UnicastRemoteObject#exportObject(java.rmi.Remote, int)
exportObject这个名字翻译成中文就是导出对象,它就是创建远程对象中核心的方法。
所以这也就是为什么我们的接口实现类一定要继承UnicastRemoteObject类,因为继承了之后就会默认在构造函数里面调用了,如果不继承,就需要手动的调用这个👇静态函数:
import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;//远程对象的实现类必须要继承UnicastRemoteObjectpublic class IRemoteImpl extends UnicastRemoteObject implements IRemote { public IRemoteImpl() throws RemoteException { //如果不继承,则需要手动调用exportObject静态方法 //UnicastRemoteObject.exportObject(this,0); } @Override public String hack() throws RemoteException { //demo }}
再看回exportObject这个函数的参数,第一个传的是一个对象,第二个是new UnicastServerRef(port),在UnicastServerRef里面会有处理网络请求的逻辑,sun.rmi.server.UnicastServerRef#UnicastServerRef(int)
这👆这里面又多了一个LiveRef类,继续跟进-> sun.rmi.transport.LiveRef#LiveRef(java.rmi.server.ObjID, int)
一共有3个参数,对象id、端口、true。继续跟进一下看看这里的getLocalEndpoint(port):
sun.rmi.transport.tcp.TCPEndpoint#getLocalEndpoint(int)
返回的其实是一个TCPEndpoint类的对象,该对象代表了本地的TCP端点,包含了端口号为port
的端点信息。通过这个对象,可以获取和操作与该TCP端点相关的属性和行为,如连接状态、数据传输等。
跟进到sun.rmi.transport.tcp.TCPEndpoint#TCPEndpoint(java.lang.String, int)
再回到sun.rmi.transport.LiveRef#LiveRef(java.rmi.server.ObjID, sun.rmi.transport.Endpoint, boolean)
host就已经是我们的ip,端口依然是0.继续跟进来到sun.rmi.server.UnicastServerRef#UnicastServerRef(int)
这里UnicastServerRef调用了父类函数sun.rmi.server.UnicastRef#UnicastRef(sun.rmi.transport.LiveRef)
后面都是一直在重复调exportObject,只不过都是在不同的类里面调,我只关注一些核心的方法就好了,一路跟进来到sun.rmi.server.UnicastServerRef#exportObject(java.rmi.Remote, java.lang.Object, boolean),在这个方法里终于创建了客户端的代理对象stub,至于为什么stub会出现在服务端的代码里,是流程上是从服务端创建好,然后放到注册中心,然后客户端去注册中心获取,获取之后再进行操作。
继续看看stub是怎么创建的,跟进sun.rmi.server.Util#createProxy
这里的implClass从名字来看就实现类,也就是我们写的IRemoteImpl接口实现类。继续往下看,有一个判断,这段代码的目的是根据一系列条件判断是否需要创建一个代理对象。具体条件如下:
- 如果
forceStubUse
为true
,则创建代理对象。 - 如果
forceStubUse
为false
,且ignoreStubClasses
为false
且stubClassExists(remoteClass)
为true
,则创建代理对象。 - 其他情况下,不创建代理对象。
可以点进去看看sun.rmi.server.Util#stubClassExists
这段代码的目的是检查给定的远程类是否有对应的代理类。如果存在代理类,则返回true
;如果不存在,则将该类缓存到withoutStubs
中,并返回false
。通过缓存没有代理类的远程类,可以避免重复的类加载尝试,提高效率。然后又是导出对象,sun.rmi.server.UnicastServerRef#exportObject(java.rmi.Remote, java.lang.Object, boolean)
后面的一些网络socket的部分就暂时忽略了。还有一个地方需要提一下,那就是sun.rmi.transport.tcp.TCPEndpoint#newServerSocket
如果listenport等于0的话,那么最终就会随机给一个值来作用端口号。
所以创建远程服务的这个流程就是把远程对象发布出去了,并发在了一个随机的端口上。
java.rmi.registry.LocateRegistry#createRegistry(int)
创建注册中心,端口默认是1099。并返回一个RegistryImpl的对象。
sun.rmi.registry.RegistryImpl#RegistryImpl(int)到这里其实跟之前服务端创建远程对象的步骤很相似的,也是LiveRef和UnicastServerRef
sun.rmi.server.UnicastServerRef#exportObject(java.rmi.Remote, java.lang.Object, boolean)
在本文开头有提到RMI使用了代理(stub)和骨架(skeleton)的概念。注意:从Java 5开始,RMI移除了Skeleton类,客户端直接通过Stub与服务端通信,但从代码逻辑上来看,它的作用仍然存在。这里的setSkeleton
方法的作用是将骨架与远程对象关联起来,确保服务器端能够正确处理来自客户端的调用请求,负责将客户端的调用请求转发到实际的远程对象上。继续看看这个setSkeleton
方法都做了什么:
从代码逻辑上看,先是检查缓存,看是否已经知道该类没有骨架类。如果没有缓存,尝试创建骨架对象。最后看看绑定的操作,这里用bind和rebind其实都可以。直接看看rebind的代码逻辑。
sun.rmi.registry.RegistryImpl#bind
sun.rmi.registry.RegistryImpl#rebind
bind
方法:用于将一个远程对象绑定到一个名称上。如果该名称已经绑定到其他对象,则抛出AlreadyBoundException
异常。rebind
方法:用于将一个远程对象重新绑定到一个名称上。如果该名称已经绑定到其他对象,原来的绑定会被覆盖,不会抛出异常。
客户端:你,想买一杯奶茶。 服务端:奶茶店,实际制作奶茶的地方。 Stub(客户端代理):
1.Stub 是客户端的“外卖员”。
2.当你下单(调用方法)时,“外卖员”会代替你联系奶茶店(服务端)。
3.它把你提供的需求(例如奶茶种类、数量等)打包好,通过网络传输到奶茶店所在的城市。
Skeleton(服务端代理):
1.Skeleton 是奶茶店的“接单员”。
2.它负责接收来自 Stub 的订单(请求),解包后交给奶茶师傅(服务端真实方法)制作奶茶。
3.制作完成后,Skeleton 会将结果(例如奶茶完成的消息或奶茶实体)打包传回给 Stub。
网络传输:
1.就像快递的运输环节一样,Stub 和 Skeleton 通过网络实现数据传输,这部分由 RMI 框架内部处理。
0x05 总结
RMI(Remote Method Invocation,远程方法调用)是Java提供的一种机制,允许一个Java对象调用另一个运行在不同JVM(Java虚拟机)中的Java对象的方法。
0x06 抽奖🎁
《ChatGPT时代:GPTs开发详解》:
定制化GPTs(Custom GPTs)是由OpenAI推出的一种创新技术,它允许用户根据自己的特定需求和应用场景来创建定制版本的GPTs。定制化GPTs结合了用户自定义的指令、额外的专业知识以及多样化的技能,旨在为用户提供日常生活、工作或特定任务中的更多帮助和支持。
《AI智能化办公:讯飞星火AI使用方法与技巧从入门到精通》:
本书以讯飞星火认知大模型为例,全面系统地阐述其基础知识、操作方法与技巧,以及相关实战案例。
更多 >> 技术分享
欢迎大家关注EureKaSec,无论是技术交流还是有兴趣加入我们团队,都欢迎随时联络沟通。
如有问题
联系作者
EureKaSec
人划
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...