ISO-2022-JPによるXSSの話
はじめに
久しぶりに文字コードのXSSの話が盛り上がってるので、久しぶり(3年以上ぶり!)にブログを書きました。あけましておめでとうございます。今年と来年とそのさきも3年くらいよろしくお願いします。
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, """);
};
const escapeMessage = (text) => {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
};
属性値内の `"` やテキストノード内の `>` `<` はエスケープされますので、通常であれば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可能というケースもあるかもしれません(要調査)。文字コード周辺の複雑さはおもしろいですね。
コーヒーを飲む量をかなり減らした
在宅生活が本格化してからは多い時では1日10杯以上コーヒーを飲んでいたけど、さすがに飲みすぎなので1日1杯に減らした話。
2年以上ぶりにブログを書いてるんだけど、ほんとに個人的などうでもいい話です。このブログにはテクニカルな話は今後もほとんど書くことはないと思うので、テクニカルな話が読みたい人は会社のエンジニアブログを読んでください!(それもあんまりテクニカルな内容じゃないけど)
もともとコーヒーが好きで、あんまり覚えてないんだけどたしか小学校3,4年生くらいのころから日常的にコーヒーを飲むようになったような気がする。親が飲んでたコーヒーがいい香りだったのでわけてもらって飲み始めたのがきっかけだったような記憶が。
で、コロナ禍以前はオフィスで自分で淹れたりバリスタの研修を受けた同僚に淹れてもらったりで毎日6,7杯は飲んでた。朝起きてコーヒー飲んで、会社についたら1杯、午前中にもう1杯、ランチ後に1杯、みたいな。ニコチン中毒な人と変わらない行動パターン。
学校や施設などで終日研修みたいなときには、近くでコーヒーを確保できる店があるかを現地到着後まず確認したり、そういった店が見当たらないときには缶コーヒーの甘さにうんざりしながらもとりあえずカフェイン摂取を優先させたり。アルコール中毒な人と変わらない行動パターン。
コロナにより在宅勤務体制になって以降はさらにひどく、いつでもコーヒーが飲める状態=いつでもコーヒーを飲んでいる状態になって、カップが空になったら次のコーヒーを淹れてるみたいな勢い。1日の水分摂取が全部コーヒーっていうのも当たり前な生活。たぶん毎日2リットル以上コーヒーを飲んでた。
で、べつにカフェインをとりすぎて眠れないなんてことはなくって、「まだあまり眠くないけどこれ以上起きてると明日に差し障るからとりあえず横になるか」って布団に入ったら、以降まったく記憶がないくらい瞬間的に眠りに落ちてるみたいな毎日。なので睡眠という点に関しては特にカフェインとりすぎて困るみたいなことはなかったんだけど、土日に頭痛が起こるのがひどくなってきて、これは何とかしないとなと思って徐々にコーヒーの量を減らした。
もともと中高生くらいから、ときどき土日の午後に頭痛が起こることがあって、特に外出してるときによく発生するなと思ってたんだけど、そこまでひどい頭痛でもなく発生頻度も高くないし1時間もすれば治まるのでそんなものか程度にしか考えてなかった。ここ数年は2,3週間に1回くらいは発生するしそれなりにひどい頭痛なので色々原因を考えてみると、土日は平日と行動パターンが違い、外出してるときは特に、コーヒーを飲む量が少なくなり午後になるとカフェインが足りなくなって頭痛が発生しているようだと思い至った。なので、カフェインが足りないんだったら摂取すればいいってことで、土日も外出先で積極的にコーヒーを飲むようにしてたんだけど、うっかりコーヒーを数時間飲み忘れると午後には頭痛に襲われる、みたいな感じだった。
で、コロナ禍での在宅生活だと、外出しないものの頭痛の発生頻度はさらに上がり、平日でも夕方になると頭痛が起こる日が増えてきたり、痛みもひどく頭痛薬を飲むか濃いコーヒーを急いで飲むか寝るかみたいな、明らかにこのままではよくないなって状態になってしまった。
そんなわけで、これはカフェインの量を減らすしかないと一念発起し、1か月くらい時間をかけながらコーヒーを飲む量を徐々に減らし、今は朝に1杯飲むくらいになった。昼にデカフェを飲むことはあるけど。
結果的に、この数か月は頭痛もほぼ発生しておらず、まあそりゃそうだよねって感じです。そんなわけで、みなさんもコーヒーの飲みすぎには気を付けましょう。
alertを出したいんだ俺たちは
利用者に対してメッセージを意識的に伝えたり、あるいは何かしらの行動を促すために、ダイアログボックスを表示するための機能がブラウザーには複数実装されている。alertの聖地、兵庫県出身者として、その手の機能を以下にまとめた。
| 機能 | UIの占有など | 機能、特徴 |
|---|---|---|
javascriptの window.alert メソッド |
タブモーダル | 任意のメッセージが表示可能。繰り返し表示される場合にそれを抑止する機能がほとんどのブラウザーに実装されている。 |
javascriptの window.prompt メソッド |
タブモーダル | 任意のメッセージが表示可能。任意の1行テキストが入力できる。繰り返し表示される場合にそれを抑止する機能がほとんどのブラウザーに実装されている。 |
javascriptの window.confirm メソッド |
タブモーダル | 任意のメッセージが表示可能。OK、キャンセルのボタンを持つ。繰り返し表示される場合にそれを抑止する機能がほとんどのブラウザーに実装されている。 |
HTML5の <dialog> 要素 |
タブモーダル | 任意のUIをHTMLで作成可能。showModal メソッドで表示させた場合、close メソッドを呼び出して明示的にダイアログを閉じるまでHTML内への操作はできない。一部ブラウザーでのみサポートされている。 |
| BASIC認証のログインダイアログ | タブモーダル | HTTPで401応答を返すことで表示されるログインダイアログ。realmで指定可能な文字種が限定的であり、またrealmで指定した文字列が表示されるブラウザーと表示されないブラウザーがあるため、任意メッセージの表示には不向き。繰り返し表示を抑制する機能はないため、何度も繰り返して表示させない方がいい。 |
javascriptの window.print メソッド |
ウィンドウモーダル。一部のブラウザーではタブモーダル | javascriptから window.print() することで表示される、印刷を促すダイアログ。任意メッセージの表示はできない*1。多くのブラウザーではウィンドウモーダルで表示されるため、他のタブに切り替えてそのページを閉じるということは難しい。繰り返し表示を抑制する機能はないため、何度も繰り返して表示させない方がいい。 |
Edge、IEの ms-get-started: プロトコルや calculator: プロトコル |
ウィンドウモーダル | これらのプロトコルスキームを window.open すると、外部のアプリに切り替えていいかのダイアログが表示される。任意メッセージの表示には不向き。ウィンドウモーダルで表示されるため、他のタブに切り替えてそのページを閉じるということは難しい。繰り返し表示を抑制する機能はない。Edgeの場合、ダイアログが表示されるごとにFile Picke UI Hostというプロセスが起動する。ダイアログを閉じていなくても複数のダイアログを連続して表示可能。これらの特徴から、ループ内から繰り返しダイアログを表示するのは非常に煩わしいため、そのようなコードは避けた方がいい。 |
他にも色々あるけれど、とりあえず人間に対して任意のメッセージを伝えるにはalertがいちばん使いやすいのでこれからも積極的に使っていきたいところ。
[雑記] コミックマーケット93向けに本を書きました
@nash_fsさんに誘われて、明日のコミックマーケット93で合同誌「WebMix」に記事を書きました。
- 日時 / 場所:C93 金曜日 (1日目) 東キ-37b
- コミケ Web カタログ URL:https://webcatalog.circle.ms/Perma/Circle/10345504/
掲載内容は
- あなたのWebアプリケーション、OWASP TOP 10 に対応できていますか? @hasegawayosuke
- 国際化ドメインとは @KageShiron
- chrome: URLs リファレンス @llmakko_cafe
- React 改善 @iskmsy
- Webベーステキストエディタの技術と実装 @nolze
です。
[Joke] ローカルネットワークに対するクロスオリジンの攻撃からシステムを守る簡単な方法
インターネット上に公開されているわなサイトを経由して社内イントラネットなどのローカルネットワークへXSSやCSRFなどのクロスオリジンでの攻撃を仕掛けようという試みは古くから存在します。現実にこれらが行われたという話は聞いたことはありませんが、理論上は可能であるという話をよく聞きます。対策としてまず検討すべきは、社内システムであっても通常のシステム同様に脆弱性を生まないようセキュアなものにするということです。それらが難しい場合には、少しの手間でローカルネットワークに対するクロスオリジンでの攻撃を低減させることができます。
フィドラでクロスオリジンからの攻撃を低減させる手順
Fiddler を起動し「Rules」メニューから「Customize Rules...」をクリックします。エディターが立ち上がってルール用のFiddlerScriptの編集画面が出てきますので、以下を追記します。
function isLocalServer(host:String):Boolean { // TODO: 環境に合わせてここを修正して下さい const localServers = ["server-name-1", "server-name-2"]; var re = /^([\d]{1,3})\.([\d]{1,3})\.([\d]{1,3})\.([\d]{1,3})$/; var m = re.exec(host); var i; // プライベートIPアドレスのホストはローカルのサーバーとみなす if (m !== null) { m[1] = m[1]|0; m[2] = m[2]|0; m[3] = m[3]|0; m[4] = m[4]|0; if (m[1] > 255 || m[2] > 255 || m[3] > 255 || m[4] > 255) return false; if (m[1] === 10) return true; if (m[1] === 172 && (m[2] >= 16 && m[2] <= 31)) return true; if (m[1] === 192 && m[2] === 168) return true; } // ローカルサーバーの一覧に存在するかチェック for (i = 0; i < localServers.length; i++) { if (host.Equals(localServers[i]) ){ return true; } } return false; } class Handlers { // 省略 static function OnBeforeRequest(oSession: Session) { // 省略 // ターゲットがローカルサーバー if (isLocalServer(oSession.hostname)) { var from = oSession.oRequest.headers["origin"]; if (from !== "") { from = from.replace(/^https?:\/\//,""); } else { var m = /\/\/([^\/]+)\//.exec(oSession.oRequest.headers["Referer"]); if (m !== null) { from = m[1].replace( /^[^@]+@/,""); } } // 呼び出し元がローカルサーバーではない if (!isLocalServer(from)) { FiddlerApplication.Log.LogFormat("Blocked {0} from {1}", oSession.fullUrl, from); oSession.utilCreateResponseAndBypassServer(); oSession.oResponse.headers.Add("Content-Type", "text/plain"); oSession.oResponse.headers.HTTPResponseCode = 404 ; oSession.oResponse.headers.HTTPResponseStatus = "404 Not Found"; } } } }
たったこれだけで、外部のわなサイトを経由したクロスオリジンの攻撃からローカルシステムをある程度守れます。
サイボウズ バグハンター合宿に行ってきた
先週の11月3日-4日に開催されたサイボウズ バグハンター合宿に参加してきた。
3年前は個人戦だったけれど今回はチーム戦ということに加え、クソつまらない脆弱性を多数報告した場合は評価が下がる採点方式だったので、最初はチームの足を引っ張らないようホームラン級のバグを探そうとしたけれど、最近すっかり技術的なことをやっていないのでなかなかそういうバグも見つけることができず、ゼロよりマシだろうということで結局わりとくだらないバグをいくつか報告した次第。とはいえ、他の人はあまりやらないだろうなという「クロスオリジンでの情報漏えい」みたいな自分としては好きなタイプのバグをひとつ見つけて報告することができたので、その点は満足。結果的にチーム順位は最下位、個人順位ではかろうじて10位に入るという感じでした(下から数えた方が早い)。Masato Kinugawaさん強すぎ。
いろんな攻撃手法に関して、以前なら自分の技として適切な場面でそれを取り出して使えることが多かったけれど、だんだんと手を動かす時間が減るにつれ他の人の攻撃例を見て「ああ、そうか。そこでその攻撃手法が使えたんだ!」みたいな感覚になっていってたんだけど、さらに今回は「なにそれ。そんな攻撃方法あったのか」みたいに全く知らない技がいくつもあったりして、すっかり時代においていかれた感を味わってきた。
実際にサイトを検証するのは、CTFと違って「問題のための問題」みたいなのではなく、実際に生きて動いてるWebサイトなのでどれだけ不合理で不自然なかたちであっても脆弱性が存在すればそれは現実のものなわけだし、あるいは特定の挙動についてあるサービスではサービスの提供方針として脆弱性として認定されるものが他のサービスでは脆弱性とは認定されないというような運用指針に寄るものがあったり、そういう点でリアルな世界なので学びも大きいし超楽しかった。
次回が開催されるのかはわからないけれど、もし開催されるのであれば次は事前にちゃんと予習復習をして、もうちょっと人権をちゃんと確保したい。
[SECURITY] podcast 第1回 セキュリティの『アレ』
@ntsujiさん、@MasafumiNegishiさん、mtakeshiさんのポッドキャスト「セキュリティの『アレ』」の収録にお邪魔してきました!
- 第1回 動画のアレからポッドキャストのアレに帰ってきたよ!スペシャル « podcast - セキュリティの『アレ』
- http://tsujileaks.com/media/are_170423.mp3
人に聞かせるために話すっていうよりは、辻さん根岸さんに解説してもらいながら3人との会話を自分自身が楽しむみたいな感じでした。ゆるゆるグデグデな感じですけどよろしくお願いします!

