作者:天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/qlg3IzyIc79GABSSUyt-OQ
1. Jdk7U21漏洞簡介
談到java的反序列化,就繞不開一個經典的漏洞,在ysoserial 的payloads目錄下 有一個jdk7u21,以往的反序列化Gadget都是需要借助第三方庫才可以成功執行,但是jdk7u21的Gadget執行過程中所用到的所有類都存在在JDK中,JRE版本<=7u21都會存在此漏洞
2. Jdk7u21漏洞原理深入講解
2.1 漏洞執行流程
整體的惡意對象的封裝整理成了腦圖,如下圖所示

這里用到了TemplatesImpl對象來封裝我們的惡意代碼,其封裝和代碼執行的流程在《Java 反序列化系列 ysoserial Hibernate1》中針對這種利用已經進行了詳細的講解,基本原理是通過動態字節碼生成一個類,該類的靜態代碼塊中存儲有我們所要執行的惡意代碼,最終通過TemplatesImpl.newTransformer()實例化該惡意類從而觸發其靜態代碼塊中的惡意代碼,關于TemplatesImpl的詳細分析可以去查看java 反序列化系列 Hibernate1中去學習了解。
首先最外層的是LinkedHashSet 類,看過該類源碼的同學應該都清楚,該類其實是基于HashMap實現的。我們首先來看LinkedHashSet的readObject方法。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap
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));
// 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);
}
}
}
該方法最后可以看到有一個for循環,將LinkedHashSet對象在序列化時一個一個被序列化的元素在反序列化回來。該循環體中有一行代碼 map.put(e,PRESENT) 這里的map變量指向的是一個LinkedHashMap對象,PRESENT常量的值是一個空的Object對象由下圖可知

此時的變量e指向的是我們實現封裝進LinkedHashSet里的TemplatesImpl對象,里面存有我們的惡意代碼

接下來我們來看LinkedHashMap.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值的hash是否一致 如果不一致則證明是一個新的元素從而加入到當前的HashMap對象中,如果hash一致則進行判斷該元素是否存在于當前的HashMap中如果存在則返回oldValue,如果不存在則加入當前HashMap對象中。
這里核心關鍵點就是如何讓程序執行到key.equals,此時的key指向的是我們通過動態代理生成的Proxy對象,我們知道調用Proxy對象的任何方法,本質上都是在調用,InvokcationHandler 對象中被重寫的invoke方法。因為生成Proxy對象時傳入的參數是InvokcationHandler的子類AnnotationInvocationHandler,所以自然要調用AnnotationInvocationHandler.invoke()方法。
我們來看該方法的具體實現

通過觀察代碼我們可以看到接下來會調用equalsImpl()方法,傳入的var3參數是封裝了我們惡意代碼的TemplatesImpl對象
private Boolean equalsImpl(Object var1) {
if (var1 == this) {
.....
} 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);
......
在這里我們可以看到有這么一行代碼var8 = var5.invoke(var1);這里就會調用TemplatesImpl.newTransformer()從而實例化惡意類,這里的var1我們清楚是我們傳遞進來的TemplatesImpl對象,但是var5的結果是怎么來的還需要分析一下。
從代碼中可以看到Method var5 = var2[var4]; var4=0 而var2= this.getMemberMethods();
跟入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();
AccessibleObject.setAccessible(var1, true);
return var1;
}
});
}
該方法會循環獲取AnnotationInvocationHandler.type中的方法,我們可以看到type對象指向了一個Templates.class對象

Templates是一個接口,該接口中只有兩個抽象方法

所以getMemberMethods()方法返回的結果就是兩個Method對象,一個是newTransformer的Method對象,一個是getOutputProperties的Method對象,這樣我們是如何通過反射調用的TemplatesImpl.newTransformer()方法的邏輯就清晰了

2.2 如何構造滿足條件的hash值
但是有一個問題還沒有解決,那就是剛才所講的所有代碼邏輯,都要在key.equals(k)可以執行的前提下才可以,那么究竟怎樣才能執行key.equals(k)呢,我們來重新看一遍LinkedHashMap.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))) {
......
}
可以看到 需要滿足一些條件 才可以執行到key.equals(k)接下來就詳細講一講如何才能滿足以上這些條件,這是筆者個人覺得整個漏洞利用中最難也是最讓人拍案叫絕的思路。
首先第一次調用map.put()時傳入的參數e是我們封裝了惡意代碼的TemplatesImpl對象,另一個參數就是一個空的Object對象

由下圖代碼可知,我們需要計算出key 也就是惡意TemplatesImpl對象的hash值

深入看hash方法的實現
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
這里調用TemplatesImpl.hashCode()方法來得出hash值然后進行固定的異或操作,得出的最終結果進行返回,下面的截圖中就是此次運算得出的hash值

接下來通過indexFor()函數 得到其hash索引 這里返回的索引值是12,并將值符給變量i 這里傳入的table.legth,table是一個Entry數組,用來存放我們通過map.put()傳入的鍵值對,并作為后續判斷新傳入的鍵值對和舊鍵值對是否重復的依據
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}

接著就開始了第一次判斷,首先當前table變量指向的Entry對象是空的,所以自然e 為null 在這里就不符合了,所以循環體內的代碼不會執行
for (Entry<K,V> e = table[i]; e != null; e = e.next)
跳過for循環體,然后計數器自增,并將此TemplatesImpl對象本身,還有其Hash值和索引放入到之前說到的table變量中。

接下來就開始第二次循環了,第二次傳入的key就是觸發TemplatesImpl.newTransformer()的媒介 Proxy對象了這個對象里有我們特意封裝進去的AnnotationInvocationHandler對象。

接下來問題就來了首先for循環中要滿足e不為空,這就要求這次循環并計算Proxy對象從而得出的Hash值和Hash索引必須和上一次循環中的TemplatesImpl對象相同,這樣才能在Entry<K,V> e = table[i]這一步中,從table中取到對應索引的對象賦值給e,從而滿足e != null 。
for (Entry<K,V> e = table[i]; e != null; e = e.next)
那怎么才能讓兩個連類型都不相同的對象通過運算卻能得出一樣的hash值呢?接下載關鍵點就來了,也就是我們為什么生成Proxy對像時要傳入AnnotationInvocationHandler對象。
在計算Proxy對象的hash值的時候 我們看到最終是通過調用Proxy.hashCode()來計算hash值

Proxy是一個動態代理對象,所以經過對調用方法名稱的判斷,最終調用AnnotationInvocationHandler.hashCodeImpl()方法

以下是hashCodeImpl方法的實現,此時的var2是一個Iterator對象,用來遍歷memberValues對象中存儲的鍵值對
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;
}
可以看到memberValues中只有一個鍵值對就是,就是我們在初期通過反射生成AnnotationInvocationHandler對象時傳入的HashMap對象中的那個鍵值對 key是一個字符串"f5a5a608" Value值適合第一次循環時用來計算hash值的同一個TemplatesImpl對象



我們在看一看var3此時的值。

AnnotationInvocationHandler計算hash最關鍵的是這一段代碼。簡單來說就是127乘var3 key的hash值,然后和var3的value值的hash值進行異或操作
var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())
下面貼出memberValueHashCode方法的關鍵代碼,返回var3的value值也就是TemplatesImpl對象的Hash值。
private static int memberValueHashCode(Object var0) {
Class var1 = var0.getClass();
if (!var1.isArray()) {
return var0.hashCode();
至此所得到的結果都是和第一次循環時得到的Hash值相同,但接下來就要解決如果在經過與127 * ((String)var3.getKey()).hashCode()進行異或操作后,保持結果不變。
我們知道0和任何數字進行異或,得到的結果都是被異或數本身。所以我們要讓127 * ((String)var3.getKey()).hashCode()的結果等于0 也就是(String)var3.getKey()).hashCode()的值要為零
還記得我們var3的 key是什么么?是一個字符串 值為"f5a5a608" 這個字符串非常有意思我們看一下這個字符串的hash值是多少


結果是0,完全符合我們的要求,這樣127乘以0自然結果是0,0在同TemplatesImpl對象的hash值進行異或,得到的結果自然也是TemplatesImpl對象的hash值本身。這樣就符合我們的要求。通過了LinkedHashMap.put方法中的for循環的判斷,由于hash值相同,所以計算出的索引相同,e的值就為之前的TemplatesImpl對象,所以e不為null 結果為true
for (Entry<K,V> e = table[i]; e != null; e = e.next)
接下來好要通過if 判斷中的前兩個條件,因為&& 和|| 有短路效果,所以這三個條件我們要符合e.hash == hash為true (k = e.key) == key為flase
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
首先e.hash == hash是將第一次循環時的TemplatesImpl對象的hash取出同第二次循環時TemplatesImpl對象的hash進行對比,本來就都是同一個對象,所以自然時相同的,所以結果為true
(k = e.key) == key 將第一次循環時的key取出和第二次循環時的key做比對看是否相同,第一次循環的key是TemplatesImpl對象,而第二次循環時key時Proxy對象,所以結果為flase
如此這般,我們就通過了前兩個判斷條件,接下來自然就會執行key.equals(k)從而調用TemplatesImpl.newTransformer()方法并最終觸發我們的惡意代碼
至此jdk7u21漏洞原理分析完畢
3. 總結
此次Jdk7u21 payload中作用到的所有類,均存在于JDK自身的代碼中,無需再調用任何第三方jar包,所以當時爆出漏洞時影響極大。只要目標系統中使用的jdk版本并存在反序列化數據交互點就會存在遠程代碼執行漏洞。漏洞的觸發點在LinkedHashSet,其實我們看代碼的時候可以看到LinkedHashSet里面的方法都是調用了其父類HashSet中的方法,但是之所以不直接用HashSet的原因是LinkedHashSet里數據的下標和我們插入時的順序一樣,而HashSet順序就不一樣了。通過Hash值的匹配,然后執行到key.equals(k)最終執行到TemplatesImpl.newTransformer()方法
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1224/
暫無評論