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_FACTORY
和PROVIDER_URL
。
INITIAL_CONTEXT_FACTORY
:保存环境属性名称的常量,用于指定要使用的初始上下文工厂。该属性的值应该是将创建初始上下文的工厂类完全限定类名。
PROVIDER_URL
:保存环境属性名称的常量,用于指定服务提供者要使用的配置信息。该属性的值应包含一个URL字符串。
JNDI架构简单学习
JNDI架构提供了一个标准的、与命名系统无关的API,这个API构建在特定于命名系统的驱动程序之上。这一层有助于把应用程序和实际的数据源隔离开来,因此无论应用程序是访问LDAP、RMI、DNS还是其他的目录服务,这都没有关系。换句话说,JNDI与任何特定的目录服务实现无关,您可以使用任何目录,只要您拥有相应的服务提供程序接口(或驱动程序)即可,如下图所示:
其实就是在RMI等服务外面再套了一层API,方便调用。
JNDI-协议转换
如果JNDI
在lookup
时没有指定初始化工厂名称,会自动根据协议类型动态查找内置的工厂类然后创还能处理对象的服务请求。
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代码片段为例:
|
|
这里也会可以成功调用一个sayHello()方法:
本来lookup这里只需要传参“Hello"即可,但是就需要进行环境配置,这样传入也能正常获取代理对象并调用方法,这得益于这里的lookup()会动态地识别用户要调用的服务以及路径,然后就会自动使用rmiURLContext处理RMI请求,也是在jndi注入中比较常用的一点。
JNDI的接口实现
测试环境:
- JDK 8u71
JNDI-RMI服务
RMI
的服务处理工厂类是:com.sun.jndi.rmi.registry.RegistryContextFactory
,在调用远程的RMI
方法之前需要先启动RMI
服务,然后就可以使用JNDI
来连接并调用了。
基本代码
先开启一个RMI服务,直接用之前的代码即可:
|
|
在访问JNDI目录服务时会通过预先设置好环境变量访问对应的服务:
|
|
简单理解就是通过Context.INITIAL_CONTEXT_FACTORY告诉JNDI我要访问何种服务,通过Context.PROVIDER_URL告诉JNDI所要访问服务的路径。
然后就是初始化上下文,传入我们设置好的环境变量:
|
|
还有另外一种类似的获取上下文的操作,即不指定环境变量,那么JNDI会自动搜索系统属性System.getProperty()、applet参数和应用程序资源文件jndi.properties,所以我们也可以通过System.setProperty()设置环境变量:
|
|
然后利用提供的lookup()方法,通过查询名字获取远程对象代理类:
|
|
所以完整代码如下:
|
|
成功调用方法:
源码分析
命名服务初始化上下文
对应代码为:
|
|
有前面的代码可以看出,这里我们是传入了一个Hashtable类,里面包含了构造的两个键值对。看看会对这个进行什么处理跟进一下这里的InitialContext类的构造方法(注意要先启动RMI的Server端):
如预期,有键值对,这里的clone()方法简单来说就是一个拷贝的操作,其实调用这个方法后的Hashtable类与原先没有太大区别,这个方法后的environment参数也是没有问题的。
然后调用了init()方法,并往里面传进了environment,跟进这个方法:
然后这里调用了ResourceManager#getInitialEnvironment()方法,简单跟进一下,几个重点代码:
获取applet参数的代码:
两个点,
- 第一个框,获取到VersionHelper类的PROPS变量,变量定义如下:
里面就有我们原先放入的两个变量,这个变量记录了Context接口的常量信息,所以现在就是将props变量赋值为了上面的包含Context接口常量信息的数组
- 第二个框,就是从Hashtable中获取以APPLET变量为值的键的值,在Context接口中的APPLET变量定义:
很明显我们当初并没有往Hashtable类放入这个变量,所以这里就是一个空,即为null。
继续往后面看,代码如下:
这里的helper变量定义:
跟进VersionHelper#getVersionHelper()方法:
所以其实也就是获取到VersionHelper12类实例。所以在前面的方法调用中,其实是会调用VersionHelper12类的getJndiProperties()方法,简单看了一下,很像在调用System.getProperty()方法,这里会返回一个数组,结果全为null:
然后就调用了for循环,几个关键点:
这里就是调用了前面的数组,如果在Hashtable类中存在数组中的一个值,那么就直接进入下一个for循环,可以想出只会有两次操作是直接进入下一个for循环,那么没有定义的呢,就会进入到if条件语句,并且在前面可以知道获取到的applet变量为null,所以其实就是调用的下面的代码:
没啥大的分析过程,最后还是返回的这个Hashtable类:
可能是因为我指定了环境变量,没有使用setProperty()方法来放变量。可以换代码再跟一下,过程有细微差别,但不影响,最后在jndiSysProps变量的赋值确实有不同:
大差不差,就是前面会实例化一个Hashtable类,后面会调用Hashtable类的put()方法
还是会构造成一个Hashtable类。
所以最终结果还是这个Hashtable类:
ResourceManager#getInitialEnvironment()方法分析结束。
所以这个步骤的目的就是确保后面能传入一个“填充”好的Hashtable类。
————————
回到InitialContext类的init()方法,再粘过来一次代码:
然后后面无疑会调用getDefaultInitCtx()方法:
所以现在会调用NamingManager类的getInitialContext()方法,并传入了前面构造了的Hastable类,跟进这个方法:
按顺序解析一下这个方法在干嘛。
首先调用了getInitialContextFactoryBuilder()方法,跟进一下:
所以会返回null,然后就会进入后面的if条件:
所以很正常这里会获取到env变量(Hashtable类)的INITIAL_CONTEXT_FACTORY,所以也就是前面的RegistryContextFactory类,所以后面就不会进入if条件。
继续往后看:
所以这里应该就是会加载这个类为Class对象,然后对这个Class对象调用newInstance()方法将其实例化。所以这里就是会获取到RegistryContextFactory类实例:
并将这个实例化类赋值给了factory变量,再往后看,就会到如下代码:
所以现在会调用RegistryContextFactory类的getInitialContext()方法,并传入了env变量(就是Hashtable类),跟进getInitialContext()方法:
现在又对这个Hashatble类调用了clone()方法,“克隆”一次?先将其理解为就是前面传入的Hashtable类。然后聚焦于return调用的两个方法,分别跟进一下:
- getInitCtxURL(var1):
这里容易看出会进入if条件,然后对这个Hashtable类调用了取值的操作,这里也就是前面放进的值,所以会将var2变量赋值为"rmi://localhost:1099",所以最终这个getInitCtxURL()方法就是返回了这个var2,即"rmi://localhost:1099",也就是获取“环境变量”指定的服务地址。
- URLToContext()方法:
代码如上,简单说明一下参数问题:(var0 ==》 rmi://localhost:1099 ; var1 ==》 Hashtable类),在这个方法中,可以看到又实例化了一个rmiURLContextFactory类,然后调用了这个类的getObjectInstance()方法(传入了var0和var1),跟进一下:
可以看出来这里会进入到第一个else if语句,所以现在会调用getUsingURL()方法,继续跟进:
喔,简单瞟一眼,看到了一个老朋友,lookup()方法,有点意思。现在来跟一下这里的方法源码:
这里先实例化了一个rmiURLContext类,并传入了var1(Hashtable类),简单跟一下这里的初始化过程:
先跟进rmiURLContext类的父类GenericURLContext类的构造方法:
所以这里就是将GenericURLContext类的myEnv变量赋值为Hashtable类:
回到rmiURLContextFactory类的getUsingURL()方法:
来看看这里的lookup()方法会干嘛,所以现在会调用rmiURLContext类的lookup()方法,但是rmiURLContext类没有lookup()方法,所以会调用到父类GenericURLContext类的lookup()方法(传入了var0,如上图所示的变量):
跟进这里的getRootURLContext()方法,传入了var1参数和刚好在前面初始化类时赋值的变量myEnv(也就是Hashtable类),简单跟跟,说几个重点,方法结果如下:
这里可以看到实例化了两个类,对应代码如下:
前面的CompositeName类的初始化就不多说了,重点看RegistryContext类的初始化:
就是将这里的environment变量赋值为了Hashtable类实例,然后将这里的registry、host、port变量分别赋值,简单跟一下这里的getRegistry()方法:
如下:
然后又会调用LocateRegistry类的getRegistry()方法:
调用另一个重载的getRegistry()方法,在方法最后见到了老朋友:
在这个方法中,会创建RegistryImpl_Stub类:
最后也就会返回这个RegistryImpl_Stub类。(后面再碰到的getRootURLContext()方法都是差不多的)
回到RegistryContext类的构造方法然后host和port的赋值:
最后在rmiURLContext类的getRootURLContext()方法将CompositeName类和RegistryContext封装进了ResolveResult类并返回了它。
——————
回到lookup()方法,然后后面调用的两个方法:getResolvedObj()和getRemainingName()方法,都是获取变量值的操作,对应上面结果中的ResolveResult类中的两个变量。
所以后面的var3变量的值为RegistryContext类实例:
这几个变量的值还是挺重要的。
——————
后面又会调用RegistryContext类的lookup()方法:
这里的var2.getRemainingName会返回前面说过的变量,值为:
也还是对应前面的var2中的变量。
继续跟进RegistryContext类的lookup()方法,传入了CompositeName类实例:
然后这里会进入第一个if条件,会实例化一个RegistryContext类并返回它,在实例化时会进行一些初始化操作:
最后返回了这个RegistryContext类,这个类还是包含了一下重要信息。
然后简单跟了一下return的部分,最后就是将InitialContext类的defaultInitCtx变量赋值为了这刚RegistyContext类。
命名服务获取对象
对应代码:
|
|
前面获取到了InitialContext类并进行了一些重要的赋值操作,类变量定义如下:
所以现在会调用InitialContext类的lookup()方法:
先跟进一下这里的getURLOrDefaultInitCtx()方法:
第一个if条件不会进入,跟一下还是很好看出的,这里不多赘述。
后面的schema的值为null,调用的getURLSchema()方法也是容易看的。
所以最后会调用getDefaultInitCtx()方法,又是这个方法,但是这里却直接返回值,如下:
由于前面初始化时会将这里的gotDefault变量设置为true,所以这里会直接返回定义的defaultInitCtx变量,即前面的RegistryContext类实例。
即getURLOrDefaultInitCtx(name)
的调用就是获取RegistryContext类实例。
———
回到InitialContext类的lookup()方法,所以现在会调用RegistryContext类的lookup()方法,并传入了Hello这个参数变量:
两个点,都跟一下:
- CompositeName类的初始化:
也就是给CompositeName类的impl变量赋值。
- 调用了RegistryContext类的另一个lookup()方法,并将前面的CompositeName类传了进去:
这里又调用了这个lookup()方法,但是有很明显的区别,就是这里的var1有值。就简单理解为将一个字符串名转换成对应的Name类型对象。所以现在会进入else语句:
然后就会调用RegistryImpl_Stub类的lookup()方法,用来查找RMI服务:
也就是比较熟悉的操作了:
最后这里反序列化获取代理对象。
也就是那个代理对象:
后续代码的方法调用就和RMI的差不多了。
最终成功达到访问一次RMI服务的操作。
补充说明
动态协议切换
在前面的协议转换中,提到了一个不需指定环境变量,可以通过服务地址直接获取到相应对象,完整实现代码如下:
|
|
(注意起RMI Server服务)
这样也能实现一次远程方法调用。
现在来分析一下过程,一步一步来:
命名服务初始化
对应代码:
|
|
这里的过程其实都和前面将要求的值放入如环境属性中差不多的,刚好前面只是略过,这里稍微详细讲讲:
调用的构造方法:
调用Init()方法:
跟进ResourceManager类的getInitialEnvironment()方法:
前面实例化了一个Hashtable类,后面的getJndiProperty()方法中有读取环境属性的代码,如果读取到了就会放进Hashtable类中,但是我们并没有往环境属性中放东西,所以最终返回的这个Hashtable类是空的:
所以后续的调用get()方法是无法获取值的,所以就直接结束初始化了:
命名服务获取对象
对应代码:
|
|
会调用InitialContext类的lookup()方法:
还是分别跟一下:
- 又是getURLOrDefaultInitCtx()方法:
但是这里就有不同地方了,这里跟进getURLSchema()方法:
无疑这里会匹配到:
和/
特殊符号,所以现在会进入if条件语句而不是直接返回null。
在if语句中,进行了字符串的截取工作,所以会返回rmi这个字符串。
继续往后看:
然后就会进入这个if语句,会调用NamingManage.getURLContext()方法(这里传入了rmi字符串和一个空的Hashtable类):
然后调用了getURLObject()方法:
喔,getFactory()方法,感觉很像获取工厂类的方法,有搞头,此时的参数情况:
这就已经看到了包含有rmiURLContextFactory的字符串,也是参数传递时确实应该形成的,跟进ResourceManager类的getFactory()方法,关键代码如下:
parser.nextToken方法返回com.sun.jndi.url字符串,classSuffix就是传入的.rmi.rmiURLContextFactory,所以className变量定义如下:
所以这里找到了工厂类并实例化,最后还返回了这个对象。
所以这里的getFactory()方法就是获取工厂类对象。
————
回到NamingManager类的getURLObject()方法:
所以现在会调用工厂类rmiURLContextFactory类的getObjectInstance()方法,这里的参数分别为:(null、null、null、空的Hashtable类),跟进这个方法:
所以就是实例化一个rmiURLContext类,传入了一个空的Hashtable类,将其父类的父类GenericURLContext的myEnv变量赋值为了Hashtable类。
所以之类的getURLObject()方法就是获取到一个rmiURLContext类。
————
回到NamingManager类的getURLContext类:
毫无疑问这里符合第一个if条件,所以会返回这个rmiURLContext类
————
回到InitialContext类的getURLOrDefaultInitCtx()方法:
符合条件,直接返回这个rmiURLContext类,不会像之前一样调用getDefaultInitCtx()方法。
综上所述,这里的getURLOrDefaultInitCtx(name)方法就是会得到一个rmiURLContext类,但是里面的Hashtable类为空。
————————
- lookup(name):
跟进rmiURLContext类的lookup()方法(实际调用的是其父类GenericURLContext的lookup()方法):
前面也遇到过这个方法,和前面差不多了,var2为ResolveResult类对象,包含的两个变量(变量值有点不同):
后续的两个getter都是获取这里两个值,两个值的情况:
调用了RegistryContext类的lookup()方法:
跟进lookup()方法:
所以会进入else语句,然后就会调用RegistryImpl_Stub类的lookup()方法查找“Hello”的服务:
同样符合RMI的过程。
JNDI-DNS服务
JNDI
支持访问DNS
服务,注册环境变量时设置JNDI
服务处理的工厂类是com.sun.jndi.dns.DnsContextFactory
类。
基本代码
DNS服务就是为了解析域名,代码和前面JNDI-RMI代码大相径庭:
|
|
这里是从dns://114.114.114.114这个dns服务器上查询www.baidu.com
域名的ip地址,这里用的是JNDI目录服务,目录服务允许目录对象具有属性,那么同样也可以有值。
同样的,还可以不设置环境变量,将其放进系统属性中,代码如下:
|
|
源码分析
目录服务初始化上下文
对应代码:
|
|
跟进初始化过程:
这里进行类的初始化的是InitialDirContext类,而它的父类是InitialContext类,所以这里其实还是初始化的InitialContext类,并同样传入了Hashtable类,只不过里面放入的值是不同的。
后面的具体过程还是和前面命名服务初始化差不多的,就从不一样的地方开始看看:
在NamingManage类的getInitialContext()方法中,最后会调用DnsContextFactory类的getInitialContext()方法:
然后调用DnsContextFactory类的ulToContext()方法,参数分别为dns://114.114.114.114、DNS服务的Hashtable类:
最后会调用getContext()方法,参数分别为:
继续跟进:
所以这里是初始化了一个DnsContext类,进行了一些赋值操作:
最后返回了这个DnsContext类实例。
所以这里的步骤同样是将InitialDiContext类的父类InitialContext类变量defaultInitCtx变量赋值为DnsContext类实例,并将gotDefault变量赋值为tue。
——————
目录服务获取属性
对应代码:
|
|
在前面的初始化后,现在的context值是一个InitialDiContext类,变量定义为:
所以现在会调用InitialDiContext类的getAttibutes()方法:
还是两个点,
- 先跟进
getURLOrDefaultInitDirCtx(name)
:
跟进这里的getURLOrDefaultInitCtx()方法:
还是同样的,会直接调用最后的getDefaultInitCtx()方法,前面的代码跟一下源码即可,挺好看懂的:
同样的直接返回DnsContext类实例。并且在InitialDirContext类的getURLOrDefaultInitDirCtx()方法也是直接返回这个DnsContext类实例:
所以这里的第一个点就是获取DnsContext类实例。
————
- 跟进
getAttributes(name, attrIds)
代码:
所以现在是会调用DnsContext类的父类的父类PartialCompositeDirContext的getAttibutes()方法,跟进:
还是先简单看一下CompositeName类的初始化:
同样的还是将CompositeName类的impl变量赋值为这个。
但其实可以直接看作badidu.com,继续跟代码,所以现在会调用另一个重载的getAttributes()方法:
按顺序简单说说:
-
先是将这里的var3变量赋值为了DnsContext类实例。
-
然后调用了p_getEnvironment()方法,实际上会调用DnsContext类的这个方法:
直接返回了Hashtable类实例。
- 然后实例化了一个Continuation类:
传入了这个字符串和Hashtable类实例。
- 然后到了for循环,会调用DnsContext父类ComponentDirContext的p_getAttributes()方法:
这里的var4变量是一个HeadTail类实例,后续的HeadTail.getStatus返回2,会调用c_getAttributes方法(getHead()方法返回的是“baidu.com"):
查询逻辑为:
所以这里实际会调用DnsClient类的quey()方法,然后又会调用到doUdpQuery()方法:
前面的send()方法就是进行连接,发送请求到相应的DNS服务器,后一个就是获取数据。
最后query()方法结束获得的数据:
最后得到数据:
——————————
JNDI攻击
在学习JNDI攻击前,先简单了解两个知识点。
前置知识了解
Reference类了解
该类位于javax.naming.Reference
,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
一个示例代码:
|
|
利用的构造函数:
这里涉及到的三个变量:
就是将这个类的这三个参数赋值为对应的参数。
就利用来说,三个参数可以如下理解:
- 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端代码:
|
|
这里需要将前面的Reference
对象传进ReferenceWrapper
,这是因为Reference类没有实现Remote接口也没有继承UnicastRemoteObject
类,所以这里用ReferenceWrapper
类将其封装了一下。
- RMIClient端代码:
|
|
- 然后需要准备一个远程加载的类:
|
|
然后使用javac命令将这个文件打成class文件,然后再用python起一个HTTP服务:
|
|
然后运行Server端,再运行Client发起请求,成功弹出计算机:
原理简单来说就是把引用了恶意类的Reference
类绑定到RMI的Registry中。
在客户端调用lookup
远程获取远程类的时候,就会获取到Reference
对象,就会去寻找Reference
中指定的类,如果查找不到则会在Reference
中指定的远程地址去进行请求,请求到远程的类后会在本地执行。
这里其实算攻击客户端。
代码调试
JNDI_RMI_Server端
大致还是和RMI服务端的创建是差不多的,主要是为了看看引用类中的赋值情况。
大概看了一下,Reference类还是和前面说的差不多,主要看看ReferenceWrapper的赋值情况:
也就是将这里的wrapper变量赋值为Reference类。
稍微关注一下这里的getReference()方法。
最后在注册表中将RMI服务和ReferenceWrapper对象绑定在一起。
JNDI_RMI_Client端
打断点于lookup()方法,所以现在会调用InitialContext类的lookup()方法:
前面一部分其实是和RMI服务动态协议转换那里差不多的,主要从不同的地方说起,直接到调用RegistryImpl_Stub类的lookup()方法结束:
此时的变量情况:
在前面的RMI接口实现中分析这里在调用lookup()方法后会得到代理对象,但是如上图所示,这里会得到一个ReferenceWrapper_Stub类,跟进这里的decodeObject()方法:
这里的ReferenceWrapper_Stub类实现了RemoteReference接口,所以这里会调用ReferenceWrapper_Stub类的getReference()方法:
这里又调用了UnicastRef类的invoke()方法进行数据传输,得到了Reference对象并将其返回,所以现在var的值为:
然后又调用了NamingManager类的getObjectInstance()方法:
变量情况:
跟进NamingManager类的getObjectInstance()方法:
部分代码,每部分都分析一下:
这里会调用getObjectFactoyBuilder()方法,这里就是会返回变量的值,但是这里的值默认为null,所以不会进入后面的if条件。
看后面的代码:
refInfo就是参数传递的Reference类,符合第一个if条件,所以现在会将ref变量赋值为这个Reference类。
再往后面看,会进入第一个if语句,会将f赋值为前面Reference类初始化时传的参,如下:
即要初始化的类。
所以现在会进入第二个if条件:
所以现在会调用getObjectFactoryFromReference()方法(注意传入了Reference类和这个classFactory):
可以看到有加载类的操作,也就是恶意类JNDI_Main。在第二个loadClass()方法获取到了JNDI_Main类的class对象,第一个应该是先在本地找,找不到就去工厂路径找:
并且后面有实例化类的操作:
很明显会实例化类成功调用恶意方法。
在弹计算机后就进入异常输出,整个过程结束:
JNDI-Ldap注入
LDAP也是一种目录服务,前面分析的DNS其实只用客户端就能完成一次完整的服务,在这里我们可以利用LDAP这个目录服务来完成一次攻击。
思路是差不多的,当查询属性的值时,我们返回一个存储恶意类的Reference类给客户端,让客户端根据codebase路径查找工厂。
需要用需要引入unboundid-ldapsdk-3.2.0.jar包,直接在pom.xml引入即可:
|
|
基本代码
恶意LDAP服务端:
|
|
然后再编写一个客户端来请求:
|
|
然后还是使用之前的恶意类,按照之间的操作编译class文件并起一个http服务。
然后启动服务端,再启动客户端就可以成功弹计算机:
代码调试
LDAP_Client端
对应代码:
|
|
前面一个InitialDirContext类的初始化也是说过的,大概就是实例化了InitialDirContext类及其父类InitialContext类,并将InitialContext类的myPops赋值为了一个空的Hashtable类。
然后重点看InitialDirContext类调用的lookup()方法:
老朋友了,不多说,第一部分:
然后调用getURLContext()方法:
然后调用getURLObject()方法:
然后获取到ldap服务的工厂类并调用工厂类ldapURLContextFactoy的getObjectInstance()方法:
然后实例化ldapURLContext类,里面有一个将其父类的父类GenericURLContext类的myEnv变量赋值为空的Hashtable类的操作。
所以第一部分是获取到了ldapURLContext类
——————
所以第二部分会调用ldapURLContext类的lookup()方法:
会调用到其父类的父类的lookup()方法:
这里需要注意的是this代表ldapURLContext类,所以会调用ldapURLContext类的getRootURLContext()方法,然后就会调用ldapURLContextFactory类的getUsingURLIgnoreRootDN()方法:
最后获取到的结果如下:
后面跟进调用的lookup()方法:
然后会调用LdapCtx类的p_lookup()方法:
跟进p_lookup()方法:
var4.getStatus()
方法返回2,会匹配2的代码,跟进c_lookup()方法,直接看关键代码:
这里的var4中存在LDAP的基本信息:
这里的JAVA_ATTRIBUTES变量就是一个数组:
所以if条件中获取到了值:
然后调用了decodeObject()方法,在decodeObject()方法中调用了decodeReference()方法.
在这个decodeReference()方法方法中进行了Reference类的初始化:
看传入的参数,并且最后返回了这个var5。
回到c_lookup()方法,最后调用了getObjectInstance()方法(这里的var3就是前面的Reference类):
跟进方法:
这里的代码是和前面JNDI_RMI的攻击的其中一部分代码都是差不多的,但是还是有点区别:
然后会调用getObjectFactoryFromReference()方法:
然后会加载类,并且最后会实例化这个类,成功弹计算机:
最后还是同样异常输出然后到catch语句。
注意事项
JDK版本对JNDI的利用有一定的限制,如下:
|
|
高版本限制绕过
测试环境:JDK 8u411
——————
限制地点:
- RMI:
这里导致无法继续后面的操作。
利用本地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类:
也就是如下设置Reference类:
所以我们现在可以尝试将客户端获取到的Reference类指向可控恶意类。
继续往后面走,只要这里的getObjectFactoryFromReference类成功实例化指向类,而不像之前那样弹出计算机导致异常输出,这里就有机会调用到这个指向类的getObjectInstance()方法:
——————
JNDI-RMI过程中获取Reference类是直接从数据流中读取的,可以自己跟跟,不赘述了,然后调用到指定类的getObjectInstance()方法是差不多的。
现在在pom.xml中引入这个存在BeanFactoy类的依赖来本地测试:
|
|
代码实现及分析
直接给代码:
|
|
然后现在就只需要先运行这个Server端,再运行客户端来请求即可。
注意:这是根据BeanFactory的代码逻辑,要求传入的Reference为ResourceRef类,并不是前面的Reference类。
所以当调用到decodeObject()方法时,方法中获取到的是ResourceRef类:
即这里的var3的值:
然后会调用NamingManager类的getObjectInstance()方法,直接到关键地方: 这个方法内会调用getObjectFactoryFromReference()方法,会加载并实例化BeanFactory类:
return后就会调用到这个BeanFactory类的getObjectInstance()方法:
传入参数的参数简单说说:1.前面获取的ResourceRef类,2.Hello,3.RegistryContext类,4.空的Hashtabel类
给下RegistryContext类的值的情况:
前面的分析中经常说这个类的创建,参数传递而已,不多说。
现在来跟进BeanFactory类的getObjectInstance()方法:
可以看到进入这个if语句的条件就是需要obj为ResouceRef类或其子类,这也就是为什么要传那个类。
看try语句,前三个不多说,获取的值情况:
然后进入第二个if条件,就会尝试加载javax.el.ELProcessor类,会成功加载到Class对象。继续往后看:
会进入else语句,但是后面会报错,需要又引入一个依赖,如下:
|
|
继续看,
可以看到将其放进了HashMap类中在后续代码中又将其取出利用;
并直接调用:
即现在就会调用到calc
利用LDAP返回序列化数据,反序触发本地Gadget
同样是利用受害者本地CLASSPATH中存在漏洞的反序列化Gadget。
服务端代码:
|
|
客户端代码:
|
|
成功弹出计算机。
参考文章:
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