在上一篇文章当中我们简单的讲解了RMI的基础概念,在这一篇文章中,我们将深入剖析RMI各部分的通信过程,最后一篇再来讲漏洞利用。
有朋友说上一篇只放截图没有圈重点,这样对刚开始学的学弟学妹不太友好,那我这一篇我尽量补上。
通过之前的讲解我们知道RMI一共分为三部分分别是:客户端、服务端、注册中心。他们之间可以两两相互通信。简单画个图就是这样:
获取注册中心的方式共有两种:
一种是:Registry r = LocateRegistry.createRegistry(1088);
在创建时获取。
另一种是:Registry registry = LocateRegistry.getRegistry("127.0.0.1",1088);
远程获取。
本地获取注册中心
我们先来看第一种,本地获取注册中心。
createRegistry()
如下有两种方法:
第一种只需要传入port
(这里的port代表的就是注册中心监听的端口)
第二种方式除了传入端口之外,还需要传入两个Socket对象。
其实这两种方法最终获取到的都是RegistryImpl对象,所以对于我们来说用哪种意义不大,我们只需要分析第一种即可。
通过上图我们可以看到如果只传入port的话,他会new一个RegistryImpl()对象,我们可以跟进看一下具体的实现步骤。
可以看到内部时做了判断,首先要求端口等于1099且设置了安全管理器会进入if。否则会执行else中的代码,可以看到,不管是if还是else他们中都有我们上一篇文章中提到的LiveRef
,我们知道LiveRef
是经过封装的处理网络请求的类。
再往后就是创建stub和Skeleton了,我们省去这步,之后就到了TCPTransport.Object
:
跟进listen()方法:
在newServerSocket时会开启端口监听,接着会设置AcceptLoop线程,此时会触发run方法。
在跟进executeAcceptLoop()
:
他会获取到请求的相关信息,比如Host之类的;之后在下边会创建一个线程调用ConnectionHandler
来处理请求:
跟进ConnectionHandler
:
这里的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
来处理请求:
上面还是获取客户端传来的数据,我们主要看下面的部分:
这里只需要关注80,因为客户端发送数据的时候这里发的是80,具体后边会说。
在80部分,先创建了StreamRemoteCall()
对象,并传入var1,var1是当前连接的Connection对象。再跟进serviceCall()
:
上面还是先获取信息(ObjID、Target对象…..),再下面会调用dispath()
来处理请求,他本身几首两个参数,一个是Remote
对象,一个是当前连接的StreamRemoteCall
对象,接着跟进:
前面还是接收数据,接着会调oldDispatch()
:
最后调用了this.skel.dispatch()
,此时的this.skel
为刚刚创建的RegistryImpl_Skel
对象,接着跟进:
这里就是真正处理请求的核心了,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
来注册服务,此时的var6
是RegistryImpl
对象,这是调用createRegistry
获得的。
到这里我们也就明白了,其实无论是客户端
还是服务端
,最终调用注册中心的方法都是通过对创建的RegistryImpl
对象的调用。通过上面那部分,我们已经分析完了当注册中心监听的端口被请求时,本地是如何处理这些请求的。
远程获取注册中心
通过getRegistry
方法获得的对象是RegistryImpl_Stub
对象,与通过createRegistry
获得的对象不同,createRegistry
获得的是RegistryImpl
对象。
当我们调用这两者的方法时,其对应的处理方式也十分不同,以bind方法举例,通过createRegistry获得的注册中心调用bind方法十分简单:
我的这个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
方法:
首先会调用newCall
:
注意这里的var3
,前面说过,0对应的是bind方法,此时的var3就代表了bind方法对应的数字。
在newConnection
这里,会写入一些已经约定好的数据,比如ip、端口等,在StreamRemoteCall
里,同样会写入一些数据:
这里在最开始写入了80,也就和我们上边分析时说的80对上了,然后还会写一些数据比如要调用的方法所对应的num和ObjID之类的。
当调用完这些之后,回到bind方法:
此时会写入两个内容:
- 序列化后的
var1
;var1
为我们要绑定远程对象对应的名称
- 序列化后的
var2
;var2
为我们要绑定的远程对象
在invoke
这里会把请求发出去,接着我们看看注册中心在收到这条请求后是如何进行处理的,前面说了会调用Skel.dispatch来处理请求,我们直接看这个就可以了。
注册中心首先会read两个Object,第一个即我们刚刚write进去的字符串对象,第二个就是远程对象了,接着调用var6.bind
来绑定服务,var6
即RegistryImpl
对象,他是如何绑定服务的在上边写了。
至此,我们已经了解了当注册中心的方法被调用时,远程获取和本地获取的差异是什么。
客户端与服务端的通信
客户端与服务端的通信只发生在调用远程方法时。此时是客户端的远程代理对象与的Skel进行通信。
我们在客户端获取的是注册中心封装好的代理对象,所以默认会调用代理对象的invoke方法:
在这里会判断你调用的方法是所有对象都有的,还是只有远程对象才有的,如果是前者,则进入invokeObjectMethod
中,后者则进入invokeRemoteMethod
中。
跟入RemoteObjectInvocationHandle.invokeRemoteMethod
中:
在这里会调用this.ref.invoke,并把proxy、method、args以及method的hash传过去,this.ref是在lookup时获取到的远程对象绑定的一些端口信息。跟进UnicastRef.invoke
:
同样的,在newConnection这里会发送一些约定好了的数据。接着在marshaValue
这里,会将我们调用的方法、要传递的参数序列化写到连接中,如果传递的参数是对象,就会写入序列化对象到这里:
接着会调用StreamRemoteCall.executeCall
:
在this.releaseOutputStream方法中,会读取服务端执行的结果:
在this.out.flush时,会把之前写进去的数据发出去,服务端会返回执行结果
在调用完executeCall
后,会进入下边这个方法,把数据取出来:
调用了unmarsharValue
方法,把数据取出来,用的是jdk自带的readObject:
至此,我们清楚了客户端是如何和服务端通信的。还有服务端又是如何与客户端进行通信的呢?
当客户端在与服务端通信时,服务端实际处理请求的位置在:UnicastServerRef.dispatch
:
在这里会调用unmarshaValue
,对请求传来的参数进行处理:
在这里会判断参数的数据类型,如果是Object的话,则会反序列化,所以在这里我们如果能够找到Server注册的远程对象中,如果某个方法传递的参数类型是Object,在服务端这里会被反序列化,此时即可造成RCE。
最终通过调用invoke,来调用远程对象的方法。
至此,我们清楚了这三者之间的通信过程