說好不碰 web 題,可是上班實在太無聊所以生出來的系列文

內容基本上基於 PortSwigger Web Security Academy,今天主要看的是 JWT 相關的攻擊手段

JWT 基本上就是把資料與驗證機制 base64 之後的結果,通常拿來保存 session 相關的資訊,由三個部份組成(圖片取自 https://cdn.auth0.com/blog/legacy-app-auth/legacy-app-auth-5.png

JWT

針對 JWT 的攻擊主要有以下幾種

針對驗證機制的攻擊

接受任意簽名的 token

這個攻擊主要是利用開發者在使用 JWT Library 時對 method 的誤用

舉例來說,在 node 的 jsonwebtoken library 中有 verify()decode()兩個解讀 JWT 的方法,如果今天開發者誤用 decode() 的話,那個 JWT 的資料就會在未經驗證的情況下被拿來使用,即使被竄改也不會被拒絕

實施攻擊的方法很簡單,只要把 payload 區段中對應的參數改掉就可以了

接受沒有簽名的 token

JWT 中的 header 區段有一個 alg 參數,這會告訴驗證端這個 token 是使用哪種演算法進行簽名的,但是這個參數也是來自使用者輸入,所以理論上使用什麼可以被攻擊者直接控制的,那當然在驗證端可以設定允許的演算法種類

不過 JWT 其實也可以不簽名,只要把 alg 設為 none 就好,當然這樣非常不安全,因此通常驗證端都會直接拒絕這種類型的 token,如果要成功欺騙驗證端的話,通常必須想辦法繞過 alg 的檢查機制

值得注意的是,在進行攻擊時,即使 token 沒有簽名,payload 的最後還是要有一個 .,來提示 verify 的部份是空的,也就是長這樣

eyJraWQiOiI0Njc2ZjE3ZS1hMGY1LTQxZDUtODZmZC04MmE3N2M1YTIxY2IiLCJhbGciOiJub25lIn0%3d.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2NjYyMzMyMTd9.

這會被解析成以下的 token,可以看到並沒有 verify 的部份

{"kid":"4676f17e-a0f5-41d5-86fd-82a77c5a21cb","alg":"none"}
{"iss":"portswigger","sub":"administrator","exp":1666233217}

弱密碼暴力破解

目前主要的 alg 主要有以下幾種,主要的差別在於簽名時所使用的密鑰

  • HS: HMAC + SHA-{256, 384, 512}
  • RS: RSA + SHA-{256, 384, 512}
  • ES: ECDSA + SHA-{256, 384, 512}
  • PS: RSAPSS + SHA-{256, 384, 512}

其中 HMAC 是唯一使用對稱密碼的簽名演算法,也就是說如果使用的密碼太弱,就有可能被暴力算出來,像是使用 hashcat 結合 rockyou

hashcat -a 0 -m 16500 <jwt> <wordlist>

針對 header 參數的攻擊

JWT 對 header 的規定中,只有 alg 參數一定要存在,不過通常還會有其他參數同時存在(如前面看到的 kid),常見且對攻擊可能有用的參數有以下幾種

  • jwk (JSON Web Key): 把密鑰資訊保存在 JSON object 中
  • jwu (JSON Web Key Set URL): 可以取得密鑰的 URL
  • kid (Key ID): 當今天存在多組密鑰時,用這個 ID 確認要使用哪個密鑰

值得注意的是,這些資訊也是由使用者控制的,因此攻擊者可以透過控制這些參數,使用自己簽名的 token 進行驗證

這些攻擊的流程大致如下

建立公私鑰對 -> 

jwk 參數注入自己的密鑰

在使用非對稱密碼簽名的場景下,驗證端使用私鑰進行簽名,公鑰進行驗證,如下圖所示 jwt-pki

jwk 參數的範例如下

{
    "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
    "typ": "JWT",
    "alg": "RS256",
    "jwk": {
        "kty": "RSA",
        "e": "AQAB",
        "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
        "n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
    }
}

其中 kid 必須與 jwk 中的 kid 一樣,才會被採用

在正常情況下,驗證端應該使用自己建立的白名單上的公鑰進行驗證,不過設定錯誤的驗證端可能會選擇相信 jwk 參數中的公鑰進行驗證,這個時候攻擊者就可以使用自己的私鑰簽名,把自己的公鑰加到 jwk 中,讓驗證端使用攻擊者的公鑰進行驗證

jku 參數注入自己的密鑰

前面提到的 jwk,除了嵌入在 token 裡面,還可以以 json 文件的形式保存在(遠端)主機上,並使用 jku 參數指向該文件進行驗證

一個 JWK Set 的範例如下

{
    "keys": [
        {
            "kty": "RSA",
            "e": "AQAB",
            "kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab",
            "n": "o-yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9mk6GPM9gNN4Y_qTVX67WhsN3JvaFYw-fhvsWQ"
        },
        {
            "kty": "RSA",
            "e": "AQAB",
            "kid": "d8fDFo-fS9-faS14a9-ASf99sa-7c1Ad5abA",
            "n": "fc3f-yy1wpYmffgXBxhAUJzHql79gNNQ_cb33HocCuJolwDqmk6GPM4Y_qTVX67WhsN3JvaFYw-dfg6DH-asAScw"
        }
    ]
}

在正常情況下,驗證端應該只使用受信任的網域上的密鑰進行驗證,不過可以利用過濾機制的漏洞繞過檢查,造成驗證伺服器去使用任意密鑰,也算是 SSRF 的一種

kid 參數注入自己的密鑰

假設今天所有的密鑰都存放在驗證端的本機上,這個時候驗證端可能會選擇使用 kid 參數來指向本地儲存的密鑰檔案,但如果 kid 到本地檔案的這個過程沒有處理好,有 path traversal 漏洞的話,攻擊者就可以利用驗證端上的任意檔案進行驗證

{
    "kid": "../../path/to/file",
    "typ": "JWT",
    "alg": "HS256",
    "k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc"
}

特別是驗證端使用對稱密碼進行簽名時,攻擊者可以把 kid 指向一個不會隨著環境變動的檔案,其中最容易的就是在 Linux 上都有的 /dev/null,攻擊者可以先在自己的機器上用自己的 /dev/null 進行簽名後,再把驗證端的 kid 指向他們的 /dev/null,這樣就可以達成任意簽名