0%

从代码层面看RMI规范的实现与攻击原理(二)

@TOC

日常吐槽:这份工作确实无聊,不出活,没进步就是我上班的真实写照,学习进步还是要看下班后。

上一篇文章我们看了RMI客户端侧代码获取一个注册中心的操作,这一次我们来看看lookup函数是怎么工作的,有不对的地方大佬轻点喷。

在上一篇中我们知道最终获取到的Registry对象是由RegistryImpl_Stub不断转型过来的,那么使用该对象调用lookup函数理论上来说也就是会调用RegistryImpl_Stub中的lookup函数。
首先打断点进入到lookup函数:
在这里插入图片描述
果然是调用的RegistryImpl_Stub中的函数,到这里前期我们有两个点需要关注一是116行,一是123行。
首先我们跟进116行的方法,这里调用的是ref对象的newCall,从名字我们大概猜到可能是要发送请求了,首先我们用wireshark过滤一下1099端口,然后让代码执行完116行:
在这里插入图片描述
标记1处由客户端向注册中心发送请求,发送要使用的JRMI版本号,2处由注册中心向客户端确认版本号,至于第三处也是由客户端发起,具体干了什么我不太清楚(通过后面的分析我发现这一步应该也是类似于TCP三次握手那样的一个确认机制,注册中心返回自己的IP地址,然后客户端提取ip放到这个包里面,最后发送到注册中心,注册中心再对这次请求进行确认):
在这里插入图片描述
看这意思发送了一个ip地址过去,难道是协商要用哪一张网卡????172这个ip还是我安装wsl的时候生成的一张虚拟网卡。
在这里插入图片描述
看名字还是个虚拟以太网交换机??这我就更迷惑了。。。不过暂时这不重要。
那么现在我们就进入到这个newCall方法中看一下都做了什么吧。
在这里插入图片描述
看到进入了UnicastRef这个类的中,这里注意第343行的代码,首先调用了ref对象的getChannel方法肯定是会返回一个对象的,然后再调用这个对象的newConnection方法,看着名字已经很清楚了这应该就是发送请求的关键函数了。getChannel不出意外是获取一个socket通道,然后newConnection方法发送请求,我们看看对不对,首先进入getChannel方法:
在这里插入图片描述
看到进入了LiveRef#getChannel,这里大眼一瞧就注意到了第152行。调用了ep对象的getChannel方法,如果还记得上一篇的话,在获取注册中心的时候我们获得过一次TCPEndpoint对象然后赋值给了ep,这里就是了,ep中封装了注册中心的host与port:
在这里插入图片描述
进入到getChannel方法:
在这里插入图片描述
果然来到了TCPEndpoint类。419行又是函数套函数,不过getOutBoundTransport应该是个类函数,翻译成中文就是获取对外绑定传输,强行翻译了一波。。。。从名字看不出来什么,进到函数里面看看:
在这里插入图片描述
欧,又有,先看getLocalEndpoint函数吧,进去:

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
public static TCPEndpoint getLocalEndpoint(int port,
RMIClientSocketFactory csf,
RMIServerSocketFactory ssf)
{
/*
* Find mapping for an endpoint key to the list of local unique
* endpoints for this client/server socket factory pair (perhaps
* null) for the specific port.
*/
TCPEndpoint ep = null;

synchronized (localEndpoints) {
TCPEndpoint endpointKey = new TCPEndpoint(null, port, csf, ssf);
LinkedList<TCPEndpoint> epList = localEndpoints.get(endpointKey);
String localHost = resampleLocalHost();

if (epList == null) {
/*
* Create new endpoint list.
*/
ep = new TCPEndpoint(localHost, port, csf, ssf);
epList = new LinkedList<TCPEndpoint>();
epList.add(ep);
ep.listenPort = port;
ep.transport = new TCPTransport(epList);
localEndpoints.put(endpointKey, epList);

if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
TCPTransport.tcpLog.log(Log.BRIEF,
"created local endpoint for socket factory " + ssf +
" on port " + port);
}
} else {
synchronized (epList) {
ep = epList.getLast();
String lastHost = ep.host;
int lastPort = ep.port;
TCPTransport lastTransport = ep.transport;
// assert (localHost == null ^ lastHost != null)
if (localHost != null && !localHost.equals(lastHost)) {
/*
* Hostname has been updated; add updated endpoint
* to list.
*/
if (lastPort != 0) {
/*
* Remove outdated endpoints only if the
* port has already been set on those endpoints.
*/
epList.clear();
}
ep = new TCPEndpoint(localHost, lastPort, csf, ssf);
ep.listenPort = port;
ep.transport = lastTransport;
epList.add(ep);
}
}
}
}

return ep;
}

长的有点过粪了。。。。
看到函数中代码是异步执行的,这玩意儿我python中的异步都没咋搞清楚,更别说java了,但不影响我看代码。。。
首先new了一个TCPEndpoint类型的endpointkey,暂时不知道干什么的,然后调用了localEndpoint的get方法,看看localEndpoint是什么
在这里插入图片描述
Map类型,键为TCPEndpoint类型,值为LinkedList集合类型。所以
在这里插入图片描述
201就是根据endpointkeyMap中 取值,然后202行调用resampleLocalHost方法对主机名进行重新采样,我猜就是再获取一下对host进行解析,不妨跟进去看一看:
在这里插入图片描述
257行获取了一个字符串,看命名应该是从properties中获取hostname,跟进getHostnameProperty函数看一下:
在这里插入图片描述
这里有个GetPropertyAction类,上网查了一下粗糙的理解就是获取properties的值,进入函数看了看就是个赋值操作,不知道为甚么网上那样说。
最终的结论是resampleLocalHost方法还是返回了注册中心的IP地址,可能因为我这里在本地看不出来区别,分开的话应该就能判断出一些什么了。
然后回到getLocalEndpoint方法
在这里插入图片描述
又创建了一个TCPEndpoint对象,不过这时候有了主机名了localhost,一个LinkedList把新的ep放进去,然后又设置了一下ep的listenport属性,然后新建一个TCPTransport赋值给了eptransport,真有趣,一个套一个。。。。最后返回了ep,一个TCPEndpoint对象。
然后抛出到getOutboundTransport方法又是这样的:
在这里插入图片描述
还是要的transport你说有趣不有趣。
依次往上抛,最终还是调用的TCPTransport对象的getChannel方法。
在这里插入图片描述
这个channelTable又是一个Map在这里插入图片描述
一个端点对应一个TCP通道,很明显,我们还没有建立通道,所以这个表现在肯定是空的,所以在get的时候一是获取了个寂寞。
既然表示空的,下面的操作自然就是创建一个通道然后建立映射关系。然后返回这个通道,终于getChannel方法结束了,然后就是建立连接了,也就是调用TCPChannel对象的newConnection方法。
在这里插入图片描述
代码挺长的,说白了就是看看是不是已经有了一个连接了,如果有了就直接拿来用,如果没有就新建一个,我们直接进到新建的逻辑里面:

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
private Connection createConnection() throws RemoteException {
Connection conn;

TCPTransport.tcpLog.log(Log.BRIEF, "create connection");

if (!usingMultiplexer) {
Socket sock = ep.newSocket();
conn = new TCPConnection(this, sock);

try {
DataOutputStream out =
new DataOutputStream(conn.getOutputStream());
writeTransportHeader(out);

// choose protocol (single op if not reusable socket)
if (!conn.isReusable()) {
out.writeByte(TransportConstants.SingleOpProtocol);
} else {
out.writeByte(TransportConstants.StreamProtocol);
out.flush();

/*
* Set socket read timeout to configured value for JRMP
* connection handshake; this also serves to guard against
* non-JRMP servers that do not respond (see 4322806).
*/
int originalSoTimeout = 0;
try {
originalSoTimeout = sock.getSoTimeout();
sock.setSoTimeout(handshakeTimeout);
} catch (Exception e) {
// if we fail to set this, ignore and proceed anyway
}

DataInputStream in =
new DataInputStream(conn.getInputStream());
byte ack = in.readByte();
if (ack != TransportConstants.ProtocolAck) {
throw new ConnectIOException(
ack == TransportConstants.ProtocolNack ?
"JRMP StreamProtocol not supported by server" :
"non-JRMP server at remote endpoint");
}

String suggestedHost = in.readUTF();
int suggestedPort = in.readInt();
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE,
"server suggested " + suggestedHost + ":" +
suggestedPort);
}

// set local host name, if unknown
TCPEndpoint.setLocalHost(suggestedHost);
// do NOT set the default port, because we don't
// know if we can't listen YET...

// write out default endpoint to match protocol
// (but it serves no purpose)
TCPEndpoint localEp =
TCPEndpoint.getLocalEndpoint(0, null, null);
out.writeUTF(localEp.getHost());
out.writeInt(localEp.getPort());
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE, "using " +
localEp.getHost() + ":" + localEp.getPort());
}

/*
* After JRMP handshake, set socket read timeout to value
* configured for the rest of the lifetime of the
* connection. NOTE: this timeout, if configured to a
* finite duration, places an upper bound on the time
* that a remote method call is permitted to execute.
*/
try {
/*
* If socket factory had set a non-zero timeout on its
* own, then restore it instead of using the property-
* configured value.
*/
sock.setSoTimeout((originalSoTimeout != 0 ?
originalSoTimeout :
responseTimeout));
} catch (Exception e) {
// if we fail to set this, ignore and proceed anyway
}

out.flush();
}
} catch (IOException e) {
try {
conn.close();
} catch (Exception ex) {}
if (e instanceof RemoteException) {
throw (RemoteException) e;
} else {
throw new ConnectIOException(
"error during JRMP connection establishment", e);
}
}
} else {
try {
conn = multiplexer.openConnection();
} catch (IOException e) {
synchronized (this) {
usingMultiplexer = false;
multiplexer = null;
}
throw new ConnectIOException(
"error opening virtual connection " +
"over multiplexed connection", e);
}
}
return conn;
}

这一段代码就长的有点离大谱了,主要关注两个out.flush就是这行代码发起了请求,前面都是些前戏。。。
在这里插入图片描述
216与217行的代码已经很清楚了,新建一个socket,然后进行TCP连接,也就是进行三次握手,217执行完毕后,我们就可以使用wireshark看到:
在这里插入图片描述
妥妥的握手报文。
然后就是创建一个使用conn创建一个输出流,然后将其封装为数据输出流,然后再为输出流设置JRMI幻数与版本:
在这里插入图片描述
看一下协议幻数与版本号分别是多少:
在这里插入图片描述

然后判断连接是否可重用,嗯,我看过了,是可重用的,所以又给输出流写了TransportConstants.StreamProtocol,这个值为0x4b
这个编号我还好奇去转了一下码:
在这里插入图片描述
然后我去查了一下协议号:
在这里插入图片描述
这75是个什么鬼。。。对不上啊,只能解释为这两不是一个概念了。。。嗯,肯定不是一个概念,在out.flush发包后,我们看看看wireshark抓到的包:
在这里插入图片描述
看红框里面的16进制值为4a524d4900024b,就是协议幻数+版本号+流协议标识,知道了这个,我们就有了伪造RMI协议的第一个关键信息了。。。。。
看看对响应的处理:
在这里插入图片描述
首先封装了一个DataInputStream流对象,然后从输入流里面读了1个字节出来作为协议确认码ack,此处为78,然后判断ack是否等于TransportConstants.ProtocolAck,即0x4e,翻译为十进制就是78,这里是相等的。
在这里插入图片描述
然后继续读输入流得到suggestedHostsuggestedPort
然后设置TCPEndpoint的host为获取到的suggestedHostlocalhost也为获取到的suggestedHost,然后创建一个本地端点,通过这个端点获取主机与端口设置给输出流,然后out.flush将缓冲区的数据发送出去,通过wireshark抓包
在这里插入图片描述
看到这里发送的请求报文中确实有ip地址,但是没有看到端口啊,哪里除了问题?
在这里插入图片描述
注意到这儿获取本地端点的时候传入的端口就是0,抓包看到的请求为0也算正常。
这里就获取到了伪造RMI协议的第二个关键数据。。。。。这是越来越刑啊。。。。。

总之到目前为止,发送RMI请求的连接已经获取完毕的,接下来的工作就是客户端携带要查询的key去注册中心查询是否存在对应的对象,然后注册中心将返回一个存根给客户端,然后客户端利用这个存根再去访问服务器的skeleton,服务器骨架访问服务器查看是否存在这样一个方法,根据客户端发送过来的方法名与参数执行对应的方法,然后将执行的结果返回给客户端由客户端存根接收然后转发给客户端。
好了今天时间比较晚了,预知后事如何,请听下回分解。。。。

Buy me a coffee.

欢迎关注我的其它发布渠道