說好不碰 web 題,可是上班實在太無聊所以生出來的系列文
內容基本上基於 PortSwigger Web Security Academy,今天主要看的是 JWT 相關的攻擊手段
JWT 基本上就是把資料與驗證機制 base64 之後的結果,通常拿來保存 session 相關的資訊,由三個部份組成(圖片取自 https://cdn.auth0.com/blog/legacy-app-auth/legacy-app-auth-5.png)

針對 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): 可以取得密鑰的 URLkid(Key ID): 當今天存在多組密鑰時,用這個 ID 確認要使用哪個密鑰
值得注意的是,這些資訊也是由使用者控制的,因此攻擊者可以透過控制這些參數,使用自己簽名的 token 進行驗證
這些攻擊的流程大致如下
建立公私鑰對 ->
在 jwk 參數注入自己的密鑰
在使用非對稱密碼簽名的場景下,驗證端使用私鑰進行簽名,公鑰進行驗證,如下圖所示

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,這樣就可以達成任意簽名