93 lines
3.0 KiB
Python
93 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Guess the Seed (Weak PRNG) - CTF challenge server
|
|
|
|
Behavior:
|
|
- On startup the server picks a secret_seed = int(time.time()).
|
|
- It generates a short sequence of tokens:
|
|
token_i = sha256(str( random.Random(secret_seed + i).getrandbits(64) ).encode()).hexdigest()
|
|
for i = 0..N-1
|
|
- The server displays observed tokens (first K tokens) and a time window [secret_seed - WINDOW, secret_seed + WINDOW]
|
|
for players to brute-force the seed.
|
|
- The correct next-token is token_K (the next sequence element).
|
|
- Hitting /flag?token=<predicted> returns the flag if predicted matches token_K.
|
|
|
|
This is intentionally weak for CTF use.
|
|
"""
|
|
import time
|
|
import random
|
|
import hashlib
|
|
import os
|
|
from flask import Flask, jsonify, render_template, request, abort
|
|
|
|
app = Flask(__name__)
|
|
|
|
# Configuration
|
|
NUM_TOKENS = 5 # total tokens generated on startup
|
|
OBSERVED_TOKENS = 3 # number of tokens shown to the player (first K)
|
|
WINDOW = 3600 # +/- seconds around secret seed to publish as candidate window (1 hour)
|
|
FLAG_FILE = "flag.txt"
|
|
|
|
# Initialize state on startup
|
|
secret_seed = int(time.time())
|
|
tokens = []
|
|
|
|
def make_token_from_seed(s):
|
|
"""Generate a token deterministically from a seed (int)."""
|
|
rnd = random.Random(int(s))
|
|
val = rnd.getrandbits(64)
|
|
h = hashlib.sha256(str(val).encode()).hexdigest()
|
|
# shorten token to 16 hex chars to be nicer for players
|
|
return h[:16]
|
|
|
|
for i in range(NUM_TOKENS):
|
|
t = make_token_from_seed(secret_seed + i)
|
|
tokens.append(t)
|
|
|
|
# target is next token after observed tokens
|
|
target_token = tokens[OBSERVED_TOKENS] # token the player must predict
|
|
|
|
@app.route("/")
|
|
def challenge():
|
|
"""Return observed tokens and window data (player-facing)."""
|
|
start = secret_seed - WINDOW
|
|
end = secret_seed + WINDOW
|
|
obs = tokens[:OBSERVED_TOKENS]
|
|
return render_template("challenge.html",
|
|
observed=obs,
|
|
window_start=start,
|
|
window_end=end,
|
|
note=f"{OBSERVED_TOKENS} tokens shown — predict the next token (the {OBSERVED_TOKENS+1}th).")
|
|
|
|
@app.route("/api/observed")
|
|
def api_observed():
|
|
"""Machine-readable API returning observed tokens and window."""
|
|
start = secret_seed - WINDOW
|
|
end = secret_seed + WINDOW
|
|
return jsonify({
|
|
"observed": tokens[:OBSERVED_TOKENS],
|
|
"window_start": start,
|
|
"window_end": end,
|
|
"note": f"{OBSERVED_TOKENS} tokens shown — predict the next token (the {OBSERVED_TOKENS+1}th)."
|
|
})
|
|
|
|
@app.route("/flag")
|
|
def flag():
|
|
"""Return flag if token matches the predicted next token."""
|
|
t = request.args.get("token", "")
|
|
if not t:
|
|
abort(400, "Missing token parameter.")
|
|
if t == target_token:
|
|
if os.path.exists(FLAG_FILE):
|
|
with open(FLAG_FILE, "r") as f:
|
|
return f.read().strip() + "\n"
|
|
else:
|
|
return "FLAG_MISSING\n"
|
|
else:
|
|
abort(403, "Invalid token")
|
|
|
|
# Simple health endpoint
|
|
@app.route("/health")
|
|
def health():
|
|
return "ok\n"
|