最近在研究JNDI注入漏洞,就先浅浅的学习以下JNDI相关知识。
JNDI对各种目录服务的实现进行抽象和统一化。
在 Java 应用中除了以常规方式使用名称服务(比如使用 DNS 解析域名),另一个常见的用法是使用目录服务作为对象存储的系统,即用目录服务来存储和获取 Java 对象。
比如对于打印机服务,我们可以通过在目录服务中查找打印机,并获得一个打印机对象,基于这个 Java 对象进行实际的打印操作。
为此,就有了 JNDI,即 Java 的名称与目录服务接口,应用通过该接口与具体的目录服务进行交互。从设计上,JNDI 独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口。JNDI全称是:Java Naming and Directory Interface,它是一类给Java应用程序提供命名(Naming)和目录(Directory)功能的API编程接口。
JNDI 架构上主要包含两个部分,即 Java API和 SPI。
SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:
- RMI: Java Remote Method Invocation,Java 远程方法调用;
- LDAP: 轻量级目录访问协议;
除此之外,用户还可以在 Java 官网下载其他目录服务实现。由于 SPI 的统一接口,厂商也可以提供自己的私有目录服务实现,用户可无需重复修改代码。
命名服务(Naming Service)
它是一个类似于键值对绑定的功能,可以把一个对象作为值跟命名服务上一个名字绑定在一起,然后就可以通过这个名字到命名服务商查询以及使用先前绑定的对象。
目录服务(Directory Service)
名称服务还算比较好理解,那目录服务又是什么呢?简单来说,目录服务是名称服务的一种拓展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(attributes)信息。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象。
举个例子:以打印机服务为例,我们可以在命名服务中根据打印机名称去获取打印机对象(引用),然后进行打印操作;同时打印机拥有速率、分辨率、颜色等属性,作为目录服务,用户可以根据打印机的分辨率去搜索对应的打印机对象。
目录服务(Directory Service)提供了对目录中对象(directory objects)的属性进行增删改查的操作。
LDAP:轻量级目录访问协议,是一种开放的网络协议,用于访问和维护分布式目录服务。LDAP 提供了一种标准化的方式来查询、添加、修改和删除分布式目录中的数据。
RMI:远程方法调用,是 Java 平台提供的一种机制,用于实现分布式应用程序中的远程通信和方法调用。RMI 允许在不同的 Java 虚拟机(JVM)上运行的对象之间进行通信,并调用对方的方法,就像调用本地对象的方法一样。
总而言之,目录服务也是一种特殊的名称服务,关键区别是在目录服务中通常使用搜索(search)操作去定位对象,而不是简单的根据名称查找(lookup)去定位。
JNDI带来优点:由于JNDI提供的接口统一性,当需要访问不同类型的目录服务时,不必再像以前一样分别针对不同的服务协议来实现用于访问的客户端,都可以通过JNDI提供的同一接口访问。
通过JNDI访问Java RMI服务:
package Example1;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;
public class JndiRmiTest {
public static void main(String[] args) throws Exception{
// env是用于创建InitialContext的环境变量属性配置
Hashtable env = new Hashtable();
// Context.INITIAL_CONTEXT_FACTORY即字符串java.naming.factory.initial
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
// Context.PROVIDER_URL即字符串java.naming.provider.url
env.put(Context.PROVIDER_URL,"rmi://localhost:1099");
// 创建 InitialContext
InitialContext context = new InitialContext(env);
// 把foo这个名字和sample string绑定在一起
String name = "foo";
context.bind(name,"sample string");
// 根据绑定的名字查询对应绑定的对象
Object obj = context.lookup(name);
// 在程序中使用查询获得的对象并且打印出来
System.out.println(name + "is bound to" + obj);
}
}
通过JNDI访问LDAP服务:
package Example1;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;
public class JndiLdapTest {
public static void main(String[] args) throws NamingException {
// env是用于创建InitialContext的环境变量属性配置
Hashtable env = new Hashtable();
// Context.INITIAL_CONTEXT_FACTORY即字符串java.naming.factory.initial
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
// Context.PROVIDER_URL即字符串java.naming.provider.url
env.put(Context.PROVIDER_URL,"ldap://localhost:389");
// 创建 InitialContext
InitialContext context = new InitialContext(env);
// 把 cn=foo,dc=test,dc=org 和 String 对象 sample string绑 定在一起
String name = "cn=foo,dc=test,dc=org";
context.bind(name,"sample string");
Object obj = context.lookup(name);
System.out.println(name + "is bind to" + obj);
}
}
通过比较可以看出,使用JNDI不管是访问RMI和LDAP服务,执行的代码几乎都是一样的
JNDI中Reference的概念
为了将Java对象绑定到像RMI或者LDAP这些命名目录服务上,可以通过序列化来将特定状态下的对象转换为字节流进行传输和存储。但并不总是可以绑定对象的序列化状态,因为对象可能太大或不符合要求。
处于这样的考虑,JNDI定义了"命名引用"的概念,可以创建一个Reference,把它和要绑定得对象关联到一起,这样就只需要将对象的Reference绑定到目录服务上,而不用绑定原本的对象。Reference中会存有如何构造出关联对象的信息。命名目录服务的客户端在查询到Reference时,会根据Reference里的信息还原得到原本的绑定对象。
也就是说不在管理对象,而是通过管理Reference来管理对象。