原文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,然后后面的過程就同之前分析的一致了

