RMI 调用的流程及可能出现的反序列化攻击点
一. RMI 简述 1.1 介绍 RMI 是 Java 中的一种技术,全称为远程方法调用。它的作用是允许你在不同的 Java 虚拟机 (JVM)之间进行通信,就像在同一个 JVM 中调用方法一样,调用远程方法。这些 虚拟机 可以在不同的主机上、也可以在同一个主机上。
先了解一下 RMI 中涉及到的三个角色,它们分别为服务端,注册中心和客户端
还有这两个:
- **存根/桩(Stub):**远程对象在客户端上的代理,囊括了远程对象的具体信息,客户端可以通过这个代理和服务端进行交互。<font style="color:rgb(85, 85, 85);background-color:rgb(42, 42, 42) !important;">Stub 类实际上是一个动态代理类,类名为</font> `ClassName`+`_Stub`<font style="color:rgb(85, 85, 85);background-color:rgb(42, 42, 42) !important;">,由于是代理类,其会继承所有的接口 。</font>
- **骨架(Skeleton):**可以看作为服务端的一个代理,用来处理Stub发送过来的请求,然后去调用客户端需要的请求方法,最终将方法执行结果返回给Stub。<font style="color:rgb(85, 85, 85);background-color:rgb(42, 42, 42) !important;">Skeleton 也是一个动态代理类,类名为 </font>`<font style="color:rgb(85, 85, 85);background-color:rgb(42, 42, 42) !important;">ClassName</font>`<font style="color:rgb(85, 85, 85);background-color:rgb(42, 42, 42) !important;">+</font>`<font style="color:rgb(85, 85, 85);background-color:rgb(42, 42, 42) !important;">_Skel</font>`<font style="color:rgb(85, 85, 85);background-color:rgb(42, 42, 42) !important;"> 。</font>
两句话说明:
服务端创建远程对象,将其绑定到注册中心上,从注册中心获取了远程对象后进行服务端方法的调用
客户端通过 Stub 代理来调用远程对象,Stub 把请求交给 Skeleton,再由 Skeleton 调用真正的 RemoteObjImpl。
1.2 RMI 实现
客户端和服务端需要写相同接口,服务器端进行实现接口,客户端远程调用在接口实现类中的方法
服务端 远程调用接口
1 2 3 4 5 6 7 8 9 package org.example;import java.rmi.Remote;import java.rmi.RemoteException;public interface IRemoteObj extends Remote { public String sayhello (String keywords) throws RemoteException; }
注意:
具备远程调用的接口要继承 Remote ,该接口是一个空接口,只作为 RMI 标识接口,并且具备远程调用的接口中的方法要抛出异常 RemoteException
1 public interface Remote {}
接口的实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.example;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;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; } }
注意:
接口的实现类要继承 UnicastRemoteObject (该类提供了很多支持 RMI 的方法,用于生成 Stub 对象以及生成 Skeleton)
RMIServer: 创建远程对象。创建注册中心管理端口它的默认端口 1099。然后将远程对象绑定到注册中心上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.example;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIServer { public static void main (String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj remoteObj = new RemoteObjImpl (); Registry r= LocateRegistry.createRegistry(1099 ); r.bind("remoteObj" , remoteObj); } }
第三行代码将远程对象绑定到注册中心详细写法:
第一个参数是一个 URL ,形如: rmi://host:port/name 。其中, host 和 port 就是
RMI Registry 的地址和端口, name 是远程对象的名字。
如果 RMI Registry 在本地运行,那么 host 和 port 是可以省略的,此时 host 默认是 localhost ,port 默认 是 1099 :
1 r.bind("rmi://127.0.0.1:1099/remoteObject" ,remoteObject);
客户端 同样的具备远程调用的接口
RMIClient: 连接注册中心获取一个东西调用远程对象并调用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.example;import java.rmi.NotBoundException;import java.rmi.Remote;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMICilent { public static void main (String[] args) throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 ); IRemoteObj remoteObj = (IRemoteObj)registry.lookup("remoteObj" ); System.out.println(remoteObj.sayhello("hello" )); } }
对象的创建和调用都是在内存中以序列化和反序列化的形式进行的,所以总的 RMI 的整个流程就是序列化和反序列化实现的
二. RMI 原理分析
类加载(初始化阶段:静态代码块 / 静态变量)→ 每次实例化时先执行构造代码块 → 再执行构造函数
实例化对象时会首先触发类加载 ,在类加载的初始化阶段进行合并静态代码块和静态变量进行赋值操作,接着才会执行构造代码块,构造函数 。还要注意静态变量和静态代码块初始化只执行一次。
2.1 服务端创建远程对象 1 IRemoteObj remoteObj = new RemoteObjImpl();//创建远程调用对象
new 实例化 RemoteObjImpl 远程对象,就按照上面回顾的顺序加载,首先类加载的初始化阶段静态变量和静态代码块
接着就是来到远程对象类的构造器了,(_**在子类构造器中有隐藏的调用父类构造器的代码 super()) **_
所以先调 UnicastRemoteObject 的无参构造器它里面又掉它的有参构造
1 2 3 4 public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj { public RemoteObjImpl () throws RemoteException { }
父类无参:
1 2 3 4 protected UnicastRemoteObject () throws RemoteException { this (0 ); }
有参:接下来要将远程对象发布到网络上,Port 这里默认传的 0,就是远程对象发布到任意端口上。
1 2 3 4 5 protected UnicastRemoteObject (int port) throws RemoteException { this .port = port; exportObject((Remote) this , port); }
UnicastServerRef.exportObject 接着调到该类静态方法这儿
这里导出对象方法有两个参数,一个就是实现远程调用对象逻辑的对象,另一个就是处理网络请求的
第二个参数是实例化的是 UnicastServerRef
进入到构造器
1 2 3 public UnicastServerRef (int port) { super (new LiveRef (port)); }
参数 new LiveRef
在这里创建了一个 LiveRef
LiveRef 构造里就是以一个 ip 一个端口作为参数的构造器–>还是构造中调第二个参数是 TCPEndpoint 静态方法的返回结果的有参构造
1 2 3 public LiveRef (int port) { this ((new ObjID ()), port); }
1 2 3 public LiveRef (ObjID objID, int port) { this (objID, TCPEndpoint.getLocalEndpoint(port), true ); }
getLocalEndpoint 该方法返回一个 TCPEndPoint 对象作为 LiveRef 有参构造的第二个参数
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 public static TCPEndpoint getLocalEndpoint (int port) { return getLocalEndpoint(port, null , null ); }public static TCPEndpoint getLocalEndpoint (int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) { 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 ) { 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; .... return ep; }
参数分析完,好了终于来到了最终的这个有参构造,到这里远程对象就创建好了
1 2 3 4 5 public LiveRef (ObjID objID, Endpoint endpoint, boolean isLocal) { ep = endpoint; id = objID; this .isLocal = isLocal; }
super
最里面参数部分走完,现在就回到这里,调 UnicastServerRef 的 super()
1 2 3 public UnicastServerRef (int port) { super (new LiveRef (port)); }
在父类 UnicastRef 中赋值,即刚刚创建的 liveRef
1 2 3 public UnicastRef (LiveRef liveRef) { ref = liveRef; }
里面走完回到这里,接着走 exportObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static Remote exportObject (Remote obj, int port) throws RemoteException { return exportObject(obj, new UnicastServerRef (port)); } private static Remote exportObject (Remote obj, UnicastServerRef sref) throws RemoteException { if (obj instanceof UnicastRemoteObject) { ((UnicastRemoteObject) obj).ref = sref; } return sref.exportObject(obj, null , false ); } }
exportObject()方法 接着跟上面,从 UnicastRemoteObject 构造器–>该类的静态方法–>还是一个静态方法–> UnicastServerRef 的 exportObject
重要的一个静态函数 exportObject()导出对象 ,注意由于他是静态方法,所以远程调用类在不继承 UnicastRemoteObject 的情况下,就需要手动在远程调用对象的构造器中类调用导出对象这个静态方法(在加载子类的时候首先就会加载父类进行初始化)
最终执行 UnicastServerRef 的 exportObject:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public Remote exportObject (Remote impl, Object data, boolean permanent) throws RemoteException { Class<?> implClass = impl.getClass(); Remote stub; try { stub = Util.createProxy(implClass, getClientRef(), forceStubUse); } catch (IllegalArgumentException e) { throw new ExportException ( "remote object implements illegal remote interface" , e); } if (stub instanceof RemoteStub) { setSkeleton(impl); } Target target = new Target (impl, this , stub, ref.getObjID(), permanent); ref.exportObject(target); hashToMethod_Map = hashToMethod_Maps.get(implClass); return stub; }
最里层真正 exportObject 走完,就一层一层往外返回,返回到静态方法这里
1 2 3 4 5 public static Remote exportObject (Remote obj, int port) throws RemoteException { return exportObject(obj, new UnicastServerRef (port)); }
跟进看看具体执行,这里将方法的第二个参数也就是创建的 UnicastServerRef 且里面还包含了 LiveRef,将 sref 他赋值给远程对象的属性 ref
1 2 3 4 5 6 7 8 9 10 private static Remote exportObject (Remote obj, UnicastServerRef sref) throws RemoteException { if (obj instanceof UnicastRemoteObject) { ((UnicastRemoteObject) obj).ref = sref; } return sref.exportObject(obj, null , false ); } }
然后调到前面说的方法执行的这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public Remote exportObject (Remote impl, Object data, boolean permanent) throws RemoteException { Class<?> implClass = impl.getClass(); Remote stub; try { stub = Util.createProxy(implClass, getClientRef(), forceStubUse); } catch (IllegalArgumentException e) { throw new ExportException ( "remote object implements illegal remote interface" , e); } if (stub instanceof RemoteStub) { setSkeleton(impl); } Target target = new Target (impl, this , stub, ref.getObjID(), permanent); ref.exportObject(target); hashToMethod_Map = hashToMethod_Maps.get(implClass); return stub; }
** createProxy**
创建客户端的代理,客户端的动态代理是在服务端创建的,再放到注册中心,客户端从注册中获取再向服务端的代理请求
跟进去它
1 stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
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 public static Remote createProxy (Class<?> implClass, RemoteRef clientRef, boolean forceStubUse) throws StubNotFoundException { Class<?> remoteClass; try { remoteClass = getRemoteClass(implClass); } catch (ClassNotFoundException ex ) { throw new StubNotFoundException ( "object does not implement a remote interface: " + implClass.getName()); } if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass))) { return createStub(remoteClass, clientRef); } final ClassLoader loader = implClass.getClassLoader(); final Class<?>[] interfaces = getRemoteInterfaces(implClass); final InvocationHandler handler = new RemoteObjectInvocationHandler (clientRef); try { return AccessController.doPrivileged(new PrivilegedAction <Remote>() { public Remote run () { return (Remote) Proxy.newProxyInstance(loader, interfaces, handler); }}); } catch (IllegalArgumentException e) { throw new StubNotFoundException ("unable to create proxy" , e); } }
看这部分逻辑,stubClassExists 要返回 true,这里就返回 creatStub 方法结果
1 2 3 4 5 if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass))) { return createStub(remoteClass, clientRef); }
stubClassExists:该方法的主要作用是判断指定的远程类是否存在对应的 Stub 类,这里没有返回 false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private static boolean stubClassExists (Class<?> remoteClass) { if (!withoutStubs.containsKey(remoteClass)) { try { Class.forName(remoteClass.getName() + "_Stub" , false , remoteClass.getClassLoader()); return true ; } catch (ClassNotFoundException cnfe) { withoutStubs.put(remoteClass, null ); } } return false ; }
然后接着看 createProxy 的逻辑,下面这部分就是标准的创建动态代理了,参数有真正类的类加载器即 AppclassLoader,真正类实现的接口即 IRemoteObj ,调用处理器
1 2 3 return (Remote) Proxy.newProxyInstance(loader, interfaces, handler);
看一下调用处理器是什么,从下面的图中可以看到 ref 还是前面创建的 LiveRef@845,从始至终就这一个
1 2 3 4 5 6 public RemoteObjectInvocationHandler (RemoteRef ref) { super (ref); if (ref == null ) { throw new NullPointerException (); } }
1 2 3 protected RemoteObject (RemoteRef newref) { ref = newref; }
creatProxy 之后就创建好了动态代理 stub,可以看到 h 就是动态代理类的的调用处理器,里面还有我们的核心 ref
接着 exportObject 的逻辑,这里是判断 stub 是内置的类就执行 if 里的代码,现在是不满足的
1 2 3 if (stub instanceof RemoteStub) { setSkeleton(impl); }
然后把我们已经获得的所有信息封装进 target 中,里面的 disp 是服务端代理,stub 就是客户端代理,(为了保证服务端和客户端之间的通信)他们都有一个相同的核心东西 ref 即同一个 LiveRef 对象
1 2 Target target = new Target (impl, this , stub, ref.getObjID(), permanent);
接着将封装好的 traget exportObject 发布到网络上
1 ref.exportObject(target);
TCPTransport.exportObject ref 调也就来到了LiveRef.exportObject
LiveRef.exportObject:
ep 里面有个 transport(TCPTransport@1170),后面一直是他
1 2 3 public void exportObject (Target target) throws RemoteException { ep.exportObject(target); }
TCPEndpoint.exportObject:
这里 transport 就还是上面的(TCPTransport@1170)
1 2 3 public void exportObject (Target target) throws RemoteException { transport.exportObject(target); }
再然后就是真正执行的TCPTransport.exportObject
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 public void exportObject (Target target) throws RemoteException { synchronized (this ) { listen(); exportCount++; } boolean ok = false ; try { super .exportObject(target); ok = true ; } finally { if (!ok) { synchronized (this ) { decrementExportCount(); } } } }
来看 listen 的逻辑,Listen 监听就会开启端口
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 private void listen () throws RemoteException { assert Thread.holdsLock(this ); TCPEndpoint ep = getEndpoint(); int port = ep.getPort(); if (server == null ) { if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + port + ") create server socket" ); } try { server = ep.newServerSocket(); Thread t = AccessController.doPrivileged( new NewThreadAction (new AcceptLoop (server), "TCP Accept-" + port, true )); t.start(); } catch (java.net.BindException e) { throw new ExportException ("Port already in use: " + port, e); } catch (IOException e) { throw new ExportException ("Listen failed on port: " + port, e); } } else { SecurityManager sm = System.getSecurityManager(); if (sm != null ) { sm.checkListen(port); } } }
获取已经创建处理的 TCPEndpoint
设置端口值,若果是默认 0,设置一个端口值,服务端将远程对象发布到这个端口上
1 2 if (listenPort == 0 ) setDefaultPort(server.getLocalPort(), csf, ssf);
创建服务端 socket,新建一个线程打开线程等待客户端连接(表明网络请求和代码逻辑是分离开的)
1 2 3 4 Thread t = AccessController.doPrivileged( new NewThreadAction (new AcceptLoop (server), "TCP Accept-" + port, true )); t.start();
listen 分析完
已经发布远程对象到随机设置的那个端口了,接下来要记录一下
调用 TCPTransport 父类 Transport.exportObject—-> ObjectTable.putTarget(target)
将 target(封装的全部远程对象的信息)put 进两个服务端这边静态的表进行存储
1 2 objTable.put(oe, target); implTable.put(weakImpl, target);
到这里远程对象就在服务端创建好,且被发布出去了
小结: 服务端在创建远程对象 exportObject“导出远程对象时”会动态代理 生成客户端代理 stub,(会被 传递到客户端 (通过注册中心 ),让客户端持有这个 “代理”,间接调用服务端的真实对象。
2.2 服务端创建注册中心 创建注册中心
注册中心和服务端本质上都是一个远程服务开到一个端口上,所以下面的调试和上面差的不多
跟进创建注册中心,传入 1099 端口
1 Registry r= LocateRegistry.createRegistry(1099 );
静态方法 createRegistry,创建了 RegistryImpl
1 2 3 public static Registry createRegistry(int port) throws RemoteException { return new RegistryImpl(port); }
RegistryImpl 构造器中先进行安全检查,这里直接就走到 else 里两行代码
发现和上面服务端创建远程对象一致,都用了 UnicastSeverRef 一个服务端引用来封装创建的 LiveRef 对象,只是这里端口我们传的 1099
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public RegistryImpl (int port) throws RemoteException { if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null ) { try { AccessController.doPrivileged(new PrivilegedExceptionAction <Void>() { public Void run () throws RemoteException { LiveRef lref = new LiveRef (id, port); setup(new UnicastServerRef (lref)); return null ; } }, null , new SocketPermission ("localhost:" +port, "listen,accept" )); } catch (PrivilegedActionException pae) { throw (RemoteException)pae.getException(); } } else { LiveRef lref = new LiveRef (id, port); setup(new UnicastServerRef (lref)); } }
快速过一下 new UnicastServerRef 这里
1 2 3 4 5 6 7 8 9 10 11 public UnicastServerRef (LiveRef ref) { super (ref); } public UnicastRef (LiveRef liveRef) { ref = liveRef; }
接着跟 setup,封装了 LiveRef 的 uref 被赋给了属性 ref,在这儿调了 UnicastServerRef.exportObject,和前面一样的,只是第三个参数由 false 变为了 true ,true 创建的是永久对象,而 false 创建的是临时对象
1 2 3 4 5 6 7 8 9 private void setup (UnicastServerRef uref) throws RemoteException { ref = uref; uref.exportObject(this , null , true ); }
然后进到 UnicastServerRef.exportObject 里面,同样创建 stub,traget,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public Remote exportObject (Remote impl, Object data, boolean permanent) throws RemoteException { Class<?> implClass = impl.getClass(); Remote stub; try { stub = Util.createProxy(implClass, getClientRef(), forceStubUse); } catch (IllegalArgumentException e) { throw new ExportException ( "remote object implements illegal remote interface" , e); } if (stub instanceof RemoteStub) { setSkeleton(impl); } Target target = new Target (impl, this , stub, ref.getObjID(), permanent); ref.exportObject(target); hashToMethod_Map = hashToMethod_Maps.get(implClass); return stub; }
不同的是 createProxy 创建 stub 时,这里代码走进了 if,stubClassExists 找到了<font style="color:rgb(71, 101, 130);background-color:rgb(241, 241, 241);">RegistryImpl_Stub</font>类,所以走 creatStub,不会到下面的动态代理那了(这里 if 的出现在下面思考中给出解释)
createStub:全类名 forName 获得类,再实例化
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 private static RemoteStub createStub (Class<?> remoteClass, RemoteRef ref) throws StubNotFoundException { String stubname = remoteClass.getName() + "_Stub" ; try { Class<?> stubcl = Class.forName(stubname, false , remoteClass.getClassLoader()); Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes); return (RemoteStub) cons.newInstance(new Object [] { ref }); } catch (ClassNotFoundException e) { throw new StubNotFoundException ( "Stub class not found: " + stubname, e); } catch (NoSuchMethodException e) { throw new StubNotFoundException ( "Stub class missing constructor: " + stubname, e); } catch (InstantiationException e) { throw new StubNotFoundException ( "Can't create instance of stub class: " + stubname, e); } catch (IllegalAccessException e) { throw new StubNotFoundException ( "Stub class constructor not public: " + stubname, e); } catch (InvocationTargetException e) { throw new StubNotFoundException ( "Exception creating instance of stub class: " + stubname, e); } catch (ClassCastException e) { throw new StubNotFoundException ( "Stub class not instance of RemoteStub: " + stubname, e); } }
这里由 creatStub 创建的 stub 是一个 <font style="background-color:rgb(241, 241, 241);">RegistryImpl_Stub</font>封装着 UnicastRef,里面再一层还封装着 LiveRef
RegistryImpl_Stub 是 jdk 自带的类,继承RemoteStub 的所以会进入到 if 当中,执行 setSkeleton 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 stub = Util.createProxy(implClass, getClientRef(), forceStubUse); } catch (IllegalArgumentException e) { throw new ExportException ( "remote object implements illegal remote interface" , e); } if (stub instanceof RemoteStub) { setSkeleton(impl); } Target target = new Target (impl, this , stub, ref.getObjID(), permanent); ref.exportObject(target); hashToMethod_Map = hashToMethod_Maps.get(implClass); return stub; }
setSkeleton 中会创建一个 skeleton 作为服务端代理(forName 方式创建的)
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 public void setSkeleton (Remote impl) throws RemoteException { if (!withoutSkeletons.containsKey(impl.getClass())) { try { skel = Util.createSkeleton(impl); } catch (SkeletonNotFoundException e) { withoutSkeletons.put(impl.getClass(), null ); } } } static Skeleton createSkeleton (Remote object) throws SkeletonNotFoundException { Class<?> cl; try { cl = getRemoteClass(object.getClass()); } catch (ClassNotFoundException ex ) { throw new SkeletonNotFoundException ( "object does not implement a remote interface: " + object.getClass().getName()); } String skelname = cl.getName() + "_Skel" ; try { Class<?> skelcl = Class.forName(skelname, false , cl.getClassLoader()); return (Skeleton)skelcl.newInstance(); } catch (ClassNotFoundException ex) { throw new SkeletonNotFoundException ("Skeleton class not found: " + skelname, ex); } catch (InstantiationException ex) { throw new SkeletonNotFoundException ("Can't create skeleton: " + skelname, ex); } catch (IllegalAccessException ex) { throw new SkeletonNotFoundException ("No public constructor: " + skelname, ex); } catch (ClassCastException ex) { throw new SkeletonNotFoundException ( "Skeleton not of correct class: " + skelname, ex); } }
exportObject 方法最后还是将这些都放到 target 中,再用 exportObject 发布到网络
1 2 3 4 5 6 Target target = new Target (impl, this , stub, ref.getObjID(), permanent); ref.exportObject(target); hashToMethod_Map = hashToMethod_Maps.get(implClass);return stub;
然后还是与上面一样,记录在静态表中
1 2 3 4 5 6 7 8 9 10 public void exportObject (Target target) throws RemoteException { target.setExportedTransport(this ); ObjectTable.putTarget(target); } objTable.put(oe, target); implTable.put(weakImpl, target);
put 进表的有三个 Traget,可以都点开看看他们本质都是一样的有一个是 Impl_Stub 它不是我们之前创建的,叫分布式垃圾回收,是默认创建的一个重要对象,稍后 2.6 里面再看
绑定 实际上就是 put 进去 HashTable
断点调试
1 r.bind("remoteObj" , remoteObj);
checkAccess 进行检查是否在本地,然后检测是否进行绑定,已经绑定过就抛出异常,没有就将元编程对象 name 和对象 put 进去
1 2 3 4 5 6 7 8 9 10 11 public void bind (String name, Remote obj) throws RemoteException, AlreadyBoundException, AccessException { checkAccess("Registry.bind" ); synchronized (bindings) { Remote curr = bindings.get(name); if (curr != null ) throw new AlreadyBoundException (name); bindings.put(name, obj); } }
RMI 实现是要求注册中心和服务端在同一台主机上。而在低版本的是允许它们远程绑定,所以会出现一些漏洞
🤔 创建远程对象时没有看到显式的 skel,在创建注册中心时却看到了 skel:
因为从 JDK 1.2 开始:
普通定义的远程对象 :Stub 通过动态代理自动生成,不再需要显式 Stub 类;Skeleton 机制已被完全废弃 ,由 UnicastServerRef.dispatch() + 反射 实现。
RMI Registry :作为系统级远程对象,虽然 Skeleton 机制已被完全废弃但仍保留静态 Stub 类(RegistryImpl_Stub)和静态 SkeltonRegistryImpl_Skel以保证兼容性和稳定性。这也就是对应的上面创建注册中心创建 stub 时,当代码走进了 if,也就是 stubClassExists 找到了 <font style="color:#2F8EF4;background-color:rgb(241, 241, 241);">RegistryImpl_Stub</font>类,就实例化
还有就是 无论远程对象还是注册中心都是UnicastServerRef 接管了 Skeleton 的工作 只是注册中心:在 UnicastServerRef 里保留了“如果存在 Skel 就用 Skel,否则走反射”的逻辑。
所以在后面客户端请求注册中心时注册中心是有 skel 的,而客户端请求服务端时服务端并没有远程对象的 skel
小结
静态 Stub(RegistryImpl_Stub) → 方法里调用 UnicastRef.invoke()
动态 Stub(Proxy) → 方法转到 RemoteObjectInvocationHandler.invoke(),再调 UnicastRef.invoke()。
远程对象的 skel 已被完全废弃(jdk1.2 之后),由UnicastServerRef+ 反射机制 内部实现,不显示定义。
注册中心或 DGC 的 服务端处理是 UnicastServerRef 走的 skel(内置类)逻辑不走反射
2.3 客户端请求注册中心—客户端
现在服务端创建好远程对象和注册中心并进行了绑定。该看客户端做得事情:1.客户端从注册中心获取远程对象的代理(其实是在本地创建)。2. ·客户端向服务端发起请求(反序列化获得)
获取注册中心 获取注册中心的 stub 对象(先说一下这里是不存在漏洞的具体看分析~)
1 Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 );
认知中获取一个远程对象 是需要序列化和反序列化的,但这看代码它实际上是在本地创建 LiveRef,将主机和端口传进去,然后进行封装
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 public static Registry getRegistry (String host, int port) throws RemoteException { return getRegistry(host, port, null ); } public static Registry getRegistry (String host, int port, RMIClientSocketFactory csf) throws RemoteException { Registry registry = null ; if (port <= 0 ) port = Registry.REGISTRY_PORT; if (host == null || host.length() == 0 ) { try { host = java.net.InetAddress.getLocalHost().getHostAddress(); } catch (Exception e) { host = "" ; } } 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 ); }
之后又调了 Util.createProxy 方法(与上面服务端创建 stub 的流程是一样的,还是以 forname 形式实例化出来的)就获取到注册中心的 stub 对象,下一步就通过它去查找远程对象
所以这里从注册中心获得远程对象并没有以序列化的形式,而是传了参数在本地创建了一模一样的,所以这里没有漏洞
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 public static Remote createProxy (Class<?> implClass, RemoteRef clientRef, boolean forceStubUse) throws StubNotFoundException { Class<?> remoteClass; try { remoteClass = getRemoteClass(implClass); } catch (ClassNotFoundException ex ) { throw new StubNotFoundException ( "object does not implement a remote interface: " + implClass.getName()); } if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass))) { return createStub(remoteClass, clientRef); } final ClassLoader loader = implClass.getClassLoader(); final Class<?>[] interfaces = getRemoteInterfaces(implClass); final InvocationHandler handler = new RemoteObjectInvocationHandler (clientRef); try { return AccessController.doPrivileged(new PrivilegedAction <Remote>() { public Remote run () { return (Remote) Proxy.newProxyInstance(loader, interfaces, handler); }}); } catch (IllegalArgumentException e) { throw new StubNotFoundException ("unable to create proxy" , e); } }
查找远程对象
将名字序列化给注册中心反序列化获取 stub
1 IRemoteObj remoteObj = (IRemoteObj)registry.lookup("remoteObj" );
因为此处调试对应 class 文件是 1.1 版本,无法断点调试,静态分析就好
看一下 lookup,它的参数 String var1 是绑定的远程对象的名字
writeObject 进行序列化(那么对应的注册中心就有反序列化的操作 了),然后 super.ref.invoke(var2)处理客户端网络请求的,建立连接,返回结果 readObject反序列化赋给 var23 (var23 就是获取的远程对象的 stub)
1 2 ObjectInput var6 = var2.getInputStream(); var23 = (Remote)var6.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 public Remote lookup (String var1) throws AccessException, NotBoundException, RemoteException { try { RemoteCall var2 = super .ref.newCall(this , operations, 2 , 4905912898345647071L ); try { ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(var1); } catch (IOException var18) { throw new MarshalException ("error marshalling arguments" , var18); } super .ref.invoke(var2); Remote var23; try { ObjectInput var6 = var2.getInputStream(); var23 = (Remote)var6.readObject(); } catch (IOException var15) { throw new UnmarshalException ("error unmarshalling return" , var15); } catch (ClassNotFoundException var16) { throw new UnmarshalException ("error unmarshalling return" , var16); } finally { super .ref.done(var2); } return var23; } catch (RuntimeException var19) { throw var19; } catch (RemoteException var20) { throw var20; } catch (NotBoundException var21) { throw var21; } catch (Exception var22) { throw new UnexpectedException ("undeclared checked exception" , var22); } }
上面 lookup 从注册中心读取字节流进行反序列化,就可能存在攻击点这是一个,还有一个更隐蔽攻击面更广的点:
跟进去客户端处理网络请求这里 super.ref.invoke(var2); 真正处理网络请求的是executeCall
在它的第二个 case 里 产生 TransportConstants.ExceptionalReturn 这个异常时会通过反序列化(ex = in.readObject());当获取网络传输流里面的对象,如果注册中心返回一个恶意对象,即可能产生漏洞。
所以如果有要调用 StreamRemoteCall.executeCall()或 super.ref.invoke(var2);的都有被攻击的可能
1 2 3 4 5 6 7 8 public void invoke (RemoteCall call) throws Exception { try { clientRefLog.log(Log.VERBOSE, "execute call" ); call.executeCall(); } catch (RemoteException e) { ......
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 public void executeCall () throws Exception { ...... switch (returnType) { case TransportConstants.NormalReturn: break ; case TransportConstants.ExceptionalReturn: Object ex; try { ex = in.readObject(); } catch (Exception e) { throw new UnmarshalException ("Error unmarshaling return" , e); } if (ex instanceof Exception) { exceptionReceivedFromServer((Exception) ex); } else { throw new UnmarshalException ("Return type not Exception" ); } default : if (Transport.transportLog.isLoggable(Log.BRIEF)) { Transport.transportLog.log(Log.BRIEF, "return code invalid: " + returnType); } throw new UnmarshalException ("Return code invalid" ); } }
上面就是客户端与注册中心进行交互获取远程对象代理时可能产生的攻击点。
继续看获取到 remoteObj 远程对象代理就是一个动态代理
下一步就该客户端与服务端进行交互
小结 在客户端本地创建的注册中心 (其实是注册中心的 stub“代理对象”),发起网络请求,将名字序列化给注册中心反序列化获取远程对象的 stub
2.4 客户端请求服务端—客户端
将方法的参数序列化,反序列化返回结果
进行调试
1 System.out.println(remoteObj.sayhello("hello" ));
remoteObject 是远程对象的代理,调用方法就肯定先进处理器的 invoke 方法里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if (! Proxy.isProxyClass(proxy.getClass())) { throw new IllegalArgumentException ("not a proxy" ); } if (Proxy.getInvocationHandler(proxy) != this ) { throw new IllegalArgumentException ("handler mismatch" ); } if (method.getDeclaringClass() == Object.class) { return invokeObjectMethod(proxy, method, args); } else if ("finalize" .equals(method.getName()) && method.getParameterCount() == 0 && !allowFinalizeInvocation) { return null ; } else { return invokeRemoteMethod(proxy, method, args); } }
invokeRemoteMethod 这里又调了 ref.invoke, 只是方法的参数不一样
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 private Object invokeRemoteMethod (Object proxy, Method method, Object[] args) throws Exception { try { if (!(proxy instanceof Remote)) { throw new IllegalArgumentException ( "proxy not Remote instance" ); } return ref.invoke((Remote) proxy, method, args, getMethodHash(method)); } catch (Exception e) { if (!(e instanceof RuntimeException)) { Class<?> cl = proxy.getClass(); try { method = cl.getMethod(method.getName(), method.getParameterTypes()); } catch (NoSuchMethodException nsme) { throw (IllegalArgumentException) new IllegalArgumentException ().initCause(nsme); } Class<?> thrownType = e.getClass(); for (Class<?> declaredType : method.getExceptionTypes()) { if (declaredType.isAssignableFrom(thrownType)) { throw e; } } e = new UnexpectedException ("unexpected exception" , e); } throw e; } }
最终是进入 UnicastRef 的 invoke 里面有一个新的攻击点就是循环中调用的 marshalValue(types[i], params[i], out);
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 public Object invoke (Remote obj, Method method, Object[] params, long opnum) throws Exception { if (clientRefLog.isLoggable(Log.VERBOSE)) { clientRefLog.log(Log.VERBOSE, "method: " + method); } if (clientCallLog.isLoggable(Log.VERBOSE)) { logClientCall(obj, method); } Connection conn = ref.getChannel().newConnection(); RemoteCall call = null ; boolean reuse = true ; boolean alreadyFreed = false ; try { if (clientRefLog.isLoggable(Log.VERBOSE)) { clientRefLog.log(Log.VERBOSE, "opnum = " + opnum); } call = new StreamRemoteCall (conn, ref.getObjID(), -1 , opnum); try { ObjectOutput out = call.getOutputStream(); marshalCustomCallData(out); Class<?>[] types = method.getParameterTypes(); for (int i = 0 ; i < types.length; i++) { marshalValue(types[i], params[i], out); } } catch (IOException e) { clientRefLog.log(Log.BRIEF, "IOException marshalling arguments: " , e); throw new MarshalException ("error marshalling arguments" , e); } call.executeCall(); try { Class<?> rtype = method.getReturnType(); if (rtype == void .class) return null ; ObjectInput in = call.getInputStream(); return returnValue; } catch (IOException e) { clientRefLog.log(Log.BRIEF, "IOException unmarshalling return: " , e); throw new UnmarshalException ("error unmarshalling return" , e); } catch (ClassNotFoundException e) { clientRefLog.log(Log.BRIEF, "ClassNotFoundException unmarshalling return: " , e); throw new UnmarshalException ("error unmarshalling return" , e); } finally { try { call.done(); } catch (IOException e) { reuse = false ; } } } catch (RuntimeException e) { if ((call == null ) || (((StreamRemoteCall) call).getServerException() != e)) { reuse = false ; } throw e; } catch (RemoteException e) { reuse = false ; throw e; } catch (Error e) { reuse = false ; throw e; } finally { if (!alreadyFreed) { if (clientRefLog.isLoggable(Log.BRIEF)) { clientRefLog.log(Log.BRIEF, "free connection (reuse = " + reuse + ")" ); } ref.getChannel().free(conn, reuse); } } }
分析一下循环中调用的 marshalValue,第二个参数 value 就是调用方法的参数,看经过一堆判断后 writeObject(value)将传的参数 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 28 protected static void marshalValue (Class<?> type, Object value, ObjectOutput out) throws IOException { if (type.isPrimitive()) { if (type == int .class) { out.writeInt(((Integer) value).intValue()); } else if (type == boolean .class) { out.writeBoolean(((Boolean) value).booleanValue()); } else if (type == byte .class) { out.writeByte(((Byte) value).byteValue()); } else if (type == char .class) { out.writeChar(((Character) value).charValue()); } else if (type == short .class) { out.writeShort(((Short) value).shortValue()); } else if (type == long .class) { out.writeLong(((Long) value).longValue()); } else if (type == float .class) { out.writeFloat(((Float) value).floatValue()); } else if (type == double .class) { out.writeDouble(((Double) value).doubleValue()); } else { throw new Error ("Unrecognized primitive type: " + type); } } else { out.writeObject(value); } }
接着重载的 invoke 中会调 call.executeCall();也就是像前面的一样客户端处理网络请求的,同样会有头一个攻击点
然后 unmarshalValue 方法中反序列化结果 所以 UnicastRef 里面序列化–>发送网络请求–>再反序列化
1 Object returnValue = unmarshalValue(rtype, in);
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 protected static Object unmarshalValue (Class<?> type, ObjectInput in) throws IOException, ClassNotFoundException { if (type.isPrimitive()) { if (type == int .class) { return Integer.valueOf(in.readInt()); } else if (type == boolean .class) { return Boolean.valueOf(in.readBoolean()); } else if (type == byte .class) { return Byte.valueOf(in.readByte()); } else if (type == char .class) { return Character.valueOf(in.readChar()); } else if (type == short .class) { return Short.valueOf(in.readShort()); } else if (type == long .class) { return Long.valueOf(in.readLong()); } else if (type == float .class) { return Float.valueOf(in.readFloat()); } else if (type == double .class) { return Double.valueOf(in.readDouble()); } else { throw new Error ("Unrecognized primitive type: " + type); } } else { return in.readObject(); } }
攻击点小结: 可能的攻击点:
(注册中心—>客户端)客户端访问注册中心获取远程对象的代理时: call.executeCall()(网络请求建立连接里抛出固定异常时反序列化)和反序列化获取远程对象的代理
(服务端—>客户端)客户端请求服务端方法时: call.executeCall(); 和 unmarshalValue 反序列化获取方法返回结果
2.5 客户端请求注册中心—注册中心
客户端请求注册中心客户端干的事已经分析了,现在看看在此操作获取远程对象时注册中心发生的事儿~!
注册中心可以说是特殊的服务端,打断点时考虑之前客户端代理是 stub,所以这里操作的是注册中心的代理 skel,分析RegistryImpl_Skel 类(注册中心处理客户端交互的逻辑就在这里)。下面看是怎么走到 RegistryImpl_Skel 类的
从服务端创建注册中心 LocateRegistry.createRegistry(1099);这开始,里面还是像上面一样走到 listen 那创建了一个新的线程,所以就要去看这个线程里面的(AcceptLoop 里面的)run 方法
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 private void listen () throws RemoteException { assert Thread.holdsLock(this ); TCPEndpoint ep = getEndpoint(); int port = ep.getPort(); if (server == null ) { if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + port + ") create server socket" ); } try { server = ep.newServerSocket(); Thread t = AccessController.doPrivileged( new NewThreadAction (new AcceptLoop (server), "TCP Accept-" + port, true )); t.start(); } catch (java.net.BindException e) { throw new ExportException ("Port already in use: " + port, e); } catch (IOException e) { throw new ExportException ("Listen failed on port: " + port, e); } } else { SecurityManager sm = System.getSecurityManager(); if (sm != null ) { sm.checkListen(port); } } }
AcceptLoop.run 里面就只调了 executeAcceptLoop()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void run () { try { executeAcceptLoop(); } finally { try { serverSocket.close(); } catch (IOException e) { } } }
在这个方法里又创建了新的线程ConnectionHandler,所以就看他的 run 方法,run 里面实际上那就是调 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 private void executeAcceptLoop () { if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "listening on port " + getEndpoint().getPort()); } while (true ) { Socket socket = null ; try { socket = serverSocket.accept(); InetAddress clientAddr = socket.getInetAddress(); String clientHost = (clientAddr != null ? clientAddr.getHostAddress() : "0.0.0.0" ); try { connectionThreadPool.execute( new ConnectionHandler (socket, clientHost)); } catch (RejectedExecutionException e) { closeSocket(socket); tcpLog.log(Log.BRIEF, "rejected connection from " + clientHost); } ......
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void run () { Thread t = Thread.currentThread(); String name = t.getName(); try { t.setName("RMI TCP Connection(" + connectionCount.incrementAndGet() + ")-" + remoteHost); AccessController.doPrivileged((PrivilegedAction<Void>)() -> { run0(); return null ; }, NOPERMS_ACC); } finally { t.setName(name); } }
run0 里面会解析协议传来的字段等等,重点是调用了 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 private void run0 () { ...... try { InputStream sockIn = socket.getInputStream(); InputStream bufIn = sockIn.markSupported() ? sockIn : new BufferedInputStream (sockIn); bufIn.mark(4 ); DataInputStream in = new DataInputStream (bufIn); int magic = in.readInt(); if (magic == POST) { tcpLog.log(Log.BRIEF, "decoding HTTP-wrapped call" ); bufIn.reset(); try { socket = new HttpReceiveSocket (socket, bufIn, null ); remoteHost = "0.0.0.0" ; sockIn = socket.getInputStream(); bufIn = new BufferedInputStream (sockIn); in = new DataInputStream (bufIn); magic = in.readInt(); } catch (IOException e) { throw new RemoteException ("Error HTTP-unwrapping call" , e); } } ...... handleMessages(conn, false ); break ; ......
handleMessages 就是根据字段进入到不同 case,这里面默认进到第一个调用 serviceCall(call)
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 void handleMessages (Connection conn, boolean persistent) { int port = getEndpoint().getPort(); try { DataInputStream in = new DataInputStream (conn.getInputStream()); do { int op = in.read(); if (op == -1 ) { if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + port + ") connection closed" ); } break ; } if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + port + ") op = " + op); } switch (op) { case TransportConstants.Call: RemoteCall call = new StreamRemoteCall (conn); if (serviceCall(call) == false ) return ; break ; case TransportConstants.Ping: DataOutputStream out = new DataOutputStream (conn.getOutputStream()); out.writeByte(TransportConstants.PingAck); conn.releaseOutputStream(); break ; case TransportConstants.DGCAck: DGCAckHandler.received(UID.read(in)); break ; default : ......
serviceCall 里面 getTarget,从静态表中获取记录的 target(存放着所有的信息)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public boolean serviceCall (final RemoteCall call) { try { final Remote impl; ObjID id; try { id = ObjID.read(call.getInputStream()); } catch (java.io.IOException e) { throw new MarshalException ("unable to read objID" , e); } Transport transport = id.equals(dgcID) ? null : this ; Target target = ObjectTable.getTarget(new ObjectEndpoint (id, transport)); if (target == null || (impl = target.getImpl()) == null ) { throw new NoSuchObjectException ("no such object in table" ); } ......
所以现在下断点调试看看上面分析的 getTarget 对不对,先在服务端 getTarget 下断点启动调试,然后客户端请求,成功断到这里
往下看到 target 里有 stub,端口为 1099
接下来从 target 中获取 skel 放到分发器 disp 里面
然后 serviceCall 方法中调用 disp.dispatch(impl, call)在里面 skel 不为 null 进入 if 调 oldDispatch,传的第三个参数 num 为 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void dispatch (Remote obj, RemoteCall call) throws IOException { int num; long op; try { ObjectInput in; try { in = call.getInputStream(); num = in.readInt(); if (num >= 0 ) { if (skel != null ) { oldDispatch(obj, call, num); return ; } else { throw new UnmarshalException ( "skeleton class not found but required " + "for client version" ); } ......
oldDispatc 里面调 skel.dispatch,就终于走到最初说的 RegistryImpl_Skel 类 里面了因为还是 class 文件,还是静态分析
所以在客户端我们操作处理 stub 去干一些事情,服务端就操作处理 skel 干一些事情(注册中心看作是特殊的服务端)
具体分析一下方法,var3 的不同 case,就是客户端和注册中心进行交互的不同方式(这里调试客户端从注册中心获取远程对象默认 var3 是 2) case 2 中有反序列化然后调用 lookup,可以是一个客户端攻击注册中心的点。(因为回顾之前在客户端将远程对象的名字序列化发送出去,对应的在注册中心这边一定会有反序列化的地方的)
0->bind
1->list
2->lookup
3->rebind
4->unbind
然后观察到这些 case 中处理 list 都有反序列化,所以客户端打注册中心的点就多了
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 public void dispatch (Remote var1, RemoteCall var2, int var3, long var4) throws Exception { if (var4 != 4905912898345647071L ) { throw new SkeletonMismatchException ("interface hash mismatch" ); } else { RegistryImpl var6 = (RegistryImpl)var1; switch (var3) { case 0 : String var100; Remote var103; try { ObjectInput var105 = var2.getInputStream(); var100 = (String)var105.readObject(); var103 = (Remote)var105.readObject(); } catch (IOException var94) { throw new UnmarshalException ("error unmarshalling arguments" , var94); } catch (ClassNotFoundException var95) { throw new UnmarshalException ("error unmarshalling arguments" , var95); } finally { var2.releaseInputStream(); } var6.bind(var100, var103); try { var2.getResultStream(true ); break ; } catch (IOException var93) { throw new MarshalException ("error marshalling return" , var93); } case 1 : var2.releaseInputStream(); String[] var99 = var6.list(); try { ObjectOutput var102 = var2.getResultStream(true ); var102.writeObject(var99); break ; } catch (IOException var92) { throw new MarshalException ("error marshalling return" , var92); } case 2 : String var98; try { ObjectInput var104 = var2.getInputStream(); var98 = (String)var104.readObject(); } catch (IOException var89) { throw new UnmarshalException ("error unmarshalling arguments" , var89); } catch (ClassNotFoundException var90) { throw new UnmarshalException ("error unmarshalling arguments" , var90); } finally { var2.releaseInputStream(); } Remote var101 = var6.lookup(var98); try { ObjectOutput var9 = var2.getResultStream(true ); var9.writeObject(var101); break ; } catch (IOException var88) { throw new MarshalException ("error marshalling return" , var88); } case 3 : Remote var8; String var97; try { ObjectInput var11 = var2.getInputStream(); var97 = (String)var11.readObject(); var8 = (Remote)var11.readObject(); } catch (IOException var85) { throw new UnmarshalException ("error unmarshalling arguments" , var85); } catch (ClassNotFoundException var86) { throw new UnmarshalException ("error unmarshalling arguments" , var86); } finally { var2.releaseInputStream(); } var6.rebind(var97, var8); try { var2.getResultStream(true ); break ; } catch (IOException var84) { throw new MarshalException ("error marshalling return" , var84); } case 4 : String var7; try { ObjectInput var10 = var2.getInputStream(); var7 = (String)var10.readObject(); } catch (IOException var81) { throw new UnmarshalException ("error unmarshalling arguments" , var81); } catch (ClassNotFoundException var82) { throw new UnmarshalException ("error unmarshalling arguments" , var82); } finally { var2.releaseInputStream(); } var6.unbind(var7); try { var2.getResultStream(true ); break ; } catch (IOException var80) { throw new MarshalException ("error marshalling return" , var80); } default : throw new UnmarshalException ("invalid method number" ); } } }
小结: 客户端从注册中心获取远程调用对象时,注册中心 run 新线程从静态表中获取 target,skel,等一些操作调到RegistryImpl_Skel.dispath 处理不同交互,将客户端发送的数据反序列化(readObject),就构成了攻击
2.6 客户端请求服务端—服务端 客户端调用方法请求服务端,服务端也会像上面一样先走进 ServiceCall 方法(注册中心和服务端走的是同一套代码跟 2.5 差不多)
在这两个地方下断点
下断点后 target 里面的 stub 是 RegistryImpl_Stub 这个是上面客户端请求注册中心时的 (调用 LocateRegistry.getRegistry() 时,拿到的就是这个注册中心的 stub。)连按两下 f9stub 变成了 DGCImpl_stub 它是处理内存垃圾的,再连按两下 f9 直到使得 stub 变为 Proxy
发现 skel 是 null(也就是上面说的服务端创建远程对象是没有显式的 skel)
所以这一步就与上面不一样了,disp 的 skel 是 null
1 final Dispatcher disp = target.getDispatcher();
接着在 UnicastServerRef.dispath 中 skel 为 null 就不会调用 oldDispatch,也就不会调用 Registry_Skel 了
继续往下走获取输入流与 Method(就是调用的 sayhello)
接着是重点循环当中的 unmarshalValue,就是在 2.4 客户端反序列化服务端返回的数据时也见过,存在漏洞
1 2 3 4 5 try { unmarshalCustomCallData(in); for (int i = 0 ; i < types.length; i++) { params[i] = unmarshalValue(types[i], in); }
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 Object unmarshalValue(Class<?> type, ObjectInput in) throws IOException, ClassNotFoundException { if (type.isPrimitive()) { if (type == int.class) { return Integer.valueOf(in.readInt()); } else if (type == boolean.class) { return Boolean.valueOf(in.readBoolean()); } else if (type == byte.class) { return Byte.valueOf(in.readByte()); } else if (type == char.class) { return Character.valueOf(in.readChar()); } else if (type == short.class) { return Short.valueOf(in.readShort()); } else if (type == long.class) { return Long.valueOf(in.readLong()); } else if (type == float.class) { return Float.valueOf(in.readFloat()); } else if (type == double.class) { return Double.valueOf(in.readDouble()); } else { throw new Error("Unrecognized primitive type: " + type); } } else { return in.readObject(); } }
再调用 method.invoke 方法去调用我们所调用的 sayHello 方法,然后用 marshalValue 将 result 序列化传回客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 try { result = method.invoke(obj, params); } catch (InvocationTargetException e) { throw e.getTargetException(); }try { ObjectOutput out = call.getResultStream(true ); Class<?> rtype = method.getReturnType(); if (rtype != void .class) { marshalValue(rtype, result, out); }
小结: UnicastServerRef.dispath 中 unmarshalValue 反序列化客户端传的参数造成攻击、
🤔 emm 在 RMI 客户端请求服务端时,宏观把握有点点像学的动态代理, RemoteObjImpl 为真正类,服务端创建的 stub 客户端代理,类比的是 Proxy 类(他们都实现相同的接口 IRemoteObj),skeleton(/UnicastServerRef)就类比服务端调用处理器 了(因为都是由代理类调用转发后的,且反射调用真正类的方法,这么说仅从逻辑方面类比是服务端的调用处理器),RemoteObjectInvocationHandler 就是客户端的调用处理器 (从代码方面它是 InvocationHandler 的实现类,再提一下这个只是动态 stub 才有的,像 regietry,DGC 的 Stub 就没有这个,直接就是 stub–>代理类,UnicastRef.invoke–>处理网络请求, UnicastServerRef(里面又走 skel)—>调用处理器 )
真实流程: 客户端代理类调方法—>RemoteObjectInvocationHandler客户端的调用处理器 —>里面逻辑还是调用 UnicastRef.invoke(处理网络请求的) → 网络层 → UnicastServerRef.dispatch() 服务端调用处理器
RMI (粗略看作有两个调用处理器)是动态代理里的调用处理器又调远程进程里的调用处理器,中间靠序列化 + 网络传输连接起来。
上面只是总体把握方便理解,接着看一下具体区别
区别:
1.
动态代理中每个方法都统一通过 invoke 方法调用真正类的方法的,InvocationHandler是本地代理的处理器 ,负责在同一个进程内转发调用,也就时本地增强方法
RMI 中区别于动态代理调用处理器主要是跨进程(就有两个)
RemoteObjectInvocationHandler(上面调试跟进 creatProxy 时方法里创建的动态代理中调用处理器就是它)是客户端的处理器 ,负责跨进程发送请求 ;
原 Skeleton 的功能被(UnicastServerRef.dispatch)替代,成为服务端的处理器 ,负责跨进程接收请求并反射调用 。
服务端接收请求,通过 RMI Skeleton / Dispatcher)解析请求;反序列化为 java 对象后,服务端 Skeleton/服务端分发 通过 反射 调用 RemoteObjImpl 的对应方法 ,再将结果序列化返回给 stub
2.
动态代理的代理类直接在同一进程中调用转发(proxy 创建后在底层有转发给调用处理器)
RMI 中 Stub 并不是直接调用服务端的“调用处理器”,而是通过网络传输把请求送过去。
stub(打包+发送)
封装方法调用信息(方法名、参数、返回类型等),序列化后通过网络协议(JRMP)发送给服务端, 等待服务端响应,再反序列化结果
借用 ai结论
Stub 就像“能上网的动态代理”:不在本地 invoke,而是把调用序列化后发到远端执行。
Skeleton/服务端分发 就像 “跨 JVM 的 InvocationHandler”:解包 → 反射调真实对象 → 打包回传。
真正的业务实现永远在 **RemoteObjImpl** 上。
2.7 DGC 看看在调试中出现的 DGC 是怎么一回事儿
创建 首先在创建注册中心那里说了 put 存储进静态表的 target 有三个,其中一个就是 DGCImpl_Stub,这是我们 put 之前在静态表中就有了 DGCImpl_Stub
而此时 put 的 target 里面 stub 是 Proxy(远程对象的 stub),所以在此之前DGC 已经被创建好并放入静态表中了
就从 put 往回倒, DGCImpl.dgcLog.isLoggable(Log.VERBOSE)这句代码就创建了 DGC
dgcLog 是这个类的静态属性,之前学过调静态属性时会先类加载,完成初始化(即合并静态代码块和静态变量完成赋值)
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 static void putTarget (Target target) throws ExportException { ObjectEndpoint oe = target.getObjectEndpoint(); WeakRef weakImpl = target.getWeakImpl(); if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) { DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + oe); } synchronized (tableLock) { if (target.getImpl() != null ) { if (objTable.containsKey(oe)) { throw new ExportException ( "internal error: ObjID already in use" ); } else if (implTable.containsKey(weakImpl)) { throw new ExportException ("object already exported" ); } objTable.put(oe, target); implTable.put(weakImpl, target); if (!target.isPermanent()) { incrementKeepAliveCount(); } } } }
静态变量和静态代码块,注意到静态代码块中 dgc = new DGCImpl();实例化了一个 DGC,后续操作和创建注册中心一样
1 2 3 static final Log dgcLog = Log.getLog("sun.rmi.dgc" , "dgc" , LogStream.parseLevel(AccessController.doPrivileged( new GetPropertyAction ("sun.rmi.dgc.logLevel" ))));
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 static { AccessController.doPrivileged(new PrivilegedAction <Void>() { public Void run () { ClassLoader savedCcl = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader( ClassLoader.getSystemClassLoader()); try { dgc = new DGCImpl (); ObjID dgcID = new ObjID (ObjID.DGC_ID); LiveRef ref = new LiveRef (dgcID, 0 ); UnicastServerRef disp = new UnicastServerRef (ref); Remote stub = Util.createProxy(DGCImpl.class, new UnicastRef (ref), true ); disp.setSkeleton(dgc); Permissions perms = new Permissions (); perms.add(new SocketPermission ("*" , "accept,resolve" )); ProtectionDomain[] pd = { new ProtectionDomain (null , perms) }; AccessControlContext acceptAcc = new AccessControlContext (pd); Target target = AccessController.doPrivileged( new PrivilegedAction <Target>() { public Target run () { return new Target (dgc, disp, stub, dgcID, true ); } }, acceptAcc); ObjectTable.putTarget(target); } catch (RemoteException e) { throw new Error ( "exception initializing server-side DGC" , e); } } finally { Thread.currentThread().setContextClassLoader(savedCcl); } return null ; } }); }
还是再来一遍看一下
creatProxy 中会检测是否存在内置类DGCImpl_Stub (像 Registry_Stub 一样),检测到有进入 creatStub ,forName 全类名实例化
1 2 3 4 5 if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass))) { return createStub(remoteClass, clientRef); }
包括创建 skeleton 也是一样的实例化
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 static Skeleton createSkeleton (Remote object) throws SkeletonNotFoundException { Class<?> cl; try { cl = getRemoteClass(object.getClass()); } catch (ClassNotFoundException ex ) { throw new SkeletonNotFoundException ( "object does not implement a remote interface: " + object.getClass().getName()); } String skelname = cl.getName() + "_Skel" ; try { Class<?> skelcl = Class.forName(skelname, false , cl.getClassLoader()); return (Skeleton)skelcl.newInstance(); } catch (ClassNotFoundException ex) { throw new SkeletonNotFoundException ("Skeleton class not found: " + skelname, ex); } catch (InstantiationException ex) { throw new SkeletonNotFoundException ("Can't create skeleton: " + skelname, ex); } catch (IllegalAccessException ex) { throw new SkeletonNotFoundException ("No public constructor: " + skelname, ex); } catch (ClassCastException ex) { throw new SkeletonNotFoundException ( "Skeleton not of correct class: " + skelname, ex); } }
调用 这就是 DGC(看作特殊的远程服务)的创建过程,调用自然像上面小结说的是会走到 UnicastSeverRef.dispatch 里面
走一下进行验证
走到 dispatch 这里,这里 UnicastServerRef 里面有 DGCImpl_Skel,所以后面一定不走 UnicastServerRef 的反射,而是走 DGCImpl_Skel 分发
DGCImpl_Skel.dispatch
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 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; switch (var3) { case 0 : ObjID[] var39; long var40; VMID var41; boolean var42; try { ObjectInput var14 = var2.getInputStream(); var39 = (ObjID[])var14.readObject(); var40 = var14.readLong(); var41 = (VMID)var14.readObject(); var42 = var14.readBoolean(); } catch (IOException var36) { throw new UnmarshalException ("error unmarshalling arguments" , var36); } catch (ClassNotFoundException var37) { throw new UnmarshalException ("error unmarshalling arguments" , var37); } finally { var2.releaseInputStream(); } var6.clean(var39, var40, var41, var42); try { var2.getResultStream(true ); break ; } catch (IOException var35) { throw new MarshalException ("error marshalling return" , var35); } case 1 : ObjID[] var7; long var8; Lease var10; try { ObjectInput var13 = var2.getInputStream(); var7 = (ObjID[])var13.readObject(); var8 = var13.readLong(); var10 = (Lease)var13.readObject(); } catch (IOException var32) { throw new UnmarshalException ("error unmarshalling arguments" , var32); } catch (ClassNotFoundException var33) { throw new UnmarshalException ("error unmarshalling arguments" , var33); } finally { var2.releaseInputStream(); } Lease var11 = var6.dirty(var7, var8, var10); try { ObjectOutput var12 = var2.getResultStream(true ); var12.writeObject(var11); break ; } catch (IOException var31) { throw new MarshalException ("error marshalling return" , var31); } default : throw new UnmarshalException ("invalid method number" ); } } }
攻击点 在 DGCImpl_Stub 里面有两个方法 clean 和 dirty
两个方法里面都有 super.ref.invoke(var6);也就是 call.executeCall();客户端处理网络请求
还有就是 dirty 里面的 readObject
在 DGCImpl_Skel.dispatch 里面有两个 case 对应两个方法的交互,都存在攻击点 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 53 54 55 56 57 58 59 60 61 62 63 64 65 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; switch (var3) { case 0 : ObjID[] var39; long var40; VMID var41; boolean var42; try { ObjectInput var14 = var2.getInputStream(); var39 = (ObjID[])var14.readObject(); var40 = var14.readLong(); var41 = (VMID)var14.readObject(); var42 = var14.readBoolean(); } catch (IOException var36) { throw new UnmarshalException ("error unmarshalling arguments" , var36); } catch (ClassNotFoundException var37) { throw new UnmarshalException ("error unmarshalling arguments" , var37); } finally { var2.releaseInputStream(); } var6.clean(var39, var40, var41, var42); try { var2.getResultStream(true ); break ; } catch (IOException var35) { throw new MarshalException ("error marshalling return" , var35); } case 1 : ObjID[] var7; long var8; Lease var10; try { ObjectInput var13 = var2.getInputStream(); var7 = (ObjID[])var13.readObject(); var8 = var13.readLong(); var10 = (Lease)var13.readObject(); } catch (IOException var32) { throw new UnmarshalException ("error unmarshalling arguments" , var32); } catch (ClassNotFoundException var33) { throw new UnmarshalException ("error unmarshalling arguments" , var33); } finally { var2.releaseInputStream(); } Lease var11 = var6.dirty(var7, var8, var10); try { ObjectOutput var12 = var2.getResultStream(true ); var12.writeObject(var11); break ; } catch (IOException var31) { throw new MarshalException ("error marshalling return" , var31); } default : throw new UnmarshalException ("invalid method number" ); } } }
总结 1.攻击客户端:
客户端查询远程对象时:注册中心攻击客户端 RegistryImpl_Stub.lookup(xxx)
客户端反序列化服务端返回的方法结果:服务端攻击客户端 UnicastRef.invoke()
DGC 服务端攻击客户端 DGCImpl_Stub.dirty()
客户端发起网络请求后反序列化服务端返回的恶意对象:call.executeCall()
2.攻击注册中心:
客户端获取远程对象时:客户端攻击注册中心 RegistryImpl_Skel.dispatch(xxx)
3.攻击服务端:
客户端传递方法参数时:客户端攻击服务端 UnicastServerRef.dispatch(xxx)
DGC 客户端 攻击服务端 DGCImpl_Skel.dispatch()