JNDI注入-基础知识

前言

弄清JNDI RMI LDAP等基础概念以及手动操作实现

JNDI

JNDI 全称为 Java Naming and Directory Interface(Java 命名与目录接口) 是SUN公司提供的一种标准的 Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。

命名的意思 就是,在一个目录系统,它实现了把一个服务名称和对象或命名引用相关联,在客户端,我们可以调用目录系统服务,并根据服务名称查询到相关联的对象或命名引用,然后返回给客户端。而 目录的意思 就是在命名的基础上,增加了属性的概念,我们可以想象一个文件目录中,每个文件和目录都会存在着一些属性,比如创建时间、读写执行权限等等,并且我们可以通过这些相关属性筛选出相应的文件和目录

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

JNDI可访问的现有的目录及服务主要有:

  • RMI: Java Remote Method Invocation,Java 远程方法调用;特有的远程调用框架

  • LDAP: 轻量级目录访问协议;通用的服务与标准

  • CORBA: Common Object Request Broker Architecture,通用对象请求代理架构; 通用的服务与标准

JNDI结构

在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:

javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;

javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDirContext类;

javax.naming.event:在命名目录服务器中请求事件通知;

javax.naming.ldap:提供LDAP支持;

javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

javax.naming 中包含了访问目录服务所需的 类和接口,比如 Context、Bindings、References、lookup 等。

这里解释一下

InitialContext类

构造方法

InitialContext() 
构建一个初始上下文。
InitialContext(boolean lazy)
构造一个初始上下文,并选择不初始化它。
InitialContext(Hashtable<?,?> environment)
使用提供的环境构建初始上下文。

//java
InitialContext initialContext = new InitialContext();

构建初始上下文,其实通俗点来讲就是获取初始目录环境

常用方法

bind(Name name, Object obj) 
将名称绑定到对象。
list(String name)
枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
lookup(String name)
检索命名对象。
rebind(String name, Object obj)
将名称绑定到对象,覆盖任何现有绑定。
unbind(String name)
取消绑定命名对象。

example:

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}

References

该类表示对在命名/目录系统 外部找到的对象的引用

构造方法

Reference(String className) 
为类名为“className”的对象构造一个新的引用。
Reference(String className, RefAddr addr)
为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, String factory, String factoryLocation)
为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。

//java
String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);

参数1:className - 远程加载时所使用的类名

参数2:classFactory - 加载的class中需要实例化类的名称

参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议

常用方法

void add(int posn, RefAddr addr) 
将地址添加到索引posn的地址列表中。
void add(RefAddr addr)
将地址添加到地址列表的末尾。
void clear()
从此引用中删除所有地址。
RefAddr get(int posn)
检索索引posn上的地址。
RefAddr get(String addrType)
检索地址类型为“addrType”的第一个地址。
Enumeration<RefAddr> getAll()
检索本参考文献中地址的列举。
String getClassName()
检索引用引用的对象的类名。
String getFactoryClassLocation()
检索此引用引用的对象的工厂位置。
String getFactoryClassName()
检索此引用引用对象的工厂的类名。
Object remove(int posn)
从地址列表中删除索引posn上的地址。
int size()
检索此引用中的地址数。
String toString()
生成此引用的字符串表示形式。

example:

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class jndi {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
String url = "http://127.0.0.1:8080";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("aa",referenceWrapper);
}
}

RMI

RMI,即 Remote Method Invocation,Java 的远程方法调用。RMI 为应用提供了远程调用的接口,可以理解为 Java 自带的 RPC 框架

一个简单的 RMI hello world 主要由三部分组成,分别是接口、服务端和客户端

远程方法调用(RMI)原理与示例 - Pickle - 博客园 (cnblogs.com)

过程

当客户端调用远程对象方法时, 存根 负责把要调用的远程对象方法的方法名及其参数编组打包,并将该包向下经远程引用层、传输层 转发给远程对象所在的服务器。通过 RMI 系统的 RMI 注册表实现的简单服务器名字服务, 可定位远程对象所在的服务器。该包到达服务器后, 向上经远程引用层, 被远程对象的 Skeleton 接收, 此 Skeleton 解析客户包中的方法名及编组的参数后, 在服务器端执行客户要调用的远程对象方法, 然后将 该方法的返回值( 或产生的异常) 打包后通过相反路线返回给客户端, 客户端的 Stub 将返回结果解析后传递给客户程序。

RMI远方法程调用步骤:

  • 1、客户调用客户端辅助对象stub上的方法
  • 2、客户端辅助对象stub打包调用信息(变量、方法名),通过网络发送给服务端辅助对象skeleton
  • 3、服务端辅助对象skeleton将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象
  • 4、调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象skeleton
  • 5、服务端辅助对象将结果打包,发送给客户端辅助对象stub
  • 6、客户端辅助对象将返回值解包,返回给调用者
  • 7、客户获得返回值

Stub获取方式

Stub的获取方式有很多,常见的方法是调用某个远程服务上的方法,向远程服务获取存根。但是调用远程方法又必须先有远程对象的Stub,所以这里有个死循环问题。JDK提供了一个RMI注册表(RMIRegistry)来解决这个问题。RMIRegistry也是一个远程对象,默认监听在1099端口上,可以使用代码启动RMIRegistry,也可以使用rmiregistry命令。要注册远程对象,需要RMI URL和一个远程对象的引用。

IHello rhello = new HelloImpl();
LocateRegistry.createRegistry(1099);//人工创建RMI注册服务
Naming.bind("rmi://0.0.0.0:1099/hello", rhello);

LocateRegistry.getRegistry() 会使用给定的主机和端口等信息 本地创建一个Stub对象作为Registry远程对象的代理,从而启动整个远程调用逻辑。服务端应用程序可以向RMI注册表中注册远程对象,然后客户端向RMI注册表查询某个远程对象名称,来获取该远程对象的Stub。

动态加载类

RMI核心特点之一就是动态加载类,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,java.rmi.server.codebase属性值表示一个或多个URL位置,可以从中下载本地找不到的类,相当于一个代码库。动态加载的对象class文件可以使用Web服务的方式(如http://、ftp://、file://)进行托管。客户端使用了与RMI注册表相同的机制。RMI服务端将URL传递给客户端,客户端通过HTTP请求下载这些类。

无论是客户端还是服务端要远程加载类,都需要满足以下条件:

  • 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并且配置java.security.policy,这在后面的利用中可以看到。

  • 属性 java.rmi.server.useCodebaseOnly 的值必需为false。但是从 JDK 6u45、7u21 开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

示例

在Server与Client中都需要定义相同接口,继承Remote

C/S的接口:

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

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

Server端:

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class Server implements Hello{
//作用一:实现接口
@Override
public String sayHello() throws RuntimeException {
return "hello world";
}

//作用二:创建注册中心,绑定等功能
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
//创建远程对象
HelloImpl obj = new HelloImpl();
//绑定到rmi服务,0代表端口为随机
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj,0);
//注册中心,通过注册中心去获取远程对象
Registry registry = LocateRegistry.createRegistry(1099);
//进行名称绑定
registry.bind("hello",stub);

System.out.println("Server ready!");
}
}

Client端:

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

public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
//LocateRegistry.getRegistry()会使用给定的主机和端口等信息本地创建一个Stub对象作为Registry远程对象的代理
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
//通过lookup进行查找对应绑定的名称
Hello hello = (Hello) registry.lookup("hello");
//接受远程对象返回的“hello world”
String response = hello.sayHello();
System.out.println("remote response:"+response);
}
}

先启动Server端,在启动Client端后,接受远程返回的 hello world

LDAP

LDAP(Lightweight Directory Access Protocol)-轻量目录访问协议。但看了这个解释等于没说,其实差不多是个数据库,
具有以下特点:

  1. 基于TCP/IP协议
  2. 同样也是分成服务端/客户端;同样也是服务端存储数据,客户端与服务端连接进行操作
  3. 相对于mysql的表型存储;不同的是LDAP使用 树型存储

树层次分为以下几层:

  • dn:一条记录的详细位置,由以下几种属性组成
  • dc: 一条记录所属区域(哪一个树,相当于MYSQL的数据库)
  • ou:一条记录所处的分叉(哪一个分支,支持多个ou,代表分支后的分支)
  • cn/uid:一条记录的名字/ID(树的叶节点的编号,想到与MYSQL的表主键?)

举个例子一条记录就是

dn="uid=songtao.xu,ou=oa,dc=example,dc=com"

详细介绍:LDAP 协议入门(轻量目录访问协议) - 知乎 (zhihu.com)

参考

远程方法调用(RMI)原理与示例 - Pickle - 博客园 (cnblogs.com)

十一、RMI、JNDI、LDAP介绍+log4j漏洞分析 - FreeBuf网络安全行业门户

JNDI 注入漏洞的前世今生 - 知乎 (zhihu.com)

Java安全之JNDI注入 - nice_0e3 - 博客园 (cnblogs.com)

JNDI with RMI - 安全客,安全资讯平台 (anquanke.com)

LDAP 协议入门(轻量目录访问协议) - 知乎 (zhihu.com)

Java 中 RMI、JNDI、LADP、JRMP、JMX、JMS那些事儿(上) - 云+社区 - 腾讯云 (tencent.com)

RMI、LDAP、CORBA与JNDI攻击 - 简书 (jianshu.com)