JAVA RMI 学习笔记
https://javasec.org/javase/RMI/
A机器上面有一个class,通过远程调用,B机器调用这个class 中的方法。
RMI 定义
Java远程方法调用,即Java RMI (Java Remote Method Invocation),实现远程过程调用的应用程序编程接口。
它使客户机上运行的程序可以直接调用远程服务器上的对象。
在RMI中对象是通过序列化方式进行编码传输的。(基于序列化和反序列化就可能存在反序列化漏洞了)
RMI的基础是接口,RMI构架基于一个重要的原理:定义接口 和 定义接口的具体 实现是分开的。
逻辑图
图1
图2
RMI 能让一个Java程序去调用网络中另一台计算机的Java对象的方法,那么调用的效果就像是在本机上调用一样。
通俗的讲:A机器上面有一个class,通过远程调用,B机器调用这个class 中的方法。
RMI 结构
RMI主要分为三部分
- RMI Registry注册中心
- RMI Client 客户端
- RMI Server服务端
测试实例
RMIIntface.java
1 | package com.rmi.test; |
RMIImpl.java 继承上面接口
1 | package com.rmi.test; |
RMIServer.java 创建一个服务端
1 | package com.rmi.test; |
RMIClient.java 用户去访问 对应端口 并通过远程 的对象调用其方法
1 | package com.rmi.test; |
操作步骤
启动RMIServer
先创建一个 Hello 对象
注册端口 8989
将服务名进行绑定完成操作
启动 RMIClient
获得对应的服务注册机
通过服务名 找到对应的RMI 实例
调用对应的实例的方法
实现远程函数调用
在客户端和服务器各有一个代理,客户端的代理叫Stub,服务端的代理叫Skeleton。
结果
RMIServer 先打印 启动RMI服务在 hello。 当RMIClient 调用了 hello 方法后才回答应 call hello()
RMIClient 找到对应的 服务然后调用远程 实例的 方法得到返回值
远程 会执行 System.ou.printl("call hello()");
然后 RMIClient 得到返回值 "this is hello()"
所以才出现了 调用 hello() 函数后 RMIServer 和 RMIClient 的结果不一样
上面的实例参考https://y4er.com/post/java-rmi/
RMI 反序列化漏洞介绍
RMI
通信中所有的对象都是通过Java序列化传输的,在学习Java序列化机制的时候我们讲到只要有Java对象反序列化操作就有可能有漏洞。
为了了解 RMI
对应发反序列化漏洞的实现机制。
我们需要对 RMI
实现源码进行一个分析
前景知识:复现的时候需要JDK8 121以下版本,121及以后加了白名单限制,
RMI 实现源码分析
参考分析
RMIServer.java 源码实现分析
LocateRegistry.createRegistry(port) 分析
在 RMIServer.java 中 会调用 createRegistry(port)
函数来创建端口
createRegistry(port)
函数中
这个函数 有两个方法 对应不同的参数 只分析当前函数中的指令
LocateRegistry.java
202 行
1 | /** |
返回一个注册表对象。对象的参数为 port
然后我们查看 RegistryImpl
对象函数
RegistryImpl.class
构造函数的位置在 63行
1 | public RegistryImpl(int var1) throws RemoteException { |
第一行函数 对我们的 输入进行一个封装并返回 对应的 LiveRef 类
1 | public LiveRef(ObjID objID, int port) { |
调用构造函数进行了一个 封装
接着会调用
this.setup(new UnicastServerRef(var2));
函数
但是在 调用 setup 函数之前。 会 new UnicastServerRef(var2)
创建一个新的对象。
这个对象没有干实际作用。
UnicastServerRef.java
1 | public UnicastServerRef(LiveRef ref) { |
赋值 ref 值。
然后调用 setup
函数,函数参数为 UnicastServerRef
对象
**setup函数
**
1 | private void setup(UnicastServerRef var1) throws RemoteException { |
这个函数会传入 var1 然后调用 var1 的 exportObject()
函数
exportObject()
函数
1 | public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException { |
首先获得 这个 类
var4 = “class sun.rmi.registry.RegistryImpl” 这个类
燃煤 调用了 Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
函数
在这个函数中
createProxy()
函数
1 | public static Remote createProxy(Class<?> var0, RemoteRef var1, boolean var2) throws StubNotFoundException { |
会在 createStub
这个函数 后直接返回
1 | if (var2 || !ignoreStubClasses && stubClassExists(var3)) { |
这个 createStub
函数 生成一个字符串 然后会返回一个 RegistryImpl_Stub
对象
创建了 一个对应的 Stub
1 | private static RemoteStub createStub(Class<?> var0, RemoteRef var1) throws StubNotFoundException { |
当这个运行完了。这个 createProxy()
函数后会返回到
exportObject()
函数
然后会运行到 this.setSkeleton(var1);
函数
传入的 var1 参数 RegistryImpl[UnicastServerRef [liveRef: [endpoint:[192.168.1.173:8989](local),objID:[0:0:0, 0]]]]
setSkeleton()
函数
1 | public void setSkeleton(Remote var1) throws RemoteException { |
调用 createSkeleton
函数(和createStub
函数一样返回一个对象 RegistryImpl_Skel
对象
创建一个对应的 Skeleton
createSkeleton
函数
1 | static Skeleton createSkeleton(Remote var0) throws SkeletonNotFoundException { |
生成了这两个类后 会返回到 exportObject()
函数 创建一个 Target 类
Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
对于这个 Target 类
里面 封装了 前面的函数生成 的 Sub
Skel
等
然后调用 this.ref.exportObject(var6);
函数 参数为 上一步创建的 Target 类
1 | setup#exportObject - > createProxy (生成Stub) |
registry.bind(“hello”, rmiInterface); 分析
然后我们会调用 Registry 对象的 bind 方法。
绑定服务名和服务
1 | public void bind(String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException { |
利用 bindings.put 函数进行绑定。
这样我们的 RMISever 就启动成功了
RMIClient.java 分析
客户端分析
框出来两个关键函数
LocateRegistry.getRegistry(ip, port);
传入参数 host 和 port
这个方法做的事情是通过传入的host和port构造RemoteRef对象,并创建了一个本地代理。
getRegistry()
函数
1 | public static Registry getRegistry(String host, int port, |
开始穿件一个 liveRef 对象
然后更具下面的代码
1 | RemoteRef ref = |
如果 csf 从而生成 需要的 UnicastRef 或者 UnicastRef2
然后利用 (Registry) Util.createProxy(RegistryImpl.class, ref, false);
传入的值 是封装了 (hots,port) 等
然后构造 出 RemoteRef 对象 并创建了一个本地代理。
registry.lookup(name)
Lookup
源码
1 | public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException { |
Lookup 函数更具传入的 服务名称查找对应服务
第3行的 代码 ,利用上面通过服务端host和port等信息创建的RegistryImpl_stub对象构造RemoteCall调用对象
newcall
函数
1 | public RemoteCall newCall(RemoteObject obj, Operation[] ops, int opnum, |
利用 NewCall 方法建立了 建立了跟远程 RegistryImpl 的 Skeleton 对象的连接。
运行完 newcall 函数后 会返回到 lookup 函数
然后与远程的注册机进行交互 序列化 我们的 name (“hello”)
1 | java.io.ObjectOutput out = call.getOutputStream(); |
然后往下有一个
super.ref.invoke(var2);
函数
这个函数 里面调用了 call.executeCall()
=> StreamRemoteCall.executeCall()
方法
1 | public void invoke(RemoteCall call) throws Exception { |
StreamRemoteCall.executeCall()
方法
1 | public void executeCall() throws Exception { |
在执行完 try 里面的代码后,我们的查询请求就已经发送给服务器了。
然后服务器 会 调用 sun.rmi.registry.RegistryImpl_Skel#dispatch
函数中的 反序列化对客户端的进行反序列化,从而实现调用。
接着返回一个 客户端HelloServiceImpl的Stub对象 的序列化
然后回到
lookup
函数
下面一个 try 进行对序列化 生成 stub
对象
从而进行返回。
最终实现远程调用。
实际的实现。
- RMIServer 生成 Skeleton 对象。并且暴露接口
- RMIClient 过 LocateRegistry 或者 Naming 方式到 RMI 注册表寻找要调用的 RMI 注册信息。找到 RMI 事务注册信息后,Client 会从 RMI 注册表获取这个 RMI Remote Service 的Stub
- 从而实现远程调用
- 客户端获取的Stub 作为 RMI Client 的代理访问 Remote Service 的代理Skeleton。
- 也就是 Stub 和 Skeleton 的调用过程 并布置直接访问 服务器。而是公共注册机进行访问
图片地址
https://blog.csdn.net/qq_20597727/article/details/80861906
RMI 恶意代码调用
测试代码:
- 服务端JDK版本为
JDK1.7u21
- 服务端存在
Commons-Collections3.1
或其他可利用组件。
https://xz.aliyun.com/t/7900#toc-8
实现机制:RMI 在通信过程中,对象都会通过 系列化进行传输。其中就存在了,序列化和反序列化操作。从而可能会出现反序列化漏洞,调用恶意代码。
如果想调用 RMI 通过反序列化实现 恶意代码调用。
我们需要利用到上一篇文章的 反序列化,反射等。
不管是 服务器攻击客户端 还是 客户端攻击服务器 都利用相同的 payload
1 | public static Object payload() throws Exception { |
Jdk 8u71
以前的版本上。这个payload 不能使用
Java 官⽅修改了 sun.reflect.annotation.AnnotationInvocationHandler
的readObject函数:
在这个函数中 新建了一个 LinkedHashMap 对象,我们原本的 Map 的对象的操作变成了 基于 LinkedHashMap对象的 以前的 Map 里面的set或put 操作不再被执行,所以不能RCE了。