0x00 前言 最近在学习RMI 的反序列化漏洞,简单的记录下RMI 的实现流程和可能存在的利用点
0x01 介绍 RMI ,是Remote Method Invocation(远程方法调用) 的缩写,即在一个JVM 中java 程序调用在另一个远程JVM 中运行的java 程序,这个远程JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。
RMI 依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为Java 编写。这个协议就像HTTP 协议一样,规定了客户端和服务端通信要满足的规范。
RMI 有三个对象
1 2 3 Registry : 提供服务注册和服务获取,服务端将类名称,存放地址注册到Registry中,以供客户端获取。 Server : 远程方法的提供者。 Client : 远程方法的调用者
0x02 简单使用 三层架构 从RMI设计角度来讲,基本分为三层架构模式来实现RMI ,分别为RMI 服务端,RMI 客户端和RMI 注册中心
Client-客户端 :客户端调用服务端的方法
Server-服务端 :远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果
Registry-注册中心 :其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用(在低版本的JDK 中,Server 与Registry 是可以不在一台服务器上的,而在高版本的JDK 中,Server 与Registry 只能在一台服务器上,否则无法注册成功)
RMI服务端 远程对象 远程调用方法的对象必须继承java.rmi.Remote 接口
远程对象的实现类必须继承UnicastRemoteObject 类,如果没有继承UnicastRemoteObject ,则需手动创建
该接口是客户端和服务端共用的接口,内部定义了我们将要远程调用的对象方法sayHello()
1 2 3 4 public interface IRemoteObj extends Remote { public String sayHello (String keywords) throws RemoteException; }
RemoteObjImpl 是一个服务端远程对象,提供了一个sayHello 方法供远程调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj { protected RemoteObjImpl () throws RemoteException { super (); } @Override public String sayHello (String keywords) throws RemoteException { String upKeywords = keywords.toUpperCase(); System.out.println(upKeywords); return upKeywords; } }
主类RMIServer 主要用来创建registry
1 2 3 4 5 6 7 8 9 10 11 12 public class RMIServer { public void register () throws Exception { RemoteObjImpl remoteObj = new RemoteObjImpl (); Registry registry = LocateRegistry.createRegistry(1099 ); Naming.bind("rmi://127.0.0.1:1099/remoteObj" ,remoteObj); } public static void main (String[] args) throws Exception { new RMIServer ().register(); } }
Registry端 先将被远程调用的实现类RemoteObjImpl 实例化,并且在本地某个端口创建一个registry
再使用Naming.bind 将实例化的对象和地址对象绑定在一起
1 2 3 4 5 6 public void register () throws Exception { RemoteObjImpl remoteObj = new RemoteObjImpl (); Registry registry = LocateRegistry.createRegistry(1099 ); Naming.bind("rmi://127.0.0.1:1099/remoteObj" ,remoteObj); }
RMI客户端
该接口与服务端接口要一致,不然无法调用对应的方法
1 2 3 public interface IRemoteObj extends Remote { public String sayHello (String keywords) throws RemoteException; }
1 2 3 4 5 6 7 8 public class RMIClient { public static void main (String[] args) throws RemoteException, NotBoundException, NamingException { Registry registry = LocateRegistry.getRegistry("127.0.0.1" , 1099 ); IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj" ); String hello = remoteObj.sayHello("hello" ); System.out.println(hello); } }
0x03 流程实现 创建远程对象 在初始化的时候,会先创建一个UnicastRemoteObject 对象,在调用其exportObject 来将远程对象导出
此时的port 赋值为0 ,代表会把一个远程对象发布到一个随机端口上
它跟注册中心的1099 是不一样的
1 2 3 4 5 protected UnicastRemoteObject (int port) throws RemoteException{ this .port = port; exportObject((Remote) this , port); }
再来看一下exportObject 方法
可以看到有两个参数,第一个参数是obj ,主要是来实现逻辑的
第二个参数是**new UnicastServerRef(port)**,主要是来实现网络请求的逻辑实现
1 2 3 4 5 public static Remote exportObject (Remote obj, int port) throws RemoteException { return exportObject(obj, new UnicastServerRef (port)); }
那就跟进UnicastServerRef 中,可以看到实现了个LiveRef 对象
1 2 3 public UnicastServerRef (int port) { super (new LiveRef (port)); }
那我们在看看LiveRef 又做了什么
1 2 3 public LiveRef (int port) { this ((new ObjID ()), port); }
很明显第一个参数是个ID ,所以直接去看一下它的构造函数
1 2 3 public LiveRef (ObjID objID, int port) { this (objID, TCPEndpoint.getLocalEndpoint(port), true ); }
主要来看看这个getLocalEndpoint 是干嘛的
可以看到这边返回值是一个TCPEndpoint 类型的对象
1 2 3 public static TCPEndpoint getLocalEndpoint (int port) { return getLocalEndpoint(port, null , null ); }
TCPEndpoint 是一个网络请求的类,我们可以去看一下它的构造函数,传参进去一个IP 与一个端口 ,也就是说传进去一个IP 和一个端口 ,就可以进行网络请求。
1 2 3 public TCPEndpoint (String host, int port) { this (host, port, null , null ); }
接着继续去看一下LiveRef 的构造函数做了什么
发现host 和port 是赋值到了endpoint 里面,而endpoint 又是被封装在LiveRef 里面的
1 2 3 4 5 public LiveRef (ObjID objID, Endpoint endpoint, boolean isLocal) { ep = endpoint; id = objID; this .isLocal = isLocal; }
一路返回,在UnicastServerRef 的父类UnicastRef 中
又将LiveRef 赋值给ref ,也就是说我们只创建了一个LiveRef ,对应远程服务的端口
1 2 3 public UnicastRef (LiveRef liveRef) { ref = liveRef; }
接着我们回到最初,去看看exportObject 做了什么
1 2 3 4 5 6 7 8 9 private static Remote exportObject (Remote obj, UnicastServerRef sref) throws RemoteException { if (obj instanceof UnicastRemoteObject) { ((UnicastRemoteObject) obj).ref = sref; } return sref.exportObject(obj, null , false ); }
这里的sref 其实就是包含了刚创建的LiveRef 的UnicastServerRef
也就是说,现在调用的是UnicastServerRef 的exportObject 方法,来看看做了什么
可以看到在这边创建了stub ,原因是
RMI 会先在服务端创建一个Stub ,再把Stub 传到RMI Registry 中,最后让RMI Client 去获取Stub 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 2 3 4 5 6 7 8 9 10 11 12 13 public static Remote createProxy (Class<?> implClass, RemoteRef clientRef, boolean forceStubUse) throws StubNotFoundException { if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass))) { return createStub(remoteClass, clientRef); } }
看看那个判断中的stubClassExists 做了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 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 ; }
可以看出主要是用于通过加载远程类名称加上**”_Stub”后缀的类,来检查给定的远程类是否存在对应的 stub**类
在RMI 中,常用于在执行远程调用之前,检查远程接口和stub 类的正确性
目前我们是没有这个类的,所以也不会去执行判断里的createStub 方法
判断后就是创建动态代理的流程了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static Remote createProxy (Class<?> implClass, RemoteRef clientRef, boolean forceStubUse) throws StubNotFoundException { 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); } }
第一个参数是AppClassLoader ,第二个参数是一个远程接口 ,第三个参数是调用处理器 ,调用处理器里面只有一个 ref,还是原本那个封装的LiveRef
这边也就把动态代理创建好了
返回exportObject 中,往下走会出现一个判断,不过会跳过这个判断
因为在上面创建Stub 的时候没有走进createStub(),返回的对象没有继承 RemoteStub
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public Remote exportObject (Remote impl, Object data, boolean permanent) throws RemoteException { 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; }
接着会走到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 public Target (Remote impl, Dispatcher disp, Remote stub, ObjID id, boolean permanent) { this .weakImpl = new WeakRef (impl, ObjectTable.reapQueue); this .disp = disp; this .stub = stub; this .id = id; this .acc = AccessController.getContext(); ClassLoader threadContextLoader = Thread.currentThread().getContextClassLoader(); ClassLoader serverLoader = impl.getClass().getClassLoader(); if (checkLoaderAncestry(threadContextLoader, serverLoader)) { this .ccl = threadContextLoader; } else { this .ccl = serverLoader; } this .permanent = permanent; if (permanent) { pinImpl(); } }
这里的ref 还是LiveRef ,接着调用ref 的exportObject 方法将target 发布出去
1 2 3 4 5 6 7 public void exportObject(Target target) throws RemoteException { ep.exportObject(target); } public void exportObject(Target target) throws RemoteException { transport.exportObject(target); }
一路跟到这里sun.rmi.transport.tcp 下的TCPTransport 类的exportObject 方法
首先走到了listen 里面,开始了网络监听,也就是真正的网络请求部分
1 2 3 4 5 6 7 8 9 10 11 12 public void exportObject (Target target) throws RemoteException { synchronized (this ) { listen(); exportCount++; } }
看一下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 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(); } } }
先获取TCPEndpoint ,然后会开启一个新的ServerSocket ,并且创建一个新的线程Thread
在newServerSocket 中,如果port==0 还会给port 随机赋值
1 2 3 4 5 6 ServerSocket newServerSocket () throws IOException { if (listenPort == 0 ) setDefaultPort(server.getLocalPort(), csf, ssf); return server; }
这部分都是一些网络方面的东西,不太懂就先跳过了。。。
然后到这里的化,远程对象已经在服务端上的随机端口发布出去了
后面这部分就是记录一下远程对象发布到哪里去了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void exportObject (Target target) throws RemoteException { boolean ok = false ; try { super .exportObject(target); ok = true ; } finally { if (!ok) { synchronized (this ) { decrementExportCount(); } } } }
看一下这个**super.exportObject(target)**做了什么
主要是**ObjectTable.putTarget(target)**这部分
1 2 3 4 public void exportObject (Target target) throws RemoteException { target.setExportedTransport(this ); ObjectTable.putTarget(target); }
来看一下putTarget ,主要是在put 部分
1 2 3 4 5 6 7 8 9 10 11 12 13 static void putTarget (Target target) throws ExportException { synchronized (tableLock) { if (target.getImpl() != null ) { objTable.put(oe, target); implTable.put(weakImpl, target); if (!target.isPermanent()) { incrementKeepAliveCount(); } } } }
也就是说,将信息存储在ObjectTable 类的两个静态Map中
相当于给服务端做了个备份吧
1 2 3 4 private static final Map<ObjectEndpoint,Target> objTable = new HashMap <>(); private static final Map<WeakRef,Target> implTable = new HashMap <>();
创建注册中心 首先会进入到一个静态方法createRegistry 中,传一个端口,默认是1099
1 2 3 public static Registry createRegistry (int port) throws RemoteException { return new RegistryImpl (port); }
创建的这个RegistryImpl 对象,首先会做个安全检查
后面创建了个LiveRef ,以及创建了一个新的UnicastServerRef ,这里跟前面的创建的远程对象 是很像的
也就说这里又创建一个服务端引用 ,作为参数交给了setup 方法,跟服务端创建远程对象类似
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)); } }
我们可以先看一下UnicastServerRef 做了什么,主要是将liveRef 赋值给ref
1 2 3 4 5 6 7 8 9 10 11 public UnicastServerRef (LiveRef ref) { super (ref); } public UnicastRef (LiveRef liveRef) { ref = liveRef; } public UnicastRef (LiveRef liveRef) { ref = liveRef; }
现在我们去看一下setup 方法
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 方法
第一个参数代表远程对象,创建远程对象就是自己实现的Impl ,创建注册中心就是RegistryImpl
第三个参数代表时效选项,上次是false ,这次变成了true
接着来看一下exportObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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; }
首先也是创建stub ,直接看一下createProxy 方法
对比创建远程对象,创建注册中心到这里是直接走进去判断里面的createStub 方法
原因是因为,在stubClassExists 检测的时候
会发现现在是系统自带的类(在rt.jar->sun->rmi 可以找到RegistryImpl_Stub ),符合条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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); } }
继续往下,调用exportObject 的**setSkeleton()**方法,
这时的RegistryImpl_Stub 对象确实是RemoteStub 的子类,所以满足条件,进入setSkeleton 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ); } } }
然后这里有一个createSkeleton()方法,一看名字就知道是用来创建 Skeleton 的
可以看到Skeleton 是用**forName()**的方式创建的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 }); } }
这里的skel 是UnicastServerRef 的内部属性
也就是说创建好的skeleton 其实会存储在UnicastServerRef 的skel 属性中
接着就是走到Target 里了,也就是储存封装的数据,跟之前的一样
可以看到在objTable 表中,有三个对象
第一个对象中的value 的stub 值为DGCImpl_Stub ,是分布式垃圾回收的一个对象,它并不是我们刚才创建的,且disp 的skel 为DGCImpl_Skel
第二个对象里的value 的stub 值为**$Proxy对象,是远程对象的,且 disp的 skel**为空
第三个对象中的value 的stub 值为RegistryImpl_Stub ,是我们刚创建的对象,且disp 的skel 为RegistryImpl_Skel
绑定
bindings 就是一个Hashtable
如果当前的keySet 中找不到已经绑定的远程对象名,那么就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); } }
客户端请求注册中心 会先跳转到这
1 2 3 4 5 public static Registry getRegistry (String host, int port) throws RemoteException { return getRegistry(host, port, null ); }
跟进去getRegistry 方法中
可以看到先接收对应的参数,然后在本地创建了个LiveRef 对象,UnicastRef 封装,客户端引用
ObjID.REGISTRY_ID 对接的id==0 ,0 代表注册中心
TCPEndpoint 中的是注册中心的IP 和port=1099
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 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 ); }
后面又调用了createProxy 方法
在实现上跟注册中心相差不大,都是使用反射创建代理,也就是RegistryImpl_Stub 类,这里需要UnicastRef 参与构造函数
也就是说,注册中心创建的RegistryImpl_Stub 其实并没有传递给客户端
而是客户端利用了注册中心的ip 和port 在本地自己创建了个RegistryImpl_Stub
客户端lookup远程对象 首先跟进去RegistryImpl_Stub 的lookup 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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(); } return var23; } }
可以看到首先会调用一个newCall 方法
主要功能就是创建一个连接
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 RemoteCall newCall (RemoteObject obj, Operation[] ops, int opnum, long hash) throws RemoteException { clientRefLog.log(Log.BRIEF, "get connection" ); Connection conn = ref.getChannel().newConnection(); try { clientRefLog.log(Log.VERBOSE, "create call context" ); if (clientCallLog.isLoggable(Log.VERBOSE)) { logClientCall(obj, ops[opnum]); } RemoteCall call = new StreamRemoteCall (conn, ref.getObjID(), opnum, hash); try { marshalCustomCallData(call.getOutputStream()); } catch (IOException e) { throw new MarshalException ("error marshaling " + "custom call data" ); } return call; } catch (RemoteException e) { ref.getChannel().free(conn, false ); throw e; } }
接着返回lookup ,先获取字符串,将其写进一个输出流里面,序列化进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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); } }
当前类对象为RegistryImpl_Stub ,ref 属性为UnicastRef
所以之后调用 UnicastRef 的invoke 方法
1 2 3 4 5 6 7 public void invoke (RemoteCall call) throws Exception { try { clientRefLog.log(Log.VERBOSE, "execute call" ); call.executeCall(); } }
接着会去调用call 的**executeCall()**方法,是一个真正处理网络请求的方法
在字节流的层面负责传输:
将客户端想寻找远程对象名字接收,传给注册中心
接收注册中心传递回来的对象的字节流
如果注册中心返回的远程对象的字节流出现异常,readObject 会执行,而且in 就是数据流里面的东西
如果一个注册中心返回一个恶意的对象,客户端进行反序列化,这就会导致漏洞
这个地方更隐蔽,危险也更广
因为invoke–>executeCall 不只是lookup 存在,还有bind 、list 方法也是会调用invoke 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void executeCall () throws Exception { byte returnType; DGCAckHandler ackHandler = null ; switch (returnType) { case TransportConstants.NormalReturn: break ; case TransportConstants.ExceptionalReturn: Object ex; try { ex = in.readObject(); } }
返回lookup 继续,如果invoke 完成请求之后
接着又获取一个输入流var6 ,也就是说返回值获取到了
也会通过readObject 方式执行,var23 也就是远程对象的动态代理
远程对象会以动态代理的形式返回,里面包含了Liveref ,需要连接的ip:port 等等信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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(); } return var23; } }
所以说,客户端向注册中心获取远程对象的过程是通过反序列化 实现的,最后的远程对象是通过readObject 读出来的
如果有个恶意的注册中心,就可以通过这个来攻击客户端
客户端请求服务端 动态代理的核心就是handler 的invoke 方法
当调用远程对象的方法时,会走RemoteObjectInvocationHandler 的invoke 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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)); } }
首先会去调用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 public Object invoke (Remote obj, Method method, Object[] params, long opnum) throws Exception { try { 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(); } }
然后会去调用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 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); } }
序列化之后会调用call.executeCall 方法,这个危险点在前面有提到
所有的客户端的网络请求都会调用这个方法
接着继续往下走
如果返回值不为null ,调用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 26 public Object invoke (Remote obj, Method method, Object[] params, long opnum) throws Exception { try { call.executeCall(); try { Class<?> rtype = method.getReturnType(); if (rtype == void .class) return null ; ObjectInput in = call.getInputStream(); Object returnValue = unmarshalValue(rtype, in); alreadyFreed = true ; ref.getChannel().free(conn, true ); return returnValue; } } }
看一下unmarshalValue 这个方法逻辑
也就是说如果想要获取远程函数调用的结果,是通过反序列化获取的
这里的unmarshalValue 与前面的marshalValue 是对称的
我们传进去的是String 类型的,不符合判断里的条件,所以会读出返回值
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(); } }
注册中心响应客户端 在Registry 端,由sun.rmi.transport.tcp.TCPTransport#handleMessages 来处理请求,调用serviceCall 方法处理
serviceCall 方法中从ObjectTable 中获取封装的Target 对象,并获取其中的封装的UnicastServerRef 以及RegistryImpl 对象。然后调用UnicastServerRef 的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 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" ); } final Dispatcher disp = target.getDispatcher(); target.incrementCallCount(); try { try { setContextClassLoader(ccl); currentTransport.set(this ); try { java.security.AccessController.doPrivileged( new java .security.PrivilegedExceptionAction<Void>() { public Void run () throws IOException { checkAcceptPermission(acc); disp.dispatch(impl, call); return null ; } }, acc); } } }
看一下UnicastServerRef 的dispatch 方法
这里先判断了this.skel 是否为空,用来区别自己是Registry 还是Server
然后调用oldDispatch 方法,
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 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" ); } } op = in.readLong(); } catch (Exception readEx) { throw new UnmarshalException ("error unmarshalling call header" , readEx); } } }
跟进oldDispatch 方法,在最后调用了this.skel 也就是RegistryImpl_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 public void oldDispatch (Remote obj, RemoteCall call, int op) throws IOException { long hash; try { ObjectInput in; try { in = call.getInputStream(); try { Class<?> clazz = Class.forName("sun.rmi.transport.DGCImpl_Skel" ); if (clazz.isAssignableFrom(skel.getClass())) { ((MarshalInputStream)in).useCodebaseOnly(); } } catch (ClassNotFoundException ignore) { } hash = in.readLong(); } catch (Exception readEx) { throw new UnmarshalException ("error unmarshalling call header" , readEx); } logCall(obj, skel.getOperations()[op]); unmarshalCustomCallData(in); skel.dispatch(obj, call, op, hash); } }
跟进RegistryImpl_Skel 类的dispatch 方法,这里才是重点
主要作用是根据流中写入的不同的操作类型分发给不同的方法处理
与注册中心进行交互的几种方式在dispatch 中的对应关系
1 2 3 4 5 6 7 switch (opnum){ case 0 : case 1 : case 2 : case 3 : case 4 : }
我们当前主要是2–>lookup
在服务端,我们将remoteObj 名称序列化传到注册中心
在注册中心,会将其反序列读出来
var10 其实就是我们lookup 寻找远程对象的方法名
不只是在lookup 中,只要存在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 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; String var7; Remote var8; ObjectInput var10; ObjectInput var11; switch (var3) { case 2 : try { var10 = var2.getInputStream(); var7 = (String)var10.readObject(); } catch (IOException var89) { throw new UnmarshalException ("error unmarshalling arguments" , var89); } catch (ClassNotFoundException var90) { throw new UnmarshalException ("error unmarshalling arguments" , var90); } finally { var2.releaseInputStream(); } var8 = var6.lookup(var7); try { ObjectOutput var9 = var2.getResultStream(true ); var9.writeObject(var8); break ; } catch (IOException var88) { throw new MarshalException ("error marshalling return" , var88); } } } }
服务端响应客户端 当前的Target 是动态代理Proxy ,所以skel==null
就不会调用oldDispatch 方法了
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 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" ); } } op = in.readLong(); } }
继续往下走,获取到输入流,以及Method ,Method 就是我们之前写的**sayHello()**方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public void dispatch (Remote obj, RemoteCall call) throws IOException { MarshalInputStream marshalStream = (MarshalInputStream) in; marshalStream.skipDefaultResolveClass(); Method method = hashToMethod_Map.get(op); if (method == null ) { throw new UnmarshalException ("unrecognized method hash: " + "method not supported by remote object" ); } logCall(obj, method); }
继续往下走,循环当中的**unmarshalValue()**方法
这个之前有提到,会将客户端传过来的序列化参数反序列化出来,是存在漏洞的
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 { Class<?>[] types = method.getParameterTypes(); Object[] params = new Object [types.length]; try { unmarshalCustomCallData(in); for (int i = 0 ; i < types.length; i++) { params[i] = unmarshalValue(types[i], in); } } catch (java.io.IOException e) { throw new UnmarshalException ( "error unmarshalling arguments" , e); } catch (ClassNotFoundException e) { throw new UnmarshalException ( "error unmarshalling arguments" , e); } finally { call.releaseInputStream(); } }
继续往下走,最后利用marshalValue 方法,将结果序列化,写到字节流,返回给客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void dispatch (Remote obj, RemoteCall call) throws IOException { Object result; 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); } } }
0x03 总结 引用一下素十八 师傅的总结
RMI 底层通讯采用了 Stub(运行在客户端) 和 Skeleton(运行在服务端) 机制,**RMI **调用远程方法的大致如下:
**RMI 客户端在调用远程方法时会先创建 Stub (sun.rmi.registry.RegistryImpl_Stub)**。
**Stub **会将 Remote 对象传递给远程引用层 **( java.rmi.server.RemoteRef ) **并创建 **java.rmi.server.RemoteCall( 远程调用 )**对象。
RemoteCall 序列化 RMI 服务名称、 Remote 对象。
RM I 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
RMI 服务端的远程引用层**( sun.rmi.server.UnicastServerRef )**收到请求会请求传递给 **Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )**。
**Skeleton **调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
Skeleton 处理客户端请求:bind 、list 、lookup 、rebind 、unbind ,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
RMI 客户端反序列化服务端结果,获取远程对象的引用。
RMI 客户端调用远程方法,RMI 服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
**RMI **客户端反序列化 RMI 远程方法调用结果
0x04 参考资料 https://su18.org/post/rmi-attack/
https://www.bilibili.com/video/BV1L3411a7ax?p=1&vd_source=19d2e433219440bcf5304fbe8a00b7ff
https://wx.zsxq.com/dweb2/index/group/2212251881