JNDI注入
JNDI(Java Naming and Directory Interface)是Java提供的Java命名和目录接口。通过调用JNDI的API可以定位资源和其他程序对象。
JNDI是Java EE的重要部分,JNDI可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA
简介
JNDI提供统一的客户端API,并由管理者将JNDI API映射为特定的命名服务和目录服务,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。简单来说,开发人员通过合理的使用JNDI,能够让用户通过统一的方式访问获取网络上的各种资源和服务。如下图所示
命名服务
(Naming server)
命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
其中另一个值得一提的名称服务为 LDAP,全称为 Lightweight Directory Access Protocol,即轻量级目录访问协议,其名称也是从右到左进行逐级定义,各级以逗号分隔,每级为一个 name/value 对,以等号分隔。比如一个 LDAP 名称如下:
cn=John, o=Sun, c=US |
即表示在 c=US 的子域中查找 o=Sun 的子域,再在结果中查找 cn=John 的对象。关于 LDAP 的详细介绍见后文。
在名称系统中,有几个重要的概念:
- Bindings: 表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP。
- Context: 上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext).
- References: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。
目录服务
(Directory Service)
简单来说,目录服务是命名服务的扩展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(Attributes)信息。由此,我们不仅可以根据名称去查找(Lookup)对象(并获取其对应属性),还可以根据属性值去搜索(Search)对象。
一些常见的目录服务有:
- LDAP: 轻型目录访问协议
- Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
- 其他基于 X.500 (目录服务的标准) 实现的目录服务;
SPI
可参考:SnakeYaml
SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:
- RMI: Java Remote Method Invocation,Java 远程方法调用;
- LDAP: 轻量级目录访问协议;
- CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services);
除此之外,用户还可以在 Java 官网下载其他目录服务实现。由于 SPI 的统一接口,厂商也可以提供自己的私有目录服务实现,用户可无需重复修改代码。
使用
RMI
先起一个rmi服务端,代码忽略
注意这里我们同样需要实现接口,并且包名和RMI Server端相同,不然会报no security manager: RMI class loader disabled错误。
然后使用JNDI
package com.yyjccc.jndi.JNDI.usage; |
手动设置了属性_INITIAL_CONTEXT_FACTORY_和_PROVIDER_URL_的值来对Context进行初始化。通过对Context的初始化,JNDI能够识别我们想调用何种服务,以及服务的路径。
但实际上,在 Context#lookup()方法的参数中,用户可以指定自己的查找协议。JNDI会通过用户的输入来动态的识别用户要调用的服务以及路径
public static void main(String[] args) throws NamingException, RemoteException { |
DNS
package com.yyjccc.jndi.JNDI.usage; |
JNDI流程
上下文初始化
以RMI不设置JNDI环境变量为例
可以看到若是没有设置环境变量的Context,new InitialContext的过程正是读取环境变量和设置的JNDI环境变量
进入lookup中
注意到其实我们不管调用的是lookup、bind或者是其他initalContext中的方法,都会调用getURLOrDefaultInitCtx()方法进行检查
跟进getURLOrDefaultInitCtx方法
会通过getURLScheme()方法来获取通信协议,比如这里获取到的是rmi协议
接着跟据获取到的协议,通过NamingManager#getURLContext()来调用getURLObject()方法
最终在getURLObject()方法中,根据_defaultPkgPrefix_属性动态生成Factory类
这里scheme是rmi,所以得到的类名是rmiURLContextFactory
看一下JNDI默认支持那些动态协议转换。当我们针对JNDI进行攻击的时候可以优先考虑以下几种服务
通过动态协议转换,我们可以仅通过一串特定字符串就可以指定JNDI调用何种服务,十分方便。但是方便是会付出一定代价的。对于一个系统来讲,往往越方便,就越不安全。
假如我们能够控制string字段,那么就可以搭建恶意服务,并控制JNDI接口访问该恶意,于是将导致恶意的远程class文件加载,从而导致远程代码执行。这种攻击手法其实就是JNDI注入,它和RMI服务攻击手法中的”远程加载CodeBase”较为类似,都是通过一些远程通信来引入恶意的class文件,进而导致代码执行
Reference类
该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能
比如远程获取 RMI 服务上的对象是 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载class文件来进行实例化。
Reference类常用构造函数如下
//className为远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载 |
在RMI中,由于我们远程加载的对象需要继承UnicastRemoteObject类,所以这里我们需要使用ReferenceWrapper类对Reference类或其子类对象进行远程包装成Remote类使其能够被远程访问。
import com.sun.jndi.rmi.registry.ReferenceWrapper; |
JNDI注入
为了在命名服务或目录服务中绑定Java对象,可以使用Java序列化来传输对象,但有时候不太合适,比如Java对象较大的情况。因此JNDI定义了命名引用(Naming References),后面直接简称引用(References)。这样对象就可以通过绑定一个可以被命名管理器(Naming Manager)解码并解析为原始对象的引用,间接地存储在命名或目录服务中。
引用由Reference类来表示,它由地址(RefAddress)的有序列表和所引用对象的信息组成。而每个地址包含了如何构造对应的对象的信息,包括引用对象的Java类名,以及用于创建对象的ObjectFactory类的名称和位置。
Reference可以使用ObjectFactory来构造对象。当使用lookup()方法查找对象时,Reference将使用提供的ObjectFactory类的加载地址来加载ObjectFactory类,ObjectFactory类将构造出需要的对象。
所谓的 JNDI 注入就是控制 lookup 函数的参数,这样来使客户端访问恶意的 RMI 或者 LDAP 服务来加载恶意的对象,从而执行代码,完成利用
在 JNDI 服务中,通过绑定一个外部远程对象让客户端请求,从而使客户端恶意代码执行的方式就是利用 Reference 类实现的。Reference 类表示对存在于命名/目录系统以外的对象的引用。
具体则是指如果远程获取 RMI 服务器上的对象为 Reference 类或者其子类时,则可以从其他服务器上加载 class 字节码文件来实例化
示例
先写个恶意类EvilClass
import java.io.IOException; |
编写rmi服务端,使用Reference引用http上的class文件
package com.yyjccc.jndi.JNDI.exp; |
注意new Reference的第二个参数为全类目,第三个参数为加载资源的目录
不要搞错,可能就会出现类加载错误
被攻击的客户端使用JNDI
package com.yyjccc.jndi.JNDI.exp; |
成功弹出计算器
通过以上实例可以清晰的看到看到,如果lookup()函数的访问地址参数控制不当,则有可能导致加载远程恶意类。
JNDI接口可以调用多个含有远程功能的服务,所以我们的攻击方式也多种多样。但流程大同小异,如下图所示
原理
为了方便远程加载字节码,就别把恶意的字节码放在项目里
我直接使用yakit开启rmi服务
跟进客户端lookup方法
在RegistryContext#lookup中
先获得到远程对象
关键是后面的decodeObject对象
跟进decodeObject
首先就会判断远程对象是否为RemoteReference类型
恰好在服务端绑定的对象为ReferenceWrapper类实例,ReferenceWrapper也是实现了RemoteReference接口
那么会调用远程对象的getReference方法
进入到ReferenceWrapper类中
getReference方法就是返回了所包装的Reference
最终结果就是拿到了Reference(解开包装了),若是其他远程对象,则没有任何变化
继续跟进NamingManager#getObjectInstance
若果传入的是Reference类型对象,那么就会根据Reference中的classFactory和classFactoryLocation获取factory工厂
继续跟进
首先本地加载类,找不到在使用codebase远程加载类
跟进到VersionHelp#loadClass
这里创建URLClassLoader
跟进到loadClass
也是到了最终的方法Class.forName加载恶意字节码,第二个参数为true,就会触发恶意字节码中的静态代码块
JDK高版本限制绕过
JDK 6u132、7u122、8u113后已经默认不允许加载codebase中的远程类,如果想要根据Codebase加载位于远端服务器的类时,java.rmi.server.useCodebaseOnly的值必须为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。
上面高版本 JDK 中无法加载远程代码的异常出现在 com.sun.jndi.rmi.registry.RegistryContext#decodeObject 中
其中修改代码为
绕过这个:
方法一:令 ref 为空,从语义上看需要 obj 既不是
Reference 也不是 Referenceable。即,不能是对象引用,只能是原始对象,这时候客户端直接实例化本地对象,远程 RMI 没有操作的空间,因此这种情况不太好利用;
方法二:令 ref.getFactoryClassLocation() 返回空。即,让 ref 对象的 classFactoryLocation 属性为空,这个属性表示引用所指向对象的对应 factory 名称,对于远程代码加载而言是 codebase,即远程代码的 URL 地址(可以是多个地址,以空格分隔),这正是我们上文针对低版本的利用方法;如果对应的 factory 是本地代码,则该值为空,这是绕过高版本 JDK 限制的关键;
方法三:直接自己本地使用命令行指定com.sun.jndi.rmi.object.trustURLCodebase 参数。
我们可以从本地加载合适Reference Factory。
需要注意是,该本地工厂类必须实现javax.naming.spi.ObjectFactory接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference最后的return语句对Factory类的实例对象进行了类型转换,并且该工厂类至少存在一个getObjectInstance()方法。
Tomcat8
org.apache.naming.factory.BeanFactory就是满足条件之一,并由于该类存在于Tomcat8依赖包中,攻击面和成功率还是比较高的。
org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。
服务端和客户端都要有tomcat8依赖
<dependency> |
poc
import com.sun.jndi.rmi.registry.ReferenceWrapper; |
从BeanFactory#getObjectInstance()方法看
首先判断我们要从工厂生成的类是否是ResourceRef类的实例,接着实例化我们指定的javax.el.ELProcessor。
forceString可以给属性强制指定一个setter方法,这里将属性faster的setterName设置为了public java.lang.Object javax.el.ELProcessor.eval()
Groovy
在Groovy的官方文档(ASTest)中,可以发现的是,Groovy程序允许我们执行断言,也就意味着命令执行
@ASTTest是一种特殊的AST转换,它会在编译期对AST执行断言,而不是对编译结果执行断言。这意味着此AST转换在生成字节码之前可以访问 AST。@ASTTest可以放置在任何可注释节点上。
因此思路和Tomcat相似,借助BeanFactory的功能,使程序执行GroovyClassLoader#parseClass,然后去解析Groovy脚本。
需要依赖
<dependency> |
poc
import com.sun.jndi.rmi.registry.ReferenceWrapper; |