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. 【转】Gitlab CI & Docker & Dockerfile & Docker-compose的博客文章

    gitlab权限设置 GitLab功能系列报道-标签(Labels) Gitlab CI 使用高级技巧 docker-compose是个好东西,越用越香 基于docker-compose的Gitlab ...

  2. Shell系列(2)- 脚本执行方式

    创建shell脚本 [root@localhost sh]# vim hello.sh  shell脚本必须用.sh,同时方便文件管理 #!/bin/bash:shell文件第一行必须是这个,声明这个 ...

  3. 链式调用+对象属性与遍历+this指向+caller/callee

    之前的作业: 提示: 在开发的时候尽量在函数内部将作用都给调用好,在外部就能够直接使用 链式调用: 正常这样是不行的,因为没有具体返回值:  return 具体的对象,这样的才是链式操作,jquery ...

  4. hadoop报错

    19/11/24 08:29:08 INFO qlh.MyMapreduce: ================this is job================= 19/11/24 08:29: ...

  5. [C语言]学习之路

    实例:C语言编程题 求100到300之间所有素数 #include <stdio.h> int main(void) { int i,j; for(i = 100;i <= 300; ...

  6. 鸿蒙内核源码分析(内存管理篇) | 虚拟内存全景图是怎样的 | 百篇博客分析OpenHarmony源码 | v12.04

    百篇博客系列篇.本篇为: v12.xx 鸿蒙内核源码分析(内存管理篇) | 虚拟内存全景图是怎样的 | 51.c.h .o 内存管理相关篇为: v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有 ...

  7. CSS3思维导图

  8. 关于布隆过滤器,手写你真的知其原理吗?让我来带你手写redis布隆过滤器。

    说到布隆过滤器不得不提到,redis, redis作为现在主流的nosql数据库,备受瞩目:它的丰富的value类型,以及它的偏向计算向数据移动属性减少IO的成本问题.备受开发人员的青睐.通常我们使用 ...

  9. NOIP 模拟一 考试总结

    序列 考场上信心满满的打了nlogn的做法,我以为我稳了.据考试结束1h时发现看错题目了,打成了不连续的子序列.匆匆改了n2logn的做法.考试结束后,我发现我跪了.原来到终点才会发现我做的和人家不是 ...

  10. IDEA破解方法:重新刷新到30天【支持正版】

    IDEA破解方法:重新刷新到30天[支持正版] 步骤: 导入plugins.zhile.io 进入File-->Settings-->Plugins 点设置(齿轮符号)-->Mana ...