自作検査ツール - SQLインジェクション編

前回の日記からだいぶ日にちが空いてしまいました。

今日は、自作検査ツールのSQLインジェクションシグネチャについて書きます。

SQLインジェクションの検査シグネチャとしては、以下の5種類を用意しています。

SQLインジェクションは、かなりの頻度で脆弱性が発見されること、また一般的に危険度の高い脆弱性であることから、シグネチャの種類を多くしています。

それぞれのシグネチャについて、以下で順番に見ていきます。

A. SQLエラー検出+簡易なBlind

最初に試すのはベーシックなパターンです。

イ:【元の値】'"\'"\ … SQLエラーになる
ロ:【元の値】''""\\ … SQLエラーにならない
ハ:【元の値】'"\'"\ … SQLエラーになる(イと同じ)

それぞれで、実行されるSQL文は以下のようなものになります。

【イ、ハで実行されるSQL文】 … SQLエラーになる
SELECT * FROM test1 WHERE col2='xxx'"\'"\'

【ロで実行されるSQL文】     … SQLエラーにならない
SELECT * FROM test1 WHERE col2='xxx''""\\'

判定は、以下のいずれかの条件を満たす場合に「要注意」とします。

  • イの応答にSQLエラーメッセージ(らしきもの)が含まれる
  • 検査文字列を送った際の応答を分析し、イ≠ロかつイ≒ハを満たす

この段階で「要注意」に留めて置くのは、いくつかの理由で誤検出(SQLエラーが出力されるが、実際には有意な攻撃に利用できない等)が生じるのを防ぐためです。

なお、このシグネチャでは脆弱性を検出できないケースもあります。例えば、文字列型以外のBlind SQLインジェクションでは、イとロの両方ともSQLエラーになり、両者の応答に有意な差が出ないため、検出できません。

また、文字列型であっても、内部的に発生したSQLエラーが握りつぶされる状況では検出できません。エラーが握りつぶされるというのは、何らかのデータの検索機能で、イとロともに「マッチするデータはありません」のような同等の応答が返る場合などが該当します。

文字列型以外、あるいはSQLエラーが握りつぶされるケースについては、これ以降のいくつかのシグネチャで検出を試みます。

B. Blind 数値型・カラム名

数値型・カラム名などの「'」で括られていない部分に値が出力されるタイプの脆弱性の検査です。具体的には、以下のようなSQL文が実行されるケースです。

1: SELECT * FROM test1 WHERE col1=$p;
2: UPDATE test1 SET col1=$p1 WHERE col3>$p2;
3: SELECT * FROM test1 ORDER BY $p1 $p2;

それぞれ、数値リテラル部分(1,2)、カラム名やASC/DESC等のSQLキーワード(3)が制御可能になっています。全て、SQL文の「'」で括られていない部分が制御可能であるという点で共通しているため、ひとまとめに同じ手法で検査します。

イ:【元の値】       … 正常
ロ:【元の値】'"\'"\ … SQLエラーになる
ハ:【元の値】/*q*/  … 正常(と等価)
二:【元の値】/q**/  … SQLエラーになる
ホ:【元の値】*/q*/  … SQLエラーになる

イとロは、Aのシグネチャで既に送っているので、このBのシグネチャではハニホを送ります。

ハニホでそれぞれ実行されるSQLは、以下のようになります。

ハ:SELECT * FROM test1 WHERE col1=8/*q*/
ニ:SELECT * FROM test1 WHERE col1=8/q**/
ホ:SELECT * FROM test1 WHERE col1=8*/q*/

多くのデータベースでは、「/* ... */」のようなブロックコメントが使用できます。そのため、上のハはSQLエラーにならずに元の値(上の例では「8」)を入れた場合と同等の処理が実行されることが期待できます。一方、ニとホはSQLエラーになることが期待できます。

判定は、それぞれの検査文字列を送った際の応答を分析し、イ≠ロ、ロ≠ハ、ハ≠二、ニ≒ホの場合に「脆弱性有り」とします。

判定方法としては、シンプルなアプリでは、イ≒ハとイ≠ニを確認するのが手っ取り早いです。しかし、イとハの応答が近いものになることを常に期待できるかというと、それほど単純ではない(こともある)ので、ニ≒ホを確認するようにしています。なお、ニ≒ホの条件がないと、一部のアプリで誤検出が多く発生してしまいます。

また、カラム名やテーブル名のインジェクションが可能なケースでは、このシグネチャで検出できることが多いと思いますが、以下のように角括弧やバッククォートでテーブル名やカラム名を括る構文が使用されているケースでは検出できません。

SQL Server】
SELECT [col1] FROM [test1] ORDER BY [col1]

【MySQL】
SELECT `col1` FROM `test1` ORDER BY `col1`

この辺りになると、少なくともBlind手法で検出するのはなかなか難しいので、現状では対応するシグネチャは用意していません。

もう一つ、数値型に特化したシグネチャも一応用意しています。

イ:【元の値】       … 正常
ロ:【元の値】'"\'"\ … SQLエラーになる
ハ:【元の値】*(1)   … 正常(と等価)
二:【元の値】*()1   … SQLエラーになる
ホ:【元の値】*)(1   … SQLエラーになる

ハニホでそれぞれ実行されるSQLは、以下のようになります。

ハ:SELECT * FROM test1 WHERE col1=8*(1)
ニ:SELECT * FROM test1 WHERE col1=8*()1
ホ:SELECT * FROM test1 WHERE col1=8*)(1

先ほどのシグネチャと同じく、イ≠ロ、ロ≠ハ、ハ≠二、ニ≒ホの場合に「脆弱性有り」とします。

数値演算を使う方法は、割と広く知られている方法です。以下のページでも「+」演算子を使う方法が紹介されています。

参考:Research

使用する演算子は、「+」「-」「*」「/」の4つのいずれかならば、全てのDBMSで使えるはずなのでどれでもいいです。ただし、複雑な式にインジェクションされるケースもあり、そのようなケースでは演算子の優先順位がきいてきます。例えば、以下のようなケースでは、「+」「-」は期待通りに動きません。

【元のクエリ】
SELECT * FROM test1 WHERE col1=8*12

【操作したクエリ】
SELECT * FROM test1 WHERE col1=8+0*12

なお、実際のところは、先ほどのコメント構文を使うシグネチャでカバーできる場合が多いため、必要性はそれ程高くないシグネチャです。しかし、ブロックコメントが使えない環境もあるために、この数値演算を用いたシグネチャを作成しました。ただし、このシグネチャでは、「ORDER BY」などへのインジェクションを検出できません。

C. Blind 文字列型

「'」で括られた文字列リテラル部分に、エスケープなどがされずに出力されるタイプの脆弱性の検査です。

MySQL等の一部のDBMSでは、「"」(ダブルクォート)も括り文字として使用されることがあります。ただ、そのようなケースはまれなので、Aのシグネチャでカバーすると割り切って、このCのシグネチャでは「'」で括られた文字列のみを考慮します。

文字列リテラル部分に対するBlind検査を行なう場合、代表的なのは「【元の値】' AND '1'='1」などを使う手法では無いかと思います。この方法は、大抵はうまく機能するわけですが、以下に挙げるようにうまくいかないケースもあります。

1: SELECT * FROM test1 WHERE col2 LIKE '%$p%';
2: SELECT * FROM test1 WHERE col2 IN ('$p1', '$p2');
3: SELECT * FROM test1 WHERE col2=somefunction('$p');
4: UPDATE test1 SET col2='$p' WHERE col4='xxx';
5: INSERT INTO test1 VALUES ('$p1', '$p2');

例えば上の1で「【元の値】' AND '1'='1」を入れると、以下のようなSQL文が実行されます。

【元のクエリ】
SELECT * FROM test1 WHERE col2 LIKE '%テスト%';

【操作したクエリ】
SELECT * FROM test1 WHERE col2 LIKE '%テスト' AND '1'='1%';

下のクエリはSQLエラーにはなりませんが、元のクエリと等価なものではなくなっています。

このようなケースを含めてうまく機能しうる方法として、文字列連結演算子を使用する方法があります。

参考:Research

ただし文字列連結演算子を使う方法は、DBMS毎の構文の違いにより検査の手数が増えるデメリットがあります。The Web Application Hacker's Handbook: Discovering and Exploiting Security Flaws には、以下の3種類の文字列連結演算子が載っています。

||        … Oracle, DB2, Postgres等
+         … SQL Server, Sybase等
スペース  … MySQL(デフォルト設定時)

MySQLの場合、厳密には、スペースだけではなくU+0009〜U+000Dの空白文字類は文字列連結の役割を果たします。((また、以下のように、空白文字類を使わない文字列連結方法もあります。
SELECT 'A'"B"'C'; →「ABC」が返る))

作成したツールでは、この3種類の演算子に対応したシグネチャをそれぞれ作りました。例えば、「スペース」のシグネチャでは以下の検査文字列を送ります。

イ:【元の値】       … 正常
ロ:【元の値】'"\'"\ … SQLエラーになる
ハ:【元の値】'  '   … 正常(と等価)
二:【元の値】' ''   … SQLエラーになる
ホ:【元の値】'' '   … SQLエラーになる

ハニホでそれぞれ実行されるSQLは、以下のようになります。

ハ:SELECT * FROM test1 WHERE col2='xxx'  ''
ニ:SELECT * FROM test1 WHERE col2='xxx' '''
ホ:SELECT * FROM test1 WHERE col2='xxx'' ''

他の例と同じく、イ≠ロ、ロ≠ハ、ハ≠二、ニ≒ホの場合に「脆弱性有り」とします。

ちなみに、文字列連結は適用範囲が広い方法ですが、常にうまくいくとは限りません。

【元のクエリ】
... WHERE foo='8'*60

【ハで操作したクエリ】
... WHERE foo='8'||''*60

文字列連結演算子の優先順位が低いために、下の操作したクエリはSQLエラーになったり、元のクエリとは等価ではないものになったりします。例外的なケースですが、文字列連結シグネチャの適用が難しいケースもあるということです。

D. 更新系クエリ

商品情報の変更機能等のように、更新系のクエリ(UPDATE、INSERT、DELETE文)が発行される機能を検査するためのシグネチャです。

更新系の機能の検査は、参照系の機能に比べて難しいところがあります。しかし更新系の機能であっても、SQLエラーが発生したときに特異な応答――SQLエラーメッセージが応答に含まれていたり、そうでなくても、「システムエラーです」等のメッセージ、ステータス500、途中で切れたHTMLなど――が返る場合には、シグネチャA,B,Cで検出できることが多いです。

問題は、アプリがSQLエラーを完全に握りつぶしてしまい、SQLエラーが発生したにもかかわらず「データを更新しました」のような通常と全く同じ応答が返る場合です。そのような場合はエラーの発生を知ることができないため、シグネチャA,B,Cで検出できません。

このような面倒なケースについては手動で検査をしてもよいのですが、一応それ用のシグネチャも作っておきました。

NNNNNN-1000/*q''XXXXXX\tq*/

※ NNNNNNはランダムな数字
※ XXXXXXはランダムな英数文字列

これは、基本的に、UPDATE文のSET句やINSERT文のVALUES句に値が挿入されることを想定した検査文字列です。数値と文字列の両方を、まとめてひとつにしています。

アプリが脆弱であるとして、数値部分に上記の操作した値が挿入される場合は、DBにNNNNNN-1000の演算結果を含む値が格納されるはずです。「'」がエスケープされずに文字列に挿入される場合は、「''」が「'」になるために、「q'XXXXXX」を含む値がDBに格納されるはずです。MySQLやPostgresで「\」のエスケープがされない場合は、「\t」が解釈された結果、DBに「XXXXXX[TAB]q」を含む文字列が格納されるはずです。

あとは格納された値を引き出すことができれば、脆弱性の有無を確認できます。

例えば商品情報の変更機能では、パラメータを操作した上で商品情報変更のためのリクエストを送り、DBに値を格納させます。その後にツールは、その商品情報を参照するページに対する追加的なリクエストを送り、その応答を対象にして数値演算の結果や「q'XXXXXX」などが含まれるかを調べます。なお、応答を調べる際には、「'」が何らかのエンコードをされたり、数値がカンマ区切りになっている可能性を考慮します。

ただし、このシグネチャで更新系機能のSQLインジェクション全てを発見できるわけではありません。以下のような場合は、このシグネチャでは検査できません。

  • プルダウンやラジオボタンで選択するようなパラメータ
  • UPDATEやDELETE文のWHERE句にインジェクション可能なケース
  • カラムのサイズがあまり大きくない場合

このような制限があるために、実際のところ、更新系のSQLインジェクション検査は手動に頼る部分が大きいです。

E.文字コード

例えば、アプリがUTF-8を使っている場合には、以下の検査文字列を送ります。

【元の値】0[0xC2][0xA5]'0[0xC2]'0[0x00]'

アプリが使用する文字コードによって、多少パターンは変わります。

検査文字列には、①U+00A5([0xC2][0xA5])+「'」、②半端なバイト+「'」、③NULL文字+「'」、の3つのパターンを入れています。

①は、主にMySQLを対象に、SQLエラーを起こすことを狙っています。

[0xC2][0xA5]'
 ↓ アプリが「'」を「\'」にエスケープ
[0xC2][0xA5]\'
 ↓ U+00A5が「\」に変換される
\\'
 ↓ 末尾の「'」が余る
SQLエラー

②は、文字コード、アプリのエスケープ方式、DBMSのバージョンにもよりますが、殆ど全てのDBMS(Postgres、MySQLSQL ServerOracleDB2)を対象にしています。③のNULL文字は、一部のDBMS/Driver、あるいはPHPのバイナリセーフでない関数を狙っています。

この手の脆弱性については、一部を除いてBlind的な手法を適用しづらいため、単純にレスポンスにSQLエラーメッセージらしきものが含まれている場合に「脆弱性有り」と判定しています。

なお、実際には、文字コード系のSQLインジェクションの脆弱を持つアプリは珍しいです。また、仮に脆弱性があるならば、特定のパラメータではなく、Webサイトの大半の機能に脆弱性がある可能性が高いです。したがって、本来はサンプリングして手動検査すれば十分ですし、手動ならばアプリに合せた高い精度の検査が可能ですので(例えば、Blind手法を使うなど)、基本的には手動で確かめるべきでしょう。

その他:SQLエラーを引き出す

SQLインジェクション関連のシグネチャはもう一つあります。ただし、SQLインジェクションの検出を目的としたものではなく、SQLエラーを起こしてDBMSの種類などの情報を引き出すためのものです。

【元の値】9999999999...(9を8001個)

POSTパラメータの「元の値」の後ろに「9」を8001個付けた値を送ります。

少し補足すると、DBMSの中には、文字列リテラルの最大長の制限が存在するものがあります。例えば、Oracleの文字列リテラルは4000文字、SQL Serverは8000文字という上限があります。

文字列リテラル長の制限は、個々のカラムに設定した長さ制約とは別のものです。

例えば、Oracleで「VARCHAR2(5)」で定義したカラム「foo」があったとします。

1: SELECT * FROM test1 WHERE foo='123456789A'
2: SELECT * FROM test1 WHERE foo='123...(4001文字)'

上の10文字の文字列リテラルを含むクエリは、カラム「foo」のサイズ(5Byte)はオーバーしていますが、SQLエラーにはなりません。一方、下の4000文字を越える文字列リテラルを含むクエリは、「ORA-01704: 文字列リテラルが長すぎます。」を発生させます。

このような現象は、アプリがPrepared Statementを使用していたとしても発生しうるものです。バインドした長い値に対して、VARCHAR型カラムとの比較などを行なうと「ORA-01460: 要求された変換はできません。」が発生します。

SQL Serverについては、バージョンによって挙動が違うようです。SQL Server 2000では8000文字の制限があります。SQL Server 2005では、単純に文字列リテラルとして8000文字を超えるものも正常に扱えますが、LIKE演算子の右項の文字列が8000文字を超える場合にはSQLエラーになります。

DB2では、文字列リテラルが32672文字を超えるとSQLエラーになります。ただし、32KB近くのデータを送るのには時間が掛かる場合もあることと、DB2のサイトに余り出会うことがないので、このシグネチャではDB2を無視して8001文字としました。

なお、文字列だけでなく、OracleSQL ServerDB2には、数値リテラルについても制限があり、制限を越える長さの数値を与えるとSQLエラーが発生します。

一般に、データベースに登録するデータに関しては、アプリで事前にデータの長さなどのチェックを行なうことが多いです。しかし、参照系のクエリに渡すパラメータに関しては、長さのチェックを行なっていないアプリが多いため、長い文字列を与えることでSQLエラーを引き起こせる場合があります。

判定は、SQLエラーメッセージが応答に含まれる場合は「要注意」とします。この場合はDBMSの種類を知ることができます。そうでなくても長い文字列を与えて特異な応答が返る場合には、DBMS種類を推測する手掛かりになりますし、エラー発生時にアプリがどのような挙動を示すかを知ることができます(ただし、発生するのはSQLエラーとは限りません)。

念のため書きますが、この方法でSQLエラーメッセージが返ることと、SQLインジェクション脆弱性があることは、別の話です。一部のDBでは、Prepared Statementや、特殊文字エスケープ、入力値の文字種類チェックなどの対策を行なっていたとしても、この方法でSQLエラーを発生させられます。

検査の危険性

UPDATE文やDELETE文へのインジェクションが可能な場合、「OR 1=1」系の方法や「--」などを使って途中でクエリを終了させる検査方法を使用すると、データを壊す可能性があります。

UPDATE test1 SET col2='';waitfor delay '0:0:15'--' WHERE ...

上の例は「--」を含む値を与えています。その結果、WHERE句がコメントアウトされ、test1テーブルの全レコードのcol2カラムの値が更新されてしまいます。

実際の検査では、このような「OR 1=1」や「--」の手法を使うこともありますが、人間がアプリやパラメータを慎重に選ぶのが使用の前提になります。したがって、ツールにはそのようなパターンは入れませんでした。

しかし、それでもこのツールで使っている手法が100%安全なのかというとそうでもありません。このツールでいくつか危険な場合というのは考えられるのですが、その中でも比較的危険性が高いのは、「C. Blind 文字列型」の文字列連結を、MySQL環境に対して使用した場合だと思います。

前述のように、文字列連結演算子はDBの種類によって異なっています。MySQLではスペースなどを連結に使用することができますが、「||」や「+」は文字列連結とは別の演算子として解釈されてしまいます。

以下にいくつか例を挙げます。

mysql> SELECT * FROM product WHERE name='tomato'+'';
+----+-----------+--------+-------+
| no | category  | name   | price |
+----+-----------+--------+-------+
|  0 | vegetable | tomato |   100 |
|  1 | vegetable | carot  |    80 |
|  2 | fruit     | orange |   200 |
|  3 | fruit     | apple  |   300 |
+----+-----------+--------+-------+
4 rows in set, 8 warnings (0.00 sec)

ここで使っているproductテーブルには、全部で4つのレコードが登録されています。上のSQLでは、productテーブルから、nameが「tomato」のデータをSELECTしている(つもり)ですが、それ以外のレコードも抽出されています。

mysql> SELECT * FROM product WHERE category='fruit'||'' AND name='apple';
+----+----------+--------+-------+
| no | category | name   | price |
+----+----------+--------+-------+
|  2 | fruit    | orange |   200 |
|  3 | fruit    | apple  |   300 |
+----+----------+--------+-------+
2 rows in set, 1 warning (0.00 sec)

こちらのケースでは、「||」以降の条件が実質的に無視されています。

いずれのケースも、UPDATE文やDELETE文で同様の現象が発生すると、意図しないデータの変更や削除が行なわれる可能性があります。

MySQLに限らず、事前にDBMSの種類が判っている場合は、そのソフトにあった検査文字列のみを使用した方が安全です。それに、その方が効率的に検査できます。

また、数は少ないと思いますが、SQLエラーが発生した時にDBのコネクションを開放しないようなバグがあるアプリに対して検査を行なうと、「'」を含むパターンを繰り返し試すだけでコネクションが枯渇してサービスが利用できなくなるリスクもあります。

最後に

思いつくことを、いくつか書きます。

実装しようと思っていて実装していないのが、脆弱性検出後のexploit工程を支援する機能です。つまり、DBMSの種類の特定や、DB内のデータ抽出などをする機能です。商用ツールには、その辺りの機能が付いているものがあります。

ツールでDB種類の特定くらいまで踏み込んでやると、検出の精度が多少上がる(誤検出を減らせる)ことにもつながるかな、という思惑もあります。というのも、現状のツールでは、検出されるものの半分程度は誤検出です。特に、挙動が不安定なアプリに対してBlind系のシグネチャで検査を行なうと、誤検出が発生しやすくなります(同時に検出漏れも増えます)。

それから、Blind系の検出の精度は、「応答に有意な差があるか」を判定するロジックにかなり左右されますが、このロジックはなかなか難しいです。

差があるかを判断する際の感度を下げすぎてしまうと、検出漏れが増えます。感度を上げすぎてしまうと、ランダムにテキスト広告を差し込むようなページや、操作したパラメータが何箇所にもエコーバックされるページ、ワンタイムトークンが埋め込まれたページなどで、検出漏れや誤検出が増えます。

さらにロジックを作るのを難しくしているのは、応答の形式やサイズが様々であることです。例えば、応答は、HTMLではなくCSVJSONやバイナリデータであることもあります。応答のサイズも、非常に長い場合や短い場合があります。また、数キロバイトのデータがたった1行で返ってくるようなこともあります。

現状のツールでは、ステータスコードと、応答全体の長さ・行数と、一部の記号の出現数をもとに、有意な差があるかを判定しています。このロジックではうまく差を拾えないこともあるため、改善の余地があるかなと思います。

最後に、ちょっと話は変わりますが、検査対象として考慮すべきDBMSの種類についてです。それを考える上では、DBMSのシェアに関する情報が参考になります。

http://www.mysql.com/why-mysql/marketshare/

上のMySQLのページに載っている最初のグラフを見ると、MySQLAccess/SQL ServerOracleDB2の順番になっています。このツールでは、OracleSQL ServerMySQL、Postgres、DB2を主な対象として考慮しています。