Makina Blog
Sécurité: Problèmes de HTTP Smuggling (contrebande de HTTP) en 2015 - Partie 1
Première partie d'une série d'articles sur le HTTP Smuggling en 2015 - Injection de HTTP dans HTTP, la théorie.
version française (English version with comments available on regilero's blog.)
Le but de cette série d'articles est d'expliquer clairement ce que sont les problèmes d'HTTP smuggling (ou contrebande de HTTP en français) et pourquoi je pense que ce type de problèmes de sécurité est très important et pourrait être utilisé dans des attaques massives contre des services web modernes.
Durant les 6 derniers mois j'ai étudié l'état de plusieurs serveurs HTTP Open Source, et contribué à fixer plusieurs bugs. Le principal problème rencontré était, le plus souvent, de réussir à expliquer le problème.
Une première grosse étude sur la contrebande de HTTP fut menée en 2005 et amena plusieurs correctifs dans différents projets, cette étude fut réalisée par Chaim Linhart, Amit Klein, Ronen Heled et Steve Orrin, publiée par Watchfire elle mérite toujours une lecture attentive 10 ans après.
Aujourd'hui même la RFC 7230 HTTP/1.1 contient des protections et des avertissements à l'encontre du Request Smuggling, mais une RFC n'est qu'une référence, les choses sont très différentes quand on regarde la réalité des implémentations. Et beaucoup de gens se lancent à l'heure actuelle dans leur propre implémentation de HTTP. Un rafraîchissement du sujet s'imposait.
La plupart des liens fournis dans cette article devraient être plus simples à comprendre après avoir lu l'article. Il va y avoir une assez longue série d'articles, et nous partirons de requêtes HTTP assez simples pour arriver à certaines requêtes très étranges, avec des détails sur des failles récemment fixées sur plusieurs outils (et quelques CVE aussi). Dans ce premier article il n'y a rien de nouveau, juste une autre façon d'expliquer le problème. J'espère que cela permettra au moins un rafraîchissement des mémoires sur les problèmes à gérer.
Si vous utilisez des serveurs HTTP, et tout spécialement si vous utilisez différents agents HTTP (Reverse Proxy, Terminateurs SSL, Répartiteurs de Charge, etc.), vous devriez vous intéresser au sujet.
Si vous construisez un agent HTTP vous devriez maîtriser ce sujet, la meilleure chose serait que vous connaissiez tout cela mieux que moi.
HTTP Smuggling: Quoi?
Cacher des requêtes HTTP dans du HTTP, Injection
C'est cela, l'idée principale est de cacher du HTTP dans du HTTP.
Pour cacher un message dans un protocole il vous faut trouver une faille, un bug, dans la façon dont l'agent interprète (lit) le message.
La contrebande de Requête HTTP est simplement une injection du protocole HTTP dans le protocole HTTP. Comme toujours avec la sécurité le principal problème est l'injection. Si vous pouvez injecter du SQL dans du SQL, du HTML, javascript ou css dans une réponse HTML… vous avez un problème.
Quand on injecte du javascript dans une page HTML il faut trouver une faille dans l'application lorsqu'elle affiche du contenu utilisateur. Ici les joueurs vont traquer les failles dans la lecture (le parsing) des messages HTTP.
Il existe d'autres problèmes de sécurité très proches de la contrebande de HTTP qui sont HPP (HTTP parameters pollution, pollution de paramètres HTTP) que vous pouvez découvrir dans le papier de Stefano di Paola et Luca Carettoni à OWASP 09 et l' HTTP Response splitting (la séparation de requête HTTP).
HPP est une partie très spécifique de la contrebande de HTTP, ne prenant en considération que les paramètres utilisés dans la location et les problèmes qui surviennent des différences d'interprétation de paramètres étranges (comme des répétitions du même paramètre).
La séparation de requête ou Response splitting est une attaque utilisée sur une application (sur le backend final) où le backend renvoie plus de réponses HTTP qu'attendu. C'est un outil qui peut être utilisé dans le HTTP Smuggling, mais les failles sont peu communes (il a existé des problèmes avec des injections de retour chariot dans les redirections PHP ou dans les noms d'utilisateurs de l'authentification HTTP Digest, mais c'était il y très longtemps).
Ici je parlerais surtout du HTTP Smuggling régulier, des failles qui viennent réellement d'erreurs HTTP, d'approximations du protocole.
Pourquoi?
Nous étudierons en détail les 3 types principaux d'attaque ci-dessous. Mais si vous êtes en mesure de cacher du HTTP dans du HTTP vous pouvez effectuer plusieurs formes d'attaques, cela va de l'évitement de filtres de sécurité au détournement de sessions utilisateurs en passant par la dégradation d'une page dans un cache.
Cette histoire implique plusieurs requêtes HTTP. Nous dirons au moins 3 requêtes différentes et nous allons les nommer pour plus de clarté:
- Suzann: la requête de Smuggling (la contrebandière),
- Ivan: la requête Innocente,
- Walter: le Wookie, complice de la contrebandière, souvent une requête interdite. Et, oui, c'est un Wookie. Parce que d'habitude les contrebandiers travaillent avec des wookies.
Elles vont transiter depuis un point de départ, l'ordinateur de l'attaquant, vers un serveur HTTP (votre serveur). Et parfois elles rencontrerons un serveur middleware qui lui aussi émet du HTTP. Cela pourrait être un répartiteur de charge, un terminateur SSL, un Reverse Proxy Cache, un cache statique, etc.
Suzann la contrebandière est méchante, le but de cette requête est d'attaquer Ivan l'innocente requête.
Suzann ne sera pas une requête HTTP régulière comme ceci:
GET /suzann.html HTTP/1.1\r\n
Host: www.example.com\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Encoding: gzip, deflate\r\n
Cache-Control: max-age=0\r\n
Connection: keep-alive\r\n
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:33.0) Gecko/20100101 Firefox/33.0\r\n
\r\n
Mais plutôt quelque chose de ce type (pas toutes les erreurs en même temps :-) ):
GET http://www.example.com/suzann.html?foo=%00\tHTTP/11111111111.2\r\n
HOST: www.evil1.com\r\n
HOST: www.evil2.com\r\n
Content-length: 0\r\n
ContenT-length :\t100\r\n
Connection: keep-alive\n
Content-length:\t10\r
Transfer-eNCODIng\t\t:chunked\t\r\n
\r\n
(...)
Et votre serveur devrait rejeter la plupart des choses étranges présentes dans cette requête d'exemple (celle-ci est tellement mauvaise que je ne pense pas qu'un seul serveur puisse l'accepter).
Si une faille est trouvée dans le serveur elle pourrait être utilisée pour de la contrebande et l'attaquant peut commencer à réfléchir aux exploitations de la faille.
Nous allons commencer par un grand retour arrière et donner des détails sur le protocole HTTP pour comprendre tout cela.
HTTP
Dans l'ancien temps il n'y avait qu'une très vieille version de HTTP disponible (que nous appelons HTTP/0.9). La contrebande ne pouvait pas exister. Le seul moyen d'envoyer 3 requêtes était d'ouvrir 3 fois une connexion TCP/IP avec le serveur et à chaque fois demander le document ciblé:
--> ouverture tcp/ip conn1
GET /suzann.html\r\n
\r\n
<-- réception du document suzann.html
<html>\r\n
<head></head>\r\n
<body>Suzann</body>\r\n
</html>\r\n
<-- conn1 est fermée
--> ouverture tcp/ip conn2
GET http://your.server.com/ivan.html\r\n
\r\n
<-- réception du document ivan.html
<html>\r\n
<head></head>\r\n
<body>Ivan</body>\r\n
</html>\r\n
<-- conn2 est fermée
--> ouverture tcp/ip conn3
GET /walter.html\r\n
\r\n
<-- réception du document walter.html
<html>\r\n
<head></head>\r\n
<body>Walter</body>\r\n
</html>\r\n
<-- conn3 est fermée
HTTP est un protocole assez simple, surtout en ce temps-là. Remarquez que j'utilise
\r\n
pour représenter les caractères CR et LF, les marqueurs de fin de ligne.
Cela deviendra important plus tard.
Puis vint le temps de HTTP/1.0, avec une chose importante ajoutée, les entêtes. Vous pouvez ajouter des entêtes dans la requête, et la réponse contiendra toujours des entêtes.
La première requête devint:
--> ouverture tcp/ip conn1
GET /suzann.html HTTP/1.0\r\n
Host: your.server.com\r\n
User-agent: information gateways navigator 0.8\r\n
Accept: text/html\r\n
Foo Bar\r\n
\r\n
<-- réception du document suzann.html
HTTP/1.0 400 Bad Request\r\n
Server: Apache\r\n
Date: Fri, 24 Jul 1612 16:24:10 GMT\r\n
Content-Type: text/html\r\n
Content-Length: 138\r\n
Connection: close\r\n
\r\n
<html>\r\n
<head><title>400 Bad Request</title></head>\r\n
<body bgcolor="white">\r\n
<center><h1>400 Bad Request</h1></center>\r\n
</body>\r\n
</html>\r\n
<-- conn1 est fermée
Dans cette réponse la chose importante à remarquer est le Content-Length: 138. Si vous comptez tous les caractères, en démarrant à <html
, en incluant les caractères de fin de ligne, vous avez exactement 138 caractères, avec un octet pour chacun de ces caractère ascii7, 138 octets. On dirait que la taille est importante, et nous verrons que la taille est tout, le plus important dans notre problème. Ici nous avons aussi le code d'erreur (400), le serveur a dit que nous avons fait une erreur dans notre requête, ce qui n'est pas grave, c'est une bonne chose d'avoir dans un protocole des messages et codes d'erreur. L'erreur ici était un ':' manquant entre Foo et Bar.
Arrivons rapidement à HTTP/1.1, le vrai protocole HTTP, toujours utilisé à peu près partout. Le nouveau protocole HTTP/2 vient tout juste de commencer à le remplacer dans certains endroits très limités.
Nous avons plusieurs fonctionnalités nouvelles dans HTTP/1.1 qui vont permettre de très mauvais comportements pour Suzann la contrebandière, avec quelques autres fonctionnalités, moins importantes pour les contrebandiers (comme l'entête Host qui devient requise, elle était optionnelle auparavant).
- le mode Keep Alive
- les requêtes Pipelined
- les requêtes et réponses Chunked
Keep Alive
Avec le keep Alive nous pouvons ouvrir une connexion avec le serveur, demander Suzann, recevoir Suzann, puis demander Ivan et le recevoir, et enfin demander Walter et le recevoir, dans la même connexion TCP/IP.
En tant que client HTTP vous pouvez demander le mode KeepAlive, mais celui-ci est souvent activé par défaut par le serveur même si vous ne l'avez pas demandé. On demande ce mode en ajoutant cette entête dans la requête:
Connection: keepalive
Du côté du serveur, la connexion keep alive peut être coupée à n'importe quel moment (et la plupart des serveurs devraient essayer d'éviter les longues connexions keep alive, surtout si ils utilisent un processus dédié par connexion et ne peuvent pas supporter en mémoire plus que quelques centaines de connexions en parallèle, comme le mpm prefork du serveur httpd d'Apache).
Au niveau des entêtes de la réponse nous trouverons ceci:
Connection: close # qui signifie qu'on coupe la connexion, facile
Connection: keep-alive # qui signifie qu'on va essayer de garder la connexion quelques secondes
D'habitude, après un close
le serveur va couper la connexion tcp/ip.
Le but de ceci est de rapatrier plus rapidement toutes les ressources statiques associées à une page HTML page, en réutilisant la connexion tcp/ip ouverte pour le document, évitant l'ouverture assez lente d'une autre connexion tcp/ip.
Ce mode keep-alive dans le protocole est ce que des choses comme Comet essayent d'exploiter pour maintenir des pseudos connexions infinies en push/pull par dessus HTTP. Mais même sans ce mode avancé le mode keep-alive est très utilisé dans les environnements HTTP, surtout entre l'utilisateur final et la première partie du middleware. L'utilisation du keep-alive entre les serveurs HTTP et les proxys (à l'intérieur du middleware ou entre le middleware et le backend) est moins répandu.
Pipelines \o/
L'autre très gros apport du HTTP/1.1 est le pipelining. Le pipelining consiste à envoyer plusieurs requêtes avant d'avoir reçu les réponses de ces requêtes.
Voici un schéma de pipelining basique:
[Client] [Serveur Final]
| |
>-requ. Suzann ---------->|
>-requ. Ivan ------------>|
>-requ. Walter----------->|
|<----------- rép. Suzann-<
|<------------- rép. Ivan-<
|<----------- rép. Walter-<
Avec uniquement le Keep Alive le schéma était:
[Client] [Serveur Final]
| |
>-requ. Suzann ---------->|
|<----------- rép. Suzann-<
>-requ. Ivan ------------>|
|<------------- rép. Ivan-<
>-req. Walter------------>|
|<----------- rép. Walter-<
Avec un proxy HTTP au milieu le schéma est, couramment:
[Client] [Middleware] [Serveur Final]
| | |
>-requ. Suzann ------>| |
>-requ. Ivan -------->| |
>-req. Walter ------->| |
| >-requ. Suzann ------>|
| |<------- rép. Suzann-<
|<------- rép. Suzann-< |
| >-requ. Ivan -------->|
| |<--------- rép. Ivan-<
|<--------- rép. Ivan-< |
| >-req. Walter-------->|
| |<-------- rép Walter-<
|<------- rép. Walter-< |
Vous pouvez voir que la connexion entre l'agent du milieu (un reverse proxy cache ou un terminateur SSL) et le serveur final n'utilise pas le pipelining, sauf exception (parce que c'est une chose affreusement complexe à faire bien avec HTTP).
Parfois la connexion entre le middleware et le serveur final n'utilise même plus les connexions Keep Alive. D'autres fois il y a réutilisation d'un pool de connexions keep alive. Mais la première protection contre la contrebande de HTTP est appliquée ici en cassant le pipeline de requêtes en plusieurs requêtes successives, et en attendant la fin de la première requête avant de gérer la suivante.
Enfin, le serveur n'a aucune obligation de répondre à toutes les requêtes contenues dans un pipeline. Vous pouvez recevoir une première réponse avec une entête Connection: close
, qui va couper la connexion Keep Alive juste après la réponse.
[Client] [Middleware] [Serveur Final]
| | |
>-requ. Suzann ------>| |
>-requ. Ivan -------->| |
>-req. Walter ------->| |
| >-requ. Suzann ------>|
| |<------- rép. Suzann-<
|<------- rép. Suzann-< |
[ le client devrait ré-envoyer les requêtes Ivan & Walter]
Et ceci est la seconde grosse protection contre la contrebande de HTTP. Dès que le middleware détecte une mauvaise requête Suzann, il devrait envoyer une erreur 400 bad request ET fermer la connexion (mais si vous cherchez bien vous trouverez des exemples de proxys qui ne ferment pas les keep alive après une erreur).
Chunks
Le transfert Chunked est un moyen alternatif de transmettre un long message HTTP. Au lieu d'une transmission partant avec une entête Content-length
annonçant la taille complète finale du message vous pouvez transmettre le message en petits (ou pas) paquets (chunks), chacun annonce une taille (au format hexadécimal).
Un dernier chunk spécial, avec une taille vide, marque la fin du message.
Nous étudierons certainement en détail les transferts chunked dans les prochains articles. La chose importante avec les chunks est qu'il s'agit d'une autre méthode de manipulation de la taille du message.
Les Chunks peuvent être utilisés dans les réponses HTTP (le plus souvent) mais aussi dans les requêtes.
Pour un exemple vous pouvez lire la page Wikipedia qui explique comment les chunks peuvent être utilisés pour transférer ceci:
Wikipedia in\r\n\r\nchunks.
Sous cette forme:
4\r\n
Wiki\r\n
5\r\n
pedia\r\n
e\r\n
in\r\n\r\nchunks.\r\n
0\r\n
\r\n
Tellement plus rigolo :-)
La clef: La taille c'est important
Je l'ai dit plusieurs fois. Mais, oui, la taille c'est important. Pour injecter du HTTP dans du HTTP la clef est habituellement de tromper l'agent HTTP qui lit le message sur la taille du message.
Les requêtes et réponses HTTP sont principalement des listes de chaînes séparées par des marqueurs de fin de ligne. Et nous avons vu avec les pipelines que nous pouvons envoyer plusieurs requêtes, l'une après l'autre.
L'agent HTTP qui lit cette requête ou qui analyse la réponse DOIT savoir où cette liste de chaînes s'arrête, ainsi ce qui vient après est une nouvelle requête (ou réponse si c'est un flux en provenance du backend).
Les outils utilisés par le lecteur HTTP sont soit le mécanisme de chunks ou bien l'entête Content-Length.
Et si quelque chose se passe mal ici vous pouvez commencer à cacher quelques requêtes ou réponses. l'un des acteurs va analyser le flux et ne va pas comprendre les caractères suivants comme de nouvelles requêtes mais comme le body de la requête, ou bien il ne va pas comprendre le flux comme la réponse à la première requête mais comme une nouvelle réponse.
C'est la clef du HTTP Smuggling.
GET /suzann.html HTTP/1.1\r\n Host: example.com\r\n Content-Length: 0\r\n Content-Length: 46\r\n \r\n GET /walter.html HTTP/1.1\r\n Host: example.com\r\n \r\n
Ici si vous acceptez le premier entête Content-length vous avez 2 requêtes. Si vous prenez le second à la place, vous obtenez une requête GET, avec un body qui contient quelques octets dont vous n'avez que faire -- même s'ils ressemblent à une requête -- (une requête GET avec un body est quelque chose d'étrange, les requêtes POST ont des body et habituellement les requêtes GET n'ont que des paramètres, mais c'est autorisé).
Ok, donc on a soit une soit deux requêtes, pas de problème, mais si vous êtes un proxy qui ne voit qu'une requête et qui transmet cette requête à un backend qui va vous renvoyer 2 réponses, vous avez intérêt à savoir quoi faire de cette réponse supplémentaire. Ou bien vous auriez peut-être du détecter une mauvaise requête HTTP et éviter ce problème.
HTTP Smuggling: les bases
La contrebande de HTTP peut être utilisée dans 3 types d'attaques (principalement).
Attaque 1 : Contournement de filtres de sécurité
Le premier type d'attaque est le contournement de filtres de sécurité sur la requête interdite Walter. Dans ce type d'attaque Walter est une requête interdite (les Wookies sont une espèce interdite), mais Suzann cache Walter pour les filtres du middleware (des stormtroopers qui filtrent les docks).
Au final Walter est exécuté sur la cible, derrière les filtres (il était caché dans la cargaison).
[Attaquant] [Middleware] [Serveur Final]
| | |
>-req. Suzann(+Walter)->| |
| >-requ. Suzann(+Walter)->|
| | \-Suzann-->|
| |<---------- rép. Suzann-<
|<--------- rép. Suzann-< |
| | \-Walter-->| [*]
| |<---X------ rép. Walter-<
Le problème se produit à [*]
Remarquez ici la flèche <--X---
, le middleware n'est sans doute pas vraiment conscient qu'une requête Walter a été émise, et il peut rejeter la réponse (et la fermer). Mais la requête a déjà été émise, et ceci est suffisant pour que cela puisse être un problème.
Vous avez un exemple d'un problème de ce type dans mon précédent post de blog avec Nginx en serveur final, varnish en middleware, et quelques gargantuesques requêtes. Dans cette variante le Middleware reçoit une réponse alors qu'il pense que la première requête n'est même pas encore complètement reçue (juste pour dire qu'entre la théorie et les exploitations réelles les choses peuvent se complexifier).
Pour éviter de perdre la réponse à Walter, l'attaquant peut parfois essayer de mettre d'autres requêtes en pipeline. Mais le but de l'attaquant n'est peut-être que d'exécuter la requête Walter sans être filtré (comme accéder à un exploit de sécurité connu sur le backoffice d'un CMS dont les urls sont filtrées).
Attack 2 : Remplacement de réponses régulières
Le second type est un remplacement (defacement) d'Ivan. Lors d'une attaque réussie par Suzann, quelqu'un qui demanderait Ivan recevrait à la place une réponse Walter. ceci peut être utilisé pour empêcher l'utilisation régulière de Ivan (Déni de Service), mais cela peut devenir pire, Walter le Wookie pourrait aussi contenir du code dangereux (comme du javascript). Imaginez qu'Ivan est une librairie javascript classique sur un CDN, utilisée par quelques milliers de gens quotidiennement, si le CDN envoie le javascript Walter à la place de celui-ci…
Les requêtes doivent être dans un pipeline au niveau de l'attaquant pour effectuer ce type d'attaque.
[Attaquant] [Middleware] [Serveur Final]
| | |
>-req. Suzann(+Walter) ->| |
|-req. Ivan ---#2------->| |
| >--req. Suzann(+Walter)->| [*1*]
| | \_req. Suzann->|
| |<--------- rép. Suzann -<
|<----#1---- rép. Suzann < |
| | \_req. Walter->| t1
| >--req. Ivan ----------->| t2
| [*2*] |<--------- rép. Walter -< t3
|<----#2---- rép. Walter | |
| |<---X--------- rép Ivan |
Suzann(+Walter)
signifie que pour le Middleware il ne s'agit que d'une simple requête Suzann
mais pour le backend il y a deux requêtes en pipeline.
Le middleware voit un pipeline de deux requêtes (Suzann/Ivan) mais le backend voit un pipeline de deux requêtes (Suzann/Walter) et une troisième requête seule (Ivan).
Au niveau de la timeline vous avez aussi 3 marqueurs de temps, t1, t2 and t3.
L'attaque sera en échec si t2 se produit après t3 (pour cela la première requête Suzann peut être choisie pour être suffisamment lente pour éviter d'envoyer la réponse Walter trop vite).
La réponse régulière Ivan peut être rejetée par le middleware (qui a déjà reçu 2 réponses), c'est juste un effet de bord.
Ici la plus gros problème se situe en [*2*]
, le middleware reçoit une réponse Walter qui est assignée à une requête Ivan (requête #2).
Suzann a effectué une sorte de truc de jedi sur les gardes de l'empire des docks, ils remplissent les prochains vaisseaux cargo avec un ou plusieurs Wookies au lieu des cargaisons régulières.
Attendez, comment d'autres requêtes/personnes peuvent être impactées par le second type d'attaque?
C'est une question très importante. Dans le premier type d'attaque le but était de passer outre un filtrage de sécurité, donc le rôle de l'attaquant était évident.
Sur le deuxième type, le remplacement, l'attaquant semble être la seule personne impactée par le remplacement. Mais il faut avoir une vue plus large de l'image.
La première utilisation peut être d'obtenir une réponse sur la requête Walter (si Walter était filtrée et interdite comme dans le type 1).
La seconde utilisation, quand le middleware est un serveur de cache, est de faire du cache poisoning, où la fausse réponses sont stockées dans la cache au mauvais index.
Une attaque réussie va remplacer la réponse pour tout le monde, pas seulement pour l'attaquant. C'est l'attaque évidente, avec des conséquences très graves (Ivan a été remplacé Walter dans le cache et ceci durera le temps de l'invalidation du cache).
Mais même sans un problème de cache un attaquant peut faire qu'un proxy devienne
fou. dans certaines attaques réussies le proxy va mélanger les requêtes de plusieurs clients. Les requêtes de l'attaquant seront mélangées avec d'autres requêtes innocentes, même sans un cache, mémorisez ce point. La plupart des middlewares sont obligés de faire confiance aux réponses des backends, à leur ordonnancement, et quand les réponses des backends deviennent incompréhensibles des choses étranges se passent. ce mélange de communications entre différents utilisateurs est aussi à la base du dernier type d'attaque (type 3, détournement de droits utilisateur).
Nous verrons dans un article à venir comment un tel mélange peut survenir sans attaque de type 3.
Attaque 3 : détournement de droits utilisateur (Credentials Hijacking)
Ce troisième type d'attaque était référencée dans l'étude de Watchire de 2005. la plupart des proxy sont maintenant construits suffisamment correctement pour empêcher que ceci ne se reproduise. J'avais écrits auparavant qu'il était assez difficile de trouver de la réutilisation de connexion. Il convenait de tester un peu plus avant. La réutilisation des connexions vers les backends est en fait assez répandue chez les gros acteurs. Mais ils sont souvent suffisamment robustes pour ne pas être transmetteurs d'attaques de contrebande.
Le truc est d'injecter une requête partielle dans le flux, et d'attendre la vraie requête correcte d'un utilisateur, venant dans la même connexion au backend, et s'ajoutant à notre requête partielle. Cela signifie que le proxy est capable d'ajouter de la donnée en [+]
à une connexion tcp/ip avec le backend qui était non terminée en [-]
. mais le proxy ignore le fait que deux requêtes étaient envoyées, pour le proxy il n'y avait qu'une seule requête et il a déjà reçu la réponse.
[Attaquant] [Middleware] [Serveur Final]
| | |
>-req. Suzann[+Walter] -->| |
| >-requ. Suzann ------>|
| |<------ rép. Suzann |
|<----------- rép. Suzann | |
| | [*] |
[Innocent] | | \-requ. Walter ---->|
| | | (non terminée) | [-]
>--------------------- req. Ivan --->| |
| >-req. Ivan --------->| [+]
| |<------ rép. Walter -<
|<--------------------- rép. Walter -< |
C'était un un schéma assez complexe, mais par exemple la requête Ivan pourrait contenir une session valide que Walter n'a pas (cookies, Authentification HTTP).
Cette session valide était nécessaire pour que la requête Walter soit valide.
Les identifiants utilisés sur la requête Ivan sont volés (détournés) pour une requête Walter.
Les dommages provoqués par ce type de failles sont très grands (vous pouvez faire exécuter à un utilisateur des POST non désirés, en utilisant ses propres droits d'accès). Le Keep alive et les pipelines ne sont pas utilisés dans la plupart des proxys dans leurs communications avec les backends. Ou bien uniquement sur ceux qui sont très robustes aux transmissions d'erreurs. Implémenter un partage de connexions aux backends ou des pools est une chose dangereuse.
Transmetteurs et Séparateurs
Dans les attaques de smuggling vous aurez besoin de deux types d'acteurs réagissant différemment à quelques failles du protocole HTTP.
Au départ vous avez besoin de transmetteurs (transmitters).
Un transmetteur est un agent HTTP, un proxy, qui reçoit une requête HTTP altérée et transmet cette altération à un backend HTTP. En testant les proxys HTTP vous rencontrerez beaucoup de proxys qui nettoieront les requêtes étranges (par exemple vous utilisiez des tabulations en tant que séparateurs, mais le proxy remplace ces tabulations par des espaces quand il communique avec le backend). ici le transmetteur, par définition, ne nettoye pas les parties étranges de la requête. Les transmetteurs doivent aussi voir la requête HTTP altérée commun une requête unique.
Le second acteur de l'attaque est le séparateur (splitter), un complice involontaire. Cet agent reçoit la requête maléfique du transmetteur. Pour le séparateur cette requête transmise n'est pas unique, c'est une requête multiple (un pipeline) et cet acteur va émettre plusieurs réponses HTTP. cet agent sépare la requête.
Envoyer deux réponses pour une seule requête est suffisant, mais il pourrait aussi y en avoir une centaine.
Le séparateur a fait une erreur de lecteur et détecte un pipeline de requêtes (ou bien le transmetteur a fait cette erreur, en ne détectant pas qu'il s'agissait d'un vrai pipeline).
Nous utiliserons une typologie pour les prochains schémas:
>----qA-----> : requête HTTP pour le document A (query)
<-----rAqA--< : réponse HTTP A associée à la requête A
<-X---rAqA--< : réponse HTTP A rejetée (fermeture de connexion par exemple)
<----*rAqB*-< : réponse HTTP A associée à une requête B (pas bon du tout)
>--qA+(qB)--> : requête HTTP pour document A, masquant une requête B
[*CP*] : Cache poisoning, empoisonnement de cache
[*RS*] : Response Splitting, Séparation de réponse
Le schéma de base est:
[Origin] [Transmetteur] [Séparateur]
| | |
>-----qA+(qB)------->| |
| >-----qA+(qB)------->| [*RS*]
| |<-----------rAqA----<
|<-----------rAqA----< |
| |<-----------rBqB----<
Ici nous avons un problème de sécurité en [*RS*]
où une séparation de requête à lieu.
Si l'attaque de séparation (Splitting attack) peut être effectuée par une faille applicative vous n'avez pas besoin de transmetteur communiquant une requête HTTP altérée.
En terme de responsabilité le séparateur HTTP a un problème de sécurité réél.
Ou bien c'est ce qu'on peut imaginer, quand on discute avec les mainteneurs de projets il peut être assez difficile d'obtenir un classement des failles de HTTP splitting en tant que bugs de sécurité (en espérant que cette attitude changera dans le futur).
Le transmetteur pourrait détecter la tentative de smuggling et devrait nettoyer la requête avant de la transmettre mais ce n'est d'habitude pas considéré comme une faille de sécurité, sauf si tous les autres acteurs implémentant les RFC HTTP verraient deux requêtes là où le transmetteur n'en perçoit qu'une seule (quelque chose comme un séparateur inversé).
En utilisant cette sorte de schéma l'attaque de type 1 (contournement de filtres) est déjà comprise dans le schéma de base.
L'attaque de type 2 (defacement) a besoin d'un pipeline de requêtes. Il y a aussi besoin d'un troisième acteur, une cible. La cibles est quelque chose comme un cache qui sera la victime finale.
La cible est habituellement aussi le transmetteur.
Voici un problème de type 2:
[Origin] [Transmetteur-Cible] [Séparateur]
| | |
>-----qA+(qB)------->| |
>-----qC------------>| |
| >-----qA+(qB)------->| [*RS*]
| |<-----------rAqA----<
|<-----------rAqA----< |
| >-----qC------------>|
| [*CP*] |<-----------rBqB----<
|<-----------*rBqC*--< |
| |<-X---------rCqC----<
Ce type de comportement peut aussi mal se terminer sans caches (pas de [*CP*]
) si vous pouvez faire en sorte que la réponse <--*rBqC*--<
soit redirigée sur un autre utilisateur que l'attaquant original (évidemment ça ne devrait jamais arriver…).
Si l'attaque de séparation (Splitting) peut être lancée par une faille applicative vous n'avez toujours pas besoin d'un transmetteur mais vous avez besoin que cet acteur soit une cible.
Encapsulation et Empreintes (Fingerprinting)
Dans une attaque réelle l'attaquant peut avoir besoin de naviguer à travers plusieurs transmetteurs. comme toucher un HAProxy d'abord, puis un apache mod_proxy, puis un varnish et enfin un NGinx (oui, ça arrive).
Le premier boulot de l'attaquant The first job of the attacker est le fingerprinting ou la détection d'empreintes du middleware. Pour identifier les couches présentes au sein du middleware vous avez à disposition quelques entêtes dans les réponses HTTP (comme Server
ou des variations sur X-Cache
). mais vous avez aussi la possibilité de vérifier le comportement des différents agents pour chaque erreur du protocole.
Chaque agent possède sa propre liste d'erreurs de syntaxes rejetées. par exemple Nginx va toujours rejeter une requête HTTP utilisant des fins de lignes en CR au lieu de CRLF. Apache ne le fera pas. Varnish 3 comprends le CR en fin de lignes, Varnish 4 non, etc.
Vous pouvez construire un test d'empreinte pour identifier qui vous rejette (et parfois vous aurez de la chance et vous aurez la signature du serveur dans la page d'erreur).
Et vous pouvez aussi utiliser l'encapsulation pour cibler votre détection d'empreinte à un niveau précis.
L'Encapsulation est la possibilité de cacher votre requête HTTP au sein de plusieurs couches de contrebande d'HTTP. Habituellement les premières couches appliquent des règles strictes sur les headers HTTP et les locations, mais si vous trouvez une faille de transmetteur sur cette couche vous pouvez embarquer dans le body de la requête un autre type de faille de contrebande (une qui serait sinon détectée directement par cette couche). L'encapsulation iest disponible parce que le plus souvent le Proxy ne filtre pas le body des requêtes (et contre un filtre qui essayerait de contrôler le body de la requête vous pourriez utiliser plusieurs couches de Content-Transfer encoding).
Dans l'exemple ci-dessous le Middleware1 est un transmetteur d'un premier type de faille de contrebande noté "()" mais bloquerait toute tentative de contrebande utilisant un second type "{}".
Nous pourrions dire par exemple que la contrebande utilisant "()" se base sur une faille dans le chunked encoding et que la contrebande utilisant "{}" se base sur un doublement des entêtes Content-Length.
le Middleware2 est un transmetteur du second type de contrebande "{}" (doublement
des entêtes Content-Length
).
Le serveur final est très sensible à la contrebande "{}" issue va scinder la requête en deux.
Le but de l'attaque est d'empoisonner le cache du Middleware2 avec une réponse W
sur une requête I
. W
est interdit sur le middleware1 et aussi sur le Middleware 2.
[Attaquant] [Middleware1] [Middleware2] [Serveur Final]
| [transmetteur ()] [transmetteur {}] [ Séparateur ]
| | | |
>-qS(+qA{+qW})----->| | |
>-qB -------------->| | |
>-qI--------------->| | |
| >-qS(+qA{+qW})-->| [*RS*] |
| | >----qS-------->|
| | |<-----rSqS-----<
| |<----rSqS-------< |
|<---------rSqS-----< | |
| >-qB------------>| |
| | >--qA{+qW}----->| [*RS*]
| | [*CP*] <---------rAqA--<
| |<---*rAqB*------< |
|<--------*rAqB*----< | |
| >-qI------------>| |
| | [*CP*] <---------rWqW--<
| |<---*rWqI*------< |
|<--------*rWqI*----< | |
| | >--qB-->(...)
Pour ajouter une petite complexité vous pouvez aussi imaginer un système où une réponse HTTP est forgée (via une faille dans une application ou une attaque stockée) et cette réponse HTTP pourrait contenir une attaque de séparation de réponse. Quelque chose comme <--*rAqA(+rWqX)*--<
. Les sécurités sont toujours plus fortes dans les filtres de requêtes que dans les filtres de réponses au niveau des proxys, et la plupart des projets rejetteront ces problèmes en tant que failles de sécurité ("on doit faire confiance aux réponses des backends, vous voyez, c'est une erreur du backend").
Si les attaquants peuvent deviner les serveurs et versions à chaque étape du middleware, et si un grand nombre de failles de contrebande existent dans l'écosystème, une attaque complexe et très ciblée peut être construite.
Si vous avez compris les paragraphes précedents vous êtes près pour tester cela (ou pour lire l'étude de Watchfire de 2005).
SSL/HTTPS comme protection?
Et bien en fait, non, pas vraiment.
SSL est parfois mentionné comme une méthode de prévention de la contrebande HTTP. Ça n'est pas exact.
Avoir votre transmission de message HTTP encodée dans un tunner SSL ne prévient pas du tout la mauvaise interprétation du message par l'agent HTTP. La contrebande HTTP survient après la transmission. Peut-être qu'avoir SSL passant à travers un proxy qui n'essaye pas de comprendre le message (un pipe) peut prévenir une faille de smuggling sur ce proxy, mais c'est tout. Mmmh, oui cela peut aussi rendre les tests simples plus complexes à réaliser (parce que c'est dur de communiquer en SSL dans une session telnet) mais il ne s'agit pas d'une défense réèlle pour ce sujet (et cela ne veut pas dire que vous ne devriez pas utiliser SSL pour d'autres raisons).
Tester HTTP
Couramment une requête HTTP est faite par votre navigateur web. Vous avez quelques très bons outils de test dans ces navigateurs pour altérer les requêtes HTTP. Si vous avez déjà essayé de vous attaquer vous-même (forcément vous-même) avec du XSS ou des injections de HTML vous avez déjà certainement altéré des requêtes avec des outils comme Live HTTP Headers (et d'autres), ou bien extrait des requêtes curl depuis des vraies requêtes.
Mais pour la contrebande de HTTP vous aurez le plus souvent besoin de tester du HTTP sans navigateur, parce que vous devrez construire des requêtes vraiment mauvaises, et les navigateurs ne font jamais de mauvaises requêtes. en fait, dans le passé il y a bien eu des failles d'injection de marqueurs de fin de ligne dans l'authentification HTTP Digest ou des mauvais séparateurs dans des requêtes Ajax. Mais trouver des au sein des navigateurs qui permettent à de la contrebande de HTTP de provenir de navigateurs classiques est une exception. La contrebande n'implique généralement pas que Suzann soit un contrebandier innocent utilisant un navigateur classique.
Non, ce dont vous avez besoin est du contrôle total des tous les caractères de votre requête. par exemple vous voudrez contrôler quels caractères sont utilisés en tant qu'espaces ou en tant que marqueurs de fin de ligne.
Heureusement HTTP est un protocole basé sur du texte, et reste assez simple. Si vous n'avez jamais essayé vous pouvez tenter une session HTTP avec telnet sur le port 80 de votre serveur.
$ telnet 127.0.0.1 80
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
GET / HTTP/1.1 <---commencer à taper ici... vite!
Host: foobar <---entête requis en HTTP/1.1
<--- un dernier Entrée pour la fin de requête
HTTP/1.1 301 Moved Permanently <-- et maintenant la réponse du serveur
Server: nginx
Date: Sat, 25 Jul 2015 16:02:21 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Keep-Alive: timeout=15
Location: http://foobar.example.com/
X-Frame-Options: SAMEORIGIN
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
Mais vous avez besoin de taper vite, et vous n'avez pas de contrôle élégant des caractères. l'autre méthode pour tester est d'utiliser printf
pour afficher une requête à l'écran:
$ printf 'GET / HTTP/1.1\r\nHost:\tfoobar\r\n\r\n'
GET / HTTP/1.1
Host: foobar
Ou directement à destination du serveur (ici j'utilise netcat au lieu de telnet pour cela):
$ printf 'GET / HTTP/1.1\r\nHost:\tfoobar\r\n\r\n' | nc 127.0.0.1 80
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Sat, 25 Jul 2015 16:02:21 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Keep-Alive: timeout=15
Location: http://foobar.example.com/
X-Frame-Options: SAMEORIGIN
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
Avec ce type de requêtes vous pouvez tester facilement la réponse du serveur à une requête HTTP dégradée, par exemple essayons de remplacer tous les CR-LF (\r\n
) par des simples CR (\r
).
$ printf 'GET / HTTP/1.1\rHost:\tfoobar\r\r' | nc 127.0.0.1 80
<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
Et vous pouvez faire des requêtes avancées en n'utilisant que printf. comme un pipeline simple de requêtes:
$ printf 'GET /Suzann.html HTTP/1.1\r\nHost: example.com\r\n\nGET /ivan.html HTTP/1.1\nHost:example.com\r\n\r\n' | nc 127.0.0.1 80
Qui devient il est vrai dur à lire (et là je n'ai que le strict minimum des entêtes).
L'étape suivante est de construire votre propre outil. Pour mes tests intensifs je me suis construit des outils avec python, en utilisant la librairie socket
vous avez un client bas niveau HTTP très sympa où toutes les choses bizarres peuvent arriver, et vous avez un langage de haut niveau pour calculer des tailles (cacher des requêtes dans des chunks, compter des octets, etc.), ou pour ajouter le support du SSL.
Si vous voulez vraiment étudier la contrebande il vous faudra utiliser tcmpdump ou wireshark pour étudier les transmissions de signaux entre les différents acteurs, qui nettoie le message, qu'est-ce qui n'est pas nettoyé, comment les seuils et les temporisation affectent les comportements, etc.
L'outil final, le meilleur, c'est le code, ne soyez pas effrayés de lire le code (quand il est disponible). Apprenez le protocole et regardez ses implémentations, c'est la raison d'être du code Open Source, l'Open Source a besoin de regards critiques étudiant le code. C'est la raison pour laquelle le code open source est plus robuste, mais vous découvrirez certainement que beaucoup de lignes de codes n'ont pas encore été vérifiées.
Première conclusion
J'arrête ici ce premier article, d'autres choses bientôt, avec des failles du monde réel. Mais en attendant du nouveau contenu vous pouvez déjà lire quelques documents et tester vos outils.
Si vous utilisez HTTP (et qui ne le fait pas?), et utilisez souvent des reverse proxys, mon premier conseil est de vérifier que vous avez des versions récentes.
Les failles de contrebande sont réelles et certaines ont été fixées en 2015, évitez de garder des vieilles versions en production.
Mais je sais que cela peut être une tâche ardue. Donc mon second conseil est d'ajouter un nettoyeur de HTTP devant votre infrastructure. Quelque chose comme HAProxy. Cet outil est une protection très forte contre les smugglers (mais prenez des version récentes, bien sur). En lisant simplement la documentation de configuration de ce produit vous pourrez trouver une introduction excellente au protocole HTTP, avec les écueils courants documentés.
Pour aller plus loin
Si cet article vous a plu, n'hésitez pas à consulter notre plan de formation sur la sécurité Web.
Formations associées
Formations Outils et bases de données
Formation sécurité web
Paris Du 25 au 27 février 2025
Voir la formationFormations Python
Formation Python avancé
À distance (FOAD) Du 4 au 8 novembre 2024
Voir la formationActualités en lien
Contrebande de HTTP (HTTP Smuggling): Jetty
Détails des failles CVE-2017-7658, CVE-2017-7657 et CVE-2017-7656 (failles publiées le 2018-06-27)
Sécurité : Contrebande de HTTP, Apache Traffic Server
Détails de la CVE CVE-2018-8004 (Août 2018 - Apache Traffic Server).
Contrebande de HTTP (Smuggling): Load Balancer Apsis Pound
Détails de la faille CVE-2016-10711 (faille publiée en février 2018)