
Détecte et exploite les vulnérabilités de contrebande de requêtes HTTP via la conversion HTTP/2 vers HTTP/1.1, en utilisant des techniques automatisées de contrebande d'en-têtes pour identifier les divergences d'analyse du serveur backend.
Cet outil aide à détecter et exploiter le smuggling de requêtes HTTP dans les cas où il peut être réalisé via la conversion HTTP/2 -> HTTP/1.1 par le serveur frontal.
Le schéma est le suivant :
L'attaquant veut trouver une telle requête qui sera vue comme deux requêtes distinctes par le serveur back-end.
Si la connexion HTTP/1.1 entre le frontal et le back-end utilise keep-alive, le frontal pourrait envoyer des requêtes d'autres utilisateurs sur la même connexion. Si nous parvenons à "empoisonner" la connexion avec une requête partielle qui suit une requête légitime, nous pouvons récupérer la requête d'un autre utilisateur.
D'autres scénarios possibles incluent le contournement de la protection et des réécritures du serveur frontal, l'empoisonnement du cache ou la tromperie du cache.
Pour plus d'informations sur le HTTP Request Smuggling, veuillez vous référer à Portswigger Web Security Academy.
En HTTP/2, tous les noms et valeurs des en-têtes HTTP sont binaires. Cela signifie que techniquement, ils peuvent contenir des espaces supplémentaires ou même des retours à la ligne.
RFC7540#10.3 stipule que les implémentations qui convertissent les requêtes HTTP/2 en HTTP/1 doivent tenir compte des limitations sur le jeu de caractères qui découlent d'une telle conversion ; la plupart des implémentations les rejettent effectivement. Malgré cela, nous espérons en trouver qui autorisent de tels en-têtes. Ils corrompront la requête HTTP/1.1 convertie vers le back-end.
Un autre point est que certaines corrections récentes liées au HTTP Request Smuggling pourraient n'être implémentées que pour les analyseurs HTTP/1.1.
En général, nous espérons qu'il existe des implémentations de HTTP/2 qui ne sont pas très au courant des recherches récentes sur le HTTP Request Smuggling en HTTP/1.1 et n'incluent pas les mesures d'atténuation correspondantes.
Étonnamment, oui !
J'ai trouvé une possibilité de faire passer en contrebande un en-tête avec un espace via Cloudflare, ouvrant ainsi une porte pour le smuggling entre Cloudflare et le client (dans le cas où le logiciel d'un client Cloudflare accepte et nettoie les noms d'en-têtes). Voici l'article de blog.
Il y a aussi un autre rapport de bug bounty qui n'est pas encore public. Il utilise le fait que les logiciels personnalisés ne filtrent pas les retours à la ligne dans les en-têtes HTTP2, et le smuggling se produit à 100% (je peux voir les requêtes des autres utilisateurs).
Cependant, je comprends que ce type de vulnérabilités doit être frustrant rare : contrairement à HTTP/1.1, il n'y a pas autant d'implémentations de HTTP/2, et la plupart sont conçues avec la sécurité à l'esprit, rejetant ainsi les en-têtes suspects ou invalides.
L'outil dispose d'une sous-commande qui tente de détecter automatiquement si une cible est vulnérable à l'attaque HTTP Request Smuggling. L'algorithme derrière cette fonctionnalité est décrit dans cette section.
Pour réaliser une attaque HTTP Request Smuggling, nous devons d'abord "faire passer en contrebande" un seul en-tête (soit Content-Length soit Transfer-Encoding). Cela signifie que nous devons envoyer un en-tête qui a) contrôle où le corps de la requête se termine et b) n'est pas traité par le frontal mais est traité par le back-end.
Cela est généralement réalisé en modifiant un en-tête d'une manière ou d'une autre : ajouter des espaces ou des tabulations à la fin de son nom, remplacer la valeur par une semi-équivalente, etc.
L'idée de base de l'algorithme de détection de vulnérabilité est de détecter si le serveur traite réellement un en-tête passé en contrebande comme s'il s'agissait de Content-Length ou Transfer-Encoding. Nous faisons cela en envoyant plusieurs requêtes : certaines avec des valeurs valides et d'autres avec des valeurs invalides pour l'en-tête. Ensuite, nous essayons de détecter s'il existe un moyen de distinguer les réponses de ces deux groupes.
C'est pourquoi la sortie de l'outil ne contient pas les mots "vulnérable/invulnérable" : il dit simplement s'il peut distinguer les réponses provenant des requêtes de ces deux groupes.
L'outil considère que deux ensembles de réponses HTTP sont distinguables si au moins l'une des deux conditions suivantes est remplie :
Les timeouts sont traités comme une valeur de code d'état unique et différente de toute autre ; ainsi, l'outil remplace le schéma classique de "détection par timing".
Considérons un exemple. Supposons que nous essayions de faire passer en contrebande l'en-tête transfer-encoding en remplaçant le tiret par un underscore.
S'il apparaît que le serveur répond avec le code 400 chaque fois que nous envoyons transfer_encoding:zalupa et se bloque lorsqu'il s'agit de transfer_encoding:chunked, nous pouvons dire que le serveur traite probablement l'en-tête comme la valeur pour le transfer-encoding. Théoriquement, il pourrait s'agir soit du serveur frontal, soit du serveur back-end.
Le premier cas n'est pas intéressant car nous pourrions de toute façon envoyer la version non passée en contrebande de l'en-tête, et le second est ce que nous recherchons. Comme tout se passe en HTTP/2, le premier cas est évitable la plupart du temps : le serveur HTTP/2 détermine la fin du corps de la requête d'une autre manière qui n'est pas liée aux en-têtes de la requête et ne s'attend jamais à ce que le corps soit au format chunké HTTP/1.1 (celui avec des longueurs de chunks hexadécimaux).
Les variations concrètes des techniques de détection sont :
Nous envoyons une version passée en contrebande de Transfer-Encoding: chunked (par exemple transfer_encoding:chunked) et différents corps : le valide est 0\r\n\r\n et l'invalide est 999\r\n.
Dans le cas où les réponses sont différentes, nous pouvons être certains que le serveur back-end reçoit et traite l'en-tête passé en contrebande. Il n'y a aucune raison pour que le frontal fasse cela : HTTP/2 n'utilise pas le format chunké que nous avons envoyé, donc il serait invalide.
Nous nous attendons à ce que le back-end se bloque (c'est-à-dire que la requête expire) pour les requêtes invalides car il attend l'arrivée de plus de données.
C'est la variante de détection la plus fiable : si le serveur se bloque lors de la lecture du corps, quelque chose a probablement mal tourné car il n'y a pas d'utilisation de l'encodage de transfert HTTP/1.1 dans une requête HTTP/2.
Nous envoyons à nouveau une version passée en contrebande de Transfer-Encoding et différents corps : 0\r\n\r\n comme corps valide et X\r\n\r\n comme corps invalide.
Le cas est le même que ci-dessus, mais au lieu de lire le corps, nous nous attendons à ce que le back-end le valide au moins.
Nous envoyons une version passée en contrebande de l'en-tête Content-Length avec les valeurs 1 et .
En envoyant plusieurs paires de requêtes valides/invalides, nous pouvons réduire le risque de faux positif aléatoire. D'un autre côté, nous pouvons nous arrêter tôt si nous voyons qu'il n'y a aucun moyen de séparer les réponses sur les requêtes "valides" des réponses sur les requêtes "invalides".
L'outil tente d'employer plusieurs techniques de smuggling en modifiant un en-tête de diverses manières. Aucune d'elles n'est nouvelle et n'est pas évidente en même temps.
Pour faire passer un en-tête en contrebande, nous y ajoutons un espace. C'est la méthode la plus courante et classique. Nous espérons que l'en-tête n'est pas traité par le frontal mais est envoyé tel quel au back-end, qui supprime l'espace.
L'outil essaie une variété de caractères comme espaces : cela inclut , \t, \v, \x00 et les caractères Unicode.
Pour faire passer un en-tête en contrebande, nous remplaçons le tiret (-) par un tiret bas (_). Si le back-end est inspiré de CGI d'une manière ou d'une autre, il pourrait convertir les en-têtes comme Header-Name en la forme HEADER_NAME ; ainsi, le tiret deviendra de toute façon un tiret bas. En déterminant comment analyser le corps, un tel back-end demande supposément la valeur de CONTENT_LENGTH / TRANSFER_ENCODING depuis son dictionnaire d'en-têtes, et elle y sera.
Celui-ci est spécifique à HTTP/2. Comme HTTP/2 est un protocole binaire, nous pouvons essayer d'envoyer des retours à la ligne dans le nom ou la valeur de l'en-tête. La norme l'interdit, mais nous espérons trouver une implémentation qui l'accepte encore.
Lors de la conversion HTTP/2 -> HTTP/1.1, l'en-tête se divise en deux en-têtes différents, ce qui signifie que la requête aura un aspect différent pour le back-end.
Pour faire passer un en-tête en contrebande, nous plaçons son nom et sa valeur après un retour à la ligne : un en-tête avec le nom 'Transfer-Encoding' et la valeur 'chunked' devient un en-tête avec le nom 'fake' et la valeur 'fake\r\ntransfer-encoding: chunked'.
Supposons que le back-end utilise un langage de haut niveau et n'effectue pas assez de validation des en-têtes. Dans ce cas, il pourrait convertir les noms en majuscules avant de faire quoi que ce soit d'autre et le faire en utilisant des fonctions sensibles à l'Unicode. Heureusement, TRANSFER-ENCODING contient la lettre S, qui est ſ (\u017f) en majuscule.
De même, nous pouvons chercher un back-end qui convertira la valeur de Transfer-Encoding en minuscules : nous envoyons chunKed au lieu de chunked avec \u212a au lieu de K.
Bien sûr, il est nécessaire que le frontal transmette les noms/valeurs d'en-têtes UTF-8 au back-end.
Pour installer l'outil, exécutez go install github.com/neex/http2smugl@latest.
L'outil contient deux sous-commandes : request et detect. La première sert simplement à élaborer des requêtes HTTP/2 : la plupart des outils clients n'acceptent pas les en-têtes invalides, il est donc pratique d'en avoir un qui envoie l'entrée utilisateur telle quelle au serveur.
L'autre est detect. Il essaie diverses techniques de HTTP request smuggling pour détecter si la cible est vulnérable. L'algorithme de détection est compliqué ; j'apprécierais que vous le lisiez et m'envoyiez vos commentaires. Il est décrit ci-dessous dans la section correspondante.
http2smugl requestUtilisez cette sous-commande pour envoyer une requête http2 (probablement un peu malformée). Le premier paramètre est l'URL, et les autres sont simplement des en-têtes au format name:value (notez qu'il n'y a pas d'espace après les deux-points). L'échappement par antislash est pris en charge : vous pouvez utiliser les codes d'échappement \r, \n et \xXX. Par exemple, pour envoyer un nom d'en-tête contenant un deux-points, utilisez \x3a (par exemple name\x3awith\x3acolons:value).
http2smugl detectCette sous-commande tente de détecter le HTTP request smuggling en utilisant diverses techniques. Pour l'utiliser, exécutez simplement http2smugl detect [URL HTTPS].
La commande affichera quelque chose uniquement si elle peut détecter que le serveur analyse un en-tête passé en contrebande. Pour comprendre ce que cela signifie, veuillez lire la section correspondante.
Un support expérimental pour HTTP/3 (quic) a été implémenté. Cependant, je ne suggère pas de l'utiliser car je n'ai trouvé aucun bogue lié à HTTP/3.
Pour utiliser HTTP/3 avec la sous-commande request, fournissez le proto https+h3:// dans l'URL au lieu de simplement https. La même chose est prise en charge dans la commande detect.
Il y a aussi un drapeau --try-http3 pour la sous-commande request, qui change le comportement au cas où le protocole n'est pas spécifié dans l'URL (seulement le nom d'hôte). Si le drapeau est présent, la commande essaiera le proto https+h3 pour ces entrées sur la ligne de commande ou le fichier de cibles (ainsi que HTTP/2). Par exemple, http2smugl detect --try-http3 www.example.com essaiera à la fois HTTP/3 et HTTP/2, mais http2smugl detect --try-http3 https://www.example.com/ essaiera toujours seulement HTTP/2.
Dans cette section, je décris certains cas où l'outil dit que les réponses sont "distinguables", mais qu'aucune vulnérabilité ne pourrait exister.
L'Elastic Load Balancer d'Amazon implémente plusieurs mesures d'atténuation contre le HTTP Request Smuggling. Bien qu'elles ne rejettent pas toutes la requête par défaut, ELB ne réutilisera pas une connexion après avoir envoyé une requête avec des en-têtes "suspects". Ainsi, une attaque réelle n'est pas possible.
Pour détecter que vous avez affaire à ELB, vous pouvez utiliser l'en-tête de réponse Server. S'il est filtré, vous pouvez envoyer une requête avec l'en-tête content__length (notez deux tirets bas) et la valeur -1 : si elle est 400, il s'agit probablement d'ELB ou d'un autre WAF (voir ci-dessous).
Apache Traffic Server est principalement utilisé chez Yahoo. Il traite HTTP/2 d'une manière inhabituelle : il le convertit en HTTP/1.1 en mémoire puis ré-analyse la requête résultante. Ainsi, bien qu'un en-tête puisse techniquement être "passé en contrebande", il n'y a aucun moyen que cela aboutisse à une vulnérabilité : vous pourriez envoyer les mêmes octets à une connexion HTTP/1.1.
Le moyen le plus simple de détecter que vous avez affaire à ATS (en plus de l'en-tête Server) est d'envoyer une requête TRACE. Si un en-tête Max-Forwards: 0 est présent dans la requête, ATS renverra une réponse aux requêtes TRACE par défaut sans la transmettre au back-end.
Il semble que Microsoft IIS prenne en charge le décodage de l'encodage chunké dans les corps HTTP/2. C'est un comportement étrange ; cependant, il est innocent du point de vue de la sécurité.
Pour détecter que vous avez affaire à IIS, vous pouvez envoyer une requête avec un en-tête 'transfer-encoding:chunked' et un corps chunké incorrect. Si vous voyez Microsoft-HTTPAPI ou quelque chose comme cela dans l'en-tête Server, c'est ça.
Les WAF essaient de détecter les en-têtes suspects - c'est leur travail. Parfois, cela amène l'outil à dire "distinguable" : un WAF pourrait bloquer quelque chose comme content_length:-1 et autoriser content_length:1 simplement parce que ses filtres aboutissent à ces décisions.
Si vous voyez un WAF dans l'en-tête Server, c'est probablement un faux positif.
Si vous avez des idées sur ce sujet, contactez-moi via @emil_lerner sur Twitter ou @neexemil sur Telegram - ou postez un problème ici sur GitHub.
-1Les deux valeurs sont invalides du point de vue du frontal : il existe un autre mécanisme pour déterminer la longueur du corps en HTTP/2, et aucun corps de requête n'est réellement envoyé dans les deux cas. Si les réponses sont différentes, nous supposons que c'est le serveur back-end qui a analysé les en-têtes.
Cette méthode est la moins fiable : le frontal pourrait émettre des erreurs différentes lorsque Content-Length a une valeur invalide, et lorsqu'elle ne correspond pas à la longueur réelle.