看RMI漏洞时候,对其漏洞原理并不是很理解,所以简单调试了下源码加强下漏洞理解

由于要调试到RegistryImpl_Stub这种动态类,刚开始用的源码版本是JDK8u141,后来发现源码有些地方进行了修改,故此换回了JDK 7u80

以下是源码版本JDK 7u80的源码

创建注册中心

createRegistry

Registry registry = LocateRegistry.createRegistry(1099);

从上面这句代码入手,追溯下去,可以发现服务端创建了一个RegistryImpl对象。

LiveRef var2 = new LiveRef(id, var1);这里进行了一些数据的封装并赋值给var2

跟进下一行this.setup()

1.首先创建了个UnicastServerRef对象,把刚刚封装的var2传入了进去

2.跟进下一行setup,把创建UnicastServerRef的对象作为var1传入了setup()中

在setup()中调用了UnicastServerRef的exportObject()方法

跟进UnicastServerRef的exportObject()方法

第85行创建了RegistryImpl的代理对象RegistryImpl_stub,这个对象就是后面服务于客户端的RegistryImpl的Stub对象

第91行传入RegistryImpl,创建出RegistryImpl_Skel对象

继续回到前面,看94行,这里用skeleton、stub、UnicastServerRef对象、ObjID和一个boolean值等构造了一个Target对象,也就是这个Target对象基本上包含了全部的信息

在这上面都是没有进行网络操作,只是进行了一些对象的创建和变量的赋值操作。

接下来就是网络层的一些操作了

调用this.ref的exportObject方法,传入了Target对象,跟进LiveRef的exportObject

又调用了TCPEndpoint的exportObject方法,然后最终到达了TCPTransport的exportObject方法。以下是调用栈

TCPTransport的exportObject方法做的事情就是将上面构造的Target对象暴露出去。

此时的this是TCPTransport对象,跟进TCPTransport的listen()方法

调用TCPEndpoint#newServerSocket方法,会开启端口监听

接下来的第231行就是启动一条线程,等待客户端的请求

回到TCPTransport#exportObject,往下看

调用了父类Transport的exportObject()将Target对象存放进ObjectTable中

注册中心处理请求

接上,在TCPTransport#listen中

跟进AcceptLoop()到executeAcceptLoop

这里就是获取了请求中的一些信息,在371行创建一个线程,调用内部类ConnectionHandler来处理请求

进入ConnectionHandler,此内部类就是用来处理请求的

接收请求,在run()中获取的ServerSocket对象

跟进上图中的run0()后,handleMessages来处理请求

进入handleMessages(),在之前获取一些客户端传过来的数据。然后转到case 80

先是创建了一个StreamRemoteCall对象,并传入var1,var1是当前连接的Connection对象

紧接着跟入下一行TCPTransport#serviceCall

这里获取了OBJID,后面会获取到Target对象

最后在下面调用了UnicastServerRef#dispatch来处理请求

接着跟dispatch:

这里传递的两个参数,一个是Target对象,一个是当前连接的StreamRemoteCall对象,

接着调用到了oldDispatch

跟进oldDispatch,在最后调用到了this.skel.dispatch

可以看到this.skel就是之前创建的RegistryImpl_Skel对象,也就是说UnicastServerRef#dispatch最后会调用到RegistryImpl_Skel#dispatch来处理请求

接着跟RegistryImpl_Skel#dispatch方法:

var3是一个int类型的数组,分别对应了

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

然后会进入case对方法进行处理,可以看到case 0对应的是bind,有执行readObject,而最后var6.bind中的var6是RegistryImpl对象,由createRegistry获得

比如调用了bind,则会进入RegistryImpl_Skel#dispatch中,先反序列化readObject传过来的序列化对象,之后进行var6.bind来注册服务,而var6则是RegistryImpl对象。也就是说无论是客户端还是服务端,最终其调用注册中心的方法都是通过创建的RegistryImpl对象进行调用的。

获取注册中心

getRegistry

在前面createRegistry获得的是RegistryImpl对象,而getRegistry获得的是RegistryImpl_Stub对象

Registry registry = LocateRegistry.getRegistry("192.168.202.1",1099);

直接查看getRegistry源码最后返回的是一个RegistryImpl_Stub对象

调用bind绑定到注册中心

在两种方式获取的RegistryImpl和RegistryImpl_Stub对象中,其bind方法有什么区别呢

RegistryImpl#bind

在第一步会checkAccess,里边有一些判断,会对你当前的权限、来源IP进行判断。

之后判断这个键是否已经被绑定过,如果已经被绑定过,则抛出一个AlreadyBoundException的错误,否则将键和对象都put到Hashtable中。

这里的bindings是一个Hashtable,以键-值的方式存储了服务端注册的服务。

RegistryImpl_Stub#bind

进入RegistryImpl_Stub#bind,这里的var3,前面说过,bind方法对于的数字为0,此时的var3就代表了bind方法对应的数字

调用UnicastRef#newCall,将RegistryImpl_Stub对象传了进去,RegistryImpl_Stub存了一些服务器相关的信息。

简单来看newCall就是与远程RegistryImpl的Skeleton对象的连接

new newConnection用来创建一个TCPConnection对象,里面写了一些ip、端口

new StreamRemoteCall创建了一个StreamRemoteCall对象,把var6,LiveRef,ObjID,调用bind方法的值var3和4905912898345647071传入了StreamRemoteCall对象

以上两个都是进行数据的封装

调用完后,回到RegistryImpl_Stub#bind(因为这里RegistryImpl_Stub是动态类,故用了jdk8u141的源码调试的截图)

序列化两个内容

  • 序列化后的var1,var1为我们要绑定远程对象对应的名称
  • 序列化后的var2,var2为我们要绑定的远程对象

最后调用invoke方法把请求发出去,注册中心就会接收到请求调用RegistryImpl_Skel#dispatch进行处理。

以下是注册中心的RegistryImpl_Skel#dispatch处理请求

注册中心首先会反序列化readObject两个对象,第一个和第二个对应上面两个writeObject写入的对象,绑定远程对象对应的名称和要绑定的远程对象

接着调用var6.bind来绑定服务,var6即RegistryImpl对象,调用到RegistryImpl#bind把键和对象都put到Hashtable中。

创建远程对象

其实创建远程对象的时候,和创建注册中心是类似。生成其存根stub和skel,并且会绑定随机一个端口发布

在这里下个断点

HelloImpl继承了UnicastRemoteObject

跟进super方法,传入port为0

这里调用了exportObject,并且传入了HelloImpl对象

跟进exportObject

new UnicastServerRef()就是封装了一些host,ObjID等

经过了几次exportObject调用后,到了UnicastServerRef#exportObject

Util.createProxy创建了一个远程对象的代理对象proxy。这点和之前注册中心的并不同,注册中心创建的是RegistryImpl_Stub

跟进Util.createProxy看到,后面返回了动态代理对象,而其invocationHandler值为RemoteObjectInvocationHandler(远程调用该对象时,客户端获取到的其实是该代理对象)

回到UnicastServerRef#exportObject,看到var5的值最终是一个动态代理类

继续往下看,if判断是否为RemoteStub类,显然RemoteObjectInvocationHandler不属于RemoteStub,返回false,不执行if里面的语句

下面构造了一个Target对象,然后调用this.ref的exportObject方法,传入了Target对象

下面就是网络层的操作了

跟进exportObject,最终调用 TCPTransport#exportObject 方法

跟进listen()

调用newServerSocket开启一个随机端口监听,这里开启的是11061

回到TCPTransport#exportObject。

最终服务暴露(exportObject)时做了两件事,一是如果 socket 没有启动,启动 socket 监听;二是将 Target 实例注册到 ObjectTable 对象中。

通信调用

接下来讲客户端与服务器之间通信是怎么进行的

客户端与服务端的通信

客户端代码:

        //获取远程主机对象
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
//查找注册中心得hello对象
Hello hello = (Hello)registry.lookup("hello");
//调用方法
System.out.println(hello.hello("yangyang"));

客户端与服务端的通信发生在调用远程方法时

hello.hello(); //调用方法

前面两行,拿到的hello对象是proxy代理对象,且该代理对象的handler为RemoteObjectInvocationHandler

此时是远程代理对象RemoteObjectInvocationHandler与服务器的Skel进行通信

在Proxy调用方法时候,会进入对应的InvocationHandler的invoke方法中,也就是RemoteObjectInvocationHandler的invoke方法

在这个做了个if判断为false,跳转到else中

跟进invokeRemoteMethod

这里调用了this.ref.invoke,而this.ref则是UnicastRef对象

跟进UnicastRef#invoke

和前面一样newConnection用来创建一个TCPConnection对象,里面写了一些ip、端口

new StreamRemoteCall创建了一个StreamRemoteCall对象,把TCPConnection对象,ObjID,-5976794856777945295传入了StreamRemoteCall对象

然后往下看,for循环条件满足(前提是传递的参数是一个对象)跟入下面的marshalValue

这个方法是把调用方法的参数进行了序列化

回到UnicastRef#invoke,下面调用了executeCall()方法

跟进StreamRemoteCall#executeCall方法,这个方法就是用来发送给服务端数据的了

继续跟进releaseOutputStream方法,this.out.flush就是把数据发送给服务端

发送完数据后,回到UnicastRef#invoke,下面的unmarshalValue

unmarshalValue则是接受服务端发过来的数据进行readObject读取,然后赋值给var13返回。

到这里整个客户端与服务端通信流程就走完了,下面来看下服务端和客户端的通信

服务端和客户端的通信

和注册中心一样,服务端的接收到请求后,最后会进入UnicastServerRef#dispatch进行处理

这里有两次处理请求:

第一次是在lookup的时候

当执行lookup的时候,会进入UnicastServerRef#dispatch中,到124行进入oldDispatch

跟进oldDispatch,看到调用了DGCImpl_Skel的dispatch处理

第二次是发生在UnicastServerRef#dispatch中的第151行进行处理

当传入的参数类型是一个Object的时候,这里也会进行unmarshalValue

unmarshalValue中进行了readObject,此处就是服务端反序列化触发的一个点

RMI源码调试的更多相关文章

  1. 开启Tomcat 源码调试

    开启Tomcat 源码调试 因为工作的原因,需要了解Tomcat整个架构是如何设计的,正如要使用Spring MVC进行Web开发,需要了解Spring是如何设计的一样,有哪些主要的类,分别是用于干什 ...

  2. 在Eclipse中进行HotSpot的源码调试--转

    原文地址:http://www.linuxidc.com/Linux/2015-05/117250.htm 在阅读OpenJDK源码的过程中,经常需要运行.调试程序来帮助理解.我们现在已经可以编译出一 ...

  3. [原创]在Windows和Linux中搭建PostgreSQL源码调试环境

    张文升http://ode.cnblogs.comEmail:wensheng.zhang#foxmail.com 配图太多,完整pdf下载请点这里 本文使用Xming.Putty和VMWare几款工 ...

  4. SpringMVC DispatcherServlet 启动和加载过程(源码调试)

    在阅读本文前,最好先阅读以下内容(当然,如果对 Servlet 已经有所了解,则可跳过): http://www.cnblogs.com/cyhbyw/p/8682078.html http://ww ...

  5. 《k8s-1.13版本源码分析》-源码调试

    源码分析系列文章已经开源到github,地址如下: github:https://github.com/farmer-hutao/k8s-source-code-analysis gitbook:ht ...

  6. SpringBoot自动配置源码调试

    之前对SpringBoot的自动配置原理进行了较为详细的介绍(https://www.cnblogs.com/stm32stm32/p/10560933.html),接下来就对自动配置进行源码调试,探 ...

  7. HashMap源码调试——认识"put"操作

    前言:通常大家都知道HashMap的底层数据结构为数组加链表的形式,但其put操作具体是怎样执行的呢,本文通过调试HashMap的源码来阐述这一问题. 注:jdk版本:jdk1.7.0_51 1.pu ...

  8. .net源码调试 http://referencesource.microsoft.com/

    其实关于.net源码调试 网上的资料已经很多了,我以前转载的文章有 VS2010下如何调试Framework源代码(即FCL) 和 如何使你的应用程序调试进.NET Framework 4.5源代码内 ...

  9. eclipse中jdk源码调试步骤

    分析源码是学习一项技术内幕最有效的手段.由于正常的引入JAr包源码没法进行对源码打断点,想要深入了解源码不方便.下面就开始介绍源码调试的步骤. 1.在eclipse新建一个JAVA项目compare_ ...

随机推荐

  1. 手脱UPX壳的方法

    0X00    了解 upx UPX作为一款元老级PE加密壳,在以前的那个年代盛行,著名病毒[熊猫烧香]就是使用这款加密壳. 0X01    单步跟踪法 就是使用ollydbg加载程序后,按F8进行单 ...

  2. vulnhub靶机-XXE Lab 1

    目录 信息收集 漏洞利用 信息收集 扫描目标主机,ip为192.168.88.154 nmap扫描结果 存在robots.txt文件.直接访问其中的admin.php显示404,加一层目录访问/xxe ...

  3. 大厂需要什么样的 Android 开发?

    前言 昨天和一个百度的朋友闲聊,他说根据最近招聘 Android工程师的经验来看,大部分候选人在工作 3 年的时候基本都会遇上一道难过的坎. 为啥这么说呢? 因为工作一段时间之后,大部分工程师都已经完 ...

  4. 关于c++ STL map 和 unordered_map 的效率的对比测试

    本文采用在随机读取和插入的情况下测试map和unordered_map的效率 笔者的电脑是台渣机,现给出配置信息 处理器 : Intel Pentium(R) CPU G850 @ 2.90GHz × ...

  5. linux c语言学习笔记之守护进程

    哈尔滨理工大学软件工程专业08-7李万鹏原创作品,转载请标明出处 http://blog.csdn.net/woshixingaaa/archive/2010/06/06/5651095.aspx 守 ...

  6. JVM学习笔记-第七章-虚拟机类加载机制

    JVM学习笔记-第七章-虚拟机类加载机制 7.1 概述 Java虚拟机描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被 ...

  7. EasyExcel导入导出

    maven依赖 <!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel --> <dependency> & ...

  8. kafka查看Topic列表及消费状态等常用命令

    环境 本文中的操作均基于kafka_1.3.3.0,且所有命令经过实际验证. 常用工具 新建Topic ./kafka-topics --zookeeper 166.188.xx.xx --creat ...

  9. C# 调用C++结构体

    参考网址:C#调用C/C++动态库,封装各种复杂结构体._liguo9860的专栏-CSDN博客 现在公司要做一个使用C#程序调用C++的一个DLL库,解析文件的功能.所以在网上找了一些资料.     ...

  10. C# 串口开发

    在单片机项目开发中,上位机也是一个很重要的部分,主要用于数据显示(波形.温度等).用户控制(LED,继电器等),下位机(单片机)与 上位机之间要进行数据通信的两种方式都是基于串口的: USB转串口 - ...