RMI简介

RMI是Java中的一种远程方法调用技术,全称为Remote Method Invocation(远程方法调用)。它使得在不同的Java虚拟机(JVM)之间通过网络进行通信变得容易,使得一个Java应用程序能够调用另一个Java应用程序中的方法,就好像它们都在同一个虚拟机中一样。总的来说,它是一种远程方法调用的具体实现。那么在网络中调用方法,我们传递的信息可能是以序列化的形式传输,可能会存在反序列化漏洞,这也是这篇文章分析RMI的最终目的。

RMI案例讲解

远程接口

首先我们需要定义一个远程接口。

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

// 定义一个远程接口,继承java.rmi.Remote接口

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

Remote接口是一个标记接口,本身不提供任何方法,继承它的接口对象定义的方法都能够被RMI java虚拟机调用。

远程接口实现类

需要我们写一个实现类来重写方法。

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// 远程接口实现类,继承UnicastRemoteObject类和Hello接口
public class HelloImp extends UnicastRemoteObject implements HelloInterface{

private static final long serialVersionUID = 1L;

protected HelloImp() throws RemoteException {
super(); // 调用父类的构造函数
}
@Override
public String Hello(String name) throws RemoteException {
return "welcome to " + name;
}
}

远程对象必须继承UnicastRemoteObject类,用于生成存根(stub)和骨架(Skeleton),这两个就类似于代理。客户端和服务端之间的通信其实是这两个代理之间的通信,下文会详细说。

RMI服务端

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

// 服务端

public class RMIServer {
public static void main(String[] args) throws RemoteException, MalformedURLException {
HelloInterface h = new HelloImp(); // 创建远程对象HelloImp对象实例
LocateRegistry.createRegistry(1099); // 获取RMI服务注册器
Naming.rebind("rmi://localhost:1099/hello",h); // 绑定远程对象HelloImp到RMI服务注册器
System.out.println("RMIServer start successful");
}
}

实例化一个远程对象,获取一个RMI注册机,RMI注册机就类似于一种映射,通过一个字符串来绑定一个对象,1099端口是注册机默认的端口,最后会把我们的远程对象绑定到hello。

RMI客户端

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RemoteObjectInvocationHandler;

// 客户端

public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
HelloInterface hello = (HelloInterface) registry.lookup("hello");
System.out.println(hello.Hello("china"));
}
}

客户端的代码逻辑很简单,获取注册中心,然后在注册中心上查找有没有hello绑定的远程对象,如果有,获取到远程对象并且调用方法。没有就抛出异常了。运行一下看看效果。

成功调用远程对象的方法。跟着运行一遍对RMI服务的流程也算大致了解,但只是停留在表面,所以还需要分析原理,了解反序列化产生的原因。

RMI原理讲解

(ps:在网上找到一位师傅的博客,里面的一张图我觉得很好借用一下,师傅博客文章放在本文最下面)

这张图的思路非常清晰,下面通过调试来了解具体RMI服务的流程。

远程对象的创建

我们在这里下个断点,创建远程对象至始至终都是服务端干的事情,所以这里肯定是不会产生漏洞的,那就简单分析一下,走到UnicastRemoteObject类的构造方法,里面调用了exportObject函数,看名字就类似于发布对象的意思。

跟进函数,有两个参数,其中一个就是远程对象,另一个是UnicastServerRef对象,跟进。

又实例化了LiveRef类,这里的port默认为0,后面会随机给。LiveRef类是比较重要的,跟进一下这个类。

这里的TCPEndpoint是真正处理网络请求的类,在这个getLocalEndpoint方法获取到ip地址。

所以LiveRef类是处理网络请求的,回到最初的exportObject函数这里。我们会发现UnicastServerRef对象就是一路封装过来的处理网络请求的。再跟进到那个函数里面,走到UnicastServerRef类的exportObject函数里。

这里会创建一个stub,这个stub就是客户端处理的代理。事实上客户端的stub就是在服务端创建的,然后由服务端放到注册中心,再由客户端向注册中心里拿,利用这个stub来操作服务端的代理。有兴趣可以跟一下createProxy函数,就是一个创建动态代理的过程。最后会创建一个target,进行一个总的封装。然后继续调用exportObject函数,一路跟进会走到listen函数里

在这里面就会开启一个新的网络请求来等待客户端连接,从而调用远程方法。最后就是一路返回了,远程对象的创建流程就到此结束。

注册中心的创建

在创建注册中心那里下个断点。调试走到RegistryImpl的构造函数。把1099端口传进去,通过一系列的安全检查,我们最终会走到这里。

这里就和远程对象创建那一块很像了。自己可以跟一下,其实就是一样的。进入到setup函数,

实际上也是调用了UnicastServerRef类的exportObject函数。而有区别的地方就是在createProxy函数里面,远程对象那部分是创建一个动态代理,而注册中心这部分并不是,它会走到if判断里面。

跟进函数看一下。

其实就是判断有没有类名_Stub这样的东西,实际上是有的。

最后进入到createStub函数,通过newInstance创建。

它最后会调用setSkeleton函数里面,这个函数就是为服务端创建Skeleton的,具体细节不再跟了。接着也会创建一个总的封装Target。最后会put到哈希表里。

客户端请求注册中心

首先就是客户端获取到注册中心,它这里是把注册中心的ip和端口拿过来本地创建了一个LiveRef。

最后调用createProxy函数本地创建了一个Stub,接着也是对这个Stub进行处理,查找远程对象。

跟进lookup,是反编译出来的代码,静态去看。

接收一个字符串,第一行代码就是创建一个连接,然后把我传进去的字符串序列化写进一个流里,然后会调用一个invoke,这个方法跟进去看就知道有个executeCall方法是处理网络请求的。

后来会接收一个输入流,然后反序列化,这里会存在一个漏洞点,如果有一个恶意的注册中心,返回一个恶意的流,在客户端反序列化就会执行代码。还有一个漏洞点就是invoke那里,在executeCall函数,有一个异常处理存在反序列化,也会导致客户端被攻击

客户端请求服务端

调用Hello方法,因为查找的远程对象是一个动态代理,它会走到RemoteObjectInvocationHandler类的invoke方法,里面还有invoke方法,继续跟进。

也是创建了一个连接。这里有一个marshalValue函数,跟进去看就知道它会序列化调用方法参数的值,继续往下走。

这里有一个unmarshalValue函数,和marshalValue方法是对应的,它会反序列化从服务端接收过来的返回值,这里也能打客户端的。

结语

这篇文章算是一个半成品,因为漏洞利用这一块还没有讲。一个原因是因为理解不到位,这块该说不说还挺复杂的,另一个原因是因为真的不想再看这东西了。打算先学学后面的知识,经过时间的沉淀再回头看应该会有不一样的理解。

参考链接

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