
本文共 7183 字,大约阅读时间需要 23 分钟。
前言
学习一样东西,我们总是需要知道是什么东西,让我们看看什么jndi,jndi是什么玩意,之后会通过fastjson,去了解jndi。同时,这也是上文中所提到的rmi加载远程类中,需要用到的一个玩意
0x01、JNDI概述
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。
JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。现在JNDI已经成为J2EE的标准之一,所有的J2EE容器都必须提供一个JNDI的服务
然而这是来自百度的一段小描述,看着看着就行了,太白话了官方文,哈哈哈。
JNDI则是类似一个索引中心,它允许客户端通过name发现和查找数据和对象。其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。
代码如下:
//指定需要查找name名称String jndiName= "Test";//初始化默认环境Context context = new InitialContext();//查找该name的数据DataSource ds = (DataSourse)context.lookup(jndiName);
这里的jndiName变量的值可以是上面的命名/目录服务列表里面的值,如果JNDI名称可控的话可能会被攻击。那上面提到的命名和目录是什么?
命名服务:命名服务是一种简单的键值对绑定,可以通过键名检索值,RMI就是典型的命名服务
目录服务:目录服务是命名服务的拓展。它与命名服务的区别在于它可以通过对象属性来检索对象
举个例子:比如你要在某个学校里里找某个人,那么会通过:年级->班级->姓名这种方式来查找,年级、班级、姓名这些就是某个人的属性,这种层级关系就很像目录关系,所以这种存储对象的方式就叫目录服务。LDAP是典型的目录服务
我们只要知道jndi是对各种访问目录服务的逻辑进行了再封装就可以了,其他其实代码是一样的;
其实,仔细一琢磨就会感觉其实命名服务与目录服务的本质是一样的,都是通过键来查找对象,只不过目录服务的键要灵活且复杂一点。
那它存放的形式呢?我们通过上面知道是通过键来查找对象的,那我们就可以知道了它的存放形式
0x02、JNDI结构
在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:
//主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;javax.naming//主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;javax.naming.directory//在命名目录服务器中请求事件通知;javax.naming.event//提供LDAP支持;javax.naming.ldap//允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。javax.naming.spi
InitialContext类
1、构造方法
//构建一个初始上下文。InitialContext() //构造一个初始上下文,并选择不初始化它。InitialContext(boolean lazy) //使用提供的环境构建初始上下文。 InitialContext(Hashtable environment)
- 实现代码
InitialContext initialContext = new InitialContext();
在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
2、常用方法
//将名称绑定到对象。bind(Name name, Object obj) //枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。list(String name) //检索命名对象。lookup(String name) //将名称绑定到对象,覆盖任何现有绑定。 rebind(String name, Object obj) //取消绑定命名对象。unbind(String name)
- 实现代码
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); }}
Reference类
该类也是在javax.naming
的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。具体可以查看。
1、构造方法:
//为类名为“className”的对象构造一个新的引用。 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)
- 实现代码
String url = "http://127.0.0.1:8080";Reference reference = new Reference("test", "test", url);
在使用Reference时,我们可以直接将对象传入构造方法中,当被调用时,对象的方法就会被触发,创建Reference实例时几个比较关键的属性:
参数1:
className
- 远程加载时所使用的类名
参数2:
classFactory
- 加载的class
中需要实例化类的名称
参数3:
classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
协议
Reference类表示对存在于命名/目录系统以外的对象的引用。如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。
Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。
2、常用方法
void add(int posn, RefAddr addr) 将地址添加到索引posn的地址列表中。 void add(RefAddr addr) 将地址添加到地址列表的末尾。 void clear() 从此引用中删除所有地址。 RefAddr get(int posn) 检索索引posn上的地址。 RefAddr get(String addrType) 检索地址类型为“addrType”的第一个地址。 EnumerationgetAll() 检索本参考文献中地址的列举。 String getClassName() 检索引用引用的对象的类名。 String getFactoryClassLocation() 检索此引用引用的对象的工厂位置。 String getFactoryClassName() 检索此引用引用对象的工厂的类名。 Object remove(int posn) 从地址列表中删除索引posn上的地址。 int size() 检索此引用中的地址数。 String toString() 生成此引用的字符串表示形式。
- 实现代码
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); // 第一个参数是远程加载时所使用的类名, 第二个参数是要加载的类的完整类名(这两个参数可能有点让人难以琢磨,往下看你就明白了),第三个参数就是远程class文件存放的地址了 Reference reference = new Reference("test", "test", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("aa",referenceWrapper); }}
那我们可以想下为什么又将它传入ReferenceWrapper
中。因为Reference
没有实现Remote
接口也没有继承 UnicastRemoteObject
;
而RMI的时候说过,需要将类注册到Registry
需要实现Remote
和继承UnicastRemoteObject
类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper
将他给封装一下。
0x03、JNDI代码实现
在JNDI中提供了绑定和查找的方法
- bind(Name name, Object obj) :将名称绑定到对象中
- lookup(String name): 通过名字检索执行的对象
写个小demo演示JNDI访问RMI服务
- hello 接口:
import java.rmi.Remote;import java.rmi.RemoteException;public interface Hello extends Remote { public String sayHello(String name) throws RemoteException;}
- hello 实现类:这个类的实例一会将要被绑定到rmi注册表中
import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class HelloImpl extends UnicastRemoteObject implements Hello { public HelloImpl() throws RemoteException{ super(); } @Override public String sayHello(String name) throws RemoteException { return "Hello , " + name; }}
上面的都是简单的创建一个远程对象,和之前rmi创建远程对象的要求是一样的,下面我们创建一个类实现对象的绑定,以及远程对象的调用
import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.util.Properties;public class CallService { public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException { //配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常 Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099"); //初始化环境 InitialContext init = new InitialContext(); //创建一个rmi映射表 Registry registry = LocateRegistry.createRegistry(1099); //创建一个对象 HelloImpl hello = new HelloImpl(); //将对象绑定到rmi注册表 registry.bind("he",hello); //jndi的方式获取远程对象 Object lookup = init.lookup("rmi://127.0.0.1:1099/he"); //调用远程对象的方法 System.out.println(hello.sayHello("hahah")); }}
成功调用远程对象的sayHello方法
由于上面的代码将服务端与客户端写到了一起,所以看着不那么清晰
ps:可以对比一下rmi demo与这里的jndi demo访问远程对象的区别,加深理解
0x04、JNDI动态协议转换
我们上面的demo提前配置了jndi的初始化环境,还配置了Context.PROVIDER_URL,这个属性指定了到哪里加载本地没有的类
那么动态协议转换是个什么意思呢?其实就是说即使提前配置了Context.PROVIDERURL属性,当我们调用lookup()方法时,如果lookup方法的参数像demo中那样是一个uri地址,那么客户端就会去lookup()方法参数指定的uri中加载远程对象,而不是去Context.PROVIDERURL设置的地址去加载对象(如果感兴趣可以跟一下源码,可以看到具体的实现)。
正是因为有这个特性,才导致当lookup()方法的参数可控时,攻击者可以通过提供一个恶意的url地址来控制受害者加载攻击者指定的恶意类。
但是你以为直接让受害者去攻击者指定的rmi注册表加载一个类回来就能完成攻击吗,是不行的,因为受害者本地没有攻击者提供的类的class文件,所以是调用不了方法的,所以我们需要借助接下来要提到的东西
总结
参考文章:
发表评论
最新留言
关于作者
