Java RMI学习与解读(二)

写在前面

接上篇文章,这篇主要是跟着看下整个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 {
} @Override
public String doSomething(String thing) throws RemoteException {
return new String("Doing " + thing);
} @Override
public String say() throws RemoteException {
return "This is the say Method";
} @Override
public String sayGoodbye() throws RemoteException {
return "GoodBye RMI";
} @Override
public String sayServerLoadClient(Object name) throws RemoteException {
return name.getClass().getName();
} @Override
public 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 RMI学习与解读(一)

    Java RMI学习与解读(一) 写在前面 本文记录在心情美丽的一个晚上. 嗯.就是心情很美丽. 那为什么晚上还要学习呢? emm... 卷... 卷起来. 全文基本都是根据su18师傅和其他师傅的文 ...

  2. Java RMI学习与解读(三)

    Java RMI学习与解读(三) 写在前面 接下来这篇就是最感兴趣的Attack RMI部分了. 前面也说过,RMI的通信过程会用到反序列化,那么针对于RMI的三个角色: Server/Regisrt ...

  3. Java开发学习心得(二):Mybatis和Url路由

    目录 Java开发学习心得(二):Mybatis和Url路由 1.3 Mybatis 2 URL路由 2.1 @RequestMapping 2.2 @PathVariable 2.3 不同的请求类型 ...

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

    Java命令学习系列(二)——Jstack 2015-04-18 分类:Java 阅读(512) 评论(0) jstack是java虚拟机自带的一种堆栈跟踪工具. 功能 jstack用于生成java虚 ...

  5. Java NIO 学习笔记(二)----聚集和分散,通道到通道

    目录: Java NIO 学习笔记(一)----概述,Channel/Buffer Java NIO 学习笔记(二)----聚集和分散,通道到通道 Java NIO 学习笔记(三)----Select ...

  6. Java JDBC学习实战(二): 管理结果集

    在我的上一篇博客<Java JDBC学习实战(一): JDBC的基本操作>中,简要介绍了jdbc开发的基本流程,并详细介绍了Statement和PreparedStatement的使用:利 ...

  7. Java RMI学习

    网上资料: Java RMI Tutorial Dynamic code downloading using RMI RPC-维基:Remote procedure call implementing ...

  8. java jvm学习笔记十二(访问控制器的栈校验机制)

    欢迎装载请说明出处:http://blog.csdn.net/yfqnihao 本节源码:http://download.csdn.net/detail/yfqnihao/4863854 这一节,我们 ...

  9. Java Web学习(十二)Tomcat核心

    一.引言 其实按道理来说,学习Java web应该在前面的篇幅就写有关tomcat相关的知识点,不过近期看了一些资料,觉得以前仅仅只是知道用tomcat去发布我的项目,一些细节的东西也没有好好总结,这 ...

随机推荐

  1. elasticsearch入门到放弃之elasticsearch-head

    elasticsearch-head可理解为跟DBeaver一样是一个数据可视化工具,但是这个工具并没有理想中那么好用坑也是很多,我已经在我的github上fork了一份修改后的版本:https:// ...

  2. CTFd+ubuntu service搭建等待更新

    CTFd是一款基于Apache2.0的协议的开源CTF平台,最新版本目前为1.20.该平台功能强大,基本上能够满足目前的CTF竞赛需求,同时,该平台提供了强大的插件功能,可以自己进行插件开发实现自己的 ...

  3. 使用 VSCode 给STM32配置一个串口 printf 工程

    使用 VSCode 给STM32配置一个串口 printf 工程 gcc 重定向 printf 和 keil 不一样. 文件准备 先从以前的工程中拷过一份串口的代码来,然后在 main 函数中初始化串 ...

  4. javascript 高阶函数 实现 AOP 面向切面编程 Aspect Oriented Programming

    AOP的主要作用是吧一些跟核心业务逻辑模块无关的功能 -日志统计, 安全控制, 异常处理- 抽离出来, 再通过"动态织入"的方式掺入业务逻辑模块中. 这里通过扩展Function. ...

  5. javascript Date 日期格式化 formatDate, require.js 模块 支持全局js引入 / amd方式加载

    * 引入AMD加载方式: require.js CDN https://cdn.bootcss.com/require.js/2.3.5/require.js *  创建模块文件./js/util/d ...

  6. 关于python中的可哈希与不可哈希

    可哈希:简要的说可哈希的数据类型,即不可变的数据结构(字符串str.元组tuple.对象集objects).它是一个将大体量数据转化为很小数据的过程,甚至可以仅仅是一个数字,以便我们可以用在固定的时间 ...

  7. P4357-[CQOI2016]K远点对【K-Dtree】

    正题 题目链接:https://www.luogu.com.cn/problem/P4357 题目大意 平面上给出\(n\)个点,求第\(k\)远的点对距离. 解题思路 \(\text{K-Dtree ...

  8. FastAPI(64)- Settings and Environment Variables 配置项和环境变量

    背景 在许多情况下,应用程序可能需要一些外部设置或配置,例如密钥.数据库凭据.电子邮件服务凭据等. 大多数这些设置都是可变的(可以更改),例如数据库 URL,很多可能是敏感数据,比如密码 出于这个原因 ...

  9. openGauss X ShardingSphere,分布式方案的另一种最佳实践

    Apache ShardingSphere 持续助力于 openGauss 分布式数据库能力的构建.openGauss 数据库自 2020 年 6 月开源以来,受到了业界的广泛关注,现已吸引众多伙伴. ...

  10. Arcscene教程

    ​ ​ ​ ​ ​ ​ ​ ​ ​ ​​ ​ 筛选​ ​ ​ ​ ​ ​ ​ ​ 看不清的话可以进行如下操作:右键-->属性-->符号系统-->把高程前面的对号取消-->添加- ...