1. 程式人生 > >shiro<1.2.4反序列化分析

shiro<1.2.4反序列化分析

# 0x01、環境搭建 下載地址:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4 環境:Tomcat 8.5.27 + idea 2020.2 + jdk 1.8 +maven 3.6 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210215234228273-1168046504.png) 下載之後之後直接開啟,並open這個web資料夾即可,其他自行百度就行,其中還需要匯入一些jstl的jar等等 # 0x02、漏洞原理 shiro預設使用了`CookieRememberMeManager`,其處理cookie的流程是: ``` 得到rememberMe的cookie值 --> Base64解碼 --> AES解密 --> 反序列化 ``` 然而AES的金鑰是硬編碼的,就導致了攻擊者可以構造惡意資料造成反序列化的RCE漏洞。 payload 構造的順序則就是相對的反著來: ``` 惡意命令-->序列化-->AES加密-->base64編碼-->傳送cookie ``` 在整個漏洞利用過程中,比較重要的是AES加密的金鑰,該祕鑰預設是預設硬編碼的,所以如果沒有修改預設的金鑰,就自己可以生成惡意構造的cookie了。 **shiro特徵:** - 未登陸的情況下,請求包的cookie中沒有rememberMe欄位,返回包set-Cookie裡也沒有deleteMe欄位 - 登陸失敗的話,不管勾選RememberMe欄位沒有,返回包都會有rememberMe=deleteMe欄位 - 不勾選RememberMe欄位,登陸成功的話,返回包set-Cookie會有rememberMe=deleteMe欄位。但是之後的所有請求中Cookie都不會有rememberMe欄位 - 勾選RememberMe欄位,登陸成功的話,返回包set-Cookie會有rememberMe=deleteMe欄位,還會有rememberMe欄位,之後的所有請求中Cookie都會有rememberMe欄位 # 0x03、漏洞復現 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216001908958-531294273.png) 復現文章https://blog.csdn.net/weixin_43571641/article/details/108182722 # 0x04、漏洞分析 簡單介紹利用: - 通過在cookie的rememberMe欄位中插入惡意payload, - 觸發shiro框架的rememberMe的反序列化功能,導致任意程式碼執行。 - shiro 1.2.24中,提供了硬編碼的AES金鑰:kPH+bIxk5D2deZiIxcaaaA== - 由於開發人員未修改AES金鑰而直接使用Shiro框架,導致了該問題 ## 4.1、加密 那既然我們要分析,那入口點在哪呢?Shiro≤1.2.4版本預設使用`CookieRememberMeManager` ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216003519643-1322923860.png) 而我們看看這邊`CookieRememberMeManager`類繼承了`AbstractRememberMeManager`,我們進去看看是什麼梗 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216003652250-494315373.png) 我們可以看到這邊這個類裡面有硬編碼。然後它又繼承了`RememberMeManager`介面;我們繼續進去看看是怎麼回事 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216003854886-1017970004.png) 看名字的話可以知道這些是登陸成功,登陸失敗,退出的一些service;既然如此,肯定會呼叫這個登陸成功的介面,然後再去實現這個介面。所以我們直接在這個介面下個斷點,看看是怎麼個流程; ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216004245093-348365724.png) 這裡看到呼叫了`isRememberMe()`可以發現這個就是一個判斷使用者是否選擇了`RememberMe`選項。而我們是勾選了的 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216101136102-1067534965.png) 所以我們我們條件滿足,這邊判斷返回True,我們則進入`this.rememberIdentity(subject, token, info);` ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216101359810-98888576.png) `subject`儲存的一些登陸資訊如session等等,而`authcInfo`儲存的則是使用者名稱; ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216101644241-551989335.png) **而PrincipalCollection是一個身份集合,因為我們可以在Shiro中同時配置多個Realm,所以呢身份資訊可能就有多個;因此其提供了PrincipalCollection用於聚合這些身份資訊**,具體我們不細講,不深入去懂原理。 然後我們再F7繼續跟進`this.rememberIdentity(subject, principals);` ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216101904720-504492241.png) 這我們有點懵,將身份資訊幹嘛?我們進入該`convertPrincipalsToBytes()`方法檢視; ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216102514013-142367069.png) 看到了`serialize()`方法,難道這邊開始是進行序列化了還是啥? ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216102649184-523171584.png) ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216102819811-915443070.png) ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216102842710-810442963.png) 通過此處我們可以知道是跳了兩層,到`DefaultSerializer`類的`serialize`方法;看到這裡就懂了,這裡先轉為byte,寫入緩衝區;然後進行了一個序列化,最後通過`toByteArray()`方法返回序列化後的Byte陣列。 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216103419747-245089420.png) 然後返回到原來的地方`convertPrincipalsToBytes()`內,接下來if判斷`getCipherService()`方法不為空,則進入條件裡面裡面。我們f7進去內部看看; ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216103627969-252163348.png) 發現又是一個`cipherService`,這是什麼;我們翻譯一下,因為大部分開發都會用簡稱; ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216103706536-759161784.png) 也就是獲取密碼服務?? 什麼密碼服務?我們再繼續F7跟進發現直接推出了。那我們就 `Ctrl+左鍵` 繼續進去看。可以,發現是new了一個aes加密服務。 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216103930130-1260345931.png) 那我們點選debugger處,回到剛剛那個地方;我們就不用繼續進入了,我們就思考一下,這邊是要獲取到加密服務,如果沒獲取到,則不進入。獲取到的話,則進入該條件; ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216104348928-725406925.png) 直接F8下來,進入,然後我們再手動新增變數監視。可以發現正如我們所想的,獲取aes加密服務; 然後呼叫`encrypt()`方法,而懂點英文的,都知道這個單詞是加密的意思。那我們初步判斷這是個加密方法。我們f7跟進去看看什麼情況。 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216104606527-1526200089.png) 我們可以知道這個引數是`byte[] serialized`,也就是說,此處加密我們剛剛的序列化流的資料。 然後這邊`this.getCipherService()`我們剛剛手動新增變數查看了,這邊是獲取到了aes加密服務;然後判斷不問空,那肯定不為空啊,剛剛上面分析過了。然後我們進入條件判斷股內部。 ```java ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey()); ``` 這裡呼叫`cipherService.encrypt()`方法並且傳入序列化資料,和`getEncryptionCipherKey`方法。加密過程,我們就應該不怎麼感興趣了;有興趣的可以自己研究 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216105025534-848416519.png) 我們通過`getEncryptionCipherKey()`名字可以知道是獲取key的一個方法。那我們f7進入看看 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216105456508-1094255297.png) 哦豁,那我們再進一層看一下;發現直接就返回了,emmmmm....怎麼跟別人不一樣。那我們就不追了 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216110041886-1932473852.png) 第一步有說到,硬編碼儲存在這個地方,而構造方法就在這下面,可以看到這邊設定了key。 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216110142805-870702023.png) 我們繼續回到原來的地方,知道這邊是獲取加密的key就ok了。然後這邊使用平臺的預設字符集將字串編碼為 byte 序列,並將結果儲存到一個新的 byte 陣列中。那我們加密部分就結束了 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216110646327-889795440.png) ## 4.2、解密 由於此處,我找不到,回溯不到,那咋辦,煩惱;最後想到了我們加密的入口~~ ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216112354898-1679285620.png) ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216141345048-1360837108.png) ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216113151380-967615174.png) 既然自動跳到了這裡,那麼我們就直接在此處下個斷點,重新開始 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216113527785-865232169.png) 隨後我們進入這個`getRememberedSerializedIdentity()`方法,看看是什麼東西。此處我們依然還很懵,沒事; ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216113629436-1713371405.png) 一直f8,期間倒是沒有什麼有意思或者重點的地方; ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216113748787-777240659.png) 直到我們走到這裡,這個有一個`this.getCookie().readValue(request, response)`,這是要讀取cookice中的資料了,這必須跟入了; ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216135253939-274211510.png) 這裡給進到了這個`readvalue()`方法中了,我們先看看什麼情況。根據名字可以知道是讀取值的一個方法。讀取什麼值?請求包的值。 通過`getName()`方法得到了key為remeberMe。然後把value置空,再通過`getCookie`獲取到cookie。最後判斷cookie不為空,則進入內部;隨後獲取到cookie的值;值則為序列化內容。然後再 return回序列化內容; ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216140057575-1595594053.png) 隨後返回到上一處地方現在`remeberMe`的值不是`delete`;而是序列化內容,所以進入到第二個條件分支。 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216140400211-775906893.png) 一直到這一步,進行base64解碼,成為二進位制資料,給了decoded的byte陣列; ``` 得到rememberMe的cookie值 --> Base64解碼 --> AES解密 --> 反序列化 ``` 目前只進行了Base64解碼,那還需要aes解碼。我們繼續跟進 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216140726951-1327634783.png) 返回到了上層,此處我們知道bytes是二進位制資料,我們看看條件判斷。當bytes陣列不為空且長度大於0時,進入裡面。那我們肯定滿足,所以我們兩步f8加一步F7進入到 `convertBytesToPrincipals`看看是什麼 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216140931538-1486627714.png) 可以看出我們接下來的步驟要依依實現了。判斷key不為空,然後進入內部 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216141057047-1332921613.png) ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216141121269-1717435730.png) 而從這裡開始,就是進行aes解密的步驟了,我們F7跟進方法檢視 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216141301081-792012280.png) 這裡重新把惡意的bytes陣列重新賦值給`serialized`,然後再獲取加密服務:AES/CBC/PKCS5Padding ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216141632223-1598705303.png) 同時到達了下一步;真真正正的開始解密了,其中兩個引數,第一個是加密的bytes陣列,第二個是獲取到key,也就是硬編碼;我們 就直接進入`decrypt()`方法中 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216141904490-332132383.png) 解密過程的話,我不擅長密碼學,這種看著我頭暈,涉及到aes啥的加密解密我就會跳過。所以依舊一樣,跳!!! ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216142458819-1402118634.png) 此處繼續返回到了上一層,我們可以看出這個byteSource是aes解密出來的序列化流,然後再預設字符集將字串編碼為 byte 序列,並將結果儲存到一個新的 byte 陣列`serialized`中,那接下來我們就差反序列化了 ``` 得到rememberMe的cookie值 --> Base64解碼 --> AES解密 --> 反序列化 ``` 我們繼續return,返回到上一層 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216142822176-143446107.png) 顧名思義,一看名字就知道是反序列化的方法,我們跟進`deserialize()`方法檢視 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216142927986-1109940045.png) 看到還有一層,我們繼續F7跟進 ![](https://img2020.cnblogs.com/blog/2099765/202102/2099765-20210216143141726-218784542.png) 形成反序列化漏洞的話,沒有`readObject()`怎麼可能呢?所以我們看到了最後一道光,就這麼愉快的結束了。 # 0x05、總結 其實這個還是得學習學習加密解密的方法,才能進行編寫poc,但是此處只是瞭解個思路。具體可參考其他文章;我空餘時間的話,會繼續補全該篇文章,先去搞其他的玩意兒 [P牛部落格](https://zeo.cool/2020/09/03/Shiro 550 反序列化漏洞 詳細分析+poc編寫/#解密過程:) https://www.anquanke.com/post/id/225442#h2-7 https://mp.weixin.qq.com/s/ayZKDVnN7zEbKjo