English version is here: https://mksben.l0.cm/2021/11/css-exfiltration-svg-font.html
この記事では、SVGフォントとCSSを使って、ページ内のテキストを読み取る方法を紹介します。
CSSを使ってデータを読み取る方法はいくつか知られており、既知の手法が以下のサイトでよくまとめられています。
CSS Injection Primitives :: DoomsDay Vault
https://x-c3ll.github.io/posts/CSS-Injection-Primitives/
これらのテクニックは、入力がサニタイズされていて使えるHTMLタグが限られているケースや、Content Security Policy(CSP)の制限によってJavaScriptが使えない状況などでも、スタイルの記述ができることなど一部の条件さえ満たしていれば使えるため、攻撃者にとって有用な場合があります。
今日紹介するテクニックもそのようなテクニックの1つです。ただ、完全に新しいものではなく、以下のMichał Bentkowskiさんによる合字を使ったテクニックを少し置き換えただけのものです。
Stealing Data in Great style – How to Use CSS to Attack Web Application. - research.securitum.com
ほぼ同じ手法ではあるものの、他で言及されているのを見たことがなかったことと、MichałさんのテクニックがCSPなどの制約により使用できない状況でも、こちらは使用できる場合があり、言及する価値があると思ったため、この記事を書くことにしました。
まず、Michałさんのテクニックがどのようなものか簡単に説明します。
フォントには、合字(ligature)を設定する仕組みがあります。合字とは、複数の文字を合成して一文字にしたものです。合字をフォント側で設定すると、例えば、「a」と「b」が隣り合わせに並んでいるとき、「ab」を1つの文字とみなして字体を適用することが可能です。この仕組みが、ページ内のテキストデータの読み取り攻撃を可能にします。具体的にどのように攻撃が可能か紹介していきます。
例として、ページ内に「"secret"」という文字列があり、攻撃者はこれを読み取りたいとしましょう。このとき、このテキストが含まれる箇所をCSSセレクタで指定し、攻撃者が作成した合字を持つフォントを適用します。攻撃者はまず、「"a」が合字になっているフォントを適用します。このとき、合字の文字幅を他の文字より大きく設定しておきます。「"a」は読み取りたい部分には存在しないので、大きな文字幅のフォントはページ上には表示されないことになります。さらに、「"b」「"c」「"d」...と、別のアルファベットが合字になったフォントをそれぞれ適用していきます。そうしていくと、「"s」が合字になったフォントを適用したときに、実際に大きな文字幅の字体がページ上に表示されることになります。これが適用されたことをある方法で検出することにより、「"s」がそこにあることをリークします。どのように検出するかというと、ChromeやSafariがサポートしている「-webkit-scrollbar」というスクロールバーのスタイルを指定できるCSS疑似要素を使います。このCSS疑似要素で、合字が適用される部分に、背景画像をロードするスタイルを設定しておくのです。この背景画像は、リクエスト量を抑えるブラウザ側の配慮のため、スタイルを設定するだけではロードされず、スクロールバーが実際に表示されたときにはじめてロードされるようになっています。この仕様のおかげで、文字幅が大きいフォントが出現したときだけ、スクロールバーが現れるように対象の要素のCSSを調整しておくことで、画像のロードの有無から、「"s」がそこにあるかどうかを検出できてしまいます。「"s」がわかったら、次は、「"sa」「"sb」「"sc」...と、3文字の文字幅が大きい合字を作成して、同様の試行を繰り返し、さらに、4文字の合字、5文字の合字と、合字の文字数を増やしていくことで、最終的に対象のすべての文字を読み取ることができてしまいます。
以上がMichałさんの発明したテクニックです。Michałさんは、SVG形式のフォントをWOFF形式のフォントに変換することでこれを行いましたが、この記事で紹介する方法では、SVG形式のフォントをあえてそのまま使います。Michałさんは、ブラウザがSVGフォントのサポートを辞めたためWOFFを使用したと書いていますが、実はSafariはSVGフォントをサポートしており、現在も使えます。今から紹介する方法は、SVGフォントを使う以外はMichałさんの手法とほぼ同じです。それでもあえて紹介したいのは、SVGフォントを使わないと攻撃できない状況がありうるからです。というのも、SVGフォントは、WOFF形式のフォントなどと同じように、URLからロードすることもできるのですが、URLからロードすること無しに、フォント全てをインラインで記述して設定することもできます。こうすると、CSPがフォントリソースのロードをブロックするような状況でもフォントを定義してフォントを適用することができます。
具体的にMichałさんの手法がどのように置き換えられるかみていきます。
Michałさんの手法では、<style>タグの@font-faceからWOFFフォントをロードしていました。
<style>
@font-face {
font-family: "hack";
src: url(http://192.168.13.37:3001/font/%22/0)
}
[...]
</style>
このスタイルは、インラインのSVGフォントで次のように置き換えられます。以下は「"0」の合字だけ文字幅を大きくして、その他の文字の文字幅を小さく設定するようなフォントの定義です。
<svg>
<defs>
<font horiz-adv-x="0">
<font-face font-family="hack" units-per-em="1000"></font-face>
<glyph unicode=""0" horiz-adv-x="99999" d="M1 0z"></glyph>
<glyph unicode="1" horiz-adv-x="0" d="M1 0z"></glyph>
<glyph unicode="2" horiz-adv-x="0" d="M1 0z"></glyph>
<glyph unicode="3" horiz-adv-x="0" d="M1 0z"></glyph>
<glyph unicode="4" horiz-adv-x="0" d="M1 0z"></glyph>
<glyph unicode="5" horiz-adv-x="0" d="M1 0z"></glyph>
[...]
</font>
</defs>
</svg>
これで、CSSからfont-familyをhackに指定すると、SVG外であろうと、このSVGフォントをフォントとして使用できます。このとき、CSPでfont-src 'none' が指定されていようともブロックされることはありません。(ただし、最終的にデータの読み出しに使用するのはスクロールバーの背景画像を使った画像リクエストであることは同じなので、最低限img-srcディレクティブでリクエストを観測可能なホストが許可されている必要があります。)
実際のPoCをみていきましょう。
次のようなターゲットのページがあるとします。
https://vulnerabledoma.in/svg_font/xss.html?xss=%3Cs%3EXSS%3Cscript%3Ealert(1)%3C/script%3E
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-random';style-src 'unsafe-inline';img-src https:">
</head>
<body>
<script id="leakme" nonce="random">
const secret = "573ba8e9bfd0abd3d69d8395db582a9e";
</script>
<script nonce="random">
const params = (new URL(document.location)).searchParams;
const xss = params.get('xss');
if(xss){
document.write(xss);
}
</script>
</body>
</html>
xssパラメータにインジェクションがあり、CSPのせいでスクリプトの実行やフォントのロードはできず、scriptブロック内のsecret変数に秘密情報がある、といったページです。この状況で、SVGフォントを利用して、secretを読み出すことができることを示します。
以下のURLに"Safariで"アクセスして、「Go」ボタンをクリックすることで再現できます。
PoC: https://l0.cm/svg_font/poc.php
攻撃に利用した全てのコードはここにあります: https://github.com/masatokinugawa/css-exfiltration-svg-font
うまく動けば、以下の動画のように、複数のウインドウが開き、しばらく待っていると、「Go」ボタンがあるページ上に「573b...」と少しずつsecretが表示されていくはずです。
SVGフォントを使用した以外は、MichałさんのPoCとほぼ同じなのですが、少しだけ変更した点があります。MichałさんのPoCではiframeでターゲットのページをロードしていましたが、これをwindow.open()で開くようにしました。これは、Safariは現在デフォルトで全てのサードパーティCookieをブロックするため、iframeを使った攻撃はログイン済みのユーザーのデータを読み取る攻撃の例として現実的でないと考えたからです。また、データの受け渡し方法も変更しています。MichałさんのPoCでは、Cookieを経由して値を渡していますが、ここでも、サードパーティCookieのブロックのために背景画像のロード時にCookieをセットできないため、セッションIDをURLに付けることで代用しています。
ちなみに、一度に複数のウィンドウを開けているのは、Safariのポップアップブロッカーは1回のクリックで開けるウィンドウ数に制限がないためです。このおかげで、Safariでは、1回のクリックさえあれば、複数のウィンドウを使ってデータの読み出し試行が可能です。
以上、SVGフォントとCSSを使ってページ内のテキストを読み取る手法について紹介しました。
CSPでスクリプトの実行をブロックされることが増えてきた昨今、スクリプトを使わない攻撃はまだ何かないかといつも考えています。また何か面白いことに気付いたら紹介したいと思います。