java-jndi注入
#JNDI注入
首先在前面大致捋一下基础知识,了解什么是JNDI什么是RMI,然后再讲解利用手法
##JNDI
JNDI全称为 Java Naming and DirectoryInterface(Java命名和目录接口),是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。
JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等。
简单点说,JNDI就是一组API接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索指定的对象,而该对象可能存储在RMI、LDAP、CORBA等等。
按照我的理解,JNDI就是提供用于针对不同的协议提供不同的查询方式的服务。
##JNDI+RMI
###RMI
RMI是java中的一种协议,类似于http,加了一个rmi头,java就会使用rmi的协议请求方法去请求
###JNDI+RMI服务编写
实现对象
创建接口:
package org.example; |
实现接口功能:
package org.example; |
客户端:
package org.example; |
服务端:
|
##JNDI+RMI注入
我们首先让服务端重新绑定至恶意类
package org.example; |
然后这个T就是我们的恶意类:
只需要定义构造函数就行
import java.io.IOException; |
此时,我们再访问这个rmi服务的时候,就会触发这个远程类了
接下来,我们跟进一下lookup函数调用的过程,看看其是如何调用远程类的,再经过多层lookup函数调用以后,会获取远程访问的类,这个时候就已经获取了我们的恶意类了
接下来就会调用loadClass加载这个类
加载完以后,使用newInstance实例化这个类,此时构造函数就会被触发了
可以发现,JNDI注入的关键点在于,lookup函数的内容可控
###高版本
在JDK 6u141、7u131、8u121之后,增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项。
那么此时我们就无法像前面一样,可以那么单纯的直接使用lookup函数进行命令执行了
此时就会直接抛出异常,导致获取不到远程类
那么如何绕过呢?
####高版本jdk绕过
仔细观察前面rmi流程,可以发现,其实是在namingmanager的地方获取恶意类的,而前面的限制也只是存在于禁止远程访问而已
可以发现上面if有三个条件判断
- 其中第一个判断的r需要不是引用对象,我们从远程对象引用的基本就是引用对象了,这里就不太有操作的空间
- 而第二个通过函数返回了下面远程地址的值,如果这个是null的话,也会绕过,如果对应的 factory 是本地代码,则该值为空,这是绕过高版本 JDK 限制的关键
- 第三个就是默认的参数,只能自己配置
所以我们只需要找到一个本地的factory,触发至这个namingmanager即可
从第二个方法入手,这里我们需要关注getFactoryClassLocation函数:返回classFactoryLocation属性
而这个属性本来是null值
在下面构造函数过程中被传入
那么我们这里的Location要不传值,就只能利用本地了,那么就找找本地有没有利用的factory
跟进一下NamingManager.getObjectInstance
方法
关注这部分代码,可以发现这里会执行factory
这个类的getObjectInstance
方法
所以我们首先需要找到一个类并且可以执行getObjectInstance
方法,除此之外,这个类还需要实现ObjectFactory
这个接口,才能使用getObjectInstance
方法
根据前面的分析,总结一下我们需要
- 寻找目标本地的工厂类
- 该工厂类需要实现javax.naming.spi.ObjectFactory
- 存在一个getObjectInstance()方法
这里说一下如何找到的,首先我们知道需要实现ObjectFactory接口,所以直接用idea打开看看,直接找到这个借口,然后点击旁边的小标志就可以
在Tomcat8的依赖包中org.apache.naming.factory.BeanFactory
就满足上述条件,首先实现了javax.naming.spi.ObjectFactory,并且存在getObjectInstance方法,可以反射执行
接下来就是构造payload:
1.首先观察第一个判断,其必须属于resourceRef类的实例化对象
获取类名以后,会对其进行实例化,并且需要关注的是此处为class.newInstance,而不是使用construcot来进行newInstance,所以这里是无参构造方法,再往下审计,可以发现
这里会获取forceString字段,而这个字段是从resourceref类中的refaddr获取到的
在这里获取到forceString的值
分为两部分,contents和addrtype,其中addrtype是我们需要传入的执行的函数
其中addrtype是我们需要传入的执行的函数
首先会定位,是否含有=号,如果有的话,会将后面的值作为setterName
的值继续向下传递,在这里setterName
会作为后面beaClass要获取的方法,而paramTypes则是String.class,也就是说这里传入的参数是String类型的
再往下,可以看到这里做了一个判断,传入的refaddr中,如果不为里面的内容如forced,则会进入到接下来的代码中
获取这个参数的content
再往下,会获取这个参数的method
并将前面获取的这个参数的content作为要执行的值,最后invoke反射执行
分析完前面的流程,我们可以总结一下:
首先传入一个resourceRef类,其factory选择org.apache.naming.factory.BeanFactory
,而要调用的恶意代码执行类需要满足
- 无参构造
- 有能够执行String类型参数的方法
除此之外,在传值的过程中,根据前面的分析,在RefAddr的参数构造过程中,我们需要构造出如下格式而满足上述恶意类的要求中,我们可以选择force x=(function)
x paramjavax.el.ELProcessor
,并使用eval来执行String类型的命令
1.首先先构造一个ResourceRef
类,然后其中传入我们能够无参恶意类,这里的参数构造,我们可以直接看其构造函数就可以知道为啥要这样传入了
ResourceRef ref = new ResourceRef("javax.el.ELProcessor",null,"","",true, |
2.传入refaddr参数
使用其add方法
ref.add(new StringRefAddr("forceString","x=eval")); |
因为要使用 javax.el.ELProcessor,所以需要 Tomcat 8+或SpringBoot 1.2.x+
小结:
由于使用的是javax.el.ELProcessor类,所以是需要有tomcat8及更高版本环境下通过该库进行攻击
工具:
使用 https://github.com/welk1n/JNDI-Injection-Bypass,放在服务器上启动一个恶意 RMI Server
https://github.com/mbechler/marshalsec
##JNDI+LDAP注入
使用LDAP协议同样也可以实现jdni注入
###低版本JDK运行
在低版本的jdk中,过滤了rmi协议以后,依旧可以ldap来进行绕过
不同的是,LDAP服务中lookup方法中指定的远程地址使用的是LDAP协议,由攻击者控制LDAP服务端返回一个恶意jndi Reference对象,并且LDAP服务的Reference远程加载Factory类并不是使用RMI Class Loader机制,因此不受trustURLCodebase限制。