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組成
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(密鑰)通過特定的計算公式和加密演算法得到。
Header
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 裡面,以後客戶端發出的所有請求都會攜帶這個令牌。
簡化後的步驟如下:
- 用戶向伺服器發送用戶名、密碼以及驗證碼用於登陸系統。
- 如果用戶用戶名、密碼以及驗證碼校驗正確的話,服務端會返回已經簽名的 Token,也就是 JWT。
- 用戶以後每次向後端發請求都在 Header 中帶上這個 JWT 。
- 服務端檢查 JWT 並從中獲取用戶相關資訊。
兩點建議:
- 建議將 JWT 存放在 localStorage 中,放在 Cookie 中會有 CSRF 風險。
- 請求服務端並攜帶 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 的安全性?
- 使用安全係數高的加密演算法。
- 使用成熟的開源庫,沒必要造輪子。
- JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 風險。
- 一定不要將隱私資訊存放在 Payload 當中。
- 密鑰一定保管好,一定不要泄露出去。JWT 安全的核心在於簽名,簽名安全的核心在密鑰。
- 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中最常用的兩種演算法為HMAC
和RSA
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。類似於kid
,JKU
也可以由用戶指定輸入數據,如果沒有經過嚴格過濾,就可以指定一組自定義的密鑰文件,並指定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的有效時間足夠短