JWT安全問題

JWT 基礎概念詳解

簡介

JWT (JSON Web Token) 是目前最流行的跨域認證解決方案,是一種基於 Token 的認證授權機制。 從 JWT 的全稱可以看出,JWT 本身也是 Token,一種規範化之後的 JSON 結構的 Token

JWT 自身包含了身份驗證所需要的所有資訊,因此,我們的伺服器不需要存儲 Session 資訊。這顯然增加了系統的可用性和伸縮性,大大減輕了服務端的壓力。

可以看出,JWT 更符合設計 RESTful API 時的「Stateless(無狀態)」原則

並且, 使用 JWT 認證可以有效避免 CSRF 攻擊,因為 JWT 一般是存在在 localStorage 中,使用 JWT 進行身份驗證的過程中是不會涉及到 Cookie 的。

JWT組成

此圖片來源於://supertokens.com/blog/oauth-vs-jwt

JWT 本質上就是一組字串,通過(.)切分成三個為 Base64 編碼的部分:

  • Header : 描述 JWT 的元數據,定義了生成簽名的演算法以及 Token 的類型。
  • Payload : 用來存放實際需要傳遞的數據
  • Signature(簽名) :伺服器通過 Payload、Header 和一個密鑰(Secret)使用 Header 裡面指定的簽名演算法(默認是 HMAC SHA256)生成。

JWT 通常是這樣的:xxxxx.yyyyy.zzzzz

示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

你可以在 (//jwt.io/) 這個網站上對其 JWT 進行解碼,解碼之後得到的就是 Header、Payload、Signature 這三部分。

Header 和 Payload 都是 JSON 格式的數據,Signature 由 Payload、Header 和 Secret(密鑰)通過特定的計算公式和加密演算法得到。

img

Header 通常由兩部分組成:

  • typ(Type):令牌類型,也就是 JWT。
  • alg(Algorithm) :簽名演算法,比如 HS256。

示例:

{
  "alg": "HS256",
  "typ": "JWT"
}

JSON 形式的 Header 被轉換成 Base64 編碼,成為 JWT 的第一部分。

Payload

Payload 也是 JSON 格式數據,其中包含了 Claims(聲明,包含 JWT 的相關資訊)。

Claims 分為三種類型:

  • Registered Claims(註冊聲明) :預定義的一些聲明,建議使用,但不是強制性的。
  • Public Claims(公有聲明) :JWT 簽發方可以自定義的聲明,但是為了避免衝突,應該在 JSON Web Token Registryopen w中定義它們。
  • Private Claims(私有聲明) :JWT 簽發方因為項目需要而自定義的聲明,更符合實際項目場景使用。

下面是一些常見的註冊聲明:

  • iss(issuer):JWT 簽發方。
  • iat(issued at time):JWT 簽發時間。
  • sub(subject):JWT 主題。
  • aud(audience):JWT 接收方。
  • exp(expiration time):JWT 的過期時間。
  • nbf(not before time):JWT 生效時間,早於該定義的時間的 JWT 不能被接受處理。
  • jti(JWT ID):JWT 唯一標識。

示例:

{
  "uid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
  "sub": "1234567890",
  "name": "John Doe",
  "exp": 15323232,
  "iat": 1516239022,
  "scope": ["admin", "user"]
}

Payload 部分默認是不加密的,一定不要將隱私資訊存放在 Payload 當中!!!

JSON 形式的 Payload 被轉換成 Base64 編碼,成為 JWT 的第二部分

Signature

Signature 部分是對前兩部分的簽名,作用是防止 JWT(主要是 payload) 被篡改。

這個簽名的生成需要用到:

  • Header + Payload。
  • 存放在服務端的密鑰(一定不要泄露出去)。
  • 簽名演算法。

簽名的計算公式如下:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

算出簽名以後,把 Header、Payload、Signature 三個部分拼成一個字元串,每個部分之間用”點”(.)分隔,這個字元串就是 JWT

如何基於 JWT 進行身份驗證?

在基於 JWT 進行身份驗證的的應用程式中,伺服器通過 Payload、Header 和 Secret(密鑰)創建 JWT 並將 JWT 發送給客戶端。客戶端接收到 JWT 之後,會將其保存在 Cookie 或者 localStorage 裡面,以後客戶端發出的所有請求都會攜帶這個令牌。

 JWT 身份驗證示意圖

簡化後的步驟如下:

  1. 用戶向伺服器發送用戶名、密碼以及驗證碼用於登陸系統。
  2. 如果用戶用戶名、密碼以及驗證碼校驗正確的話,服務端會返回已經簽名的 Token,也就是 JWT。
  3. 用戶以後每次向後端發請求都在 Header 中帶上這個 JWT 。
  4. 服務端檢查 JWT 並從中獲取用戶相關資訊。

兩點建議:

  1. 建議將 JWT 存放在 localStorage 中,放在 Cookie 中會有 CSRF 風險。
  2. 請求服務端並攜帶 JWT 的常見做法是將其放在 HTTP Header 的 Authorization 欄位中(Authorization: Bearer Token

如何防止 JWT 被篡改?

有了簽名之後,即使 JWT 被泄露或者解惑,黑客也沒辦法同時篡改 Signature 、Header 、Payload

這是為什麼呢?因為服務端拿到 JWT 之後,會解析出其中包含的 Header、Payload 以及 Signature 。服務端會根據 Header、Payload、密鑰再次生成一個 Signature。拿新生成的 Signature 和 JWT 中的 Signature 作對比,如果一樣就說明 Header 和 Payload 沒有被修改。

不過,如果服務端的秘鑰也被泄露的話,黑客就可以同時篡改 Signature 、Header 、Payload 了。黑客直接修改了 Header 和 Payload 之後,再重新生成一個 Signature 就可以了。

密鑰一定保管好,一定不要泄露出去。JWT 安全的核心在於簽名,簽名安全的核心在密鑰

如何加強 JWT 的安全性?

  1. 使用安全係數高的加密演算法。
  2. 使用成熟的開源庫,沒必要造輪子。
  3. JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 風險。
  4. 一定不要將隱私資訊存放在 Payload 當中。
  5. 密鑰一定保管好,一定不要泄露出去。JWT 安全的核心在於簽名,簽名安全的核心在密鑰。
  6. Payload 要加入 exp (JWT 的過期時間),永久有效的 JWT 不合理。並且,JWT 的過期時間不易過長。

JWT攻擊

敏感資訊泄露

JWT保證的是數據傳輸過程中的完整性而不是機密性。

由於payload是使用base64url編碼的,所以相當於明文傳輸,如果在payload中攜帶了敏感資訊(如存放密鑰對的文件路徑),單獨對payload部分進行base64url解碼,就可以讀取到payload中攜帶的資訊。

加密演算法

空加密演算法

JWT支援使用空加密演算法,可以在header中指定alg為None

這樣的話,只要把signature設置為空(即不添加signature欄位),提交到伺服器,任何token都可以通過伺服器的驗證。舉個例子,使用以下的欄位

{
    "alg" : "None",
    "typ" : "jwt"
}

{
    "user" : "Admin"
}

生成的完整token為ew0KCSJhbGciIDogIk5vbmUiLA0KCSJ0eXAiIDogImp3dCINCn0.ew0KCSJ1c2VyIiA6ICJBZG1pbiINCn0

(header+’.’+payload,去掉了’.’+signature欄位)

空加密演算法的設計初衷是用於調試的,但是如果某天開發人員腦闊瓦特了,在生產環境中開啟了空加密演算法,缺少簽名演算法,jwt保證資訊不被篡改的功能就失效了。攻擊者只需要把alg欄位設置為None,就可以在payload中構造身份資訊,偽造用戶身份

修改RSA加密演算法為HMAC

JWT中最常用的兩種演算法為HMACRSA

HMAC是密鑰相關的哈希運算消息認證碼(Hash-based Message Authentication Code)的縮寫,它是一種對稱加密演算法,使用相同的密鑰對傳輸資訊進行加解密。

RSA則是一種非對稱加密演算法,使用私鑰加密明文,公鑰解密密文。

在HMAC和RSA演算法中,都是使用私鑰對signature欄位進行簽名,只有拿到了加密時使用的私鑰,才有可能偽造token。

現在我們假設有這樣一種情況,一個Web應用,在JWT傳輸過程中使用RSA演算法,私鑰pem對JWT token進行簽名,公鑰pub對簽名進行驗證

{
    "alg" : "RS256",
    "typ" : "jwt"
}

通常情況下私鑰pem是無法獲取到的,但是公鑰pub卻可以很容易通過某些途徑讀取到,這時,將JWT的加密演算法修改為HMAC,即

{
    "alg" : "HS256",
    "typ" : "jwt"
}

同時使用獲取到的公鑰pub作為演算法的密鑰,對token進行簽名,發送到伺服器端。

伺服器端會將RSA的公鑰(pub)視為當前演算法(HMAC)的密鑰,使用HS256演算法對接收到的簽名進行驗證。

配置應該只允許使用HMAC演算法或公鑰演算法,決不能同時使用這兩種演算法。

爆破密鑰

俗話說,有密碼驗證的地方,就有會爆破。

不過對 JWT 的密鑰爆破需要在一定的前提下進行:

  • 知悉JWT使用的加密演算法
  • 一段有效的、已簽名的token
  • 簽名用的密鑰不複雜(弱密鑰)

所以其實JWT 密鑰爆破的局限性很大

PyJWT庫具體地址為://github.com/jpadilla/pyjwt

修改KID參數

kid是jwt header中的一個可選參數,全稱是key ID,它用於指定加密演算法的密鑰

{
    "alg" : "HS256",
    "typ" : "jwt",
    "kid" : "/home/jwt/.ssh/pem"
}

因為該參數可以由用戶輸入,所以也可能造成一些安全問題。

任意文件讀取

kid參數用於讀取密鑰文件,但系統並不會知道用戶想要讀取的到底是不是密鑰文件,所以,如果在沒有對參數進行過濾的前提下,攻擊者是可以讀取到系統的任意文件的。

{
    "alg" : "HS256",
    "typ" : "jwt",
    "kid" : "/etc/passwd"
}

SQL注入

kid也可以從資料庫中提取數據,這時候就有可能造成SQL注入攻擊,通過構造SQL語句來獲取數據或者是繞過signature的驗證

{
    "alg" : "HS256",
    "typ" : "jwt",
    "kid" : "key11111111' || union select 'secretkey' -- "
}

命令注入

kid參數過濾不嚴也可能會出現命令注入問題,但是利用條件比較苛刻。如果伺服器後端使用的是Ruby,在讀取密鑰文件時使用了open函數,通過構造參數就可能造成命令注入。

"/path/to/key_file|whoami"

對於其他的語言,例如php,如果程式碼中使用的是exec或者是system來讀取密鑰文件,那麼同樣也可以造成命令注入,當然這個可能性就比較小了

修改JKU/X5U參數

JKU的全稱是”JSON Web Key Set URL”,用於指定一組用於驗證令牌的密鑰的URL。類似於kidJKU也可以由用戶指定輸入數據,如果沒有經過嚴格過濾,就可以指定一組自定義的密鑰文件,並指定web應用使用該組密鑰來驗證token。

X5U則以URI的形式數允許攻擊者指定用於驗證令牌的公鑰證書或證書鏈,與JKU的攻擊利用方式類似。

4、無效簽名
當用戶端提交請求給應用程式,服務端可能沒有對token簽名進行校驗,這樣,攻擊者便可以通過提供無效簽名簡單地繞過安全機制。
示例:
一個很好的例子是網站上的「個人資料」頁面,因為我們只有在被授權通過有效的JWT進行訪問時才能訪問此頁面,我們將重放請求並尋找響應的變化以發現問題

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoidGVzdCIsImFjdGlvbiI6InByb2ZpbGUifQ.FjnAvQxzRKcahlw2EPd9o7teqX-fQSt7MZhT84hj7mU

user 欄位改為 admin,重新生成新 token:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4iLCJhY3Rpb24iOiJwcm9maWxlIn0._LRRXAfXtnagdyB1uRk-7CfkK1RESGwxqQCdwCNSPaI

結構:

{"typ": "JWT", "alg": "HS256"}.
{"user": "admin","action": "profile"}.
[新的簽名]

將重新生成的Token發給服務端效驗,如訪問頁面正常,則說明漏洞存在

5. 破解HS256(對稱加密演算法)密鑰

如果HS256密鑰的強度較弱的話,攻擊者可以直接通過蠻力攻擊方式來破解密鑰,例如將密鑰字元串用作PyJWT庫示例程式碼中的密鑰的時候情況就是如此。

然後,用蠻力方式對密鑰進行猜解,具體方法很簡單:如果密鑰正確的話,解密就會成功;如果密鑰錯誤的話,解密程式碼就會拋出異常。

此外,我們也可以使用PyJWT或John Ripper進行破解測試。

PyJWT庫具體地址為://github.com/jpadilla/pyjwt

JWT tool

此工具可用於測試jwt的安全性,地址是 //github.com/ticarpi/jwt_tool
示例用法:

λ python jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn 9dm15Po

   $$$$$\ $$\      $$\ $$$$$$$$\  $$$$$$$$\                  $$\
   \__$$ |$$ | $\  $$ |\__$$  __| \__$$  __|                 $$ |
      $$ |$$ |$$$\ $$ |   $$ |       $$ | $$$$$$\   $$$$$$\  $$ |
      $$ |$$ $$ $$\$$ |   $$ |       $$ |$$  __$$\ $$  __$$\ $$ |
$$\   $$ |$$$$  _$$$$ |   $$ |       $$ |$$ /  $$ |$$ /  $$ |$$ |
$$ |  $$ |$$$  / \$$$ |   $$ |       $$ |$$ |  $$ |$$ |  $$ |$$ |
\$$$$$$  |$$  /   \$$ |   $$ |       $$ |\$$$$$$  |\$$$$$$  |$$ |
 \______/ \__/     \__|   \__|$$$$$$\__| \______/  \______/ \__|
  Version 1.3                 \______|


=====================
Decoded Token Values:
=====================

Token header values:
[+] typ = JWT
[+] alg = HS256

Token payload values:
[+] login = ticarpi

----------------------
JWT common timestamps:
iat = IssuedAt
exp = Expires
nbf = NotBefore
----------------------


########################################################
#  Options:                                            #
#                ==== TAMPERING ====                   #
#  1: Tamper with JWT data (multiple signing options)  #
#                                                      #
#             ==== VULNERABILITIES ====                #
#  2: Check for the "none" algorithm vulnerability     #
#  3: Check for HS/RSA key confusion vulnerability     #
#  4: Check for JWKS key injection vulnerability       #
#                                                      #
#            ==== CRACKING/GUESSING ====               #
#  5: Check HS signature against a key (password)      #
#  6: Check HS signature against key file              #
#  7: Crack signature with supplied dictionary file    #
#                                                      #
#            ==== RSA KEY FUNCTIONS ====               #
#  8: Verify RSA signature against a Public Key        #
#                                                      #
#  0: Quit                                             #
########################################################

Please make a selection (1-6)
> 1

其中的選項分別為:

1. 修改JWT
2. 生成None演算法的JWT
3. 檢查RS/HS256公鑰錯誤匹配漏洞
4. 檢測JKU密鑰是否可偽造
5. 輸入一個key,檢查是否正確
6. 輸入一個存放key的文本,檢查是否正確
7. 輸入字典文本,爆破
8. 輸入RSA公鑰,檢查是否正確

安全建議

一般保證前兩點基本就沒什麼漏洞了。

  • 保證密鑰的保密性
  • 簽名演算法固定在後端,不以JWT里的演算法為標準
  • 避免敏感資訊保存在JWT中
  • 盡量JWT的有效時間足夠短