2009年10月14日水曜日

htmlspecialchars/htmlentitiesはBMP外の文字を正しく扱えない

補足

この記事は旧徳丸浩の日記からの転載です(元URLアーカイブはてなブックマーク1はてなブックマーク2)。
備忘のため転載いたしますが、この記事は2009年10月14日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり

PHPの安定版(PHP5.3.0、PHP5.2.11)のhtmlspecialcharsおよびhtmlentitiesには、Unicodeの基本多言語面 (BMP)範囲外の文字、すなわち、U+10000以降の文字を正しく扱えない問題があります。
もっともシンプルな再現コードを以下に示します。
<?php
  $c = "\xF0\x90\x80\xBC"; // U+1003C
  $a = htmlspecialchars($c, ENT_QUOTES, 'UTF-8');
  echo bin2hex($a) . ':' . $a;

【処理結果】
266c743b:&lt;
U+1003Cは、Wikipediaの説明によると、大昔のギリシャの「線文字B」を表すそうで、小なり記号とは関係ないので、本来そのまま出力しなければならないものです。htmlspecialcharsおよびhtmlentitiesの内部処理で、コードポイントの下位16ビットしかみていないようで、このような結果となります。
線文字Bを扱う人口は少ないと思われますので、もう少し身近な例を探してみました。
<?php
  $c = "\xF0\xA2\x89\xBF";  // U+2227F(𢉿 …マダレに馬という字)
  $a = htmlentities($c, ENT_QUOTES, 'UTF-8');
  echo bin2hex($a) . ':' . $a;

【処理結果】
26736373696d3b:&scsim;
𢉿は、京都府長岡京市の地名で𢉿子ケ岳(からねがたけ)に使われている文字です(参考:稀少地名漢字リストGoogle Map)。一方、&scsim;はSGMLの文字実体参照のマッピングに出てくる記号で、≿(カーブした大なり記号の下に~)を表します。この実体参照形式はFireFoxなどのブラウザでは表示できませんが、そのような文字実体参照に変換される経緯は、id:moriyoshiさんの「PHPのhtmlentities()で (HTML4.0的に) 余計に実体参照に変換されてしまう文字の一覧」に説明されています。
BMP外の文字を扱う機会は少ないとは思いますが、正常系のデータが正しく扱えないという意味では、先の文字エンコーディングのチェック不備よりも重い問題だと考えます。幸い、id:moriyoshiさんが先に気づかれて、文字エンコーディングの問題と合わせて修正されていますので、おそらくPHP5.3.2からは修正されるものと思われます。PHPの最新のスナップショットにて修正ずみであることを確認しています。

影響を受けるケース

この問題を受けるのは、文字エンコーディングとしてUTF-8を使用している場合に、BMP範囲外の文字が与えられた場合です。

対策

PHP側の対応が完了するまでの間は以下のようにすればよいと思います。htmlentitiesよりはhtmlspecialcharsの方が影響が少ないこと、通常htmlentitiesを使う理由はない(参考:htmlspecialcharsと不正な文字の話 )ことから、htmlspecialcharsを使った上で、影響のある文字を扱わなければならない場合は個別に手当するしかないでしょう。Unicode5.1の範囲で、htmlspecialcharsにより不正に変換される文字は、以下の13種です。htmlentitiesを使用すると616種に増えます。𢉿もその一つです。
U+10022 線文字B音節文字
U+10026 線文字B音節文字
U+1003C 線文字B音節文字
U+20022 𠀢
U+20026 𠀦
U+20027 𠀧
U+2003C 𠀼
U+2003E 𠀾
U+E0022 言語タグ
U+E0026 言語タグ
U+E0027 言語タグ
U+E003C 言語タグ
U+E003E 言語タグ
個別に対応する方法を考えてみましたが、簡単な方法は思いつきません。いったん他の文字列に置き換えておいて、htmlspecialcharsの処理結果から、元の文字に戻す方法があると思いますが、面倒な処理になります。あるいは、影響を受ける文字を数値文字参照(&#x20022;など)に変換しておいて、htmlspecialcharsの第四パラメータ$double_encodeを0にして実行する方法もありますが、$double_encodeを0にする副作用もあります。元々実体参照や数値文字参照の形になってる文字列を変換しなくなるからです。このため現状では上記問題を許容した上で、PHP側で対応されるのを待った方がよい場合が多いような気がします。

まとめ

PHPのhtmlspecialcharsおよびhtmlentitiesには、Unicodeの基本多言語面 (BMP)範囲外の文字を正しく扱えません。次バージョン(PHP5.2.12、PHP5.3.2)では修正されると思われます。それまでの間は以下の方法で対処可能です。
  • htmlentitiesではなくhtmlspecialcharsを使用する
  • htmlspecialcharsでも影響を受ける13文字は個別に対応する、あるいは許容する

2009年10月9日金曜日

htmlspecialcharsのShift_JISチェック漏れによるXSS回避策

補足

この記事は旧徳丸浩の日記からの転載です(元URLアーカイブはてなブックマーク1はてなブックマーク2)。
備忘のため転載いたしますが、この記事は2009年10月9日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり

このエントリでは、PHPのhtmlspecialchars関数の文字エンコーディングチェック不備をついたクロスサイト・スクリプティング(XSS)脆弱性について、PHP側のパッチが提供されない状況での回避策について説明します。

何が問題か

PHPにおいて、XSS対策にはhtmlspecialcharsによって記号をエスケープすることが行われますしかし、htmlspecialcharsを利用していても、Shift_JISの先行バイトを利用して、XSSが発生する場合があります。 例えば、以下のようなINPUTがあり、外部から属性値を変更できる箇所が2カ所以上ある(以下の例では、AAAとBBB)とします。
<INPUT name="AAA" value="BBB">

ここで、AAAとBBBにそれぞれ以下のような値を与えます。文字エンコーディングはShift_JISとします。
AAA: %F1
BBB: onmouseover%3dalert(document.cookie);//
この場合、\xF1と後続の「"」が、合わせて一文字と見なされ(「■」で表します)以下のようなHTMLが与えられたとブラウザにより認識されます。
<INPUT name="■ value="onmouseover=alert(document.cookie);//">

すなわち、value=で与えたはずの「onmouseover=...」が属性値をはみ出し、イベントハンドラと見なされます。これにより、XSSが発生すると言う問題です。
一般的に、このような「Shift_JISの先行バイト」を用いたXSSは、\xF1以外にも、0x81~0x9Fおよび0xE0~0xFCの範囲のバイトが使用できます。htmlspecialchars(PHP5.2.5以降)は、これら単独の先行バイトをチェックしますが、なぜか0xF0~0xFCについてはチェックされず素通しになっています。このチェックもれが問題になっています。

今までの流れ

最近この問題が話題になっている流れを時系列に示します。
  1. 元々、htmlspecialcharsは文字エンコーディングの妥当性チェックをほとんど行っていませんでした。その当時の事情は、寺田さんの調査「htmlspecialcharsと不正な文字の話」に詳しく書かれています。
  2. その後、最近になって、「PHP5.2.5以降では文字エンコーディングのチェックが入っているけど、ちょっと中途半端だ」という調査結果を私が「htmlspecialcharsは不正な文字エンコーディングをどこまでチェックするか」にて指摘しました。
  3. このブログで、「XSSの攻撃に対する抜けが生じるかと言えば、突破の方法はちょっと思いつきません」と書いていたところ、id:t_komuraさんが「Shift_JIS では、htmlspecialchars() を使用しても XSS が可能な場合がある」にて、その突破方法を報告して下さっています。
  4. これに対して、id:IwamotoTakashiさんが、「htmlspecialcharsのパッチ私案」にて対策パッチを公開され、バグレポートを提出されましたが、「htmlspecialcharsに関する残念なお知らせ」で報告されているように、現時点ではPHP開発チームから却下されている状況です。
  5. このあたりから、一連の流れが広く知られるようになって、「もっと効果的な訴求方法があるよ」とか、海老原昂輔さんからもバグレポートが投稿されるなどの働きかけが始まっているようです。海老原さんのレポートには私のエントリも英訳されていて、本当にありがとうございます。
  6. さて、PHP本体が修正されるのが一番よいのですが、このエントリでは、PHP側の修正前に、どのようにこの問題に対策すればよいかを説明します(←イマココ)

問題が発生する条件

幸いなことに、この問題が発生するためには、色々条件がつきます。それを以下に示します。すべてAND条件です。
  1. PHPが内部で扱う文字エンコーディングがShift_JISである
  2. 入力から出力までの過程で文字エンコーディングが変換されない
  3. 入力値のバリデーションとして文字エンコーディングを検査していない
すなわち、1.と2.を合わせますと、入口・処理・出口まで一貫してShift_JISで扱っている、という条件が攻撃には必須ということになります。
入口(HTTP Request)と出口(HTTP Response)がShift_JISというサイトは珍しくなくて、ケータイサイトは大抵こうなっています。問題は、内部処理がShift_JISというところです。PHPはShift_JISの文字列リテラルをうまく扱えないので、いわゆる5C問題が発生します。そのため、「PHPの内部エンコーディングではShift_JISを避けよう」というノウハウがかなり普及しているのではないかと思います。このあたりの詳しい説明は、「Shift_JISを利用することの是非」や「第8回■主要言語の文字エンコーディングの対応状況を押さえる(ITpro)」をご覧下さい。

既存サイトの回避策

既存サイトで、一貫してShift_JISで処理している場合もやはりあるでしょう。その場合にどうこの問題に対処すればよいでしょうか。
既に稼働しているサイトの文字エンコーディングを変更するとなると、改修もさることながら、サイト全体に対するテストをしっかりやらないといけないので、そう簡単にはできないでしょう。また、文字エンコーディングのバリデーション処理を追加するのは、とてもよいことではありますが、やはりコードの改修・追加とテストが大変です。
最終的には文字エンコーディングのバリデーション処理の追加を推奨しますが、それがすぐにできない場合の暫定対応として、入口でShift_JIS→Shift_JISの変換をする、という方法があります。php.iniに以下のような設定をすることにより、入力データをShift_JIS→Shift_JISの変換を指示します。
[mbstring]
mbstring.language = Japanese
mbstring.internal_encoding = sjis-win
mbstring.http_input = sjis-win
mbstring.http_output = sjis-win
mbstring.encoding_translation = On
Shift_JIS→Shift_JISの変換というと、何もしないのではないかと思われるかもしれませんが、この指定により、不正なShift_JISに相当するバイトは除去されます。このため、Shift_JISの先行バイトを使用したXSS攻撃も防止することができます。
PHPによるアプリケーション開発に、そもそもShift_JISを使うこと自体が好ましくありませんので、あくまで暫定的・緊急に問題を回避するための手法として紹介します。また、設定変更後はサイトの動作検証を行って下さい。

新規開発する場合はどうか

これから新規開発するサイトの場合はどうでしょうか。この場合は、ぜひ以下の二点を実施して下さい
  1. アプリケーション仕様として適切な文字エンコーディングを選択する
  2. 文字エンコーディングのバリデーションを実施する
これらの内容は既にITproに詳しく書いていますので、そちらを参照して下さい。
また、これらの回に先立ち、なぜそうすべきかも説明しているので合わせてご覧いただければと思います。

まとめ

PHPのhtmlspecialcharsがShift_JISの先行バイトをきちんとチェックしていないために、半端な先行バイトを悪用したXSSが可能となることが指摘されています。これに対して、問題が発生する条件と、暫定的な対策、根本的な対策を説明しました。
文字エンコーディングの問題は、後から対策しようとするとやっかいですが、上流工程で考慮しておけば大幅に労力を削減することができます。この機会に、文字エンコーディングの問題に関心を持っていただければ幸いです。

[PR]Webアプリケーションのセキュリティ対策はHASHコンサルティングまで

本日のツッコミ(全1件)

□ 海老原昂輔 (2009年10月10日 05:35)
ご紹介いただいてありがとうございます。今回僕が行動できたのも、徳丸さんのわかりやすいエントリがあってこそです。こちらでも改めてお礼を言わせていただきます、本当にありがとうございました。

2009年9月30日水曜日

htmlspecialcharsは不正な文字エンコーディングをどこまでチェックするか

補足

この記事は旧徳丸浩の日記からの転載です。元URLアーカイブはてなブックマーク1はてなブックマーク2
備忘のため転載いたしますが、この記事は2009年9月30日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり


このエントリでは、PHPのhtmlspecialchars関数の第三パラメータ(文字エンコーディング指定)により、どこまで文字エンコーディングの妥当性チェックをしているかを報告します。 2007年4月に、寺田氏(id:teracc)の素晴らしいエントリ「htmlspecialcharsと不正な文字の話」により、htmlspecialcharsは、第三パラメータの文字コードを実質的に無視しており、不正な文字エンコーディングが指定された場合、その文字はチェックされずにすり抜けてしまうという結果が報告されています。

寺田氏のエントリには、検証に使用したPHPのバージョンが明記されていませんが、エントリが書かれた時期から考えて、PHP5.2.1またはそれ以前のバージョンと考えられます。以下は、PHPに同梱されるnews.txtからの引用です。
08 Feb 2007, PHP 5.2.1
03 May 2007, PHP 5.2.2
しかし、その後PHP5.2.5に至って、文字エンコーディングのチェックがなされるように改善されました。PHP5.2.5のnews.txtから引用します。
08 Nov 2007, PHP 5.2.5
【中略】
 - Fixed htmlentities/htmlspecialchars not to accept partial multibyte sequences. (Stas)
部分的なマルチバイト・シーケンスを受け付けないようにしたとのことですので、本当にそうなっているかを以下のコードを用いて検証しました。
<?php
function test($s, $enc) {
  $e = htmlspecialchars($s, ENT_QUOTES, $enc);
  echo bin2hex($s) . ':' . $s . ' -> ';
  echo bin2hex($e) . ':' . $e . "\n";
}

  echo phpversion() . "\n";

  test("\xC0\xAF", 'UTF-8');                  // 「/」の冗長表現(2バイト)
  test("\xE0\x80\xAF", 'UTF-8');              // 「/」の冗長表現(3バイト)
  test("\xF0\x80\x80\xAF", 'UTF-8');          // 「/」の冗長表現(4バイト)
  test("\xF8\x80\x80\x80\xAF", 'UTF-8');      // 「/」の冗長表現(5バイト)
  test("\xFC\x80\x80\x80\x80\xAF", 'UTF-8');  // 「/」の冗長表現(6バイト)
echo "-------------------------------\n";
  test("\xC0\xBC", 'UTF-8');                  // 「<」の冗長表現(2バイト)
  test("\xE0\x80\xBC", 'UTF-8');              // 「<」の冗長表現(3バイト)
  test("\xF0\x80\x80\xBC", 'UTF-8');          // 「<」の冗長表現(4バイト)
  test("\xF8\x80\x80\x80\xBC", 'UTF-8');      // 「<」の冗長表現(5バイト)
  test("\xFC\x80\x80\x80\x80\xBC", 'UTF-8');  // 「<」の冗長表現(6バイト)
echo "-------------------------------\n";
  test("A\xC2", 'UTF-8');                     // C2 (2バイトパターンなのに1バイト)
  test("A\xC2<", 'UTF-8');                    // C2 に < が続く
  test("A\xC2/", 'UTF-8');                    // C2 に / が続く
  test("A\xE6\xBC", 'UTF-8');                 // E6 BC (3バイトパターンなのに2バイト)
  test("A\xE6\xBC<", 'UTF-8');                // E6 BC に < が続く
  test("A\xE6\xBC/", 'UTF-8');                // E6 BC に / が続く
echo "-------------------------------\n";
  test("A\x8A", 'Shift_JIS');                 // 8A (Shift_JISの先行バイト)
  test("A\x8A/", 'Shift_JIS');                // 8A / が続く
  test("A\x8A<", 'Shift_JIS');                // 8A < が続く
echo "-------------------------------\n";
  test("A\xB4", 'EUC-JP');                    // B4 (EUC-JPの先行バイト)
  test("A\xB4/", 'EUC-JP');                   // B4 に / が続く
  test("A\xB4<", 'EUC-JP');                   // B4 に < が続く
このスクリプトをPHP4.4.9(PHP4系の最終バージョン)、PHP5.2.0~PHP5.2.4で実行した結果は以下のようになります。バージョン表示以外は同じ結果です。
5.2.4
c0af:/ -> c0af:/
e080af:/ -> e080af:/
f08080af:/ -> f08080af:/
f8808080af:/ -> f8808080af:/
fc80808080af:/ -> fc80808080af:/
 -------------------------------
c0bc:< -> 266c743b:&lt;
e080bc:< -> 266c743b:&lt;
f08080bc:< -> 266c743b:&lt;
f8808080bc:< -> 266c743b:&lt;
fc80808080bc:< -> 266c743b:&lt;
 -------------------------------
41c2:A■ -> 41c200:A■ 
41c23c:A■< -> 41266c743b:A&lt;
41c22f:A■/ -> 41c22f:A■/
41e6bc:A■ -> 41e6bc00:A■ 
41e6bc3c:A■< -> 41266c743b:A&lt;
41e6bc2f:A■/ -> 41e6bc2f:A■/
 -------------------------------
418a:A■ -> 418a:A■
418a2f:A■/ -> 418a2f:A■/
418a3c:A■< -> 418a266c743b:A■&lt;
 -------------------------------
41b4:A■ -> 41b4:A■
41b42f:A■/ -> 41b42f:A■/
41b43c:A■< -> 41b4266c743b:A■&lt;
不正な文字エンコーディングは■で表示しています。UTF-8の冗長表現の場合でも記号がエスケープされているので、文字エンコーディング指定がまったく無視されているわけではありませんが、Shift_JISやEUC-JPに関しては、文字エンコーディングISO-8859-1が指定されているかのような動作です*1。ISO-8859-1はhtmlspecialcharsのマニュアルによれば、文字エンコーディング指定が省略された時のデフォルト値ですから、「htmlspecialcharsの第三パラメータは指定しても意味がない」と思う人が出ても不思議ではありません。続いて、PHP5.2.5~PHP5.2.11、PHP5.3.0の結果を示します(PHP5.2.7は欠番になったので試していません)。やはり、バージョンの表示以外は同じ結果です。
5.2.5
c0af:/ -> c0af:/
e080af:/ -> e080af:/
f08080af:/ -> f08080af:/
f8808080af:/ -> f8808080af:/
fc80808080af:/ -> fc80808080af:/
 -------------------------------
c0bc:< -> 266c743b:&lt;
e080bc:< -> 266c743b:&lt;
f08080bc:< -> 266c743b:&lt;
f8808080bc:< -> 266c743b:&lt;
fc80808080bc:< -> 266c743b:&lt;
 -------------------------------
41c2:A■ -> :
41c23c:A■< -> :
41c22f:A■/ -> :
41e6bc:A■ -> :
41e6bc3c:A■< -> :
41e6bc2f:A■/ -> :
 -------------------------------
418a:A■ -> :
418a2f:A■/ -> 418a2f:A■/
418a3c:A■< -> 418a266c743b:A■&lt;
 -------------------------------
41b4:A■ -> :
41b42f:A■/ -> 41b42f:A■/
41b43c:A■< -> 41b4266c743b:A■&lt;
確かに一部の■が取り除かれていますが、全てではありません。上記結果より、以下のような処理となっていることが伺えます。
  1. UTF-8の冗長表現は許容している
  2. UTF-8の冗長表現であっても、記号のエスケープは行われる
  3. UTF-8として不正な文字(冗長表現は別)が1カ所でもあれば、出力は空になる、
  4. Shift_JIS、EUC-JPの「半端な先行バイト」が入力の末尾にあると、出力は空になる
  5. Shift_JIS、EUC-JPの先行バイトに続くバイトが不正の場合、先行バイトは単独の文字として扱われる
個人的には、どうしてこんな中途半端な仕様にしたのだろうと思いますが、上記の仕様でXSSの攻撃に対する抜けが生じるかと言えば、突破の方法はちょっと思いつきません(上記第4項が防御に貢献しています)。しかしながら、半端な先行バイトやUTF-8の冗長表現を許容する点で危なっかしいことも確かですし、不正なシーケンスを出力する必要は全くないわけですから、以下のような仕様にすべきだと私は考えます。
  • 冗長なUTF-8は不正なエンコーディングとして扱う(出力を空にする)
  • Shift_JIS、EUC-JPの2バイト目が不正な場合も、エラーとして出力を空にする

htmlspecialcharsの変遷

htmlspecialcharsの仕様の変化を時系列で整理します。PHPのマニュアルによると、htmlspecialcharsの第三パラメータが追加されたのはPHP4.1.0(2001年12月)、そのパラメータが実際に機能しだしたのは、前述のようにPHP5.2.5(2007年11月)です。その他も含め、以下にhtmlspecialcharsの変遷をまとめました。
PHP3.0    1998/06/06    htmlspecialcharsの提供
PHP4.0.3  2000/10/11    第2パラメータ(quote_style)追加
PHP4.1.0  2001/12/10    第3パラメータ(charset)追加
                        UTF-8、Shift_JIS、EUC-JPもサポートされている
PHP5.2.5  2007/11/08    文字エンコーディングの検査が追加(完全ではない)
ごらんのように、htmlspecialcharsは、PHP3からサポートされている*2由緒正しい関数で、早い時期に文字エンコーディング指定ができるようになりました。しかし、文字コードのセキュリティという観点から言えば、当初の仕様では役にたたず、2007年11月のPHP5.2.5に至ってようやく最低限のチェックがなされるようなりました。

PHPプログラマの意識はどうか

次に、現場のPHPプログラマの意識について考察してみます。長い間charsetパラメータが有効でなかったため、「htmlspecialcharsのcharsetパラメータは指定しても無意味」という意識が、一部のPHPプログラマに定着しているのではないかと感じています。例えば、過去にはてなの日記「Shift_JISを利用することの是非」にて取り上げた「はじめてのPHPプログラミング 基本編5.3対応」という書籍には、以下のような記述があります。
(htmlspecialcharsは)通常は第2引数まで指定すればほぼ問題ありませんが、念には念を入れるのであれば第3引数(文字エンコーディング)まで指定した方が良いでしょう。 「はじめてのPHPプログラミング基本編5.3対応」P203より引用
これを読んだ読者は、第3引数は指定する必要はないのだなと思うことでしょう。また、同じエントリのコメント欄も興味深い内容です。
テストしましたがPHPに元々入っているhtmlspecialcharsに関してはSJISを第三引数に指定しても全く意味がないようです。 SJISとして認識できない半端文字の場合はSJIS指定しても貫通する様子なのですがどうでしょう。
[id:arrayさんのコメントより引用]
統計をとったわけではありませんが、この意見が現状のPHPプログラマの意識を象徴しているように感じます。ですが、現在のhtmlspecialcharsは不完全ながら文字エンコーディングをチェックしていますので、意識を変えていかなければいけませんね。

まとめ

htmlspecialcharsの第三パラメータにより文字エンコーディングを指定することにより、不正な文字エンコーディングによる攻撃をある程度防御していることが分かりました。しかし、前述のように中途半端な挙動であるため、入口でのバリデーションにより文字エンコーディングの妥当性チェックをしておくべきでしょう。また、PHP5.2.5以降を使用することが必須で、特別な理由がない限りPHPの最新バージョンを使うべきです。 また、今回のテーマに関連して、id:t_komuraさんの素晴らしいプレゼンテーションが公開されていますので、あわせて参考になさってください。私が指摘していない内容についても言及されています。

追記(2009/10/05)

id:t_komura さんがこの件に関して検証して下さいました。そのブログエントリ「Shift_JIS では、htmlspecialchars() を使用しても XSS が可能な場合がある」によると、
PHP の htmlspecialchars() では、SJIS(Shift_JIS) の場合、\xf0 - \xfc を単独で指定しても排除しない(PHP 5.3.0 で確認)
だそうです。 まぁ、入力・内部処理・出力を全てShift_JISで通す場合に問題になるものですので、通常このような文字エンコーディングの選択はしない方がよいとは思いますが、あり得ないことではないので注意が必要ですね。 文字コードの選択には、ITproに書いた解説をご参照ください。また、Shift_JISを通して使うことの問題については、はてなダイアリーに書いた「Shift_JISを利用することの是非」をご参照ください。

*1 UTF-8の際に不正な文字エンコーディングがあると末尾にナル文字がついていますが、これは単純なバグでしょうね。
*2 PHP/FIにはHtmlSpecialCharsという関数があったようですが

2009年1月30日金曜日

とくまるひろしのSession Fixation攻撃入門

 やぁ、みんな,元気?とくまるひろしです。今日はSession Fixation攻撃の方法をこっそり教えちゃうよ。
 いつもは防御側で漢字の名前でやってるんだけど,きょうは攻撃側ということで,名乗りもひらがなに変えたんだ。だってさ,今度デブサミでご一緒するはせがわようすけさんとか,はまちちゃんとか,ひらがなの人たちの方が格好良さそうじゃないか。
 では始めよう。
# このエントリは、はてなダイアリーの過去のエントリの転載です。

用意するもの

大まかな手順

それでは,やってみよう

セッションIDを準備する

 これは二つの方法があるよ。一番ふつーなのは,標的サーバーを閲覧してそこで発行されたセッションIDを記録することだね。ふつーはこれでいける。たいてい,サイトに行っただけでセッションIDが発行されるからね。セッションIDはCookieとかにPHPSESSIDとかJSESSIONIDのように,SESSやSESSIONとIDを組み合わせた名前であることが多い。値は英数字を組み合わせた乱数のようになっているはずだ。たぶん。
 でも,たまにサイトにログインしないとセッションIDが発行されない場合がある。無料で会員になれるサイトだったら,会員になってログインすればいい。セッションIDを記録したらログアウトしておいた方がいいかな。この時セッションIDがつけなおされていたら,そのサイトの攻撃は難しいと思うよ。
 簡単に会員になれないサイトの場合はどうするか。その場合は,適当に英数字の組み合わせでセッションIDを作る。そんなんでうまくいくかって?うまくいかないこともあるんだけど,PHPで書いてあるサイトなら大丈夫。PHPは寛大なんだ。
 これで,セッションIDの準備は終わりだよ。

標的ユーザにセッションIDを強要してサイトに誘導する

 この部分がSession Fixation攻撃のキモだね。なんせFixationって言うくらいだから,強要が大事なんだ。
 セッションIDを強要する方法はいくつかあるけど,簡単なものから順に説明するね。

(1)URL埋め込みのセッションIDを使う
 PHPJavaなんかだと,セッションIDをURLに持てるんだよ。まぁ設定にもよるんだけどさ。
 確かめ方は簡単だ。ブラウザCookieをオフにして,そのサイトを使ってみる。URLにPHPSESSIONIDとかJSESSIONIDとか出てきたらしめたものだ。そのURLをそのままメールにコピペして,標的ユーザに送りつける。一人二人じゃだめだぜ。大量に送るんだ。
 えっ,Cookieをオフにする方法はどうするかって? おいおい,よしてくれよ。僕らはスーパーハッカーを目指しているんじゃなかったのかい? それくらいは自分で調べてくれ。

(2)Cookie Monsterを使う
 URLが使えない場合は標的ユーザのCookieをセットしてやらないといけないんだが,これはちょっと面倒だ。
 一番簡単な方法はCookie Monsterかな。セサミストリートに出てくる怪物…じゃなくてぇ,そういうバグがあるんだ。Firefoxの2.Xとか,Safariとかに。
 まず,.ne.jpとか,.or.jpとか.co.jpのドメインサーバーを用意する。.co.jpドメインとるのは通常難しいから,.ne.jpか.or.jpかな。IEでも,tokyo.jpみたいな地域ドメインだとこれが使える。おっと,標的サーバードメインと末尾が同じでないとダメだ。サーバーはレン鯖でおk。そこで,次のようなCookieを吐いてやるんだ。
Set-Cookie: JSESSIONID=XXXXXXXXXXXXXXXXXXXX; path=/; domain=.ne.jp
 XXXの部分は,さっき用意したセッションIDな。別にCGI使わなくても,JavaScriptでも,METAタグでもいい。METAタグの場合は,次のようにする。
<META http-equiv="Set-Cookie" content="JSESSIONID=XXXXXXXXXXXXXXXXXXXX; path=/; domain=.ne.jp">
 標的ユーザがうっかりこのコンテンツを踏むと,彼の自慢のSafari君の中では,.ne.jpドメインCookieがセットされるわけ。その後彼は,example.ne.jp(標的サイト)に誘導されると,さっきのCookieが送られて,ばきゅーん,セッションIDがFixationされちゃう,みたいな。
 CGIへの誘導は,やっぱりメールかな。ブログでもいいよ。

(3)HTTPヘッダインジェクションを使う
 段々難しくなるな。標的サーバーHTTPヘッダインジェクションの脆弱性があれば,それを使ってCookieをセットできる場合が多い。やり方までは説明しないよ。それくらいは自分で勉強してくれ。

(4)XSSを使う
 クロスサイト・スクリプティングを使っても,Cookieをセットできる。けどね,ふつーそんなことは僕らしないのよ。なぜ? XSS使ってCookie盗んじゃえばふつーになりすましできるもんね。まぁどうしてもやりたければ止めないけど。
 あぁ,でも(2)のCookie Monsterちゃんとの組み合わせならありかな。標的サイトにはXSSないけど,同じ属性ドメインや地域ドメインの別サイトのXSSを使わせてもらう。だって,tokyo.jpみたいな地域ドメインも個人でとれるけど,そんなとこから足がついたらいやだもんね。だから,同じ地域ドメインで別サイトのXSSを使わせてもらう。BODYの中でも,さっきのMETAタグは有効だから,JavaScript使わなくても楽勝でCookieセットできるよ。こんな流れだ。
hogehoge.fugafuga.setagaya.tokyo.jpにXSS発見
  ↓
hogehoge.fugafuga.setagaya.tokyo.jpのXSSに標的ユーザを誘導
  ↓
<META http-equiv="Set-Cookie" content="JSESSIONID=XXXXXXXXXXXXXXXXXXXX; path=/; domain=.tokyo.jp">
  ↓
.tokyo.jpドメインセッションIDがCookieにセットされる
  ↓
標的サイトのxxx.tokyo.jpに誘導
  ↓
Session Fixation発動
 うーん,これIEでも有効だから凶悪だなぁ。地域ドメイン使っている人はCookie使わない方がいいかも…って無理か。Session Fixation対策を入念にしとけってとこかな。

(5)その他
 他にもCookieセットする方法はあるけど,まぁレアかな。昔の本とかだと,標的ユーザが席離れた隙にCookieセットする,みたいなことが書いてあったりするけど,ばっかじゃねぇのと思うよね。Cookieセットする暇があったら,キーロガーでもなんでも仕込めるじゃない。この間も逮捕されたばかりだな。空き巣に入ってキーロガー仕掛けた奴が。
 だから,さ。離席中にCookieセットする,みたいなのを読むと,まるで,女の子に無理に酒飲ませて泥酔させてさ,おいおいこいつ何するんだと思って見てたら,下着の色確認しただけで満足する,みたいな。却ってそれ,へんたーい,って感じがするのよ。
 あぁ,そうそう,DNS攻撃する手もあるか。でも,それも泥酔攻撃と同じで変よね。というわけで(1)か(2)がふつーだと思うよ。

罠に掛かるのを見張る

 ここまで準備ができたら,ワナに使ったセッションIDでサイトをひたすら定期的にアクセスし続ける。これは,標的さんがワナに掛かるのを見張る意味もあるんだけど,もう一つ重要な意味があるよ。
 それは,これをしないと,セッションタイムアウトっていうのが起こるわけ。だから,定期的に,そう10分くらいおきにアクセスするべきだろうね。
 えっ,そんな面倒くさい? 情けないこと言うね。手でアクセスするわけないじゃない。スクリプトよ,す・く・り・ぷ・と。まぁ,wgetとバッチファイルでもポーリングくらいはできるけど,ハッカー目指すみんなはRubyPythonあたりでクールな監視のスクリプトを作ってくれ。ハッカーの癖にひねくれた奴だと,wshvbscriptjscriptってのもいそうだね。まぁ,好きにしてくれ。

セッションハイジャックする

 ユーザがワナに掛かったら,いよいよセッションハイジャックだ。ようは,なりすましだな。
 一番ふつーなのは,ユーザが、仕掛けたセッションIDのままログインしてしまうのを待つわけ。そしたらさ,こっちはそのセッションIDを知っているわけだから,そいつでなりすましできるってこと。
 ログイン機能がないサイトでも,ハイジャックできる場合があるよ。でも,レアかな。たまに,個人入力画面が何段階かに分かれていて,途中の情報をセッションに入れている場合なんか、見れる場合があるね。「hiddenは危険」なんてIPAのサイトに書いてあるもんだから,セッションの方が安全だろうと思って墓穴を掘るケースよね。こういう場合はhiddenの方がずっと安全です。おっと,安全の話をしているんじゃなかった。
 えっ,ユーザがログインしたとたんにセッションIDが振り直された? あぁ,そういうサイトは狙ってもダメね。他をあたりなさい。狙えるサイトは,いくらでもあると思うよ,きっと。たぶん。




無粋な注意書き

 このコンテンツはSession Fixationの啓蒙を目的として,同攻撃手法を分かりやすく、親しみやすく説明することを目的にしています。
 この情報を元にした悪用は法律で禁止されています。あくまで防御目的のために,攻撃手法の理解のためにお読み下さい。

フォロワー