原文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 |
<dependency> |
例如
1 |
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; |
將D:/Evil/Evil.calss拖入IDEA即可反編譯,可以看見javassist動態構建出了如下類
至於為什么要繼承AbstractTranslet,和構造函數中寫命令執行的payload就涉及到下面POC的構造,暫時只需要了解javassist的大概功能
TemplatesImpl
之前參考網上分析文章的POC是從ysoserial中修改得來的,代碼中使用了ysoserial的一些類,我修改了一下將POC核心部分單獨提取出來方便理解
1 |
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; |
主要就是利用了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
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance
這里得到POC中兩項屬性的構造條件,即_name
不能為null,_class
為null,然后進入defineTransletClasses()
其實最終的觸發點就在380行_class[_transletIndex].newInstance()
,defineTransletClasses()
是對_class
和_transletIndex
賦值
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses
代碼比較長這里就直接復制出來了
1 |
private void defineTransletClasses() |
defineTransletClasses()
執行完以后,回到getTransletInstance()
,此時_class[_transletIndex]
已經為Evil
類的一個類對象,調用newInstance()
實例化Evil
即可觸發該類構造函數或者靜態代碼塊中的代碼
所以總結以上條件,便可理解TemplatesImpl
的構造
1 |
setFieldValue(templates,"_bytecodes",targetByteCodes); |
動態代理
以上POC是需要反序列化TemplatesImpl
類並調用其getOutputProperties()
方法才能觸發,即可以放入FastJson的反序列化處,但若沒有觸發getOutputProperties()
的點,就需要尋找其他手段
代理是為了在不改變目標對象方法的情況下對方法進行增強,比如,我們希望對方法的調用增加日志記錄,或者對方法的調用進行攔截
假設有一個Person
類實現了IPerson
接口中的say
方法,但現在要在say
方法前后實現一些邏輯,那么借助動態代理實現如下
1 |
import java.lang.reflect.InvocationHandler; |
AnnotationInvocationHandler
AnnotationInvocationHandler
就是一個InvocationHandler
的實現類,也在下面的POC中起到關鍵作用
先貼出整理好的POC
1 |
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; |
可見最后unserialize(obj)
只是反序列化了一個LinkedHashSet
類就觸發了命令執行
Java在反序列化的時候會調用ObjectInputStream
類的readObject()
方法,如果被反序列化的類重寫了readObject()
,那么該類在進行反序列化時,Java會優先調用重寫的readObject()
方法
LinkedHashSet
沒有readObject()
但是繼承自HashSet
HashSet
實現了Serializable
接口並且有readObject()
方法,所以在反序列化LinkedHashSet
時會調用其父類HashSet
的readObject()
,可以在該函數處下斷點運行POC進一步跟蹤調試
java.util.HashSet#readObject
到309行的邏輯是將POC中add到set
的templates
和proxy
加入到map
中,
PRESENT
是一個常量,就是一個新的object對象
繼續跟進put
方法,會在第二次調用map.put
時進入下面的475行的位置,即現在傳入的key
是proxy
java.util.HashMap#put
這段代碼本意是判斷最新的元素是否已經存在的元素,如果不是已經存在的元素,就插入到table中,e.key為前一個元素即templates
,key為當前元素proxy
table[i]
就是一個鍵為我們構造的templates
的Map
當前的e.key
和key
,一個是templates
,另一個是POC中的proxy
,顯然不同,(k = e.key) == key
為false
這條鏈想要完成是需要進入key.equals(k)
的,依據短路特性,那么必須要e.hash == hash
為true,也就是需要滿足 hash(templates)== hash(proxy)
,看起來貌似不可能,但漏洞作者確實做到了(大寫的佩服)
這里hash的繞過方法就暫時放在下面,先接着跟蹤key.equals(k)
由於POC中使用動態代理,這里調用Templates.equals()
就會進入handler
的invoke
sun.reflect.annotation.AnnotationInvocationHandler#invoke
var1
就是上圖中的key
,var2
是equals
方法對象,var3
是傳入的參數數組,即上圖中的k
(TemplatesImpl)
繼續跟入equalsImpl
sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl
分析之前先看一下這個類的相關方法和屬性
首先是構造函數
sun.reflect.annotation.AnnotationInvocationHandler#AnnotationInvocationHandler
在構造handler時ctor.newInstance(Templates.class, map)
即這里的this.type
和this.memberValues
分別是Templates.class
和map
sun.reflect.annotation.AnnotationInvocationHandler#getMemberMethods
並未對this.memberMethods
賦值,所以這里進入if分支,最后返回的是this.type
的所有方法,即Templates
的所有方法
sun.reflect.annotation.AnnotationInvocationHandler#asOneOfUs
判斷var1
對象若是一個AnnotationInvocationHandler
實例的話則轉換為AnnotationInvocationHandler
然后接着看equalsImpl
1 |
private Boolean equalsImpl(Object var1) {//var1是POC中構造的templates |
既然調用了Templates
中的所有方法,自然包括getOutputProperties()
,即完成了命令執行
Hash繞過
java.util.HashMap#hash
hash()
中調用了對象本身的hashCode()
調用hash(templates)
的時候,這個類沒有重寫,調用的是templates
默認的hashCode()
方法
當調用hash(proxy)
的時候,則會跳到AnnotationInvocationHandler.invoke()
sun.reflect.annotation.AnnotationInvocationHandler#invoke
sun.reflect.annotation.AnnotationInvocationHandler#hashCodeImpl
該方法會從memberValues
中進行遍歷,並且依次計算key.hashCode()
,而這個memberValues
是我們在初始化AnnotationInvocationHandler
的時候傳入的map
sun.reflect.annotation.AnnotationInvocationHandler#memberValueHashCode
所以
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
之后
如果放在set.add
之前會直接在本地觸發命令執行,並且得到的序列化之后的數據不能反序列化成功
java.util.HashSet#add
這是因為add方法中會直接調用map.put,然后后面的過程就同之前分析的一致了