hacktricks/pentesting-web/race-condition.md

25 KiB

Condition de course


Utilisez Trickest pour construire et automatiser des workflows grâce aux outils communautaires les plus avancés.
Obtenez l'accès aujourd'hui :

{% embed url="https://trickest.com/?utm_campaign=hacktrics&utm_medium=banner&utm_source=hacktricks" %}

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

Autres moyens de soutenir HackTricks :

Exploiter les conditions de course

Le principal problème pour abuser des conditions de course est que vous avez besoin que les requêtes soient traitées en parallèle avec un très court intervalle de temps (généralement >1ms). Dans la section suivante, différentes solutions sont proposées pour rendre cela possible.

Attaque par paquet unique (HTTP/2) / Synchronisation du dernier octet (HTTP/1.1)

HTTP2 permet d'envoyer 2 requêtes dans une seule connexion TCP (alors qu'en HTTP/1.1 elles doivent être séquentielles).
L'utilisation d'un seul paquet TCP élimine complètement l'effet du jitter réseau, ce qui a clairement un potentiel pour les attaques par condition de course aussi. Cependant, deux requêtes ne suffisent pas pour une attaque de course fiable à cause du jitter côté serveur - variations dans le temps de traitement des requêtes de l'application causées par des variables incontrôlables comme la contention CPU.

Mais, en utilisant la technique de 'synchronisation du dernier octet' avec HTTP/1.1, il est possible de pré-envoyer la majeure partie des données en retenant un petit fragment de chaque requête, puis de 'compléter' 20-30 requêtes avec un seul paquet TCP.

Pour pré-envoyer la majeure partie de chaque requête :

  • Si la requête n'a pas de corps, envoyez tous les en-têtes, mais ne définissez pas le drapeau END_STREAM. Retenez un cadre de données vide avec END_STREAM défini.
  • Si la requête a un corps, envoyez les en-têtes et toutes les données du corps sauf le dernier octet et le drapeau END_STREAM. Retenez un cadre de données contenant le dernier octet.

Ensuite, préparez-vous à envoyer les cadres finaux :

  • Attendez 100ms pour vous assurer que les cadres initiaux ont été envoyés.
  • Assurez-vous que TCP_NODELAY est désactivé - il est crucial que l'algorithme de Nagle regroupe les cadres finaux.
  • Envoyez un paquet ping pour réchauffer la connexion locale. Si vous ne faites pas cela, la pile réseau du système d'exploitation placera le premier cadre final dans un paquet séparé.

Enfin, envoyez les cadres retenus. Vous devriez pouvoir vérifier qu'ils ont atterri dans un seul paquet en utilisant Wireshark.

{% hint style="info" %} Notez que cela ne fonctionne pas pour les fichiers statiques sur certains serveurs, mais les fichiers statiques ne sont pas pertinents pour les attaques par condition de course. {% endhint %}

En utilisant cette technique, vous pouvez faire en sorte que 20-30 requêtes arrivent simultanément au serveur - indépendamment du jitter réseau :

Adaptation à l'architecture cible

Il est important de noter que de nombreuses applications sont situées derrière un serveur frontal, et celles-ci peuvent décider de transférer certaines requêtes via des connexions existantes vers le back-end, et de créer de nouvelles connexions pour d'autres.

En conséquence, il est important de ne pas attribuer un timing de requête incohérent au comportement de l'application, tel que des mécanismes de verrouillage qui ne permettent qu'à un seul thread d'accéder à une ressource à la fois. De plus, le routage des requêtes frontales est souvent effectué sur une base par connexion, donc vous pourriez être en mesure d'améliorer le timing des requêtes en effectuant un réchauffement de connexion côté serveur - envoyer quelques requêtes insignifiantes dans votre connexion avant de réaliser l'attaque (cela consiste juste à envoyer plusieurs requêtes avant de commencer l'attaque réelle).

Mécanismes de verrouillage basés sur la session

Certains frameworks tentent de prévenir la corruption accidentelle des données en utilisant une forme de verrouillage des requêtes. Par exemple, le module de gestion de session natif de PHP ne traite qu'une requête par session à la fois.

Il est extrêmement important de repérer ce type de comportement car il peut sinon masquer des vulnérabilités trivialement exploitables. Si vous remarquez que toutes vos requêtes sont traitées séquentiellement, essayez d'envoyer chacune d'elles en utilisant un token de session différent.

Abuser des limites de taux ou de ressources

Si le réchauffement de la connexion ne fait aucune différence, il existe diverses solutions à ce problème.

En utilisant Turbo Intruder, vous pouvez introduire un court délai côté client. Cependant, comme cela implique de diviser vos requêtes d'attaque réelles en plusieurs paquets TCP, vous ne pourrez pas utiliser la technique d'attaque par paquet unique. En conséquence, sur des cibles à fort jitter, l'attaque est peu susceptible de fonctionner de manière fiable, quel que soit le délai que vous définissez.

Au lieu de cela, vous pourriez être en mesure de résoudre ce problème en abusant d'une fonctionnalité de sécurité courante.

Les serveurs Web retardent souvent le traitement des requêtes si trop sont envoyées trop rapidement. En envoyant un grand nombre de requêtes factices pour déclencher intentionnellement la limite de taux ou de ressources, vous pourriez être en mesure de provoquer un délai côté serveur approprié. Cela rend l'attaque par paquet unique viable même lorsque l'exécution retardée est requise.

{% hint style="warning" %} Pour plus d'informations sur cette technique, consultez le rapport original sur https://portswigger.net/research/smashing-the-state-machine {% endhint %}

Exemples d'attaques

  • Turbo Intruder - Attaque par paquet unique HTTP2 (1 point de terminaison) : Vous pouvez envoyer la requête à Turbo Intruder (Extensions -> Turbo Intruder -> Envoyer à 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électionnez 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 qui utilise une liste de mots du presse-papiers :

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

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

  • Tubo Intruder - Attaque en un seul paquet HTTP2 (Plusieurs points de terminaison) : Si 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 un dépassement de limite, vous pourriez simplement ajouter la même requête 50 fois dans le groupe.
  • Pour le préchauffage de 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 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'accès, vous pourriez commencer par envoyer la requête qui va vers l'état caché puis 50 requêtes juste après qui exploitent l'état caché.

BF Brut

Avant les recherches précédentes, voici quelques charges utiles utilisées qui tentaient simplement d'envoyer les paquets aussi rapidement que possible pour provoquer un RC.

  • Repeater : Consultez les exemples de la section précédente.
  • Intruder : Envoyez la requête à Intruder, réglez le nombre de threads sur 30 dans le menu Options, 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

Dépassement de limite / TOCTOU

C'est le type le plus basique de condition de concurrence où les 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, y compris :

  • Utiliser plusieurs fois une carte cadeau
  • Noter plusieurs fois un produit
  • Retirer ou transférer de l'argent au-delà du solde de votre compte
  • Réutiliser une seule solution CAPTCHA
  • Contourner une limite de taux anti-force brute

Sous-états cachés

D'autres RC plus compliqués exploiteront les sous-états dans l'état de la machine qui pourraient permettre à un attaquant d'abuser d'états auxquels il n'était jamais censé avoir accès, mais il existe une petite fenêtre pour que l'attaquant y accède.

  1. Prédire les sous-états cachés et intéressants potentiels

La première étape consiste à identifier tous les points de terminaison qui écrivent dessus, ou lisent des données à partir de celui-ci et utilisent ensuite ces données pour quelque chose d'important. Par exemple, les utilisateurs peuvent être stockés dans une table de base de données qui est modifiée par l'inscription, les modifications de profil, l'initiation de réinitialisation de mot de passe et la complétion de réinitialisation de mot de passe.

Nous pouvons utiliser trois questions clés pour écarter les points de terminaison qui sont peu susceptibles de provoquer des collisions. Pour chaque objet et les points de terminaison associés, demandez :

  • Comment l'état est-il stocké ?

Les données stockées dans une structure de données persistante côté serveur sont idéales pour l'exploitation. Certains points de terminaison stockent leur état entièrement côté client, comme les réinitialisations de mot de passe qui fonctionnent en envoyant un JWT par email - ceux-ci peuvent être ignorés en toute sécurité.

Les applications stockent souvent un certain état dans la session utilisateur. Celles-ci sont souvent quelque peu protégées contre les sous-états - plus à ce sujet plus tard.

  • Modifions-nous ou ajoutons-nous ?

Les opérations qui modifient des données existantes (telles que changer l'adresse email principale d'un compte) ont un potentiel de collision important, tandis que les actions qui ajoutent simplement à des données existantes (telles que l'ajout d'une adresse email supplémentaire) sont peu susceptibles d'être vulnérables à autre chose que des attaques de dépassement de limite.

  • Sur quoi l'opération est-elle basée ?

La plupart des points de terminaison opèrent sur un enregistrement spécifique, qui est recherché à l'aide d'une 'clé', telle qu'un nom d'utilisateur, un jeton de réinitialisation de mot de passe ou un nom de fichier. Pour une attaque réussie, nous avons besoin de deux opérations qui utilisent la même clé. Par exemple, imaginez deux implémentations plausibles de réinitialisation de mot de passe :

  1. Chercher des indices

À ce stade, il est temps de lancer des attaques RC sur les points de terminaison potentiellement intéressants pour essayer de trouver des résultats inattendus par rapport aux réponses habituelles. Tout écart par rapport à la réponse attendue, comme un changement dans une ou plusieurs réponses, ou un effet secondaire comme un contenu d'email différent ou un changement visible dans votre session pourrait être un indice indiquant que quelque chose ne va pas.

  1. Prouver le concept

La dernière étape consiste à prouver le concept et à le transformer en une attaque viable.

Lorsque vous envoyez un lot de requêtes, vous pouvez constater qu'une paire de requêtes initiales déclenche un état final vulnérable, mais que les requêtes ultérieures l'écrasent/le rendent invalide et que l'état final n'est pas exploitable. Dans ce scénario, vous voudrez éliminer toutes les requêtes inutiles - deux devraient suffire pour exploiter la plupart des vulnérabilités. Cependant, réduire à deux requêtes rendra l'attaque plus sensible au timing, vous devrez donc peut-être réessayer l'attaque plusieurs fois ou l'automatiser.

Attaques sensibles au temps

Parfois, vous ne trouverez pas de conditions de concurrence, mais les techniques pour envoyer des requêtes avec un timing précis peuvent encore révéler la présence d'autres vulnérabilités.

Un tel exemple est lorsque des horodatages de haute résolution sont utilisés au lieu de chaînes aléatoires sécurisées cryptographiquement pour générer des jetons de sécurité.

Considérez un jeton de réinitialisation de mot de passe qui est seulement randomisé en utilisant un horodatage. Dans ce cas, il pourrait être possible de déclencher deux réinitialisations de mot de passe pour deux utilisateurs différents, qui utilisent tous les deux le même jeton. Tout ce que vous avez à faire est de synchroniser les requêtes pour qu'elles génèrent le même horodatage.

{% hint style="warning" %} Pour confirmer par exemple la situation précédente, vous pourriez simplement demander 2 jetons de réinitialisation de mot de passe en même temps (en utilisant une attaque par paquet unique) et vérifier s'ils sont les mêmes. {% endhint %}

Vérifiez l'exemple dans ce laboratoire.

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

Payer & ajouter un article

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

Confirmer d'autres emails

L'idée est de vérifier une adresse email et de la changer pour une autre en même temps pour découvrir si la plateforme vérifie la nouvelle changée.

Selon ce compte-rendu Gitlab était vulnérable à une prise de contrôle de cette manière car il pourrait envoyer le jeton de vérification d'email d'un email à l'autre email.

Vous pouvez également vérifier ce laboratoire pour en savoir plus à ce sujet.

É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 existe 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 pourraient être écrits et ensuite le jeton pour confirmer le compte nouvellement créé est écrit. Cela signifie que pendant un court moment, 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'email.

Vérifiez ce laboratoire pour voir un exemple.

Contourner le 2FA

Le pseudo-code suivant montre comment un site web pourrait être vulnérable à une variation de course de cette attaque :

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

Comme vous pouvez le voir, il s'agit en fait d'une séquence multi-étapes dans la durée d'une seule requête. Plus important encore, elle passe par un sous-état dans lequel l'utilisateur a temporairement une session valide connectée, mais l'authentification multifacteur n'est pas encore appliquée. Un attaquant pourrait potentiellement exploiter cela en envoyant une demande de connexion avec une demande à un point de terminaison sensible et authentifié.

Persistance éternelle OAuth2

Il existe plusieurs fournisseurs OAuth. Ces services vous permettent de créer une application et d'authentifier les utilisateurs que le fournisseur a enregistrés. Pour ce faire, le client devra autoriser votre application à accéder à certaines de leurs données à l'intérieur du fournisseur OAuth.
Jusqu'ici, juste une connexion classique avec Google/LinkedIn/GitHub... où une page s'affiche disant : "L'application <InsertCoolName> souhaite accéder à vos informations, voulez-vous l'autoriser ?"

Condition de concurrence dans authorization_code

Le problème apparaît lorsque vous l'acceptez et que cela envoie automatiquement un authorization_code à l'application malveillante. Ensuite, cette application abuse d'une condition de concurrence dans le fournisseur de services 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é que l'application accède à vos données pour 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.

Condition de concurrence 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 permissions pour que l'application malveillante accède à 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 pour abuser des conditions de concurrence également dans les WebSockets.

Références

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

Autres moyens de soutenir HackTricks :


Utilisez Trickest pour construire et automatiser des workflows facilement, alimentés par les outils communautaires les plus avancés.
Obtenez l'accès aujourd'hui :

{% embed url="https://trickest.com/?utm_campaign=hacktrics&utm_medium=banner&utm_source=hacktricks" %}