從0到1掌握某Json-TemplatesImpl鏈與ysoserial-jdk7u21的前因后果


本文首發於先知社區:

https://xz.aliyun.com/t/7096

前言

作為一名安全研究人員(java安全菜雞),知道拿到exp怎么打還不夠,還得進一步分析exp構造原理與漏洞原理才行。本篇文章主要分析FastJson1.2.24中針對TemplatesImpl鏈的構造原理以及ysoserial中針對jdk7u21基於TemplatesImpl加動態代理鏈的構造原理。內容可能巨詳細,希望沒接觸過這部分的同學可以耐心看下去。

1.TemplatesImpl初相識

FastJson1.2.24中基於com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl這條鏈的入口點在TemplatesImpl的getOutputperties函數。當然本篇文章不再描述具體如何到這一步,有興趣的可以參考我之前的一篇文章。一步一步學習某Json1.2.47遠程命令執行漏洞
因此在下圖所示下斷點在此,這里環境為jdk7u21。

這里首先將調用newTransformer(),首先定義類TransformerImpl的對象,其入口參數第一個為Translet的對象

因為此時調用了getTransletInstance(),跟進看看

其返回的是一個Translet類的對象,這里面判斷_name 不能為null,否則就拿不到Tranlet對象,后面_class能不能為null,先留着,但是這里明顯看到后面要用到_class

這里要用到_class,說明_class里面肯定是有東西的,要么是我們自己賦值,要么就是通過上面的defineTransletClasses()得到了,我們先跟進defineTransletClasses()看看

這里首先判斷_bytecodes不能為null,為null將拋出錯誤信息

接着這里將拿到類加載器,通過它我們就可以對目標類進行加載,我們知道classloader除了調用loadclass來加載類以外,還可以調用findclass里的definedclass來通過加載字節碼來在jvm中加載類,那么它肯定是classloader的子類,跟進看看其確實繼承自ClassLoader,並且也看到了熟悉的defineclass函數,我們只要知道此時經過該類的definclass就能進行類的加載

繼續往下看,下圖中實際上取的是_bytecodes[i],並且i的范圍也是我們可控的,這里我們知道defineClass是可以通過字節數組來在JVM創建類的,所以這里通過將惡意類的字節碼放到_bytecodes里面就能夠加載到JVM里

接着將拿到剛剛加載到JVM中的類的父類要求其父類必須是ABSTRACT_TRANSLET,不滿足則放到_auxlclass里面,要是新加載的所有類的父類沒有一個是ABSTRACT_TRANSLET的話,后面此時_transletIndex < 0處的判斷就要報錯,因為_transletIndex初始值為-1


此時我們已經知道通過defineTransletClasses函數我們可以通過defineclass來從_bytecodes中加載惡意類,所以我們肯定要讓這里的_class為null

我們已經知道_class數組中存儲的是加載進來的惡意類,下標_transletIndex就是該惡意類對應的下標,所以接着就調用newInstance()來實例化我們的惡意類,那么我們把要執行的命令放在惡意類的static區或者構造方法中都可以

其中我們的其中惡意類如下,至於要聲明兩個transform是由於這里是繼承自抽象類,所以在其子類中必須實現,這里不聲明的話idea也會提示讓你實現,idea真香2333~

事實也證明如此,我們可以clac了

並且經過以上分析最終的payload可以縮減為以下形式:
整個調用過程挺短的,實際上就是
TemplatesImpl -> getOutputProperties()
TemplatesImpl -> newTransformer()
TemplatesImpl -> getTransletInstance()
此時在getTransletInstance()函數中將調用惡意類的構造函數,即
_class[_transletIndex].newInstance()導致RCE

2.AnnotationInvocationHandler完美鏈接

這一部分的分析主要就是通過最外層的readObject反序列化直達getOutputProperties()的調用,即newTransformer()的調用。而ysoserial中已經包含了該鏈的構造過程,其getobject函數就能拿到該鏈最外層的對象,而調試ysoserial也很容易,不傳命令的話會默認傳calc.exe

這里我們傳入要執行的命令后調用createTemplatesImpl即可,那么我們可以看看yso中是如何構造該對象的

這里首先通過class.forname獲得了三個我們構造該鏈要用到的類,然后將要執行的命令和這三個類傳入createTemplatesImpl 來返回一個經過精心構造的TemplatesImpl對象

這里首先newInstance()獲得一個初始的TemplatesImpl對象,用於后面的裝飾,然后創建首先創建一個CtClass的容器,我們可以用它按需讀取類文件來構造 CtClass 對象,並且保存 CtClass 對象以便以后使用

Java 字節碼以二進制的形式存儲在 .class 文件中,每一個 .class 文件包含一個 Java 類或接口。Javaassist 就是一個用來 處理 Java 字節碼的類庫。它可以在一個已經編譯好的類中添加新的方法,或者是修改已有的方法,並且不需要對字節碼方面有深入的了解。同時也可以去生成一個新的類對象,通過完全手動的方式。
由上面的解釋也可以看出來這是一個功能強大的類。這里將首先會插入一個最原始的模板類,這個也就是作為我們用來執行命令的惡意類

通過以下這句代碼我們就能夠獲得惡意模板類的對象,通過它我們就能夠對該類的結構進行修改

然后將為該惡意模板類創建靜態代碼塊,並插入rce的代碼

這里rce的代碼可以后面自己改,因此也可以自己根據需求定制

接着就是為該類起名字和設置該類的父類為abstranlet

接着就是獲取該經過加工以后的類的字節碼,以字節數組的形式保存,並通過反射的方式來設置templatesImple對象的_bytecodes變量值

其中setFieldValue函數第一個參數就是我們要設置的對象,第二個為屬性,第三個參數為要設置的值

以上就完成了templatesImple的構造和惡意類的構造,但是如果不結合fastjson的反序列化特點的話就要找到一個新的readobject來鏈接到該templatesImple觸發點,我們直接在hashset的readObject的中下斷點進行調試,因為最終返回的是linkedHashSet的對象,因此入口點即在HashSet的readObject()函數

這里實際上將hashSet中的對象調用readObject()函數反序列化讀出來然后放到有序列表的map中

由map.put就即將進入漏洞觸發分析,因為后面要用到動態代理。所以這里簡單分析一下這個技術:
首先要定義被代理的接口及其實現該接口的子類

接着要定義代理類,需繼承自InvocationHandler,也就是位於被代理類處理順序之前的類,在其構造函數中傳入被代理類的對象,當調用被代理類的函數時將觸發代理類的invoke函數,此處是重點,通過反射機制來實現

定義完被代理類以及代理類之后,還需通過Proxy類將兩者進行綁定方可使用,這里要用Proxy.newProxyInstance來創建代理對象,通過其即可完成被代理的類與動態代理的綁定,然后通過該proxy對象就可能對被代理的類的函數進行調用,從而觸發動態代理

運行結果如下圖所示

在invoke處下個斷點也可以清晰的看到此時method為hello,this.subject為SubjectImpl對象,args為world,即通過為被代理類綁定代理將可以在代理中運行新的代碼塊

了解了動態代理技術之后,就可以順理成章地引入AnnotationInvocationHandler了,它就是一個動態代理,其繼承自InvocationHandler

在其構造函數中有兩個成員變量,兩個均可控,並且在yso的payload中也通過反射機制為其this.type賦值為Templates類,並在newInstance中為memberValues賦值為只有一個鍵為f5a5a608,值為foo的map,當然后面將會對該鍵對應的值進行覆蓋,放入惡意templatesImpl的對象,至於為什么要這樣賦值后面說

目前我們只要知道這里是讓AnnotationInvocationHandler作為Templates接口的代理。回到yso的paylaod,繼續往下看,這里在linkedhashset中放了兩個對象,其中linkedhashset是繼承自hashset類的,放入的元素第一個是惡意的templateImpl,第二個是代理類handler

那么因為這里為了調試我們之前已經直接在hashset的readobject處下過斷點

此時第一次反序列化得到的即為放入的惡意templateImpl類的對象,然后將其放到map中

第二次反序列化得到的即為臨時的proxy對象,代表的接口Templates類,這里的map.put即是漏洞的入口點

跟進map.put看看,我們知道一個Map中不能包含相同的key,每個key只能映射一個value,但是兩個key的hash可以一樣,那么就放在相同的hash桶里,那么能不能插入新的值,put內部肯定是有一定的判斷邏輯的,那么這里面就包含了動態代理的觸發

put函數首先要對要放入的key計算一個hash,此時key為proxy對象,跟進此函數看看

其將會調用key.hashCode函數,那么我們知道當調用proxy對象的函數時將觸發動態代理類的invoke函數,因此此時此時從下圖①或②中都能夠看到已經成功通過proxy對象進入到動態代理類AnnotationInvocationHandler當中

此時判斷我們調用的是hashcode函數,將會進一步調用hashCodeImpl函數

在這個函數內部才是真正對map的key進行一個hash的計算

這里實際上將用127乘對AnnotationInvocationHandler的memberValues的鍵計算的hash以及值計算的hash,那么之前我們分析yso的payload時知道賦值給membervalue的鍵為鍵為f5a5a608,值為惡意的templateImpl對象,那么此時這個循環將執行一次並且計算key的hascode為0,那么實際上var1的值即為membervalue的鍵對應值的hascode,其值為惡意的TemplatesImpl對象

此時計算得到的hash為106298691,那么為什么要這么設置呢,之后就可以明白

計算完的hash還要經過移位操作然后得到最終的hash值為104622798


回到map.put函數的if判斷,那么此時e.hash就是計算map第一個鍵的hash,而map第一個鍵就是惡意的TemplatesImpl對象,因此計算其hash肯定為104622798

所以之所以yso的payload要這么設置正是因為如此,也就是map的鍵為什么要設置為f5a5a608的原因,繼續往下看

這里用Entry來對要放入的map的中的鍵進行遍歷,其Map.Entry是Map聲明的一個內部接口,此接口為泛型,定義為Entry<K,V>,它表示Map中的一個實體(一個key-value對),接着看這個判斷:

其中有&&連接兩部分
① e.hash == hash
② ((k = e.key) == key || key.equals(k))
那么此時先理清key和k都是什么,這里的key和k如下圖所示,這里的key就是Templates類型的proxy對象,k就是之前第一次放入map中的惡意的Templates對象,此時作為equals函數的入口參數,實際上調用的被代理對象的equals方法,那么這里正和我們的思路,那么想要觸發動態代理,我們知道e.key是TemplatesImpl的對象,key是Templates類型的proxy對象,那么判斷肯定不成立,那么就能夠執行或邏輯右邊的表達式,那么此時條件①的已經滿足,因此直接調用key.equals(k)

那么實際上這里就跳又到annotation這里了,就是我們之前設置的動態代理類,直接到invoke函數處,判斷調用的是equals函數

這里將會又再次跳到equalsImpl函數,其中入口參數var3[0]為傳入的TemplateImpl惡意類,繼續跟進

此時1處templateImpl肯定不等於annotation並且2處這里this.type在yso的payload中設置為下圖所示,通過反射令其type為Templates類

所以這里就是判斷templateImpl是不是Templates類的對象,因為Templates是TemplatesImpl的父類,那么這肯定為true

注:
class.inInstance(obj)
這個對象能不能被轉化為這個類
1.一個對象是本身類的一個對象
2.一個對象能被轉化為本身類所繼承類(父類的父類等)和實現的接口(接口的父接口)強轉
3.所有對象都能被Object的強轉
4.凡是null有關的都是false ,即class.inInstance(null)



此時將會調用getMemberMethods()函數,這個函數內部實際上就是返回annotation這個類的type變量對象的類的所有方法,那么這里實際上返回的就是Templates這個類的兩個方法了
①.Transformer newTransformer()
②.Properties getOutputProperties()

接下來就到了最終的漏洞觸發點,我感覺叫鏈接點比較好,在這里通過反射機制來調用newTransformer(),其中var1就是我們之前構造的惡意的TemplatesImpl類

這里我們來回顧一下getOutputProperties處,其中下面的newTransformer和上面反射的newTransformer完美的符合在一起

此時由函數調用棧也可以看到此時回到了TemplateImpl這個類中,至此利用鏈分析結束

3.從jdk7u25和jdk7u21的對比中分析修復

jdk7u25是jdk7u21的后一個版本,運行后結果如下圖所示

那么上面分析的利用AnnotationInvocationHandler作為動態代理打到newTransformer在jdk7u25中已經被修復,運行時將會報錯如上圖所示,其中是在反序列化的過程中有一步是通過反射機制調用了AnnotationInvocationHandler的readObject函數

我們知道在yso的payload中通過反射機制來給AnnotationInvocationHandler的type賦值為Templates類

那么再次執行下圖代碼:

當執行hashset中第二個proxy對象的readObject時,實際上將會在其中調用AnnotationInvocationHandler的readObject函數來恢復動態代理,最終到readObject函數處

此時的type為javax.xml.transform.Templates,進一步調用AnnotationType.getInstance

跟進看看getInstance函數

此時進一步調用Templates.getAnnotationType()函數

此時返回為null,繼續返回getInstance中

此時var1為null,那么將Templates實例傳入Annotationtype的構造函數

此時在AnnotationType的構造函數將調用isAnnotation對var1進行判斷

那么明顯Templates類跟annotation沒關系,可以看到annotation和其他兩種數據類型就是java類類型里面定義的,屬於class類,即引用數據類型。比如最常見的枚舉,枚舉類型是Java 5中新增特性的一部分,它是一種特殊的數據類型,之所以特殊是因為它既是一種類(class)類型卻又比類類型多了些特殊的約束,但是這些約束的存在也造就了枚舉類型的簡潔性、安全性以及便捷性。這里實際getModifiers()就是取Templates的修飾符,而Templates是接口類型的,所以兩者無關,所以二進制位不可能有所重合,即肯定&&后為0。

所以此處必定進入if拋出錯誤

所以回到AnnotationInvocationHandler的readObject中將捕獲到該錯誤從而拋出Non-annotation type in annotation serial stream

再回到jdk7u21里面,我們可以看到這里雖然在annotationtype中也捕獲到了type不滿足條件,並拋出了錯誤

但是catch以后直接retuen了,並沒有再次拋出錯誤,讓上層捕獲,因此流程將繼續走下去

所以反序列化將會繼續執行,並且能夠恢復我們的動態代理Templates,感覺是個邏輯錯誤,開發人員可能一不注意就會犯錯,要發現這些點對於安全研究人員來說開發技能也是必備的。

總結

前前后后分析下來也花了幾個晚上,真是學到了不少。整個漏洞利用中包含了很多java中的技術點,最大的感受就是Java的反射特性真的是太重要了,可以說是無處不在2333。挖掘漏洞需要大量調試和分析,補漏洞在這里拋出一個錯誤就可以讓漏洞消失,當然調試的過程中也更加熟悉了java這門語言。

 

 

補充:

hashset用來存儲對象(內部還是基於hashmap存儲)

hashmap存儲鍵值對,hashmap速率高於hashset

 

 在hashset的readobejct時map.put放入獲取的臨時內部proxy類來表示被代理的接口為Templates,代理類為一個hashmap,里面存的鍵為f5...值為templatesImpl的對象


免責聲明!

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



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