没人比我更懂RMI(中)

在上一篇文章当中我们简单的讲解了RMI的基础概念,在这一篇文章中,我们将深入剖析RMI各部分的通信过程,最后一篇再来讲漏洞利用。

有朋友说上一篇只放截图没有圈重点,这样对刚开始学的学弟学妹不太友好,那我这一篇我尽量补上。

通过之前的讲解我们知道RMI一共分为三部分分别是:客户端、服务端、注册中心。他们之间可以两两相互通信。简单画个图就是这样:

image-20230822145352149

获取注册中心的方式共有两种:

一种是:Registry r = LocateRegistry.createRegistry(1088);在创建时获取。

另一种是:Registry registry = LocateRegistry.getRegistry("127.0.0.1",1088);远程获取。

本地获取注册中心

我们先来看第一种,本地获取注册中心。

createRegistry()如下有两种方法:

image-20230822150942148

第一种只需要传入port(这里的port代表的就是注册中心监听的端口)

第二种方式除了传入端口之外,还需要传入两个Socket对象。

其实这两种方法最终获取到的都是RegistryImpl对象,所以对于我们来说用哪种意义不大,我们只需要分析第一种即可。

通过上图我们可以看到如果只传入port的话,他会new一个RegistryImpl()对象,我们可以跟进看一下具体的实现步骤。

image-20230822152426268

可以看到内部时做了判断,首先要求端口等于1099且设置了安全管理器会进入if。否则会执行else中的代码,可以看到,不管是if还是else他们中都有我们上一篇文章中提到的LiveRef,我们知道LiveRef是经过封装的处理网络请求的类。

再往后就是创建stub和Skeleton了,我们省去这步,之后就到了TCPTransport.Object

image-20230822154504105

跟进listen()方法:

image-20230822155148288

在newServerSocket时会开启端口监听,接着会设置AcceptLoop线程,此时会触发run方法。

image-20230822155032317

在跟进executeAcceptLoop():

他会获取到请求的相关信息,比如Host之类的;之后在下边会创建一个线程调用ConnectionHandler来处理请求:

image-20230822155513463

跟进ConnectionHandler

image-20230822160103475

这里的var2就是上面传入的ServerSocket对象,再跟进run0()

这个run0代码有点多我就不截图了,直接贴上,方便大家阅读

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
private void run0() {
TCPEndpoint var1 = TCPTransport.this.getEndpoint();
int var2 = var1.getPort();
TCPTransport.threadConnectionHandler.set(this);

try {
this.socket.setTcpNoDelay(true);
} catch (Exception var31) {
}

try {
if (TCPTransport.connectionReadTimeout > 0) {
this.socket.setSoTimeout(TCPTransport.connectionReadTimeout);
}
} catch (Exception var30) {
}

try {
InputStream var3 = this.socket.getInputStream();
Object var4 = var3.markSupported() ? var3 : new BufferedInputStream(var3);
((InputStream)var4).mark(4);
DataInputStream var5 = new DataInputStream((InputStream)var4);
int var6 = var5.readInt();
if (var6 == 1347375956) {
if (TCPTransport.disableIncomingHttp) {
throw new RemoteException("RMI over HTTP is disabled");
}

TCPTransport.tcpLog.log(Log.BRIEF, "decoding HTTP-wrapped call");
((InputStream)var4).reset();

try {
this.socket = new HttpReceiveSocket(this.socket, (InputStream)var4, (OutputStream)null);
this.remoteHost = "0.0.0.0";
var3 = this.socket.getInputStream();
var4 = new BufferedInputStream(var3);
var5 = new DataInputStream((InputStream)var4);
var6 = var5.readInt();
} catch (IOException var29) {
throw new RemoteException("Error HTTP-unwrapping call", var29);
}
}

short var7 = var5.readShort();
if (var6 == 1246907721 && var7 == 2) {
OutputStream var8 = this.socket.getOutputStream();
BufferedOutputStream var9 = new BufferedOutputStream(var8);
DataOutputStream var10 = new DataOutputStream(var9);
int var11 = this.socket.getPort();
if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
TCPTransport.tcpLog.log(Log.BRIEF, "accepted socket from [" + this.remoteHost + ":" + var11 + "]");
}

byte var15 = var5.readByte();
TCPEndpoint var12;
TCPChannel var13;
TCPConnection var14;
switch (var15) {
case 75:
var10.writeByte(78);
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE, "(port " + var2 + ") suggesting " + this.remoteHost + ":" + var11);
}

var10.writeUTF(this.remoteHost);
var10.writeInt(var11);
var10.flush();
String var16 = var5.readUTF();
int var17 = var5.readInt();
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE, "(port " + var2 + ") client using " + var16 + ":" + var17);
}

var12 = new TCPEndpoint(this.remoteHost, this.socket.getLocalPort(), var1.getClientSocketFactory(), var1.getServerSocketFactory());
var13 = new TCPChannel(TCPTransport.this, var12);
var14 = new TCPConnection(var13, this.socket, (InputStream)var4, var9);
TCPTransport.this.handleMessages(var14, true);
return;
case 76:
var12 = new TCPEndpoint(this.remoteHost, this.socket.getLocalPort(), var1.getClientSocketFactory(), var1.getServerSocketFactory());
var13 = new TCPChannel(TCPTransport.this, var12);
var14 = new TCPConnection(var13, this.socket, (InputStream)var4, var9);
TCPTransport.this.handleMessages(var14, false);
return;
case 77:
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE, "(port " + var2 + ") accepting multiplex protocol");
}

var10.writeByte(78);
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE, "(port " + var2 + ") suggesting " + this.remoteHost + ":" + var11);
}

var10.writeUTF(this.remoteHost);
var10.writeInt(var11);
var10.flush();
var12 = new TCPEndpoint(var5.readUTF(), var5.readInt(), var1.getClientSocketFactory(), var1.getServerSocketFactory());
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE, "(port " + var2 + ") client using " + var12.getHost() + ":" + var12.getPort());
}

ConnectionMultiplexer var18;
synchronized(TCPTransport.this.channelTable) {
var13 = TCPTransport.this.getChannel(var12);
var18 = new ConnectionMultiplexer(var13, (InputStream)var4, var8, false);
var13.useMultiplexer(var18);
}

var18.run();
return;
default:
var10.writeByte(79);
var10.flush();
return;
}
}

TCPTransport.closeSocket(this.socket);
} catch (IOException var32) {
TCPTransport.tcpLog.log(Log.BRIEF, "terminated with exception:", var32);
return;
} finally {
TCPTransport.closeSocket(this.socket);
}

}

总结一下就是,先是TCP传输连接处理方法,然后根据不同的调用类型进行相应处理,我们重点关注handleMessages来处理请求:

image-20230822161209922

上面还是获取客户端传来的数据,我们主要看下面的部分:

image-20230822161256820

这里只需要关注80,因为客户端发送数据的时候这里发的是80,具体后边会说。

在80部分,先创建了StreamRemoteCall()对象,并传入var1,var1是当前连接的Connection对象。再跟进serviceCall()

image-20230822161710312

上面还是先获取信息(ObjID、Target对象…..),再下面会调用dispath()来处理请求,他本身几首两个参数,一个是Remote对象,一个是当前连接的StreamRemoteCall对象,接着跟进:

image-20230822162739226

前面还是接收数据,接着会调oldDispatch():

image-20230822162922253

最后调用了this.skel.dispatch(),此时的this.skel为刚刚创建的RegistryImpl_Skel对象,接着跟进:

image-20230822163235287

image-20230822163348059

这里就是真正处理请求的核心了,var3是传递过来的int类型的参数,在这里有如下对应关系(附代码):

  • 0—>bind
  • 1—>list
  • 2—>lookup
  • 3—>rebind
  • 4—>unbind
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
switch (var3) {
case 0:
RegistryImpl.checkAccess("Registry.bind");

try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var77) {
throw new UnmarshalException("error unmarshalling arguments", var77);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var80);

try {
var2.getResultStream(true);
break;
} catch (IOException var76) {
throw new MarshalException("error marshalling return", var76);
}
case 1:
var2.releaseInputStream();
String[] var79 = var6.list();

try {
ObjectOutput var81 = var2.getResultStream(true);
var81.writeObject(var79);
break;
} catch (IOException var75) {
throw new MarshalException("error marshalling return", var75);
}
case 2:
try {
var8 = var2.getInputStream();
var7 = (String)var8.readObject();
} catch (ClassNotFoundException | IOException var73) {
throw new UnmarshalException("error unmarshalling arguments", var73);
} finally {
var2.releaseInputStream();
}

var80 = var6.lookup(var7);

try {
ObjectOutput var82 = var2.getResultStream(true);
var82.writeObject(var80);
break;
} catch (IOException var72) {
throw new MarshalException("error marshalling return", var72);
}
case 3:
RegistryImpl.checkAccess("Registry.rebind");

try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var70) {
throw new UnmarshalException("error unmarshalling arguments", var70);
} finally {
var2.releaseInputStream();
}

var6.rebind(var7, var80);

try {
var2.getResultStream(true);
break;
} catch (IOException var69) {
throw new MarshalException("error marshalling return", var69);
}
case 4:
RegistryImpl.checkAccess("Registry.unbind");

try {
var8 = var2.getInputStream();
var7 = (String)var8.readObject();
} catch (ClassNotFoundException | IOException var67) {
throw new UnmarshalException("error unmarshalling arguments", var67);
} finally {
var2.releaseInputStream();
}

var6.unbind(var7);

try {
var2.getResultStream(true);
break;
} catch (IOException var66) {
throw new MarshalException("error marshalling return", var66);
}
default:
throw new UnmarshalException("invalid method number");
}

在这里会对每个调用的方法进行处理,比如你调用了bind方法,就会先readObject反序列化你床哟来的序列化对象,之后再调用var6.bind来注册服务,此时的var6RegistryImpl对象,这是调用createRegistry获得的。

到这里我们也就明白了,其实无论是客户端还是服务端,最终调用注册中心的方法都是通过对创建的RegistryImpl对象的调用。通过上面那部分,我们已经分析完了当注册中心监听的端口被请求时,本地是如何处理这些请求的。

远程获取注册中心

通过getRegistry方法获得的对象是RegistryImpl_Stub对象,与通过createRegistry获得的对象不同,createRegistry获得的是RegistryImpl对象。

当我们调用这两者的方法时,其对应的处理方式也十分不同,以bind方法举例,通过createRegistry获得的注册中心调用bind方法十分简单:

image-20230822165156451

我的这个JDK版本比较低,在稍微高一点的版本,在bind中首先会进行checkAccess,里面有一些判断,会对你当前的权限,来源IP进行判断,在高版本JDK中不允许除了localhost之外的地址注册服务,也是在这里进行判断的。之后就是判断这个键是否被绑定过,如果被绑定则抛异常,反之则将键和对象值都put到Hashtable中。

如果是远程调用bind方法,就比较麻烦了。测试伪代码:

1
2
3
Registry registry = LocateRegistry.createRegistry(8888);
Registry reg = LocateRegistry.getRegistry("127.0.0.1",8888);
reg.bind("name",obj)

这里我先创建了注册中心,之后通过getRegistry的方式远程获取注册中心,此时获得到的对象为RegistryImpl_Stub,跟入其bind方法:

image-20230822170127462

首先会调用newCall

image-20230822170303837

注意这里的var3,前面说过,0对应的是bind方法,此时的var3就代表了bind方法对应的数字。

newConnection这里,会写入一些已经约定好的数据,比如ip、端口等,在StreamRemoteCall里,同样会写入一些数据:

image-20230822170627293

这里在最开始写入了80,也就和我们上边分析时说的80对上了,然后还会写一些数据比如要调用的方法所对应的num和ObjID之类的。

当调用完这些之后,回到bind方法:

image-20230822170803058

此时会写入两个内容:

  • 序列化后的var1var1为我们要绑定远程对象对应的名称
  • 序列化后的var2var2为我们要绑定的远程对象

invoke这里会把请求发出去,接着我们看看注册中心在收到这条请求后是如何进行处理的,前面说了会调用Skel.dispatch来处理请求,我们直接看这个就可以了。

image-20230822163348059

注册中心首先会read两个Object,第一个即我们刚刚write进去的字符串对象,第二个就是远程对象了,接着调用var6.bind来绑定服务,var6RegistryImpl对象,他是如何绑定服务的在上边写了。

至此,我们已经了解了当注册中心的方法被调用时,远程获取和本地获取的差异是什么。

客户端与服务端的通信

客户端与服务端的通信只发生在调用远程方法时。此时是客户端的远程代理对象与的Skel进行通信。

我们在客户端获取的是注册中心封装好的代理对象,所以默认会调用代理对象的invoke方法:

image-20230822190737766

在这里会判断你调用的方法是所有对象都有的,还是只有远程对象才有的,如果是前者,则进入invokeObjectMethod中,后者则进入invokeRemoteMethod中。

跟入RemoteObjectInvocationHandle.invokeRemoteMethod中:

image-20230822191142591

在这里会调用this.ref.invoke,并把proxy、method、args以及method的hash传过去,this.ref是在lookup时获取到的远程对象绑定的一些端口信息。跟进UnicastRef.invoke

image-20230822192250289

同样的,在newConnection这里会发送一些约定好了的数据。接着在marshaValue这里,会将我们调用的方法、要传递的参数序列化写到连接中,如果传递的参数是对象,就会写入序列化对象到这里:

image-20230822192434594

image-20230822192608384

接着会调用StreamRemoteCall.executeCall

image-20230822192852263

在this.releaseOutputStream方法中,会读取服务端执行的结果:

image-20230822192932757

在this.out.flush时,会把之前写进去的数据发出去,服务端会返回执行结果

在调用完executeCall后,会进入下边这个方法,把数据取出来:

image-20230822193158889

调用了unmarsharValue方法,把数据取出来,用的是jdk自带的readObject:

image-20230822193348959

至此,我们清楚了客户端是如何和服务端通信的。还有服务端又是如何与客户端进行通信的呢?

当客户端在与服务端通信时,服务端实际处理请求的位置在:UnicastServerRef.dispatch

image-20230822194020581

在这里会调用unmarshaValue,对请求传来的参数进行处理:

image-20230822194335117

image-20230822194354798

在这里会判断参数的数据类型,如果是Object的话,则会反序列化,所以在这里我们如果能够找到Server注册的远程对象中,如果某个方法传递的参数类型是Object,在服务端这里会被反序列化,此时即可造成RCE。

image-20230822194843997

最终通过调用invoke,来调用远程对象的方法。

至此,我们清楚了这三者之间的通信过程


没人比我更懂RMI(中)
http://example.com/2023/08/22/没人比我更懂RMI2/
作者
Yuanyi
发布于
2023年8月22日
许可协议