从代码层面看RMI规范的实现与攻击原理(二)
现在的工作真的是太鸡儿无聊了,不出活也没有进步,拉胯。
上一篇文章我们看了RMI客户端侧代码获取一个注册中兴的操作,这一次我们来看看lookup
函数是怎么工作的,有不对的地方大佬轻点喷。
在上一篇中我们知道最终获取到的Registry
对象里面是由RegistryImpl_Stub
不断转型过来的,那么使用该对象调用lookup
函数理论上来说也就是会电泳RegistryImpl_Stub
中的lookup
函数。
首先打断点进入到lookup
函数:
果然是调用的RegistryImpl_Stub
中的函数,到这里前期我们有两个点需要关注一是116行,一是123行。
首先我们跟进116行的方法,这里调用的是ref
对象的newCall,从名字我们大概猜到可能是要发送请求了,首先我们用wireshark过滤一下1099端口,然后让代码执行完116行:
标记1处由客户端向注册中心发送请求,发送要使用的JRMI版本号,2处由注册中心向客户端确认版本号,至于第三处也是由客户端发起,具体干了什么我不太清楚:
看这意思发送了一个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
函数吧,进去:
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就是根据endpointkey
从Map
中 取值,然后202行调用resampleLocalHost
方法对主机名进行重新采样,我猜就是再获取一下对host进行解析,不放跟进去看一看:
257行获取了一个字符串,看命名应该是从properties中获取hostname,跟进getHostnameProperty
函数看一下:
这里有个GetPropertyAction
类,上网查了一下粗糙的理解就是获取properties的值,进入函数看了看就是个赋值操作,不知道为甚么网上那样说。
最终的结论是resampleLocalHost
方法还是返回了注册中心的IP地址,可能因为我这里在本地看不出来区别,分开的话应该就能判断出一些什么了。
然后回到getLocalEndpoint
方法
又创建了一个TCPEndpoint
对象,不过这时候有了主机名了localhost,一个LinkedList
把新的ep
放进去,然后又设置了一下ep的listenport
属性,然后新建一个TCPTransport
赋值给了ep
的transport
,真有趣,一个套一个。。。。最后返回了ep
,一个TCPEndpoint
对象。
然后抛出到getOutboundTransport
方法又是这样的:
还是要的transport
你说有趣不有趣。
依次往上抛,最终还是调用的TCPTransport
对象的getChannel
方法。
这个channelTable
又是一个Map
一个断点对应一个TCP通道,很明显,我么你现在还没有建立通道,所以这个表现在肯定是空的,所以在get的时候一是获取了个寂寞。
既然表示空的,下面的操作自然就是创建一个通道然后建立映射关系。然后返回这个通道,终于我么你的getChannel
方法结束了,然后就是建立连接了,也就是调用TCPChannel
对象的newConnection
方法。
代码挺长的,说白了就是看看是不是已经有了一个连接了,如果有了就直接拿来用,如果没有就新建一个,我们直接进到新建的逻辑里面:
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
就报我们的包发送出去了。。。。
然后再到298行的flush继续发送剩下的包,这里在调试的时候有点把我给搞混了,好像每一次发送包都要走早这个函数,所以会反复的进去,我有点搞不清楚哪个是哪个。
总之到目前为止,发送RMI请求的连接以及获取完毕的,接下来的工作就是客户端携带要查询的key去注册中心查询是否存在对应的对象,然后注册中心将返回一个存根给客户端,然后客户端利用这个存根再去访问服务器的skeleton,服务器骨架访问服务器查看是否存在这样一个方法,根据客户端发送过来的方法名与参数执行对应的方法,然后讲执行的结果返回给客户端由客户端存根接收然后转发给客户端。
好了今天时间比较晚了,预知后事如何,请听下回分解。。。。