0x00 前言 前一篇关于RMI的文章写的稍微粗糙了些,写这一篇再巩固一下RMI的内容并做一些延伸。
0x01 一些细节 网上关于RMI服务端和客户端编写的文章方法很多,我上一篇是分为接口、服务端、客户端三个部分编写的,当时是将rmi registry和rmi server都写到了服务端类中:
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 package com.Anchor;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.UnicastRemoteObject;public class RMIServer implements Hello { public static void main (String[] args) { try { RMIServer obj = new RMIServer (); Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 8888 ); Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("Hello" , stub); } catch (Exception e) { System.out.println("Server Exception: " + e.toString()); e.printStackTrace(); } } public String sayHello () throws RemoteException { return "Hello, World" ; } }
因为是在本地环境下,为了避免麻烦,就直接写到了一起。但是在低版本的jdk中,注册中心rmi registry与rmi server是可以分离的,甚至可以是运行在不同的主机上。但官方文档中是这样说:
出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。
也就是说不建议将rmi registry和rmi server分离到不同主机来运行,我们后文会提到为什么会有这样的安全考虑。
另外该段代码是使用UnicastRemoteObject的exportObject方法来导出远程对象,并监听8888端口运行一个 rmi server。
1 Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 8888 )
接着是通过registry.bind("Hello", stub)将”Hello”与stub的映射关系绑定到rmi registry上,这种绑定方式不需要书写完整的RMI URL,只需写对象名称”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 29 30 package com.Anchor;import java.rmi.Naming;import java.rmi.Remote;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.server.UnicastRemoteObject;public class RMIServer2 { public class RemoteHello extends UnicastRemoteObject implements Hello { protected RemoteHello () throws RemoteException{ super (); } public String sayHello () throws RemoteException{ return "Hello, World" ; } } private void start () throws Exception{ RemoteHello rh = new RemoteHello (); LocateRegistry.createRegistry(1099 ); Naming.bind("rmi://127.0.0.1:1099/Hello" , rh); } public static void main (String[] args) throws Exception{ RMIServer2 rs2 = new RMIServer2 (); rs2.start(); } }
这里使用了java.rmi.Naming类,该类又叫做命名服务,提供了一种方便的方式来绑定和查找远程对象。可以看一下它的bind方法的源码,发现它是通过解析URI绑定远程对象,将URI拆分成主机、端口和远程对象名称,最后使用的仍是Registry类的bind来进行操作的:
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void bind (String name, Remote obj) throws AlreadyBoundException, java.net.MalformedURLException, RemoteException { ParsedNamingURL parsed = parseURL(name); Registry registry = getRegistry(parsed); if (obj == null ) throw new NullPointerException ("cannot bind to null" ); registry.bind(parsed.name, obj); }
我们在rmi client中也可以使用Naming类的lookup方法来查找远程对象(可以看到client端简洁了许多):
1 2 3 4 5 6 7 8 9 10 11 12 package com.Anchor;import java.rmi.Naming;public class RMIClient { public static void main (String[] args) throws Exception { Hello hello = (Hello) Naming.lookup("rmi://192.168.0.107:1099/Hello" ); System.out.println(hello.sayHello()); } }
上面说到,相比于直接使用Registry类, Naming类在绑定和查找操作时需要的是一个URL,需要说明的时,很多文章都指出该URL的形式一定要下面这样:
但其实并不是。首先,其中的rmi:是可以省略的,也就是可以写成这样://host:port/objName(但不要少了前面的双斜杠,否则会报错);其次,如果服务端或者客户端和RMI registry在同一主机上运行,那么host和port也可以省略,也就是直接远程对象名objName。
另外,对于绑定方法,在Naming类和Registry类,除了bind还要rebind,两者区别如下:
rebind是指“重绑定”,如果“重绑定”时rmi registry已经有了这个服务name的存在,则之前所绑定的Remote Object将会被替换;而bind在执行时如果“绑定”时rmi registry已经有这个服务name的存在,则系统会抛出错误。
所以除非有特别的业务要求,一般建议使用rebind方法进行Remote Object绑定。
0x02 RMI利用 讲了这么多,只是涉及了rmi具体通信原理以及如何实现,下面来从利用的层面讲讲。
关于利用的方法,可以分别从攻击rmi registry、攻击rmi server、攻击rmi client角度来实现,由于篇幅和本人水平有限(对java反序列化的知识掌握不深),本文就略带介绍一下利用方式,具体实现在以后的文章中进行。
攻击rmi registry 也许你也注意到上文中提到的,rmi server可以通过bind、rebind等方法申请在rmi registry上进行绑定注册,那么想想是否可以伪造成一个远程的rmi server注册一个恶意远程服务上去呢?
前文也说过,在低版本的jdk本中(8u121),rmi registry和rmi server可以不在一台服务器上,此时是可以利用这种方法攻击注册中心的。但是通常情况下,我们遇到的场景都是高版本的jdk,此时这种攻击注册中心的方式就不太适用。
由于我没能下载到低版本的jdk,部分源码片段我就截取了网上的文章进行说明。
我们先来看前面贴上的java.rmi.Naming#bind方法的源码,其中它会将URL拆解后的结果作为参数放到getRegistry方法中从而得到一个registry:
1 Registry registry = getRegistry(parsed);
那么查看这个getRegistry方法源码:
1 2 3 4 5 private static Registry getRegistry (ParsedNamingURL parsed) throws RemoteException { return LocateRegistry.getRegistry(parsed.host, parsed.port); }
可以看到这里归根到底调用的还是LocateRegistry类的getRegistry方法,继续跟踪进去:
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 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 ); }
这里getRegistry方法有好几种重载方式,前面所说的传入不同形式的URL,正好对应了这几种重载方式,我这里粘贴的是最终调用的这个核心的getRegistry方法。前面的if判断是处理port和host的,然后分别实例化了liveRef和RemoteRef对象,这一段过程应该是用过来处理网络通信相关的,不用管,直接定位最后的返回语句,调用了createProxy方法。关于这个方法,要涉及到动态代理的内容(给自己挖个坑,以后研究),它会根据传入的class对象,返回一个代理,也就是说,这里传入RegistryImpl.class后,得到的其实是一个RegistryImpl_Stub对象,即远程的Registry接口在本地的代理。
我们再回到Naming类的bind方法中,接下来就是执行registry.bind(parsed.name, obj);语句,这里调用了registry的bind方法,其实是调用的RegistryImpl_Stub类中的bind方法,我们定位到该类的bind方法:
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 void bind (String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException { try { StreamRemoteCall var3 = (StreamRemoteCall)this .ref.newCall(this , operations, 0 , 4905912898345647071L ); try { ObjectOutput var4 = var3.getOutputStream(); var4.writeObject(var1); var4.writeObject(var2); } catch (IOException var5) { throw new MarshalException ("error marshalling arguments" , var5); } this .ref.invoke(var3); this .ref.done(var3); } catch (RuntimeException var6) { throw var6; } catch (RemoteException var7) { throw var7; } catch (AlreadyBoundException var8) { throw var8; } catch (Exception var9) { throw new UnexpectedException ("undeclared checked exception" , var9); } }
前面处理一些约定好的数据,比如host、port以及要绑定的对象和对象名称,我们直接定位到invoke那一句,invoke这里会把请求发出去,这里会调用Skeleton代理即sun.rmi.registry.RegistryImpl_Skel类中的dispatch方法来处理请求,需要注意的是,在这里之后是无法通过断点进入到sun.rmi.registry.RegistryImpl_Skel#dispatch方法中的,看一篇文章说是在bind函数执行处sun.rmi.registry.RegistryImpl#bind下一个断点,直接运行就可以得到调用栈,再回去找就行了。
我们定位查看重要逻辑代码:
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 switch (var3) { case 0 : try { var11 = var2.getInputStream(); var7 = (String)var11.readObject(); var8 = (Remote)var11.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(var7, var8); try { var2.getResultStream(true ); break ; } catch (IOException var93) { throw new MarshalException ("error marshalling return" , var93); } case 1 : var2.releaseInputStream(); String[] var97 = var6.list(); try { ObjectOutput var98 = var2.getResultStream(true ); var98.writeObject(var97); break ; } catch (IOException var92) { throw new MarshalException ("error marshalling return" , var92); } 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); } case 3 : try { var11 = var2.getInputStream(); var7 = (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(var7, var8); try { var2.getResultStream(true ); break ; } catch (IOException var84) { throw new MarshalException ("error marshalling return" , var84); } case 4 : try { 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" ); }
这里var3类型是int型,取值从0到4,依次对应bind/rebind/unbind/lookup/list等请求。从这个switch语句我们可以得知,Registry注册中心能够接收bind/rebind/unbind/lookup/list等请求,而在接收五类请求方法的时候,只有我们bind,rebind,unbind和lookup方法进行了反序列化数据调用readObject函数(再给自己挖个坑,后面补个java反序列化的内容),可能导致直接触发了反序列化漏洞产生。
需要说明的是lookup是从rmi client角度攻击rmi registry, 而bind/rebind/unbind是从rmi server角度攻击rmi registry
由于无法下载到对应的jdk版本,这里我无法演示,可以具体看看这个文章 是如何利用该处反序列化进行实际操作的。
高版本的jdk中,进行了如下修复:
在8u121之后,在bind方法里面增加了一个checkAccess方法,该方法会检查是否为localhost,不是则会抛出下面这个异常:
1 2 3 catch (PrivilegedActionException var4) { throw new AccessException (var0 + " disallowed; origin " + var2 + " is non-local host" ); }
不过我们看源码就知道反序列化语句readObject其实在bind调用之前就执行了,并没有什么用。
然后在8u141修改为在RegistryImpl_Skel中执行readObject之前就执行了checkAccess方法,这样bind,rebind,unbind就没失效了。
以我现在手上的java为例(java版本为1.8.0_391),sun.rmi.registry.RegistryImpl_Skel类的dispatch方法中的switch语句对应处理bind请求的片段如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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); }
可以看到第一句就调用了checkAccess进行了限制。
0x03 小结 写这篇时我结合了网上的文章再看的源码,但是还是感觉其中的过程有些绕,对于rmi利用这块,有很多有待去学习。
参考文章:
《java安全漫谈》
RMI机制