写在前面#

接上篇文章,这篇主要是跟着看下整个RMI过程中的源码并对其做简单的分析

RMI源码分析#

还是先回顾下RMI流程:

  1. 创建远程对象接口(RemoteInterface)
  2. 创建远程对象类(RemoteObject)实现远程对象接口(RemoteInterface)并继承UnicastRemoteObject类
  3. 创建Registry&Server端,一般Registry和Server都在同一端。
    • 创建注册中心(Registry)LocateRegistry.getRegistry("ip", port);
    • 创建Server端:主要是实例化远程对象
    • 注册远程对象:通过Naming.bind(rmi://ip:port/name ,RemoteObject) 将name与远程对象(RemoteObject)进行绑定
  4. 远程对象接口(RemoteInterface)应在Client/Registry/Server三个角色中都存在
  5. 创建Client端
    • 获取注册中心LocateRegistry.getRegistry('ip', prot)
    • 通过registry.lookup(name) 方法,依据别名查找远程对象的引用并返回存根(Stub)
  6. 通过存根(Stub)实现RMI(Remote Method Invocation)

创建远程接口与远程对象#

在new RemoteObject的过程中主要做了这三件事

  1. 创建本地存根stub,用于客户端(Client)访问。
  2. 启动 socket,监听本地端口。
  3. Target注册与查找。

先抛出一段RemoteInterface和RemoteObject的代码

RemoteInterface

import java.rmi.Remote;
import java.rmi.RemoteException;public interface RemoteInterface extends Remote{String doSomething(String thing) throws RemoteException;String say() throws RemoteException;String sayGoodbye() throws RemoteException;String sayServerLoadClient(Object name) throws RemoteException;Object sayClientLoadServer() throws RemoteException;
}

RemoteObject

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {protected RemoteObject() throws RemoteException {}@Overridepublic String doSomething(String thing) throws RemoteException {return new String("Doing " + thing);}@Overridepublic String say() throws RemoteException {return "This is the say Method";}@Overridepublic String sayGoodbye() throws RemoteException {return "GoodBye RMI";}@Overridepublic String sayServerLoadClient(Object name) throws RemoteException {return name.getClass().getName();}@Overridepublic Object sayClientLoadServer() throws RemoteException {return new ServerObject();}
}

那么接下来看看我们之前提到的在代码中必须要写的一些内容

Remote#

那么首先看创建远程对象接口(RemoteInterface)部分,这个接口在上篇文章中提到过,需要继承java.rmi.Remote接口且该接口中声明的方法要抛出RemoteException异常,在Remote接口的注释中提到:

这个接口用于识别某些接口是否可以从非本地虚拟机调用方法,且远程对象必须间接或直接的实现这个接口;也提到了我们之前说的"特殊的远程接口",当一个接口继承了java.rmi.Remote接口后,在该接口上声明的方法才可以被远程调用。

个人感觉有点像一个类似于序列化的标记式接口,用来标记这个接口的实现类是否可以被远程调用该类中的方法。

RemoteException#

异常类,注释中说明了,任何一个继承了java.rmi.Remote的远程接口,在其接口中的方法需要throws RemoteException异常,该异常是指远程方法调用执行过程中可能发生的与通信相关的异常。

流程与代码分析#

用于使用JRMP导出远程对象(export remote object)并获取存根,通过存根与远程对象进行通信

主要是构造方法和exportObject(Remote),这个点在Longofo师傅的文章有提到,当实现了远程接口而没有继承UnicastRemoteObject类的话需要自己调UnicastRemoteObject.exportObject(Remote)方法导出远程对象。

构造方法

/*** Creates and exports a new UnicastRemoteObject object using an* anonymous port.* @throws RemoteException if failed to export object* @since JDK1.1*/
protected UnicastRemoteObject() throws RemoteException
{this(0);
}

exportObject(Remote)

/*** Exports the remote object to make it available to receive incoming* calls using an anonymous port.* @param obj the remote object to be exported* @return remote object stub* @exception RemoteException if export fails* @since JDK1.1*/public static RemoteStub exportObject(Remote obj)throws RemoteException{/** Use UnicastServerRef constructor passing the boolean value true* to indicate that only a generated stub class should be used.  A* generated stub class must be used instead of a dynamic proxy* because the return value of this method is RemoteStub which a* dynamic proxy class cannot extend.*/return (RemoteStub) exportObject(obj, new UnicastServerRef(true));}

这两个方法最终都会走向重载的exportObject(Remote obj, UnicastServerRef sref)方法

初始化时会创建UnicastServerRef 对象并调用其exportObject方法

在方法中会通过createProxy()方法,创建RemoteObjectInvocationHandler处理器,给RemoteInterface接口创建动态代理

之后回到UnicastServerRef#exportObject方法,new了一个Target对象,在该对象中封装了远程对象的相关信息,其中就包括stub属性(一个动态代理对象,代理了我们定义的远程接口)

之后调用liveRef的exportObject方法

接着调用sun.rmi.transport.tcp.TCPEndpoint#exportObject方法(调用栈如下图),最终调用的是TCPTransport#exportObject()方法在该方法中开启了监听本地端口,并调用了Transport#exportObject()

在该方法中调用了ObjectTable.putTarget()方法,将 Target 实例注册到 ObjectTable 对象中。

而在ObjectTarget类中提供了两种方式(getTarget的两种重载方法)去查找注册的Target,分别是参数为ObjectEndpoint类型对象以及参数为Remote类型的对象

回过头看一下动态代理RemoteObjectInvocationHandler,继承 RemoteObject 实现 InvocationHandler,因此这是一个可序列化的、可使用 RMI 远程传输的动态代理类。主要是关注invoke方法,如果传入的method对象所代表的类或接口的 class对象是Object.class就走invokeObjectMethod否则走invokeRemoteMethod

invokeRemoteMethod方法中最终调用的是UnicastRef.invoke方法,UnicastRef 的 invoke 方法是一个建立连接,执行调用,并读取结果并反序列化的过程。反序列化在 unmarshalValue调用readObject实现

如上就是在创建远程接口并实例化远程对象过程中的底层代码运行的流程(多掺杂了一点动态代理部分),这里借一张时序图。

建议各位师傅也是打个断点跟一下比较好,对于整体在实例化远程对象时的一个流程就比较清晰了。

创建注册中心#

创建注册中心主要是Registry registry = LocateRegistry.createRegistry(1099);

打断点debug进去,首先是实例化了一个RegistryImpl对象

进入有参构造,先new LiveRef对象,之后new UnicastServerRef对象并作为参数调用setup方法

setup方法中依旧调用UnicastServerRef#exportObject方法,对RegistryImpl对象进行导出;与上一次不同的是这次会直接走进if中创建stub,因为if判断中调用了stubClassExists方法,该方法会判断传入的类是否在本地有xxx_stub类。

而RegistryImpl显然是有的,所以会走进createStub方法

该方法中反射拿到构造方法然后实例化RegistryImple_Stub类来创建代理类。

调用setSkeleton创建骨架

也是反射操作,实例化RegistryImple_Skel类

最终赋值给UnicastServerRef.skel属性

在UnicastServerRef类中通过dispatch方法实现了对远程对象方法的调用并将结果进行序列化并通过网络传到Client端

public void dispatch(Remote var1, RemoteCall var2) throws IOException {try {long var4;ObjectInput var40;try {var40 = var2.getInputStream();int var3 = var40.readInt();if (var3 >= 0) {if (this.skel != null) {this.oldDispatch(var1, var2, var3);return;}throw new UnmarshalException("skeleton class not found but required for client version");}var4 = var40.readLong();} catch (Exception var36) {throw new UnmarshalException("error unmarshalling call header", var36);}MarshalInputStream var39 = (MarshalInputStream)var40;var39.skipDefaultResolveClass();Method var8 = (Method)this.hashToMethod_Map.get(var4);if (var8 == null) {throw new UnmarshalException("unrecognized method hash: method not supported by remote object");}this.logCall(var1, var8);Class[] var9 = var8.getParameterTypes();Object[] var10 = new Object[var9.length];try {this.unmarshalCustomCallData(var40);for(int var11 = 0; var11 < var9.length; ++var11) {var10[var11] = unmarshalValue(var9[var11], var40);}} catch (IOException var33) {throw new UnmarshalException("error unmarshalling arguments", var33);} catch (ClassNotFoundException var34) {throw new UnmarshalException("error unmarshalling arguments", var34);} finally {var2.releaseInputStream();}Object var41;try {var41 = var8.invoke(var1, var10);} catch (InvocationTargetException var32) {throw var32.getTargetException();}try {ObjectOutput var12 = var2.getResultStream(true);Class var13 = var8.getReturnType();if (var13 != Void.TYPE) {marshalValue(var13, var41, var12);}} catch (IOException var31) {throw new MarshalException("error marshalling return", var31);}} catch (Throwable var37) {Object var6 = var37;this.logCallException(var37);ObjectOutput var7 = var2.getResultStream(false);if (var37 instanceof Error) {var6 = new ServerError("Error occurred in server thread", (Error)var37);} else if (var37 instanceof RemoteException) {var6 = new ServerException("RemoteException occurred in server thread", (Exception)var37);}if (suppressStackTraces) {clearStackTraces((Throwable)var6);}var7.writeObject(var6);} finally {var2.releaseInputStream();var2.releaseOutputStream();}}

注册中心与远程服务对象注册的大部分流程相同,差异在:

  • 远程服务对象使用动态代理,invoke 方法最终调用 UnicastRef 的 invoke 方法,注册中心使用 RegistryImpl_Stub,同时还创建了 RegistryImpl_Skel
  • 远程对象默认随机端口,注册中心默认是 1099(当然也可以指定)

服务注册#

这部分其实就是Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven", remoteObject);的实现

依旧是打断点跟进去看下

进入 java.rmi.Naming#bind() 方法后先会解析处理我们传入的url。先调用java.rmi#parseURL(name)方法后进入intParseURL(String str)方法。该方法内部先会对我们传入的url(rmi://127.0.0.1:1099/Zh1z3ven)做一些诸如协议是否为rmi,是否格式存在问题等判断,之后做了字符串的处理操作,分别获取到我们传入的url中的host(127.0.0.1)、port(1099)、name(Zh1z3ven)字段并作为参数传入java.rmi.Naming的内置类ParsedNamingURL的有参构造方法中去

也就是对该内置类中的属性进行赋值操作

之后回到Naming#bind()方法,将实例化的ParsedNamingURL对象赋值给parsed并作为参数带入java.rmi.Naming#getRegistry方法

最终进入getRegistry(String host, int port, RMIClientSocketFactory csf)方法,调用栈如下,后续依旧是创建动态代理的操作。动态代理部分和创建远程对象时操作差不多,就不再跟了

来看一下java.rmi.Naming#bind()中最后一步,此时会调用RegistryImpl_Stub#bind方法进行name与远程对象的一个绑定。

方法内逻辑也比较清晰,获取输出流之后进行序列化的然后调用UnicastRef#invoke方法

大致服务注册,也就是name与远程对象绑定就是这么一个逻辑,这里与su18师傅文章中不太一样的点就是,我跟入的是第二个invoke方法,而su18师傅进入的是第一个invoke方法,这里就有些不解了,待研究。

总结#

借一张su18师傅的图。Server/Registry/Client三个角色两两之间的通信都会用到java原生的反序列化操作。也就是说我们有一端可控或可以伪造,那么传入一段恶意的序列化数据直接就可以RCE。也就是三个角色都有不通的攻击场景。

END#

调试的时候深感吃力,RMI源码其实我上面提到的可能还是有很多不清楚的地方。

其实只要自己打断点debug跟一下,对于RMI的一个工作流程就很清晰了,有些点如果没有刚需可以不用跟的很深入。

后面就是针对RMI的攻击手法了,下篇更。

Java RMI学习与解读(二)相关推荐

  1. Java命令学习系列(二)——Jstack

    转载自 Java命令学习系列(二)--Jstack jstack是java虚拟机自带的一种堆栈跟踪工具. 功能 jstack用于生成java虚拟机当前时刻的线程快照.线程快照是当前java虚拟机内每一 ...

  2. Java后端学习日记(二):POJO的基本概念,编写,转化和简化

    专栏目录 Java后端学习日记(一):第一个Springboot应用--Hello World! Java后端学习日记(二):POJO的基本概念,编写,转化和简化 Java后端学习日记(三):Spri ...

  3. java基础学习总结(二)——开篇(转载于孤傲苍狼博主的心得体会)

    由于孤傲苍狼博主16年后,就没有更新博客了.其中有些文章看不了,挺可惜的.为了避免后续文章也会有类似情况,因此,他的博文基本都会转载过来,也会标注转载,做一个备份.不过文章太多,不会每篇文章都有转载于 ...

  4. Java日期学习笔记(二):JDK1.8新特性

    Java 8另一个新增的重要特性就是引入了新的时间和日期API,它们被包含在java.time包中.借助新的时间和日期API可以以更简洁的方法处理时间和日期. 在介绍本篇文章内容之前,我们先来讨论Ja ...

  5. java入门学习笔记(二)—— Eclipse入门学习之快捷键、java语言基础知识之各类关键字及其用法简析

    一.Eclipse入门学习 1. 快捷键 对于一个编辑器,快捷键必不可少,是十分好用且有效的工具. 对于一个初学者,首先掌握了如下快捷键. (很多通用的快捷键不多说) Ctrl + / -- 注释当前 ...

  6. Java后端学习笔记 -- JavaWeb(二):JavaScript

    写在开头:本文是学习Java后端开发的个人笔记,便于自己复习.文章内容引用了尚硅谷的javaweb教学,有兴趣的朋友可以上B站搜索. JavaScript     Ⅰ JavaScript介绍     ...

  7. java基础学习笔记(二)

    1.数组排序之选择法排序和冒泡排序? 选择法排序原理:数组第一位和后续位置数值比较,最大或最小的调换位置后放在第一位:依次比较将第二大或小的值调换位置后放在第二位置:代码如下: for (int j ...

  8. Java基础学习之(二)—对象与类的方法参数

    一.Java中,方法参数的使用情况: 1.一个方法不能修改一个基本数据类型的参数: 2.一个方法可以改变一个对象参数的状态: 3.一个方法不能让对象参数引用一个新的对象: 例子代码为: package ...

  9. JAVA基础学习day25--Socket基础二-多线程

    一.上传图片 1.1.示例 /* 上传图片 */ import java.net.*; import java.io.*; import java.util.*; import java.text.* ...

最新文章

  1. 关于反射GetType().GetProperties()的疑惑
  2. 快速排序算法javascript实现
  3. zzuliOJ【土豪婷婷请吃饭】【解法:Java二维数组】
  4. CCNP-EIGRP不等价负载均衡
  5. P5304-[GXOI/GZOI2019]旅行者【最短路】
  6. 【前端自动化构建】之 自动化部署
  7. 深度学习(一)神经网络中的池化与反池化原理
  8. java二叉树的遍历,递归与非递归方法
  9. springboot+Vue+java零食销售网上商城系统多商家
  10. hdp对应hadoop的版本_查看Hadoop组件版本
  11. 斐讯K3c基于frp内网穿透
  12. 仓库管理系统c#语言代码,C#仓库管理系统+完整源代码
  13. 企业邮箱地址格式是什么?企业邮箱地址类型汇总
  14. (转)证券公司私募(PB)整体服务
  15. html2canvas实现网页局部存为图片和打印
  16. 关于Ng-alain的Acl的使用
  17. Python豆瓣爬虫(2)BeautifulSoup库
  18. 杨震霆(carboy) -传奇人物
  19. 统计找出一千万以内,一共有多少质数
  20. CLOUDXNS 使用体验

热门文章

  1. 王者荣耀微信哪个服务器人多,王者荣耀微信区和qq区哪个厉害
  2. 基于Linux的tty架构及UART驱动详解
  3. 程序员必备的10款工具软件
  4. 安卓手机哪个服务器信号最强,安卓手机最强性能前十排行,第一名再逆袭第二名有争议...
  5. Matlab常见错误---带有下标的赋值维度不匹配。
  6. 多用户商城系统 KgMall2.1发布!
  7. 【黑马头条训练营】day02-黑马头条-App端文章展示
  8. [VB.NET源码]学习教程(PDF)
  9. 【机器人】基于指数积的机械臂正运动学算法
  10. SpringMVC+Vue实现前后端的志愿者招募网站