• who is JNDI?


    前言

    学习一样东西,我们总是需要知道是什么东西,让我们看看什么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中类的引用功能。

    在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。具体可以查看Java技术回顾之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”的第一个地址。  
    Enumeration<RefAddr> getAll() 
    	检索本参考文献中地址的列举。  
    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文件,所以是调用不了方法的,所以我们需要借助接下来要提到的东西

    总结

    参考文章:

    https://www.cnblogs.com/nice0e3/p/13958047.html#autoid-1-0-0

    https://paper.seebug.org/1091/#jndi_4

    https://mp.weixin.qq.com/s/GJ9Dio_7A8RCeipilIHXEg

    https://xz.aliyun.com/t/6633#toc-1

    https://www.anquanke.com/post/id/205447

    https://y4er.com/post/attack-java-jndi-rmi-ldap-2/

    https://security.tencent.com/index.php/blog/msg/131

    https://medium.com/@m01e/java安全-jndi注入-ab5134574323

    https://zhishihezi.net/b/5d644b6f81cbc9e40460fe7eea3c7925#open

  • 相关阅读:
    SQL对Xml字段的操作
    五种常见的ASP.NET安全缺陷
    EntityFramework中常用的数据删除方式
    002_ASP.NET 换主题
    001_ASP.NET MVC 实用教程 论坛项目 北盟网校 原创视频教程
    LINQ to Entities 比较日期
    windows10多桌面创建 切换 和分屏
    winform的combox下拉框绑定数据源
    C# 怎么让winform程序中的输入文本框保留上次的输入
    dos 批量重命名 bat
  • 原文地址:https://www.cnblogs.com/0x7e/p/14578370.html
Copyright © 2020-2023  润新知