RMI小记 这期主要就是通过debug,一步步调试来找到RMI的反序列化点,并了解RMI的具体流程。
这部分我是看组长的视频学的,看了两三遍了。下面是视频的网址
https://www.bilibili.com/video/BV1L3411a7ax?p=2&vd_source=6a92aae104751f5bc065a281b15c4fcb
下面是要用到的class
服务端 下面是服务端要用到的代码
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 import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class testRMIServer { public static void main (String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj remoteObj = new RemoteObjImpl (); Registry r = LocateRegistry.createRegistry(1099 ); r.bind("remoteObj" , remoteObj); } } import java.rmi.Remote;import java.rmi.RemoteException;public interface IRemoteObj extends Remote { public String sayHello (String keywords) throws RemoteException; } import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;import java.util.Locale;public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj { public RemoteObjImpl () throws RemoteException { } @Override public String sayHello (String keywords) throws RemoteException { String upKeywords = keywords.toUpperCase(); System.out.println(upKeywords); return upKeywords; } }
创建远程服务 先在这打个断点
然后先强制步入再步过就会到
这是一个静态赋值
我们再步入就会回到原来的testRMIServer
,然后我们的这个IRemoteObj
这个类有一个父类,接着步入去看,它继承自UnicastRemoteObject
。我们再去查看这个UnicastRemoteObject
的源码。
这个IRemoteObj
的静态值都在构造函数之前赋值。然后我们现在是已经有这个RemoteObj
,接下来就是分析怎么把这个对象发布到网上去。
再强制步入,进入到RemoteObjImpl
的父类的构造函数,再步入就会到下面这里
这里他传递了一个port = 0
这里传递的是一个默认值,这个部分会把远程对象发布到一个随机的端口上。然后我们继续步过到一个调用exportObject
这个函数的地方,强制步入。
这就是一个发布对象的静态函数。这个就是核心函数。所以上面给的注释中说如果不继承UnicastRemoteObject就需要手工导出
,如果继承了这个类,就在构造函数里调用了。
然后这个静态函数呢有创建了一个UnicastServerRef
类,就是服务端引用,这个就是用来处理网络请求用的。继续步入,跟到UnicastServerRef
这里
这里又创建了一个LiveRef
类,这里开始跟视频有点不一样了,应该是JDK版本的一些差异,但大差不差,这个var1
,就是port
。继续步入跟进去
这个var1
还是port
,继续步入跟进去看构造函数。
var1
是objID
,这个很清楚;var2
是port
。然后我们看一下这个构造函数的第二个参数。
1 2 3 public static TCPEndpoint getLocalEndpoint (int var0) { return getLocalEndpoint(var0, (RMIClientSocketFactory) null , (RMIServerSocketFactory) null ); }
我们再来看一下这个TCPEndpoint
的构造函数
1 2 3 public TCPEndpoint (String var1, int var2) { this (var1, var2, (RMIClientSocketFactory) null , (RMIServerSocketFactory) null ); }
这里的var1
是ip host
,var2
是端口port
,然后我这里的null
是RMIClientSocketFactory
接口,我就再看了下这个接口的源码
很明显它负责服务端ServerSocket
的创建。
我们继续步入进入LiveRef
的构造函数,继续步过
然后这里我们就可以看到我们的ip和port,然后这里的TCPTransport
这个才是真正处理网络请求的东西,就是说我们的这个TCPEndpoint
也是一层封装,然后这里我们需要记一下这个LiveRef
,这里挡住了是601。
然后我们步过回来,一直步过回到UnicastServerRef
,再步入就会到UnicastServerRef
父类的构造函数,但其实这个对应的一个是服务端,一个是客户端。
这里的LiveRef
还是这个601。然后我们步过出来到exportObject
步入进去
然后这里的LiveRef还是601,我们继续步入。就到了exportObject
的一个构造函数
这里实际上创建了一个代理,var1
是impl
,var2
是data
,var3
是permanent
,var4
是implClass
,var5
是stub
,这个stub
就是客户端真正的操作的代理,用来处理网络请求的。然后两下步入进入到createProxy
里
这个var0
是implClass
就是远程对象的类,var1
是ClientRef
就是一个封装的LiveRef
这里的LiveRef
还是601,var2
是forceStubUse
然后步过两次就会进入到一个判断
1 2 3 4 5 if (var2 || !ignoreStubClasses && stubClassExists(var3)) { return createStub(var3, var1); } else { ... }
实际上就是只要stub
类存在就为真,执行createStub
1 2 3 4 5 6 7 8 9 10 11 private static boolean stubClassExists (Class<?> var0) { if (!withoutStubs.containsKey(var0)) { try { Class.forName(var0.getName() + "_Stub" , false , var0.getClassLoader()); return true ; } catch (ClassNotFoundException var2) { withoutStubs.put(var0, (Object)null ); } } return false ; }
但其实有一些类是已经定义好了的
如果要调用这些类的话返回值就是真。然后再步过到else这段
1 2 3 4 5 6 7 8 9 10 11 12 13 final ClassLoader var4 = var0.getClassLoader();final Class[] var5 = getRemoteInterfaces(var0);final RemoteObjectInvocationHandler var6 = new RemoteObjectInvocationHandler (var1);try { return (Remote)AccessController.doPrivileged(new PrivilegedAction <Remote>() { public Remote run () { return (Remote)Proxy.newProxyInstance(var4, var5, var6); } }); } catch (IllegalArgumentException var8) { throw new StubNotFoundException ("unable to create proxy" , var8); }
这段就是创建动态代理的一个标准的流程。
这个var0
是implClass
,var1
是ClientRef
,var2
是forceStubUse
,var3
是remoteClass
,var4
是loader
加载器AooClassLoader
,var5
是interface
远程接口,var6
是handler
调用处理器这里面也还是封装了一个LiveRef@601
然后步过下来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public Remote exportObject (Remote var1, Object var2, boolean var3) throws RemoteException { Class var4 = var1.getClass(); Remote var5; try { var5 = Util.createProxy(var4, this .getClientRef(), this .forceStubUse); } catch (IllegalArgumentException var7) { throw new ExportException ("remote object implements illegal remote interface" , var7); } if (var5 instanceof RemoteStub) { this .setSkeleton(var1); } Target var6 = new Target (var1, this , var5, this .ref.getObjID(), var3); this .ref.exportObject(var6); this .hashToMethod_Map = (Map)hashToMethod_Maps.get(var4); return var5; }
var1
是impl
,var2
是data
,var3
是permanent
,var4
是implClass
,var5
是stub
,var6
是target
。
这里有一个setSkeleton
,这个是跟stub
对应的一个函数,如果这个impl
是系统内置的就会调用这个。
然后继续步过,这里它创建了一个target
这个可以理解为一个总封装,就是创建的这些有用的都会放到这里面
我这里直接ctrl
查看这个Target
源码(这里我按视频这里跟不进去不知道为什么),所以这段我就用视频里的了
反正就是有id
,有weakimpl
远程对象,disp
服务端引用里面有一个LiveRef@601
,stub
里也有LiveRef@601
同时stub
里的id
和Target
也有着同一个id。所以核心就是这个LiveRef
然后步过一下
1 this .ref.exportObject(var6);
他把这个targer
发布出去了。再步入我们来看这个exportObject
这里有一个TCPEndpoint@862
还有一个TCPTransport@866
所以这里先调用Endpoint
的exportObject
实际上也就调用了这个TCPTransport@866
这个
这有一个listen()
这就是说它会真正的去处理网络请求
步入进去看listen()
源码
很明显这个this
就是上面提到的TCPTransport@866
继续步过会到下面这段
1 2 3 4 5 6 7 8 9 try { this .server = var1.newServerSocket(); Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction (new AcceptLoop (this .server), "TCP Accept-" + var2, true )); var3.start(); } catch (BindException var4) { throw new ExportException ("Port already in use: " + var2, var4); } catch (IOException var5) { throw new ExportException ("Listen failed on port: " + var2, var5); }
这里呢创建了一个新的服务端Socket
等待连接,然后又创建了一个新的线程,这个线程很明显就是来处理连接之后干嘛的,然后查看一下这个AcceptLoop()
的源码
AcceptLoop()
里有一个run()
函数,如果后续客户端连到了这个线程的话就会走进这个run
的逻辑
1 2 3 4 5 6 7 8 9 10 public void run () { try { this .executeAcceptLoop(); } finally { try { this .serverSocket.close(); } catch (IOException var7) { } } }
然后他的逻辑主要是在executeAcceptLoop()
这里
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 private void executeAcceptLoop () { if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) { TCPTransport.tcpLog.log(Log.BRIEF, "listening on port " + TCPTransport.this .getEndpoint().getPort()); } while (true ) { Socket var1 = null ; try { var1 = this .serverSocket.accept(); InetAddress var16 = var1.getInetAddress(); String var3 = var16 != null ? var16.getHostAddress() : "0.0.0.0" ; try { TCPTransport.connectionThreadPool.execute(TCPTransport.this .new ConnectionHandler (var1, var3)); } catch (RejectedExecutionException var11) { TCPTransport.closeSocket(var1); TCPTransport.tcpLog.log(Log.BRIEF, "rejected connection from " + var3); } } catch (Throwable var15) { Throwable var2 = var15; try { if (this .serverSocket.isClosed()) { return ; } try { if (TCPTransport.tcpLog.isLoggable(Level.WARNING)) { TCPTransport.tcpLog.log(Level.WARNING, "accept loop for " + this .serverSocket + " throws" , var2); } } catch (Throwable var13) { } } finally { if (var1 != null ) { TCPTransport.closeSocket(var1); } } if (!(var15 instanceof SecurityException)) { try { TCPEndpoint.shedConnectionCaches(); } catch (Throwable var12) { } } if (!(var15 instanceof Exception) && !(var15 instanceof OutOfMemoryError) && !(var15 instanceof NoClassDefFoundError)) { if (var15 instanceof Error) { throw (Error)var15; } throw new UndeclaredThrowableException (var15); } if (!this .continueAfterAcceptFailure(var15)) { return ; } } } }
这里都是网络请求的一些操作,就是开了一个新的线程,网络请求的线程和真正的代码逻辑的线程是独立的
这里结束之后,会产生一个默认的端口,就是这个listen()
里面的。这就已经把远程对象发布出去了,但这时候发布给一个随机的端口,客户端默认是不知道的。然后我们继续步过到super.exportObject
步入
1 2 3 4 public void exportObject (Target var1) throws RemoteException { var1.setExportedTransport(this ); ObjectTable.putTarget(var1); }
这个就是一个记录用的。继续步入一直到setExportedTransport
这里再步过出去到ObjectTable.putTarget(var1);
这里调用了一个静态方法
然后继续步过就可以到
1 2 objTable.put(var1, var0); implTable.put(var2, var0);
这是两个Map
,var0
就是之前说的target
,它把target
保存在了系统的一个静态的表里。
这就是一个服务端的一整个发布的流程。
创建注册中心 这里,就在Registry r = LocateRegistry.createRegistry(1099)
这打个断点。强制步入进去
1 2 3 public static Registry createRegistry (int port) throws RemoteException { return new RegistryImpl (port); }
这就是一个创建注册中心的静态方法
然后我们进RegistryImpl()
这有个if
打个断点,然后步入下来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (var1 == 1099 && System.getSecurityManager() != null ) { try { AccessController.doPrivileged(new PrivilegedExceptionAction <Void>() { public Void run () throws RemoteException { LiveRef var1x = new LiveRef (RegistryImpl.id, var1); RegistryImpl.this .setup(new UnicastServerRef (var1x, (var0) -> { return RegistryImpl.registryFilter(var0); })); return null ; } }, (AccessControlContext)null , new SocketPermission ("localhost:" + var1, "listen,accept" )); } catch (PrivilegedActionException var3) { throw (RemoteException)var3.getException(); } } else { LiveRef var2 = new LiveRef (id, var1); this .setup(new UnicastServerRef (var2, RegistryImpl::registryFilter)); }
就是先对var1
也就是端口,进行判断是否等于1099,然后检查系统的安全管理是否不为空。
然后再步过下来,就直接到下面这段else
然后创建了一个新的LiveRef
和一个新的UnicastServerRef
,这里的话跟上面服务端的创建方式是差不多的。
我们来看一下他这里的var2
,就是这个lref
,这里的port
也就是前面设置的1099
然后可以步入去看一下这个UnicastServerRef
的源码,这里跟上面的这个服务端使用的UnicastServerRef
是一样的,就是这个setup
有一点点不一样,我们可以步入去看
1 2 3 4 private void setup (UnicastServerRef var1) throws RemoteException { this .ref = var1; var1.exportObject(this , (Object)null , true ); }
然后继续步入到exportObject
这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public Remote exportObject (Remote var1, Object var2, boolean var3) throws RemoteException { Class var4 = var1.getClass(); Remote var5; try { var5 = Util.createProxy(var4, this .getClientRef(), this .forceStubUse); } catch (IllegalArgumentException var7) { throw new ExportException ("remote object implements illegal remote interface" , var7); } if (var5 instanceof RemoteStub) { this .setSkeleton(var1); } Target var6 = new Target (var1, this , var5, this .ref.getObjID(), var3); this .ref.exportObject(var6); this .hashToMethod_Map = (Map)hashToMethod_Maps.get(var4); return var5; }
这里的var3
是permanent
值为true
说明这个创建的对象是永久的,这个var5
是stub
,这里就是创建了一个stub
然后继续步入去看createProxy()
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 public static Remote createProxy (Class<?> var0, RemoteRef var1, boolean var2) throws StubNotFoundException { Class var3; try { var3 = getRemoteClass(var0); } catch (ClassNotFoundException var9) { throw new StubNotFoundException ("object does not implement a remote interface: " + var0.getName()); } if (var2 || !ignoreStubClasses && stubClassExists(var3)) { return createStub(var3, var1); } else { final ClassLoader var4 = var0.getClassLoader(); final Class[] var5 = getRemoteInterfaces(var0); final RemoteObjectInvocationHandler var6 = new RemoteObjectInvocationHandler (var1); try { return (Remote)AccessController.doPrivileged(new PrivilegedAction <Remote>() { public Remote run () { return (Remote)Proxy.newProxyInstance(var4, var5, var6); } }); } catch (IllegalArgumentException var8) { throw new StubNotFoundException ("unable to create proxy" , var8); } } }
继续步入去看stubClassExists()
1 2 3 4 5 6 7 8 9 10 11 12 private static boolean stubClassExists (Class<?> var0) { if (!withoutStubs.containsKey(var0)) { try { Class.forName(var0.getName() + "_Stub" , false , var0.getClassLoader()); return true ; } catch (ClassNotFoundException var2) { withoutStubs.put(var0, (Object)null ); } } return false ; }
首先它会判断有没有系统中_Stub
后缀的,然后这个是有的,有一个RegistryImpl_Stub.class
这个是JDK自带的类。然后用forName
对这个类进行初始化,然后这里跟视频里的那个是不太一样的,JDK
版本的差异,我的版本这直接给他返回了一个true
,但实际的效果是差不多的。
然后再步过到exportObject
这里,我们步过下来看stub
,也就是这里的var5
我们这创建的注册中心的stub
就是这个RegistryImpl_Stub
,而我们当时创建的服务端的stub
是一个动态代理,就是靠这个动态代理的调用处理器来找到文件。
光标指着的这里,就是说如果var5
也就是stub
如果是服务端定义好的,就会调用下面的setSkeleton
方法,我们直接点进去看这个setSkeleton
,然后下个断点,步入进去。
1 2 3 4 5 6 7 8 9 public void setSkeleton (Remote var1) throws RemoteException { if (!withoutSkeletons.containsKey(var1.getClass())) { try { this .skel = Util.createSkeleton(var1); } catch (SkeletonNotFoundException var3) { withoutSkeletons.put(var1.getClass(), (Object)null ); } } }
这里的有一个判断,就是判断这个impl
也就是这里var1
是否有Skeleton
,Skeleton
就是服务端的处理网络请求的代理。然后步过就会到下面的try
这段语句,创建一个Skeleton
,步入去看怎么创建的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static Skeleton createSkeleton (Remote var0) throws SkeletonNotFoundException { Class var1; try { var1 = getRemoteClass(var0.getClass()); } catch (ClassNotFoundException var8) { throw new SkeletonNotFoundException ("object does not implement a remote interface: " + var0.getClass().getName()); } String var2 = var1.getName() + "_Skel" ; try { Class var3 = Class.forName(var2, false , var1.getClassLoader()); return (Skeleton)var3.newInstance(); } catch (ClassNotFoundException var4) { throw new SkeletonNotFoundException ("Skeleton class not found: " + var2, var4); } catch (InstantiationException var5) { throw new SkeletonNotFoundException ("Can't create skeleton: " + var2, var5); } catch (IllegalAccessException var6) { throw new SkeletonNotFoundException ("No public constructor: " + var2, var6); } catch (ClassCastException var7) { throw new SkeletonNotFoundException ("Skeleton not of correct class: " + var2, var7); } }
很明显,这个创建方式,跟前面的那个stub
差不多,也是直接一个forName
创建。
然后步过到return
,然后出来,就有了这个skel
,这是UnicastServerRef
的一个内部变量。它在这个impl
也就是这个var1
,在它的ref
里。
然后下一步就是创建一个target
把这些都放进去。
接下来步过到下面这个exportObject
这里,我们步入进去到synchronized(this)
这里再过下来到super.exportObject(var1)
,然后我们步入进去看
1 2 3 4 public void exportObject (Target var1) throws RemoteException { var1.setExportedTransport(this ); ObjectTable.putTarget(var1); }
把target
放进去的时候会调用一个静态方法,我们到pubTarget
这里步入去看
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 static void putTarget (Target var0) throws ExportException { ObjectEndpoint var1 = var0.getObjectEndpoint(); WeakRef var2 = var0.getWeakImpl(); if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) { DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + var1); } synchronized (tableLock) { if (var0.getImpl() != null ) { if (objTable.containsKey(var1)) { throw new ExportException ("internal error: ObjID already in use" ); } if (implTable.containsKey(var2)) { throw new ExportException ("object already exported" ); } objTable.put(var1, var0); implTable.put(var2, var0); if (!var0.isPermanent()) { incrementKeepAliveCount(); } } } }
我们直接步过到put
这里看
所有创建好的远程对象都放在这里,我们已经创建好了一个叫RemoteObjImpl
的远程对象然后又创建了一个RegistryImpl
,但是这里有三个。然后我们来看下value
然后可以看到这有一个DGCImpl_Stub
,这个不是我们所创建的,DGC是分布式垃圾回收,是一个默认会创建的对象。另外两个obj
其中一个的stub
是$Proxy
就是最开始创建远程服务的stub
里面放了个LiveRef
,它还有一个disp
这是UnicastServerRef
里面也放了个LiveRef
,这两个是不一样的,但是他们的端口是一样的;另外一个的stub
是RegistryImpl_Stub
,这个obj
的disp
里有一个skel
是RegistryImpl_Skel
,然后stub
和skel
里都有个LiveRef
这两个是一样的。
绑定 在r.bind("remoteObj", remoteObj);
这里打个断点,强制步入进去
1 2 3 4 5 6 7 8 9 10 public void bind (String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException { synchronized (this .bindings) { Remote var4 = (Remote)this .bindings.get(var1); if (var4 != null ) { throw new AlreadyBoundException (var1); } else { this .bindings.put(var1, var2); } } }
组长这JDK的bind
比我这多了一段checkAccess("Registry.bind")
,这个就是在本地绑定,都是能通过的。可能是因为这一步没什么必要,所以稍微新一点的JDK没有这一段。
然后这个bindings
就是一个HashTable
,如果里面有叫这个var1
的也就是我上面设置的remoteObject
,就会抛出一个已经绑定的异常,没有就把它put
进去。
在RMI
的实现上要求服务端和注册中心在一台机子上,但是在低版本JDK中有一些实现上的问题所以允许远程绑定的
客户端 下面是客户端需要的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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" ); } } import java.rmi.Remote;import java.rmi.RemoteException;public interface IRemoteObj extends Remote { public String sayHello (String keywords) throws RemoteException; }
请求注册中心(客户端) 在Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
打个断点,开始debug,强制步入,再步入。
然后步过下来,主要执行的就是下面这段代码
1 2 3 4 5 6 7 8 LiveRef liveRef = new LiveRef (new ObjID (ObjID.REGISTRY_ID), new TCPEndpoint (host, port, csf, null ), false ); RemoteRef ref = (csf == null ) ? new UnicastRef (liveRef) : new UnicastRef2 (liveRef); return (Registry) Util.createProxy(RegistryImpl.class, ref, false );
这里只是进行了一个本地创建,把IP和端口传进来之后就创建了一个LiveRef
把IP和端口放进去进行了封装,然后又调了createProxy
,进去看createProxy
之后把断点放在var3 = getRemoteClass(var0);
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 public static Remote createProxy (Class<?> var0, RemoteRef var1, boolean var2) throws StubNotFoundException { Class var3; try { var3 = getRemoteClass(var0); } catch (ClassNotFoundException var9) { throw new StubNotFoundException ("object does not implement a remote interface: " + var0.getName()); } if (var2 || !ignoreStubClasses && stubClassExists(var3)) { return createStub(var3, var1); } else { final ClassLoader var4 = var0.getClassLoader(); final Class[] var5 = getRemoteInterfaces(var0); final RemoteObjectInvocationHandler var6 = new RemoteObjectInvocationHandler (var1); try { return (Remote)AccessController.doPrivileged(new PrivilegedAction <Remote>() { public Remote run () { return (Remote)Proxy.newProxyInstance(var4, var5, var6); } }); } catch (IllegalArgumentException var8) { throw new StubNotFoundException ("unable to create proxy" , var8); } } }
然后这个流程就跟服务端创建RegistryImpl_Stub
的流程是一摸一样的,服务端创建的stub
并没有通过序列化反序列化传过来,而是把参数传过来,然后在本地又创建了一个
然后这就有了一个RegistryImol_Stub
,然后这里就获取到了注册中心的stub
对象,然后下一步就是通过这个来获取远程对象,但实际上也是获取这个动态代理stub
。
然后我们去看lookup
的逻辑,点进去打断点步过
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 public Remote lookup (String var1) throws AccessException, NotBoundException, RemoteException { try { StreamRemoteCall var2 = (StreamRemoteCall)this .ref.newCall(this , operations, 2 , 4905912898345647071L ); try { ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(var1); } catch (IOException var15) { throw new MarshalException ("error marshalling arguments" , var15); } this .ref.invoke(var2); Remote var20; try { ObjectInput var4 = var2.getInputStream(); var20 = (Remote)var4.readObject(); } catch (IOException | ClassNotFoundException | ClassCastException var13) { var2.discardPendingRefs(); throw new UnmarshalException ("error unmarshalling return" , var13); } finally { this .ref.done(var2); } return var20; } catch (RuntimeException var16) { throw var16; } catch (RemoteException var17) { throw var17; } catch (NotBoundException var18) { throw var18; } catch (Exception var19) { throw new UnexpectedException ("undeclared checked exception" , var19); } }
我们把这个名称也就是我们这里的remoteObj
传进去然后穿进去之后,把它写进了一个输出流里面,也就是序列化进去了,然后这里既然有序列化那么注册中心那里就需要反序列化进行读取,那么就说明注册中心那里有一个反序列化点。
然后它会拿UnicastRef
调用invoke
方法,继续步入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void invoke (RemoteCall var1) throws Exception { try { clientRefLog.log(Log.VERBOSE, "execute call" ); var1.executeCall(); } catch (RemoteException var3) { clientRefLog.log(Log.BRIEF, "exception: " , var3); this .free(var1, false ); throw var3; } catch (Error var4) { clientRefLog.log(Log.BRIEF, "error: " , var4); this .free(var1, false ); throw var4; } catch (RuntimeException var5) { clientRefLog.log(Log.BRIEF, "exception: " , var5); this .free(var1, false ); throw var5; } catch (Exception var6) { clientRefLog.log(Log.BRIEF, "exception: " , var6); this .free(var1, true ); throw var6; } }
它会调用一个executeCall()
方法,这个就是一个真正处理网络请求的方法,客户端的网络请求都是通过它来实现的
然后这个方法里有一个异常
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 switch (var1) { case 1 : return ; case 2 : Object var14; try { var14 = this .in.readObject(); } catch (Exception var10) { this .discardPendingRefs(); throw new UnmarshalException ("Error unmarshaling return" , var10); } if (!(var14 instanceof Exception)) { this .discardPendingRefs(); throw new UnmarshalException ("Return type not Exception" ); } else { this .exceptionReceivedFromServer((Exception)var14); } default : if (Transport.transportLog.isLoggable(Log.BRIEF)) { Transport.transportLog.log(Log.BRIEF, "return code invalid: " + var1); } throw new UnmarshalException ("Return code invalid" ); }
这里有一个2号异常会通过反序列化来获取流里的对象,设计的本意可能是通过反序列化来获取这一个异常类的更详细的信息,但如果注册中心返回一个恶意的流,那么客户端就会在这里进行反序列化,也会导致客户端被攻击,然后这个反序列化点更隐蔽影响范围也更广,因为所有的stub
里处理网络请求的都会调用这个方法
然后我们返回去看lookup
它完成了这个invoke
之后,它又会去获取一个输入流,然后通过反序列化的方式读出来
1 2 ObjectInput var4 = var2.getInputStream();var20 = (Remote)var4.readObject();
就是这个var20
就是读回来的动态代理,就是那个远程对象的动态代理。既然这是反序列化读出来的,那么如果有一个恶意的注册中心就可以通过这个方式来攻击客户端
请求服务端(客户端) 在remoteObj.sayHello("hello");
这里打个断点进去,强制步入然后会跳到invoke
这
因为我们当前获取的这个remoteObj
是一个动态代理,动态代理不管调用什么方法都会走到调用处理器里面的invoke
方法。
然后步过下来到return invokeRemoteMethod(proxy, method, args);
,这里调用了一个UnicastRef.invoke
,但是这是一个重载的方法,步入看一下这个invoke
的逻辑。
这里还是创建了一个连接,比较类似。
步过下来,有一个marshalValue((Class)((Object[])var11)[var12], var3[var12], var10);
,这个marshalValue
会先序列化一个值,序列化的就是我们传的这个参数”hello”
可以来看一下这个函数的逻辑
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 protected static void marshalValue (Class<?> var0, Object var1, ObjectOutput var2) throws IOException { if (var0.isPrimitive()) { if (var0 == Integer.TYPE) { var2.writeInt((Integer)var1); } else if (var0 == Boolean.TYPE) { var2.writeBoolean((Boolean)var1); } else if (var0 == Byte.TYPE) { var2.writeByte((Byte)var1); } else if (var0 == Character.TYPE) { var2.writeChar((Character)var1); } else if (var0 == Short.TYPE) { var2.writeShort((Short)var1); } else if (var0 == Long.TYPE) { var2.writeLong((Long)var1); } else if (var0 == Float.TYPE) { var2.writeFloat((Float)var1); } else { if (var0 != Double.TYPE) { throw new Error ("Unrecognized primitive type: " + var0); } var2.writeDouble((Double)var1); } } else { var2.writeObject(var1); } }
首先判断是不是基础类型,如果不是就writeObject
进去,然后步过下来,它又调用了executeCall()
,就是说所有客户端的请求都会调用这个方法
然后我们再步过下来可以看到它会调用一个unmarshalValue
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 protected static Object unmarshalValue (Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException { if (var0.isPrimitive()) { if (var0 == Integer.TYPE) { return var1.readInt(); } else if (var0 == Boolean.TYPE) { return var1.readBoolean(); } else if (var0 == Byte.TYPE) { return var1.readByte(); } else if (var0 == Character.TYPE) { return var1.readChar(); } else if (var0 == Short.TYPE) { return var1.readShort(); } else if (var0 == Long.TYPE) { return var1.readLong(); } else if (var0 == Float.TYPE) { return var1.readFloat(); } else if (var0 == Double.TYPE) { return var1.readDouble(); } else { throw new Error ("Unrecognized primitive type: " + var0); } } else { return var0 == String.class && var1 instanceof ObjectInputStream ? SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)var1) : var1.readObject(); } }
我们可以看到最后怎么去获取结果就是用反序列化来进行获取,这里虽然跟组长的不太一样,我这里做了一些限制。就是说如果var0
不是字符类型或者var1
不是ObjectInputStream
的实例,那么就会调用var1.readObject
这个就是客户端的另一个反序列化点。
这里客户端的流程就完了
然后我们之前说的那个executeCall()
这个点,所处理的协议就是JRMP
协议。基本上通过JRMP
的攻击的就是这个stub
。
客户端请求注册中心(注册中心) 上面有提到服务端操作的是这个skeleton
,所以我们需要把断点下到_skel
即RegistryImpl_Skel
里来查看流程,我们先进入到Registry r = LocateRegistry.createRegistry(1099);
的createRegistry()
方法里,然后进入到RegistryImpl
,然后到下面的this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));
这个setup()
方法里,再进入到exportObject()
方法里,然后进入this.ref.exportObject(var6);
的exportObject()
方法里。继续进入exportObject()
方法,接着进入,继续到this.transport.exportObject(var1);
的exportObject()
方法,然后这里有个listen()
方法,他开了个网络监听出去,我们从这里进去,到下面有一个Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new AcceptLoop(this.server), "TCP Accept-" + var2, true));
我们去看这个新的线程AcceptLoop()
,然后去看这个线程里的run()
方法,它调用了一个this.executeAcceptLoop()
,我们继续进入去看。
它调用了一个TCPTransport.connectionThreadPool.execute(TCPTransport.this.new ConnectionHandler(var1, var3));
,我们继续进去看这个ConnectionHandler()
方法,然后下来去看它的run()
方法,它实际上就是调用了一个this.run0()
我们接着去看这个run0()
方法,它会去协议里传过来的一些字段还有写POST
的,但是最主要的是他会调用一个TCPTransport.this.handleMessages(var14, true);
我们继续进入到这个handleMessages()
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 void handleMessages (Connection var1, boolean var2) { int var3 = this .getEndpoint().getPort(); try { DataInputStream var4 = new DataInputStream (var1.getInputStream()); do { int var5 = var4.read(); if (var5 == -1 ) { if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + var3 + ") connection closed" ); } return ; } if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + var3 + ") op = " + var5); } switch (var5) { case 80 : StreamRemoteCall var6 = new StreamRemoteCall (var1); if (!this .serviceCall(var6)) { return ; } break ; case 81 : case 83 : default : throw new IOException ("unknown transport op " + var5); case 82 : DataOutputStream var7 = new DataOutputStream (var1.getOutputStream()); var7.writeByte(83 ); var1.releaseOutputStream(); break ; case 84 : DGCAckHandler.received(UID.read(var4)); } } while (var2); } catch (IOException var17) { if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + var3 + ") exception: " , var17); } } finally { try { var1.close(); } catch (IOException var16) { } } }
默认的case
是case 80
然后它调用一个this.serviceCall(var6)
,进去看这个serviceCall()
,有一段Target var5 = ObjectTable.getTarget(new ObjectEndpoint(var39, var40));
,就是从静态表里去获取一个Target
,然后我们把断点下这就可以了。开始调试!
不知道为什么怎么调都调不进去,悲
那就直接用组长的吧
这个target
里有一个stub
其实就是服务端的RegistryImpl_stub
,然后他会final Dispatcher var6 = var5.getDispatcher();
来获取一个Dispatcher
就是一个分发器,然后获取到的分发器就是这个UnicastServerRef
后面他会调用disp
的dispatch
方法,步入进去然后这个dispatch
又会调用一个oldDispatch()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private void oldDispatch (Remote var1, RemoteCall var2, int var3) throws Exception { ObjectInput var6 = var2.getInputStream(); try { Class var7 = Class.forName("sun.rmi.transport.DGCImpl_Skel" ); if (var7.isAssignableFrom(this .skel.getClass())) { ((MarshalInputStream)var6).useCodebaseOnly(); } } catch (ClassNotFoundException var9) { } long var4; try { var4 = var6.readLong(); } catch (Exception var8) { throw new UnmarshalException ("error unmarshalling call header" , var8); } Operation[] var10 = this .skel.getOperations(); this .logCall(var1, var3 >= 0 && var3 < var10.length ? var10[var3] : "op: " + var3); this .unmarshalCustomCallData(var6); this .skel.dispatch(var1, var2, var3, var4); }
在最后他又会调用一个skel.dispatch()
方法,这里就是调用RegistryImpl_Skel
的dispatch()
了。
客户端的时候我们处理了这个stub
,到服务端我们就会去处理这个skeleton
,注册中心实际上就是一个特殊的服务端。
这个dispatch()
下面有一段case
语句,不同的case
对应着不同的方法,case 0
就是bind
方法,case 1
就是list()
方法,case 2
是lookup()
方法
然后我们可以看到在注册中心的Skel
里直接使用了readObject()
反序列化读取
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 case 0 : RegistryImpl.checkAccess("Registry.bind" ); try { var10 = (ObjectInputStream)var7.getInputStream(); var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var10); var81 = (Remote)var10.readObject(); } catch (IOException | ClassNotFoundException | ClassCastException var78) { var7.discardPendingRefs(); throw new UnmarshalException ("error unmarshalling arguments" , var78); } finally { var7.releaseInputStream(); } var6.bind(var8, var81); try { var7.getResultStream(true ); break ; } catch (IOException var77) { throw new MarshalException ("error marshalling return" , var77); } case 1 : ... case 2 : ... case 3 : RegistryImpl.checkAccess("Registry.rebind" ); try { var10 = (ObjectInputStream)var7.getInputStream(); var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var10); var81 = (Remote)var10.readObject(); } catch (IOException | ClassNotFoundException | ClassCastException var71) { var7.discardPendingRefs(); throw new UnmarshalException ("error unmarshalling arguments" , var71); } finally { var7.releaseInputStream(); } var6.rebind(var8, var81); try { var7.getResultStream(true ); break ; } catch (IOException var70) { throw new MarshalException ("error marshalling return" , var70); } case 4 : ... default : throw new UnmarshalException ("invalid method number" );
前面有说过,我们这个远程对象的名称是通过序列化传过去的,那么注册中心这六是通过反序列化读取出来的。这里就会有一个反序列化点,我们的客户端就可以通过这来攻击注册中心,只要他有readObject()
方法。
客户端请求服务端(服务端) 这一块前面这跟网络相关的部分都是一样的也会到disp.dispatch()
这里
这里的skel
是DGCImpl_Skel
就暂时先不看。继续下来
当前的target
是这个动态代理,然后再下来也会调用这个dispatch()
这时的disp
是UnicastServerRef
,然后再步入进去到UnicastServerRef
的dispatch
,到这里都是跟前面一样的。
不一样的是这里的skel
是空的,所以就会运行到下面的代码
1 2 3 MarshalInputStream var7 = (MarshalInputStream)var41; var7.skipDefaultResolveClass(); Method var42 = (Method)this .hashToMethod_Map.get(var4);
但是最重要的是下面这段代码
1 2 this .unmarshalCustomCallData(var41);var9 = this .unmarshalParameters(var1, var42, var7);
它会把传过来的参数值反序列化出来,我们步入去看unmarshalParameters()
方法,然后他会调用unmarshalParametersChecked()
或unmarshalParametersUnchecked()
方法,然后这两个方法都会调用unmarshalValue()
方法
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 protected static Object unmarshalValue (Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException { if (var0.isPrimitive()) { if (var0 == Integer.TYPE) { return var1.readInt(); } else if (var0 == Boolean.TYPE) { return var1.readBoolean(); } else if (var0 == Byte.TYPE) { return var1.readByte(); } else if (var0 == Character.TYPE) { return var1.readChar(); } else if (var0 == Short.TYPE) { return var1.readShort(); } else if (var0 == Long.TYPE) { return var1.readLong(); } else if (var0 == Float.TYPE) { return var1.readFloat(); } else if (var0 == Double.TYPE) { return var1.readDouble(); } else { throw new Error ("Unrecognized primitive type: " + var0); } } else { return var0 == String.class && var1 instanceof ObjectInputStream ? SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)var1) : var1.readObject(); } }
这个就是对传入的值进行一个类型的检查然后反序列化出来。
然后步过出来之后,上面的这个var9
也就是这个params
就是我们传过来的这个字符串,就是这个hello
然后下来到这里
1 2 3 4 5 6 Object var10; try { var10 = var42.invoke(var1, var9); } catch (InvocationTargetException var33) { throw var33.getTargetException(); }
这里是真正的去进行远程调用。然后就是这个返回值会把它进行序列化,然后到客户端再进行反序列化。这是一个对称的过程。所以客户端和服务端可以互相打
1 2 3 4 5 6 try { ObjectOutput var11 = var2.getResultStream(true ); Class var12 = var42.getReturnType(); if (var12 != Void.TYPE) { marshalValue(var12, var10, var11); }
客户端请求服务端(dgc) DGC
是RMI
的一个分布式垃圾回收的模块。那么它在哪呢
就是在创建完远程对象的时候会把他封装进一个Target
里,然后会把所有的Target
放到一个静态表里,主要就是下面的这段。
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 static void putTarget (Target var0) throws ExportException { ObjectEndpoint var1 = var0.getObjectEndpoint(); WeakRef var2 = var0.getWeakImpl(); if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) { DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + var1); } synchronized (tableLock) { if (var0.getImpl() != null ) { if (objTable.containsKey(var1)) { throw new ExportException ("internal error: ObjID already in use" ); } if (implTable.containsKey(var2)) { throw new ExportException ("object already exported" ); } objTable.put(var1, var0); implTable.put(var2, var0); if (!var0.isPermanent()) { incrementKeepAliveCount(); } } } }
步过下来
这里这个target
的stub
存的就是这个DGCImpl_Stub
然后我们继续步过,看看它put
进去之后会发生什么
这时我们这里的stub
变成了那个动态代理,但实际上我们把这个动态代理放进去的时候这个静态表里就有了一个target
这主要就是因为下面这段代码
1 2 3 if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) { DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + var1); }
这个DGCImpl.dgcLog
是一个静态变量,当我们对这个类的静态变量进行调用的时候,会完成这个类的初始化
然后我们看DGCImpl
的源码一直下来到static{}
这段
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 static { leaseCheckInterval = (Long)AccessController.doPrivileged(new GetLongAction ("sun.rmi.dgc.checkInterval" , leaseValue / 2L )); scheduler = ((RuntimeUtil)AccessController.doPrivileged(new RuntimeUtil .GetInstanceAction())).getScheduler(); DGC_MAX_DEPTH = 5 ; DGC_MAX_ARRAY_SIZE = 10000 ; dgcFilter = (ObjectInputFilter)AccessController.doPrivileged(DGCImpl::initDgcFilter); AccessController.doPrivileged(new PrivilegedAction <Void>() { public Void run () { ClassLoader var1 = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader()); try { DGCImpl.dgc = new DGCImpl (); final ObjID var2 = new ObjID (2 ); LiveRef var3 = new LiveRef (var2, 0 ); final UnicastServerRef var4 = new UnicastServerRef (var3, (var0) -> { return DGCImpl.checkInput(var0); }); final Remote var5 = Util.createProxy(DGCImpl.class, new UnicastRef (var3), true ); var4.setSkeleton(DGCImpl.dgc); Permissions var6 = new Permissions (); var6.add(new SocketPermission ("*" , "accept,resolve" )); ProtectionDomain[] var7 = new ProtectionDomain []{new ProtectionDomain ((CodeSource)null , var6)}; AccessControlContext var8 = new AccessControlContext (var7); Target var9 = (Target)AccessController.doPrivileged(new PrivilegedAction <Target>() { public Target run () { return new Target (DGCImpl.dgc, var4, var5, var2, true ); } }, var8); ObjectTable.putTarget(var9); } catch (RemoteException var13) { throw new Error ("exception initializing server-side DGC" , var13); } } finally { Thread.currentThread().setContextClassLoader(var1); } return null ; } }); }
然后我们步过,就到了DGCImpl.dgc = new DGCImpl();
这段,然后再看下面这段
1 2 3 4 5 final UnicastServerRef var4 = new UnicastServerRef (var3, (var0) -> { return DGCImpl.checkInput(var0); }); final Remote var5 = Util.createProxy(DGCImpl.class, new UnicastRef (var3), true ); var4.setSkeleton(DGCImpl.dgc);
然后这段就和前面创建代理的时候很像了,然后步过就会到上面打断点的这行,然后我们步入进去,然后就会步过到下面这段
1 2 3 if (var2 || !ignoreStubClasses && stubClassExists(var3)) { return createStub(var3, var1); }
首先它会判断系统里有没有这个DGCImpl_Stub
,然后我们下面可以看到是有这个类的
然后他就会实例化一个系统的DGCImpl_Stub
。接下来我们步过出来回到上面的这段static{}
这里就创建了一个DGCImpl_Stub
类,他的功能和注册中心是一样的也是有一个端口,只不过注册中心的那个端口用来注册服务,这个端口用来远程回收服务就是这个端口不是固定的。
这一部分创建完之后就会把它放到一个target
里
以上就是DGC
的创建过程
然后他调用的时候就会走到这个disp
里
然后就是跟注册中心一样的流程,接下来看一下功能
先来看这个DGCImpl_Stub
(客户端),他主要就是两个方法,一个clean
,一个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 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 public void clean (ObjID[] var1, long var2, VMID var4, boolean var5) throws RemoteException { try { StreamRemoteCall var6 = (StreamRemoteCall)this .ref.newCall(this , operations, 0 , -669196253586618813L ); var6.setObjectInputFilter(DGCImpl_Stub::leaseFilter); try { ObjectOutput var7 = var6.getOutputStream(); var7.writeObject(var1); var7.writeLong(var2); var7.writeObject(var4); var7.writeBoolean(var5); } catch (IOException var8) { throw new MarshalException ("error marshalling arguments" , var8); } this .ref.invoke(var6); this .ref.done(var6); } catch (RuntimeException var9) { throw var9; } catch (RemoteException var10) { throw var10; } catch (Exception var11) { throw new UnexpectedException ("undeclared checked exception" , var11); } } public Lease dirty (ObjID[] var1, long var2, Lease var4) throws RemoteException { try { StreamRemoteCall var5 = (StreamRemoteCall)this .ref.newCall(this , operations, 1 , -669196253586618813L ); var5.setObjectInputFilter(DGCImpl_Stub::leaseFilter); try { ObjectOutput var6 = var5.getOutputStream(); var6.writeObject(var1); var6.writeLong(var2); var6.writeObject(var4); } catch (IOException var16) { throw new MarshalException ("error marshalling arguments" , var16); } this .ref.invoke(var5); Connection var7 = var5.getConnection(); Lease var22; try { ObjectInput var8 = var5.getInputStream(); var22 = (Lease)var8.readObject(); } catch (IOException | ClassNotFoundException | ClassCastException var17) { if (var7 instanceof TCPConnection) { ((TCPConnection)var7).getChannel().free(var7, false ); } var5.discardPendingRefs(); throw new UnmarshalException ("error unmarshalling return" , var17); } finally { this .ref.done(var5); } return var22; } catch (RuntimeException var19) { throw var19; } catch (RemoteException var20) { throw var20; } catch (Exception var21) { throw new UnexpectedException ("undeclared checked exception" , var21); } }
然后我们看一下DGCImpl_Skel
(服务端),服务端主要就是看有没有反序列化点
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 public void dispatch (Remote var1, RemoteCall var2, int var3, long var4) throws Exception { if (var4 != -669196253586618813L ) { throw new SkeletonMismatchException ("interface hash mismatch" ); } else { DGCImpl var6 = (DGCImpl)var1; StreamRemoteCall var7 = (StreamRemoteCall)var2; ObjID[] var8; long var9; switch (var3) { case 0 : VMID var34; boolean var36; try { ObjectInput var37 = var7.getInputStream(); var8 = (ObjID[])((ObjID[])var37.readObject()); var9 = var37.readLong(); var34 = (VMID)var37.readObject(); var36 = var37.readBoolean(); } catch (IOException | ClassNotFoundException | ClassCastException var32) { var7.discardPendingRefs(); throw new UnmarshalException ("error unmarshalling arguments" , var32); } finally { var7.releaseInputStream(); } var6.clean(var8, var9, var34, var36); try { var7.getResultStream(true ); break ; } catch (IOException var31) { throw new MarshalException ("error marshalling return" , var31); } case 1 : Lease var11; try { ObjectInput var12 = var7.getInputStream(); var8 = (ObjID[])((ObjID[])var12.readObject()); var9 = var12.readLong(); var11 = (Lease)var12.readObject(); } catch (IOException | ClassNotFoundException | ClassCastException var29) { var7.discardPendingRefs(); throw new UnmarshalException ("error unmarshalling arguments" , var29); } finally { var7.releaseInputStream(); } Lease var35 = var6.dirty(var8, var9, var11); try { ObjectOutput var13 = var7.getResultStream(true ); var13.writeObject(var35); break ; } catch (IOException var28) { throw new MarshalException ("error marshalling return" , var28); } default : throw new UnmarshalException ("invalid method number" ); } } }
所以这个DGC
和注册中心一样都是会被攻击的,特点就是他是一个自动生成的,只要创建了远程对象就会有这个DGC
服务
我们如果去攻击这个远程对象那么我们就需要知道这个参数类型的,但是攻击DGC
就不需要
JDK高版本绕过 组长这里的版本是8u65
,然后我这的版本是8u124
,但还是会有一定的利用点存在
比较重要的就是这个UnicastRef
,这个UnicastRef
里有一个invoke()
,这个有一个JRMP Client
的反序列化攻击,所有RMI
的客户端都会受到反序列化攻击,8u121
版本修复了之后,它的客户端实际上是没有修复的,只修复了针对服务端的攻击。然后从我这8u124
的版本的源码来看,客户端的代码是有一些改变但是也并没有完全修(如修?),
如果能用服务端来发起一个客户端请求,就可以在服务端上导致一个反序列化攻击。那么就要找客户端请求是怎么发送的,能不能手动的弄出来一次。
就是要想怎么去调用invoke()
,调用这个的就是在这两个stub
里,一个是DGCImpl_Stub
一个是RegistryImpl_Stub
还有一个就是这个动态代理,动态代理只有我们在绑定创建服务端的时候生成。
想要调用invoke()
就先来弄一个stub
,想要弄一个stub
就要调用createProxy
。
DGC
的stub
有两个地方调用,第一个是在静态代码块(没办法干涉),第二个是DGCClient
里的一个内部类EndpointEntry
的构造函数里创建了一个DGC
服务。因为这一整个流程是固定的,在JDK里已经写死了,如果我们想改变它的流程,就需要一个反序列化的点能够触发创建stub
然后调用invoke()
的流程。
在一个已经跑起来的程序中,我们想改变它的逻辑实际上是比较难的,这种动态的特性基本上只能靠反序列化来实现。所以就可以以EndpointEntry
这个类作为入口,想办法创建一个这个类,然后生成一个DGC
,下一步让这个DGC
发起客户端请求。
我们要找到一个反序列化点,要在一个反序列化点里创建一个DGC
服务。然后我们就找这个EndpointEntry
的流程是什么
先是调用了一个lookup()
1 2 3 public static EndpointEntry lookup (Endpoint var0) { ... }
继续往上找,我们要找到一个反序列化点,然后再这个反序列化点李创建一个DGC
服务
然后到这里
1 2 3 4 5 6 static void registerRefs (Endpoint var0, List<LiveRef> var1) { EndpointEntry var2; do { var2 = DGCClient.EndpointEntry.lookup(var0); } while (!var2.registerRefs(var1)); }
接着往上找就有分叉路,有一个是到ConnectionInputStream
,这里调用这个registerRefs()
函数的时候会创建。
1 2 3 4 5 6 7 8 9 10 void registerRefs () throws IOException { if (!this .incomingRefTable.isEmpty()) { Iterator var1 = this .incomingRefTable.entrySet().iterator(); while (var1.hasNext()) { Map.Entry var2 = (Map.Entry)var1.next(); DGCClient.registerRefs((Endpoint)var2.getKey(), (List)var2.getValue()); } } }
另一个是LiveRef
的read()
方法的一段else
里会调用
1 2 3 4 5 6 7 8 9 if (var0 instanceof ConnectionInputStream) { ConnectionInputStream var6 = (ConnectionInputStream)var0; var6.saveRef(var5); if (var4) { var6.setAckNeeded(); } } else { DGCClient.registerRefs(var2, Arrays.asList(var5)); }
然后我们继续网上找,看上面这个registerRefs()
什么时候被调用,然后到了StreamRemoteCall
的releaseInputStream
这里,然后这个StreamRemoteCsll
就是一个jrmp
的攻击点,我们继续去看这个releaseInputCall
在哪调的。它在各个skel
里调用,到这里流程就通了,我们可以在服务端找到一个地方,能够让他创建一个dgc
服务,然后下一步我们只要让这个dgc
服务发起一个请求就可以了。
但是我们可以看到在这个ConnectionInputStream
的registerRefs()
里有一个判断(源码在上面),接下来我们就需要让他走进这个判断里,也就是让他的incomingRefTable
不为空就可以执之后的流程,但是正常调试流程的话他就是空的,所以就是想一下怎么让他不是空
让他不为空,就要找给他赋值的地方然后只有一个地方用了put
1 2 3 4 5 6 7 8 9 10 void saveRef (LiveRef var1) { Endpoint var2 = var1.getEndpoint(); Object var3 = (List)this .incomingRefTable.get(var2); if (var3 == null ) { var3 = new ArrayList (); this .incomingRefTable.put(var2, var3); } ((List)var3).add(var1); }
所以首先要调用这个方法,然后在上面提到的这个LiveRef
的read()
方法里调用了,刚才也说进不去底下的else
但是能进上面的这个if
,然后他就会调用这个saveRef
,然后我们就需要知道这个read()
函数在哪里被调用,继续上去找,就会发现在UnicastRef
的readExternal()
这个是JAVA原生反序列化的另一种,和readObject
类似但不完全一样,就是反序列化的时候如果有readExternal()
他也会调用。
那么就已经完成了想要创建dgc
的这个逻辑。
我们首先序列化一个UnicastRef
对象,然后在里面保存一个这个ref
,我们把它传过去可以正常反序列化,然后在他的反序列化流程里,他会调用这个read()
方法,然后这个方法就会调用这个savaRef
,这个saveRef
就会把这个incomingRefTable
赋值,就不会为空了。下一步就到skel
里,接下来就正常走反序列化的流程,到他的这个releaseInputStream()
的时候就会调用registerRefs()
,此时就是上面说的这个判断,不为空了。所以他的反序列化只是用来给这个incomingRefTable
赋值,然后让他走到正常的调用流程,真正的触发攻击是在正常的调用流程里面。
但是到后面也只是创建了一个dgc
对象但不能发起请求
1 2 3 4 5 6 7 8 9 10 11 12 13 private EndpointEntry (Endpoint var1) { this .endpoint = var1; try { LiveRef var2 = new LiveRef (DGCClient.dgcID, var1, false ); this .dgc = (DGC)Util.createProxy(DGCImpl.class, new UnicastRef (var2), true ); } catch (RemoteException var3) { throw new Error ("internal error creating DGC stub" ); } this .renewCleanThread = (Thread)AccessController.doPrivileged(new NewThreadAction (new RenewCleanThread (), "RenewClean-" + var1, true )); this .renewCleanThread.start(); }
然后下面有一段RenewCleanThread()
他是调用了自己里面的一个线程,我们点进去看。最后面有一段
1 2 3 4 5 6 7 8 9 10 11 12 13 AccessController.doPrivileged(new PrivilegedAction <Void>() { public Void run () { if (var4) { EndpointEntry.this .makeDirtyCall(var5, var6); } if (!EndpointEntry.this .pendingCleans.isEmpty()) { EndpointEntry.this .makeCleanCalls(); } return null ; } }, DGCClient.SOCKET_ACC);
然后我们去看这个makeDirtyCall
方法,其中有一段
1 2 3 4 5 6 7 8 9 10 11 try { Lease var20 = this .dgc.dirty(var4, var2, new Lease (DGCClient.vmid, DGCClient.leaseValue)); var8 = var20.getValue(); long var10 = DGCClient.computeRenewTime(var5, var8); var12 = var5 + var8; synchronized (this ) { this .dirtyFailures = 0 ; this .setRenewTime(var10); this .expirationTime = var12; } }
也就是说,在利用的后半段,满足那个if
条件之后,从创建dgc
到调用dgc
的dirty
方法来触发客户端请求,导致了jrmp
的反序列化,后半段的流程完全就是JDK里写好了的流程,相当于就是这么设计的。
就是说在这个skel
里,在他释放一个InputStream
的时候,他就会有一个创建dgc
的过程,但正常来说默认是走不进去的,主要就是因为有一个if
判断,然后我们想要改变这个if
判断我们做的事就是传了一个UnicastRef
,然后在他反序列化的时候让他走进了这个流程。
这么做的好处就是我们在服务端发起一个客户端的反序列化请求,然后我们之前有说服务端的请求是会被过滤的,但注册中心想要反序列化的时候,正常来说服务端反序列化是会被过滤的,但是客户端反序列化就是jrmp
的那个是不会被过滤的,就是说我们还是可以绕过这些限制进行攻击。
总结 具体的攻击手法在ysoserial 里基本都写好了,来看下这个exploit
。
这个RMIRegistryExploit
就是直接攻击注册中心,就是我们前面说直接攻击注册中心的,这种攻击在8u121
之后就修复了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void exploit (final Registry registry, final Class<? extends ObjectPayload> payloadClass, final String command) throws Exception { new ExecCheckingSecurityManager ().callWrapped(new Callable <Void>(){public Void call () throws Exception { ObjectPayload payloadObj = payloadClass.newInstance(); Object payload = payloadObj.getObject(command); String name = "pwned" + System.nanoTime(); Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class); try { registry.bind(name, remote); } catch (Throwable e) { e.printStackTrace(); } Utils.releasePayload(payloadObj, payload); return null ; }}); }
然后是JRMPClient
这是对RMIClient
的攻击,跟注册中心很类似,这个是攻击DGC
服务,跟上面这个攻击的条件都是一样的。
接着是JRMPListener
,我们让服务端向客户端发起一个请求,这个时候客户端就会发起一个JRMP
攻击,然后我们可以用他来制造一个恶意服务端,来导致JRMP
客户端也就是这个真正的服务端/注册中心来执行代码,这个也可以用来反制,用来攻击普通的JRMP
客户端。这个就是一个伪造的RMI
服务,如果作为一个RMI
客户端连了就会被攻击。
在这个payloads
里有一个JRMPListener
,这个比较少用,就是在一个普通的反序列化点里,得先有一个反序列化点了,然后传一个UnicastRef
对象,他反序列化的时候会暴露一个RMI
接口出来,他会把一个普通的反序列化点转换成一个RMI
的反序列化点,作用就是可能会绕过一些过滤。
然后就是payloads
里的JRMPClient
,这个是很常用的,是整个RMI
分析里最重要的一个链,前面哦们讲的所有东西都是针对RMI
服务去打的,他的目标就是RMI
服务,如果服务器没有开RMI
,要么就是通过刚刚这个JRMPListener
给他开一个,没开的话也可以通过这个JRMPClient
他也是一个二次反序列化链,但是这个的利用条件要比JRMPListener
要宽,这个链放的不是UnicastRef
他放的是一个RemoteObjectInvocationHandler
,但实际上也是用了UnicastRef
的反序列化,区别就是非RMI
的反序列化时我们也能用这个payload
,让他开一个客户端请求,再用exploit
接。这就是一个非常常用的一个二次反序列化链,用来绕过各种服务端的限制,比如shiro
这些。同时他还有一个特点,他是从一个输入流的readExternal
方法然后把它变成了一个readObject
方法可以在这两个之间转换,在别的利用链里也能用到