Web Cache Poisoning

Sur Twitter je suis tombé sur pas mal de papiers décrivant les attaques dites web cache poisoning. Bien que les papiers soient très bien expliqués j'avoue avoir eu du mal à les comprendre sans tester les exploit en live. Heureusement je suis tombé sur cette application poison.digi.ninja développée par DigiNinja.

DigiNinja nous met à disposition 5 laboratoires afin de tester différents scénarios d'attaque. Dans cet article nous ne nous attarderons cependant que sur le premier exercice.

I/ La théorie

Du coup, comment fonctionne un système de cache? Eh bien pour commencer un utilisateur va requêter une page web contenue sur un serveur web. La requête va d'abord atterrir sur le serveur de cache or comme ce dernier ne dispose pas de la page en question dans son cache, il va forwarder la requête au serveur web.

Le serveur web va renvoyer la réponse au serveur de cache qui va la stocker en RAM puis la forwarder au client. Ainsi lorsqu'un nouveau client tentera d'accéder à la page whatever.php, c'est le serveur cache qui répondra sans que le serveur web n'intervienne dans les échanges:

Ce type de cache s'appelle du cache frontend. On notera qu'il existe aussi du cache backend utilisé pour stocker des données et y accéder plus rapidement. La base de données REDIS (NoSQL est un exemple de cache backend).

L'attaque cache poisoning consiste donc à empoisonner le cache du serveur cache de manière à délivrer du contenu volontairement falsifié. Cette attaque nous permettra, entre autres, de déposer une charge JavaScript qui s’exécutera sur le navigateur des clients (une XSS).

Le serveur de cache, lorsqu'il reçoit une requête pour accéder à une ressource doit déterminer lui même s'il dispose d'une copie de la ressource dans son cache. Pour cela il va se baser sur ce que l'on appelle des cache keys. Ces cache keys sont des informations présentes dans la requête d'un client qui vont être utilisées par le serveur de cache afin d'identifier de manière formelle l'accès spécifique à une ressource.

Par exemple dans la requête suivante:

GET /whatever.php HTTP/1.1
Host: whiteflag.blog
User-Agent:Mozilla......

Les cache keys sont:

  • la page web requêtée (whatever.php)
  • le header HTTP Host qui contient le nom de domaine whiteflag.blog

Et c'est tout. Alors bien évidemment ce comportement peut poser problème. Imaginons une application qui stocke la variable langue dans un cookie. Si un utilisateur requête la page via cette requête:

GET /whatever.php HTTP/1.1
Host: whiteflag.blog
User-Agent:Mozilla......
Cookie: langue=en

La page web sera cachée par le serveur en anglais. Or par défaut, le header Cookie ne fait pas partie des cache key donc si un second utilisateur requête cette même page mais en français cette fois:

GET /whatever.php HTTP/1.1
Host: whiteflag.blog
User-Agent:Mozilla......
Cookie: langue=fr

Eh bien le serveur de cache considérera que la ressource n'a pas changé et donc il délivrera la page stockée dans son cache en version anglaise

En revanche si ce second client requête la ressource avec cette requête HTTP:

GET /whatever.php?somevar=whatever HTTP/1.1
Host: whiteflag.blog
User-Agent:Mozilla......
Cookie: langue=fr

La cache key:

/whatever.php?somevar=whatever

Est différente de:

/whatever.php

Donc le serveur de cache stockera une nouvelle page qui, cette fois, sera en français.

II/ Exploitation d'un cache poisoning

Voici la page web que nous allons empoisonner:

Cette page nous indique qu'elle charge une ressource en fonction du nom de l'host requêté. En manipulant différents headers HTTP utilisés par des reverse proxy on peut remarquer que le contenu du header X-Forwaded-Host est directement affiché dans la réponse HTTP.

En envoyant la requête suivante:

GET https://365ea951.poison.digi.ninja:2443/basic.php?exploiting HTTP/1.1

Host: 365ea951.poison.digi.ninja:2443
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
Referer: https://poison.digi.ninja/
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0, no-cache
X-Forwarded-Host: hello_there
Pragma: no-cache

On obtient cette réponse:

On remarque que la valeur du header X-Forwarded-Host est directement affiché dans notre page web. Nous venons donc de trouver notre point d'injection. Pour empoisonner le cache il ne nous restera plus qu'à modifier le contenu de ce header en y ajoutant un payload XSS:

Comme vous pouvez le voir ci-dessus, l'attaque est fonctionnelle ce qui implique que toute personne qui se connectera sur cette page exécutera la charge JavaScript.

III/ Pour aller plus loin

Pour que notre réponse soit cachée par le serveur il a fallu que nous spécifions une cache key qui n'existe pas dans le serveur de cache. C'est pour cela que j'ai rajouté le paramètre "exploit" dans l'URL:

https://a505119a.poison.digi.ninja:2443/basic.php?exploit

Ce qui serait plus intéressant pour un attaquant c'est de déterminer à partir de quand le cache expire de manière à empoisonner la page principal d'un site. Parfois ce temps d'expiration est directement indiqué dans un header HTTP comme c'est le cas ici:

Le header HTTP age nous indique que le cache va expirer dans 5 secondes. En timant précisément ce header il nous sera possible d'empoisonner le cache de la page principale. De plus le header vary affiche directement quelles sont les cache keys prises en compte par le serveur de cache:

On apprend ainsi que le header HTTP "Accept-Encoding" est considéré comme une cache key

De manière générale il reste assez compliqué de trouver un point d'injection pour notre attaque via cache poisoning. L'attaque consiste à bruteforcer l'ensemble des headers HTTP potentiellement considérés comme des cache keys jusqu'à en trouver un valide. Heureusement pour nous le chercheur James Kettle (@albinowax ) a développé un module pour Burpsuite appelé param-miner. Ce module est instalable directement depuis le market place de l'outil BurpSuite (version pro) et s'occupe de bruteforcer automatiquement les cache keys. Pour le moment je n'ai pas pu le tester en condition réelle mais je ne manquerais pas d'en faire un retour plus tard :)!