ISO-2022-JPによるXSSの話

はじめに

久しぶりに文字コードXSSの話が盛り上がってるので、久しぶり(3年以上ぶり!)にブログを書きました。あけましておめでとうございます。今年と来年とそのさきも3年くらいよろしくお願いします。

blog.tokumaru.org

HTTPレスポンスヘッダーやHTML metaタグでの文字エンコーディング名の指定がない場合に攻撃者がISO-2022-JP特有のバイト列をHTML内に埋め込むことでブラウザーがそのコンテンツをISO-2022-JPであると誤認する、そのときに動的にJavaScript内の文字列を生成しているとするとUS-ASCIIでいうバックスラッシュではなくJIS X0201の円記号を挿入することでエスケープが無効になる、結果としてJavaScriptの文字列外に攻撃者はコードを置くことができるというのが徳丸さんの書かれている記事になります。

対策としては、徳丸さんの記事にあるとおりHTTPレスポンスヘッダーやHTML metaタグで文字エンコーディング名を明記するようにするという以外にも、もしかすると想定される文字エンコーディングに特有のおまじない文字列をHTMLの先頭付近に埋め込んでおくとかも効果があるかもしれません(未確認)。

この記事では、JavaScript文字列における円記号のエスケープ以外の攻撃パターンを少し検討してみたいと思います。

なお検証にはWindows版のGoogle Chrome 131.0.6778.205を使用しています。コンテンツをISO-2022-JPと誤認させるには他のブラウザーでは若干の調整が必要となりますが、本稿では省略します。

1. metaタグによるContent Security Policy指定のバイパス

以下のようなHTMLがあったとします。 先述の通り、HTTPレスポンスヘッダーやHTML metaタグによる文字エンコーディング名の指定はないものとします。

<!doctype html>
<html>
  <head>
    <title>[ここにタイトルが入ります]</title>
    <meta http-equiv="content-security-policy" content="default-src 'none'">
  </head>
  <body>
    <div>
      [ここにメッセージが入ります]
    </div>
  </body>
</html>

metaタグにてCSPとして`default-src 'none'`が指定されています。 また、タイトル部分にはHTMLとしてエスケープ済みの任意の文字列を、メッセージ部分にはエスケープされていない任意の文字列をそれぞれ挿入できるものとします。 メッセージ部分はエスケープされていないのでXSS可能ですが、metaタグでのCSPが効いているためスクリプトの実行はできません。

攻撃者はこのとき、タイトル部分に「(0x1B)(0x24)(0x42)(0x24)(0x24)」というバイト列を、メッセージ部分には「(0x1B)(0x28)(0x42)</title><script>...」という文字列を与えます。

<!doctype html>
<html>
  <head>
    <title>(0x1B)(0x24)(0x42)(0x24)(0x24)</title>
    <meta http-equiv="content-security-policy" content="default-src 'none'">
  </head>
  <body>
    <div>
      (0x1B)(0x28)(0x42)</title><script>...
    </div>
  </body>
</html>

タイトル部分の先頭3バイト 0x1B 0x24 0x42 はこれ以降の文字列をJIS X0208-1983つまり日本語全角文字列へと切り替えるためのエスケープシーケンスで、ISO-2022-JPブラウザーに判定させやすくするために続く2バイト0x24 0x24、日本語文字の「い」を与えています。

これにより、ISO-2022-JPエスケープシーケンスが改めて見つからない限りはJIS X0208-1983としてtitleタグ内がが解釈され、</title> やその後続の <meta> はタグとして解釈されなくなります。

攻撃者がメッセージ部分として指定した文字列の先頭3バイトは 0x1B 0x28 0x42 で、これは後続の文字列をUS-ASCIIに切り替えるためのエスケープシーケンスです。ですので、メッセージ部分として埋め込まれた以降の文字列はブラウザーにはUS-ASCIIとして解釈されます。このメッセージ部分はXSS可能ですので、攻撃者はここでtitleタグを閉じるための </title> やスクリプトとして実行するための <script> を埋め込み、無事にCSPをバイパスして任意のスクリプト実行につなげることができました。

2. 属性値やテキストノードのエスケープのバイパス

以下のようなHTMLがあったとします。

<!doctype html>
<html>
  <body>
    <input type="text" value="[ここに属性値が入ります]">
    [ここにメッセージが入ります]
  </body>
</html>

属性値はダブルクォート `"` のみがエスケープされて挿入され、メッセージ部分は `>` `<` `&` の3文字のみがエスケープされるものとします。エスケープするための関数をJavaScriptで書くとしたら以下のようなものです。

const escapeAttributeValue = (value) => {
  return value.replace(/"/g, "&quot;");
};

const escapeMessage = (text) => {
  return text
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
};

属性値内の `"` やテキストノード内の `>` `<`エスケープされますので、通常であればXSSは不可能です。

ここで、攻撃者は属性値には「(0x1B)(0x24)(0x42)」を、メッセージには「(0x1B)(0x28)(0x42)"onmouseover=alert(1)」という文字列を与えます。

<!doctype html>
<html>
  <body>
    <input type="text" value="(0x1B)(0x24)(0x42)">
    (0x1B)(0x28)(0x42)"onmouseover=alert(1)
  </body>
</html>

属性値の3バイト 0x1B 0x24 0x42 は後続の文字列をJIS X0208-1983つまり日本語全角文字列へと切り替えるためのエスケープシーケンスです。ですので、inputタグ内の属性値を閉じる `"` やinputタグ自身を閉じる `>` は解釈されないことになります。 それに続く、メッセージ部分の3バイト 0x1B 0x28 0x42 は後続の文字列をUS-ASCIIに切り替えるエスケープシーケンスで、それに続く `"` によってvalue属性が閉じられ、onmouseover属性が有効となることで攻撃者の挿入したJavaScriptが動くことになります。

まとめ

上に挙げた例は、二つとも現実にはなかなか遭遇しにくいケースだとは思います。とはいえ、ISO-2022-JPと解釈されることによるXSSが「JavaScript内の文字列を生成している」という場合に限定されるのかというとそういうわけではないということで、ちょっとまとめてみました。

いずれとも、攻撃者の挿入できる文字列が2か所・2種類必要ですが、もしかすると工夫すれば2か所に同じ文字列が出力されるときにもXSS可能というケースもあるかもしれません(要調査)。文字コード周辺の複雑さはおもしろいですね。