1. 簡介

用戶端 session cookie 通常依賴加密簽章來保護完整性。然而,簽章並不會加密 session 內容,其安全性取決於保持 signing key 機密。在 Simon Koeck 描述的 Google DevSite 案例中,儲存在 Flask session 中的 OAuth flow object 暴露了 OAuth client secret。該應用程式還將該 secret 重複用作 Flask session signing key,使攻擊者能夠偽造 session 資料。偽造的 session 隨後可以到達 OAuth callback 中 不安全的 pickle 反序列化 路徑,導致在受影響的應用程式條件下產生未經認證的 remote code execution (RCE) [1] 。

這是一個針對應用程式的漏洞鏈,這不是在指 Flask 預設的 session 本身用 pickle,也不是說它天生有這種 RCE 漏洞。Flask 的標準用戶端 session 儲存已簽章的 JSON 資料;在報告的案例中,session 欄位攜帶了一個 Base64 編碼的 pickle object,應用程式後來對其進行了反序列化。外部 session 格式與巢狀序列化物件之間的區別是理解此問題的關鍵。

為什麼 DevSite 的 OAuth 與 pickle 機制會變成駭客眼中的大禮包? | 資訊安全新聞

2. 技術架構與利用序列

受影響的流程涉及 /oauth2authorize 和 /oauth2callback 端點。在授權啟動期間,應用程式將 OAuth flow state 放置在用戶端 session 中。對 cookie 的分析揭示了一個內嵌的 flow object,其中包含 client secret。由於相同的 secret 被重複用作 Flask session signing key,擁有該值會破壞 session 的完整性保護。偽造的 session 然後可以向 callback 的 pickle 反序列化 sink 提供攻擊者控制的序列化位元組 [1] 。

sequenceDiagram autonumber participant A as Analyst / Attacker participant S as Target Server A->>S: GET /oauth2authorize?return_url=/ S-->>A: Redirect and session cookie Note over A: Decode outer session payload Note over A: Inspect nested flow object and identify exposed secret Note over A: Assess signing-key reuse A->>S: Request OAuth callback with a forged session Note over S: Session signature validation succeeds Note over S: Callback retrieves nested flow data Note over S: Unsafe pickle deserialization is reached S-->>A: Observable execution side effect (for example, delay)
範圍說明: 此圖總結了報告的漏洞鏈。這並不意味著每個 Flask 應用程式都使用 pickle 進行 session 處理,也不意味著暴露的 OAuth client secret 會在沒有額外的 signing-key 重複使用和不安全反序列化條件的情況下自動導致 RCE。

3. 程式分析

以下程式碼片段說明了報告中描述的主要分析步驟。它們的目的在解釋資料格式和信任邊界失敗;它們不是針對即時服務的完整、即用型 exploit。

程式 1:解碼 Flask Session Payload

Flask session cookie 通常包含一個 URL-safe Base64 編碼的 payload、一個 timestamp 和一個由點號分隔的 signature。當使用壓縮時,payload 元件以點號開頭。解碼並在適當情況下解壓縮該元件會揭示外部 session 資料,這是 JSON 而不是 pickle stream [1] 。

  1. import base64 # Base64 module, used to decode the payload segment of the Flask session cookie
  2. import json # JSON module, used to parse the decompressed bytes into a Python dictionary
  3. import zlib # zlib module, used to decompress the session payload if it was compressed
  4. def b64decode_urlsafe(value):
  5. # Compute the '=' padding needed, since URL-safe base64 strings often omit padding
  6. padding = "=" * (-len(value) % 4)
  7. # Decode using the URL-safe base64 variant (cookies use '-' and '_' instead of '+' and '/')
  8. return base64.urlsafe_b64decode(value + padding)
  9. # Split the cookie string on the last two '.' characters into: payload, timestamp, signature
  10. payload, timestamp, signature = cookie.rsplit(".", 2)
  11. # A leading '.' on the payload indicates Flask compressed it with zlib before encoding
  12. compressed = payload.startswith(".")
  13. # Strip the leading '.' if present to get just the encoded content; otherwise use it as-is
  14. encoded_payload = payload[1:] if compressed else payload
  15. # Base64-decode the encoded string to recover the raw bytes
  16. raw = b64decode_urlsafe(encoded_payload)
  17. # If the payload was compressed, decompress it with zlib to restore the original bytes
  18. if compressed:
  19. raw = zlib.decompress(raw)
  20. # Parse the restored bytes as JSON to obtain the session content as a dictionary
  21. session_data = json.loads(raw)

session dictionary 可能包含一個欄位,其值本身是一個 Base64 編碼的 byte sequence。該內部值必須單獨分析。像 pickletools 這樣的 pickle disassembler 可以幫助檢查 pickle opcodes 而無需重建物件;在分析期間,避免對不受信任的資料使用 pickle.loads() 。

程式 2:了解 Session Signature

報告的問題取決於 OAuth client secret 也被用作 Flask session signing key。Flask 的 session signing 行為是透過 itsdangerous 實作的,確切的 key derivation、salt、digest、payload representation 和 signature encoding 可能因函式庫版本和設定而異。下面的範例說明了涉及的 HMAC primitives;它本身並不是 Flask cookie 的完整驗證 routine。

  1. import hmac
  2. # Illustrative only: use the target application's exact
  3. # itsdangerous configuration and version for a valid assessment.
  4. derived_key = hmac.digest(
  5. client_secret_bytes,
  6. b"cookie-session",
  7. "sha1",
  8. )
  9. message = f"{payload}.{timestamp}".encode("utf-8")
  10. raw_digest = hmac.digest(derived_key, message, "sha1")
  11. # raw_digest is binary data. A Flask cookie's displayed signature
  12. # uses URL-safe Base64 encoding, so compare like-for-like values.
  13. # A complete verifier must also match the target's key derivation
  14. # and signing configuration exactly.

原始 HMAC digest 與 cookie 中的 signature text 表示形式不同。一個可靠的驗證聲明應記錄目標版本和設定,應用正確的 encoding,並使用與原始 cookie 相同的格式比較產生的 signature。報告中的發現是使用已展示的 key reuse 和 signature validation,而不僅僅是 digest 的計算 [1] 。

程式 3:巢狀 Flow Data 和 Session Structure

在報告的應用程式中,外部 Flask session 是一個 JSON 相容的 dictionary。一個 flow-related 欄位包含 bytes 的 Base64 表示,在巢狀 JSON structure 中使用 Flask 的 bytes-tagging convention(鍵 " b" )。callback 將該 flow entry 與 CSRF token 關聯,並稍後解碼位元組以進行反序列化 [1] 。

  1. # Schematic structure based on the reported data flow.
  2. # This is a structural illustration, not a complete forged cookie.
  3. session_data = {
  4. "_permanent": True,
  5. "google_oauth2_csrf_token": "example-state",
  6. "google_oauth2_flow_example-state": {
  7. " b": "BASE64_ENCODED_SERIALIZED_BYTES"
  8. },
  9. }

僅僅構造一個 dictionary 並不能證明成功的攻擊。structure 必須匹配應用程式預期的 Key 和 type,session 必須通過目標的 signature validation,並且 callback 必須將巢狀位元組傳遞給不安全的反序列化 sink。這些在受影響的應用程式條件下將 session-signing 弱點與程式碼執行連接起來。

程式 4:基於計時的反序列化概念驗證

當物件的 reduction instructions 指定 callable 時,Python 的 pickle protocol 可以在物件重建期間使用 callable。在原始文章中,使用基於 sleep 的 side effect 來測試目標是否到達反序列化路徑。延遲作為非破壞性信號很有用,但結果應精確描述:這支持了攻擊者控制的重建行為已被執行的結論,而不是逐一獨立證明所有可能的命令執行形式 [1] 。

  1. import time
  2. class Sleep:
  3. def __init__(self, seconds):
  4. self.seconds = seconds
  5. def __reduce__(self):
  6. return (time.sleep, (self.seconds,))

此範例解釋了機制,且只應在授權的隔離測試環境中執行。安全缺陷是應用程式決定使用 pickle 反序列化攻擊者控制的位元組,而不僅僅是 __reduce__ method 的存在。

4. 緩解措施與安全建議

修復必須解決鏈中的每個環節,而不是將問題視為單一的序列化 bug。

  • 分離 credentials 和 signing keys: 切勿將 OAuth client secret 重複用作 Flask 的 SECRET_KEY 。產生專用於應用程式的高 entropy signing key,並將其與 OAuth credentials 分開。
  • 輪換暴露的 credentials: 輪換任何可能已暴露的 OAuth client secret,並審查相關 credentials 或 tokens 是否需要撤銷。
  • 使受影響的 sessions 無效: 輪換受損的 session signing key 並使使用該 key 簽章的現有 sessions 無效。如果應用程式有多個 instances,請仔細協調 key rotation。
  • 避免不安全反序列化: 切勿對可能受用戶端或其他不受信任來源影響的資料呼叫 pickle.loads() 。請使用僅含資料的格式,如 JSON 或 Protocol Buffers,並配合 schema validation 和嚴格的 type checks。
  • 將 OAuth flow state 保留在伺服器端: 將敏感的 flow objects 和 secrets 儲存在伺服器端 datastore 中。在可行的情況下,僅在用戶端 cookie 中放置一個不透明的、不可預測的 identifier。
  • 最小化 cookies 中的敏感資料: 已簽章的 cookies 提供完整性,而非機密性。不要將 secrets 或敏感的內部 objects 放置在用戶端可讀取的 session payloads 中。
  • 新增回歸測試: 驗證 OAuth 流程狀態不會洩露客戶端密鑰,確保工作階段簽署金鑰是獨立的,並且在未反序列化前就拒絕格式錯誤或攻擊者控制的序列化資料。

5. 結論

Google DevSite 事件說明了幾個個別不同的弱點如何結合成為關鍵漏洞:嵌入在用戶端可讀取 session 資料中的 secret、將該 secret 重複用作 signing key,以及 callback 中不安全的 pickle 反序列化。教訓並非 Flask 的預設 session cookie 固有地執行 pickle 資料。相反,應用程式通過暴露 credential、削弱 session integrity 以及反序列化攻擊者可以控制的位元組,跨越了信任邊界。因此,有效的修復需要 credential isolation、key rotation、伺服器端處理敏感的 OAuth state,以及在信任邊界消除不安全反序列化。