hacktricks/pentesting-web/race-condition.md

28 KiB

Condition de course


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

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

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Exploitation de la condition de course

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

Attaque à un seul paquet (HTTP/2) / Synchronisation du dernier octet (HTTP/1.1)

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

Mais, en utilisant la technique de 'synchronisation du dernier octet' d'HTTP/1.1, il est possible d'envoyer préalablement 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 envoyer préalablement 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 le drapeau 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. Retenez un cadre de données contenant le dernier octet.

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

  • Attendez 100 ms 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 de ping pour réchauffer la connexion locale. Si vous ne le faites pas, 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 sont arrivés 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. Mais les fichiers statiques sont sans importance pour les attaques RC. {% endhint %}

En utilisant cette technique, vous pouvez faire en sorte que 20 à 30 requêtes arrivent simultanément sur le serveur, indépendamment des variations du réseau :

Adaptation à l'architecture cible

Il est important de noter que de nombreuses applications se trouvent derrière un serveur frontal, et celui-ci peut décider de transférer certaines requêtes sur des connexions existantes vers l'arrière-plan, et de créer de nouvelles connexions pour d'autres.

Par conséquent, il est important de ne pas attribuer des délais de requête incohérents au comportement de l'application, tel que des mécanismes de verrouillage qui n'autorisent qu'un seul thread à accéder à une ressource à la fois. De plus, le routage des requêtes côté frontal est souvent effectué sur une base de connexion, vous pouvez donc lisser le délai des requêtes en effectuant un préchauffage de la connexion côté serveur - envoyer quelques requêtes insignifiantes sur votre connexion avant de lancer l'attaque (il s'agit simplement d'envoyer plusieurs requêtes avant de commencer l'attaque proprement dite).

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 de requête. Par exemple, le module de gestionnaire de session natif de PHP ne traite qu'une seule requête par session à la fois.

Il est extrêmement important de repérer ce type de comportement, car il peut masquer des vulnérabilités facilement exploitables. Si vous remarquez que toutes vos requêtes sont traitées séquentiellement, essayez de les envoyer chacune avec un jeton de session différent.

Abus des limites de taux ou de ressources

Si le réchauffement de la connexion ne fait aucune différence, il existe différentes 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 en un seul paquet. Par conséquent, sur des cibles à forte gigue, 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 pouvez résoudre ce problème en abusant d'une fonctionnalité de sécurité courante.

Les serveurs Web retardent souvent le traitement des requêtes s'il en est envoyé trop rapidement. En envoyant un grand nombre de requêtes factices pour déclencher intentionnellement la limite de taux ou de ressources, vous pouvez provoquer un délai approprié côté serveur. Cela rend l'attaque en un seul paquet viable même lorsque l'exécution retardée est nécessaire.

{% 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'attaque

  • Tubo Intruder - Attaque en un seul paquet HTTP2 (1 point d'extrémité): Vous pouvez envoyer la requête à Turbo intruder (Extensions -> Turbo Intruder -> Send to Turbo Intruder), vous pouvez modifier dans la requête la valeur que vous souhaitez forcer pour %s comme dans csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s puis sélectionnez examples/race-single-packer-attack.py dans la liste déroulante :

Si vous allez envoyer différentes valeurs, vous pouvez 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 site web ne prend pas en charge HTTP2 (seulement HTTP1.1), utilisez Engine.THREADED ou Engine.BURP à la place de Engine.BURP2. {% endhint %}

  • Tubo Intruder - Attaque à un seul paquet HTTP2 (Plusieurs points d'extrémité): Si vous avez besoin d'envoyer une requête à un point d'extrémité, puis plusieurs à d'autres points d'extrémité pour déclencher l'exécution de code à distance (RCE), 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 dépasser la limite, vous pouvez simplement ajouter la même requête 50 fois dans le groupe.
  • Pour chauffer la connexion, vous pouvez 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 deux étapes de sous-états, vous pouvez ajouter des requêtes supplémentaires entre les deux requêtes.
  • Pour un RC à multi-point d'extrémité, vous pouvez commencer par envoyer la requête qui va vers l'état caché puis 50 requêtes juste après qui exploitent l'état caché.

Bruteforce 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, définissez le nombre de threads à 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

La bibliothèque asyncio de Python est utilisée pour écrire du code asynchrone de manière concurrente. Elle permet d'exécuter des tâches en parallèle, ce qui est particulièrement utile pour les opérations d'entrée/sortie (I/O) intensives, telles que les appels réseau.

L'asynchronisme est basé sur le concept de coroutines, qui sont des fonctions spéciales pouvant être suspendues et reprises ultérieurement. Cela permet d'éviter les blocages et d'optimiser l'utilisation des ressources.

asyncio fournit également des primitives pour gérer les événements, les boucles d'événements et les tâches. Les événements sont des objets qui se produisent lorsqu'une opération asynchrone est terminée, tandis que les boucles d'événements sont responsables de l'exécution des coroutines et de la gestion des événements.

Pour utiliser asyncio, vous devez définir des coroutines en utilisant le mot-clé async et les exécuter dans une boucle d'événements à l'aide de la fonction run_until_complete. Vous pouvez également utiliser des mots-clés tels que await pour suspendre l'exécution d'une coroutine jusqu'à ce qu'une opération asynchrone soit terminée.

Voici un exemple simple d'utilisation de asyncio pour effectuer une requête HTTP asynchrone :

import asyncio
import aiohttp

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    url = "https://example.com"
    response = await fetch(url)
    print(response)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Dans cet exemple, la fonction fetch est une coroutine qui utilise la bibliothèque aiohttp pour effectuer une requête HTTP asynchrone. La fonction main est également une coroutine qui appelle fetch et attend la réponse. Enfin, la boucle d'événements exécute la coroutine main jusqu'à ce qu'elle soit terminée.

asyncio est un outil puissant pour écrire du code asynchrone en Python, ce qui peut améliorer les performances et l'efficacité de vos applications.

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 concurrence où des vulnérabilités apparaissent dans des endroits qui limitent le nombre de fois où vous pouvez effectuer une action. Par exemple, 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 bogue.

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édent de votre solde de compte
  • Réutiliser une solution CAPTCHA unique
  • Contourner une limite de taux anti-brute-force

Sous-états cachés

Les RC les plus complexes exploiteront des 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 d'extrémité qui écrivent ou lisent des données à partir de ceux-ci, puis utilisent 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 lors de l'inscription, de la modification du profil, de l'initiation de la réinitialisation du mot de passe et de la finalisation de la réinitialisation du mot de passe.

Nous pouvons utiliser trois questions clés pour exclure les points d'extrémité qui sont peu susceptibles de provoquer des collisions. Pour chaque objet et les points d'extrémité associés, demandez-vous :

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

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

Les applications stockent souvent certains états dans la session utilisateur. Ceux-ci sont souvent quelque peu protégés contre les sous-états - nous en parlerons plus tard.

  • Effectuons-nous une modification ou un ajout ?

Les opérations qui modifient des données existantes (comme le changement de l'adresse e-mail principale d'un compte) ont un potentiel de collision important, tandis que les actions qui se contentent d'ajouter des données existantes (comme l'ajout d'une adresse e-mail 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 d'extrémité fonctionnent 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, imaginons deux implémentations plausibles de réinitialisation de mot de passe :

  1. Rechercher des indices

À ce stade, il est temps de lancer des attaques RC sur les points d'extrémité potentiellement intéressants pour essayer de trouver des résultats inattendus par rapport aux résultats réguliers. Toute déviation de la réponse attendue, telle qu'un changement dans une ou plusieurs réponses, ou un effet de second ordre tel que des contenus d'e-mail différents ou un changement visible dans votre session, pourrait être un indice indiquant un problème.

  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 initiale déclenche un état final vulnérable, mais que les requêtes ultérieures l'écrasent/invalident 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, passer à 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 peut-être pas de conditions de concurrence, mais les techniques de livraison de requêtes avec une synchronisation précise peuvent toujours révéler la présence d'autres vulnérabilités.

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

Considérons un jeton de réinitialisation de mot de passe qui n'est aléatoire qu'à l'aide d'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 deux le même jeton. Il vous suffit de synchroniser les requêtes de manière à ce 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 à paquet unique) et vérifier s'ils sont identiques. {% endhint %}

Consultez l'exemple dans ce laboratoire.

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

Payer et ajouter un article

Consultez ce laboratoire pour voir comment payer dans un magasin et ajouter un article supplémentaire pour lequel 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 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 cet article, 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.

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

États cachés de la base de données / 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 période 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, 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 pour voir un exemple.

Contourner l'authentification à deux facteurs (2FA)

Le pseudo-code suivant démontre comment un site Web pourrait être vulnérable à une variation de cette attaque basée sur la concurrence :

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 en plusieurs étapes dans le cadre d'une seule requête. Plus important encore, elle passe par un sous-état dans lequel l'utilisateur dispose temporairement d'une session valide connectée, mais où la MFA n'est pas encore appliquée. Un attaquant pourrait potentiellement exploiter cela en envoyant une demande de connexion accompagnée d'une demande à un point de terminaison sensible et authentifié.

Persistance éternelle d'OAuth2

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

Condition de concurrence dans authorization_code

Le problème survient lorsque vous l'acceptez et envoie automatiquement un authorization_code à l'application malveillante. Ensuite, cette application exploite une condition de concurrence 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 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 pouvez 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.

Références

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥


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

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