Hacking JWTs: How a Weak Password Reset Token Can Lead to Full Account Takeover + Video

Listen to this Post

Featured Image

Introduction:

In the world of web application security, the password reset functionality is often treated as an afterthought, yet it remains one of the most critical attack surfaces. A recent deep dive by security researcher Dipesh Paul, inspired by a finding from Aswin Thambi Panikulangara, highlights a classic but devastating flaw: the misuse of JSON Web Tokens (JWTs) for password resets. When these tokens contain only sequential User IDs, are signed with a weak secret, and lack an expiration, they transform a simple “forgot password” feature into a direct line for Account Takeover (ATO).

Learning Objectives:

  • Understand the inherent risks of using poorly configured JWTs in high-security functions like password resets.
  • Learn how to identify weak JWT implementations through analysis of their structure and secrets.
  • Execute a full attack chain: cracking a JWT secret, forging a new token, and exploiting sequential User IDs to take over any account.
  • Implement the proper remediation using cryptographically secure random tokens to mitigate the risk.

You Should Know:

1. The Anatomy of a Vulnerable JWT Implementation

The vulnerability stems from a series of poor design choices that, when combined, create a critical security hole. In the lab scenario presented, the application uses JWTs to handle password reset links. When a user requests a password reset, the backend generates a JWT and sends it via email.

To understand the flaw, you must first inspect the token. A JWT consists of three parts: Header, Payload, and Signature. You can decode the Base64Url-encoded parts of any JWT using online tools or command-line utilities.

Command (Linux/macOS):

 Example: Decode JWT Payload (replace <jwt_token> with your actual token)
echo '<jwt_token>' | cut -d "." -f2 | base64 -d 2>/dev/null | jq .

What you would find:

In the vulnerable app, decoding the payload reveals something like:

{
"user_id": 1025,
"iat": 1741622400
}

The token only contains the `user_id` (which is sequential) and the “Issued At” (iat) timestamp. Crucially, it lacks an `exp` (expiration) claim. Furthermore, the lab is configured so that the token is signed with an extremely weak secret, like “secret” or “password,” making it susceptible to offline cracking.

Step‑by‑step guide: Identifying the weakness

  1. Intercept the Request/Response: Use a proxy like Burp Suite to capture the password reset email link or the HTTP request containing the JWT.
  2. Copy the JWT: Copy the full token string.
  3. Inspect the Payload: Use a tool like `jq` or the debugger at jwt.io to decode the payload. Look for claims. If you only see a user identifier and no expiration, alarm bells should ring.
  4. Check for Sequential IDs: If your User ID is “1024” and you create a new account to find your ID is “1025”, it confirms the IDs are predictable and sequential.

2. Exploiting the Weakness: Cracking and Forging Tokens

Once you have identified a weak JWT, the attack path becomes clear. Since the token lacks an expiration, it is valid forever. The only thing standing between an attacker and account takeover is the signature secret. If that secret is weak, it can be cracked.

The tool of choice for this is hashcat, a powerful password recovery tool. You must first extract the JWT signature hash into a format `hashcat` can understand.

Command (Linux/Windows – using hashcat):

 Format the JWT for hashcat (HMAC-SHA256 mode is 16500)
echo "<full_jwt_token>" > jwt_hash.txt

Run hashcat with a wordlist (e.g., rockyou.txt) against the JWT
hashcat -m 16500 -a 0 jwt_hash.txt /usr/share/wordlists/rockyou.txt --force

If the secret is weak, `hashcat` will output the cracked secret (e.g., “supersecret”).

Step‑by‑step guide: The Account Takeover

  1. Crack the Secret: Use `hashcat` as shown above to discover the signing secret.
  2. Forge a New Token: With the secret in hand and knowledge of the payload structure, you can forge a token for any user. If you know your own `user_id` is 1025 and you want to take over the admin account (likely user_id 1), you create a new JWT with the payload {"user_id": 1}.
  3. Sign the Token: Use a scripting language or an online tool to sign the new header and payload with the cracked secret.

– Python Example:

import jwt
 Replace with the cracked secret
secret = "supersecret"
 Payload for the target admin user
payload = {"user_id": 1}
 Forge the token
forged_token = jwt.encode(payload, secret, algorithm="HS256")
print(forged_token)

4. Execute the Takeover: Submit this forged token to the password reset endpoint. The application verifies the signature (which is now valid), extracts user_id=1, and allows you to reset the admin’s password.

3. The Secure Fix: Implementing Cryptographic Tokens

The solution is not to abandon JWTs entirely, but to use them correctly or, for this specific use case, replace them with a more suitable alternative. The fundamental principle is that a password reset token must be:
– Single-use: It should expire immediately after being used.
– Short-lived: It should have a strict expiration time (e.g., 15-30 minutes).
– Unpredictable: It must be generated by a cryptographically secure random number generator.
– Context-Bound: It should be tied to a specific user session or request.

Step‑by‑step guide: Remediation Code

Instead of a JWT containing just a user ID, the secure version generates a long, random string.

import secrets
import hashlib
from datetime import datetime, timedelta

Token Generation 
def generate_secure_token(user_id):
 Generate a cryptographically secure random token (64 bytes, hex encoded)
reset_token = secrets.token_urlsafe(64)

Store a hash of the token in the database, along with the user ID and expiry
token_hash = hashlib.sha256(reset_token.encode()).hexdigest()
expiry = datetime.utcnow() + timedelta(minutes=15)

Database operation (Pseudo-code)
 db.store_reset_token(user_id=user_id, token_hash=token_hash, expires_at=expiry)

Return the plaintext token to be sent to the user via email
return reset_token

Token Verification 
def verify_reset_token(plain_token, user_id):
 Hash the provided token
token_hash = hashlib.sha256(plain_token.encode()).hexdigest()

Database operation (Pseudo-code)
 stored_record = db.get_reset_token(user_id=user_id, token_hash=token_hash)

if stored_record and stored_record.expires_at > datetime.utcnow():
  Token is valid. Proceed with password reset.
  Invalidate the token immediately after use.
 db.delete_reset_token(stored_record.id)
 return True
 else:
 return False

4. Defense in Depth: Additional Hardening

Beyond just changing the token type, several additional layers can prevent this class of vulnerability.

  • Rate Limiting: Implement strict rate limiting on the password reset request endpoint. This prevents an attacker from brute-forcing tokens or even from bombarding a specific user with reset emails.
  • Audit Logging: Log all password reset requests and completions. Anomalies, such as multiple resets for a single account from different IPs in a short time, should trigger alerts.
  • User Notification: Always notify a user via email when their password has been successfully reset. If the reset was unauthorized, they have immediate knowledge to take action (e.g., contact support, lock their account).

Command (Linux – for local testing rate limiting with iptables):
While this is for network-level blocking, it illustrates the concept of limiting connections.

 Limit new connections to the web server on port 80 to 10 per minute
sudo iptables -A INPUT -p tcp --dport 80 -m limit --limit 10/minute --limit-burst 20 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j DROP

What Undercode Say:

  • The Devil is in the Defaults: The JWT vulnerability discussed isn’t a flaw in the JWT standard itself, but in its implementation. Using default or weak secrets and omitting standard claims like `exp` is the equivalent of leaving the front door locked but the key under the mat.
  • Simplicity is the Enemy of Security: While generating a secure random string might seem less elegant than using a JWT, it removes the complexity of signature verification and secret management from a critical flow. For password resets, the simplest, most direct cryptographic solution is often the most secure.

This deep dive into a seemingly simple vulnerability underscores a fundamental truth in cybersecurity: an attacker doesn’t need to find a complex, zero-day exploit to compromise a system. They often just need to look where developers took shortcuts. The transition from a weak, static JWT to a strong, random, expiring token is not just a code change; it is a shift in mindset from “this should work” to “this must be unforgeable.” As web applications continue to rely on stateless authentication mechanisms, developers must be vigilant not to sacrifice security for architectural convenience, especially when dealing with the keys to the kingdom—account access.

Prediction:

As JWT usage continues to dominate modern web authentication and API security, we will see a rise in automated scanning tools specifically designed to detect weak JWT configurations. These tools will not just check for the “none” algorithm attack, but will actively attempt to crack weak secrets and analyze payload entropy. Consequently, bug bounty programs will deprioritize simple JWT issues in favor of more complex logic flaws, forcing researchers to chain multiple minor misconfigurations—like a weak secret combined with a missing expiration—to demonstrate critical impact. The future of this attack vector lies in these composite vulnerabilities, where the sum of the parts is far more dangerous than any single flaw.

▶️ Related Video (80% Match):

🎯Let’s Practice For Free:

IT/Security Reporter URL:

Reported By: The Genetic – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky