たまに以下のようにJavaScriptの文字列リテラルに値が入るアプリを見ることがあります。
<script> var foo="●"; ... </script>
値は「●」の箇所にHTMLエスケープされて出力されます(下の方の例も同じ)。
こんなケースでどうXSSするか?という話です。
簡単にXSSできるケース
以下のパターンだとXSSするのは簡単です。
<script> var foo="●"; var bar="●"; ... </script>
?foo=\&bar=-alert(123)//
のような値を与えるだけです。
難しいケース
次はこんなパターンを考えます。
<script> var foo="●"; var bar="●"; ... </script>
こうなると難易度はぐっと上がります。というよりも、ほとんどの場合はXSSできません。
しかし、状況次第ではXSSできることもあります。
攻撃方法
HTMLの文字コードにはUTF-8が指定されているものの、UTF-8として不正なバイトシーケンスがHTMLに出力できる状況であるとします。
そんな状況ならば、?foo=%F0&bar=-alert(123)//
のような値を与えることでXSSできます。
%F0(0xF0)はUTF-8の4バイト文字の先頭バイトです。IE6だと%F0の後ろの3バイトを食いつぶしてくれます。JavaScriptコード上で、[0xF0]の後ろに「"」(0x22)、「;」(0x3B)、LF(0x0A)の3バイトがありますが、それらがうまいこと食いつぶされるということになります。
HTMLの改行文字がLFではなくCR LFならば、後ろの4バイトを食いつぶすために、UTF-8の「5バイト文字」を使う必要があります。厳密にいうと「5バイト文字」というのは規格上存在しませんが、IE6には存在するようで、fooに「%F8」を入れれば後ろの4バイトが食いつぶされてうまくXSSできます。
IE6+UTF-8での"食いつぶし"
余談ですがIE6のUTF-8処理はかなりユニークです。
ここでは「©」(U+00A9)という文字をとりあげて説明します。
この文字は、UTF-8でエンコードすると[0xC2][0xA9]というバイトになります。これを2進数(ビット)であらわすと、以下のようになります。
0xC2 0xA9
11000010 10101001
UTF-8では2バイト目以降の先頭2ビット(上の赤字部分)は「10」で固定です。固定なので、コードポイントを示すデータではなく、「2バイト目以降である」ことを示す意味しか持っていません。とらえようによってはどうでもいい部分ということです。
IE6のUTF-8デコーダは、この2ビットを無視してデコードします。これを利用すると、ある文字を複数のバイト列で表現することができます。
11000010 00101001 ←0xC2 0x29 11000010 01101001 ←0xC2 0x69 11000010 10101001 ←0xC2 0xA9(ただしいU+00A9) 11000010 11101001 ←0xC2 0xE9
IE6は、上の4つのバイト表現をすべて「©」(U+00A9)と解釈してしまいます。
このようにIE6のデコーダはかなりルーズにできています。それもあって、直後の1バイトが食いつぶされるだけでなく、先の例のように3バイト(もしくはそれ以上)が食いつぶされるような現象が発生します。