SQL Injectionシグネチャの更新

気がつけば3年ぶりの日記更新となりました。
相変わらずWeb/スマホ等のセキュリティは続けてます。
そろそろバイナリもやろうかとも思い、IDA Proを購入してみました。
購入に際してはKinugawaさんの記事を参考にさせてもらいました。

ところで、最後に自作検査ツールについて書いてから6年ほど経ちました。
その間に細々とですがシグネチャの追加や変更を行ってきました。
またこのGW前後にも変更を加えましたので、それについて書こうと思います。

まずはSQL Injectionのシグネチャを取り上げます。

6年前の関連するエントリはこちらです。
2009-05-31 T.Teradaの日記 | 自作検査ツール - SQLインジェクション編

6年間に行われた変更の目的は、正確性と安全性の向上です。

正確性の向上は、False Positive/False Negativeの両方を減らすことを目指すものです。Time-based手法のシグネチャ追加をはじめとして、各種シグネチャの追加・調整をしました。

安全性では、主にMySQLで意図しない変更処理等が行われないように、シグネチャの追加・順番の変更などをしました。

個々の説明の前に、まずシグネチャを大まかに分類すると、下記の5つになります。

  • A. SQLエラー検出+簡易なBlind
  • B. Blind 文字列型
  • C. Blind 数値型・カラム名
  • D. Time-based
  • E. その他

前回の日記とは分類も多少変えています。
下記でひとつひとつ見ていきます。

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

現状のパターンは下記です。

    <値>           <SQL処理>
イ:【元の値】zq'q  エラーになる
ロ:【元の値】z''q  エラーにならない
ハ:【元の値】zq'q  エラーになる(イと同じ)
ニ:【元の値】zq'q  エラーになる(イと同じ)
ホ:【元の値】z''z  エラーにならない

シングルクォートで括られた文字列リテラル内に値が入ることを想定しています。

原理は単純で、「'」ではSQLエラーが発生する一方で、「''」ではエラーが発生せず、両者の応答に差が出ることを期待したものです。少なくともメジャーなDBMS全て(MySQLOracleMSSQL、Postgres、DB2SQLITE)で動作します。

判定では応答の差に加えて、下記も見ます。

  • SQLエラーらしきものが応答に含まれているか
  • エコーバックされる値の変化(ex.「z''q」→「z'q」)

なお、ツールが必要そうだと判断した場合には、【元の値】の後ろではなく前に「zq'q」等を付けたパターンも実施します。

6年前の日記によると、当時のシグネチャは下記でした。

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

"」「\」は現状のものには含まれていません。色々と詰め込み過ぎると支障が出かねないため、「"」「\」を使うテストは独立させて「E.その他」に移動しました。

検出した場合は、6年前と同じく「要注意」レベルで報告します。

B. Blind 文字列型

6年前は、文字列連結を使用したシグネチャが3つありました。
使用していた演算子等は下記です(元ネタはThe Web Application Hacker's Handbook: Discovering and Exploiting Security Flaws)。

||        … Oracle, DB2, Postgres等
+         … SQL Server, Sybase等
スペース  … MySQL

話が脱線しますが、MySQLのスペースで文字列結合がされる挙動はBNF Grammars for SQL-92, SQL-99 and SQL-2003BNFを見るとSQL仕様に合致した動作といえます。ちなみにPostgresの場合は改行で文字列を連結することができます。

シグネチャの話に戻ると、文字列連結系は「'||'」「'+'」等を使うもので、SELECT文以外のSQL文や、IN句に挿入される場合などでも検出しうる、汎用性が高いシグネチャです。

現状のものは、6年前と比べて下記の変更が施されています。

  1. 誤検出を減らすよう確認ステップを追加
  2. ' AND ''='」を追加
  3. Postgresのクォートされた数値用のシグネチャを追加
  4. MySQLでの安全性向上のためシグネチャの順番を変更

それぞれの変更の内容について説明します。

誤検出を減らすよう確認ステップを追加

||」の例で説明すると、現状は下記のようになっています。

    <値>                      <SQL処理>     <次へ進む条件>
イ:【元の値】'||'             正常(と等価)  「'」への応答と異なる
ロ:【元の値】'|||             SQLエラー     上(イ)応答と異なる
ハ:【元の値】'|||             SQLエラー     上(ロ)応答に近い
二:【元の値】'||'             正常(と等価)  上(ハ)応答と異なる
ホ:【元の値】'|_'             SQLエラー     上(二)応答と異なる
ヘ:【元の値】'||ltrim('')||'  正常(と等価)  上(ホ)応答と異なる
ト:【元の値】'||tlimr('')||'  SQLエラー     上(ヘ)応答と異なる
チ:【元の値】'||tlimr('')||'  SQLエラー     上(ト)応答に近い

6年前から追加されているのは、ヘ/ト/チのステップです。これらをクリアしてはじめて「脆弱性あり」で報告します。ホまで確認できたら「要注意」です。

ヘ/ト/チでやっているのは、SQLの関数であるltrim()が動作することの確認です。SQLの関数は多くありますが、その中でltrim()を選んだのは、当時調べた範囲では、多くのDBMS種類・バージョンで利用可能な関数であり、またシグネチャ内に組み込みやすかった(素直に空文字列を返す)からです。trim()はDB2 V9より前ではサポートされていないため使いませんでしたが、逆に滅多にお目にかかることがない国産の某DBMSはtrim()のみをサポートしていたりと、選択は悩ましいところではあります。

他の文字列連結である「'+'」「' '」(スペース)についても、ヘ/ト/チと同様のチェックを追加しました。ただし「' '」(スペース)は単純なリテラル同士しか結合できない制約があるため、MySQLのConditional Block Commentを使っています。

    <値>               <SQL処理>     <次へ進む条件>
ヘ:【元の値】'/*q!*/'  正常(と等価)  上(ホ)応答と異なる
ト:【元の値】'/*!q*/'  SQLエラー     上(ヘ)応答と異なる
チ:【元の値】'/*!q*/'  SQLエラー     上(ト)応答に近い
' AND ''='」を追加

今更という感じの基本パターンですが、これは直近に追加しました。

上記の「'||'」と同じように、最後のltrim()まで動けば「脆弱性あり」、その手前までであれば「要注意」とします。

SQL以外のインジェクションも「要注意」にはなる可能性がありますが、むしろ半分はそこを狙っています。

Postgresのクォートされた数値用のシグネチャを追加

つい最近まで、このツールは下記条件のインジェクションを検出しませんでした。

  • 比較的新しいバージョンのPostgres
  • (AND) クォートされた数値部分
  • (AND) Blind

下記がクエリの例です。noカラムはinteger型で、シングルクォートで括られた「2」の部分にインジェクション可能です。

testdb1=# SELECT * FROM product WHERE no='2';
 no | category |  name  | price 
----+----------+--------+-------
  2 | fruit    | orange |   200
(1 row)

以前のバージョンのPostgresでは「'||'」のパターンで検出できていました。つまり「no='2'||''」は、「no='2'」または「no=2」と同様に扱われていました。

ところが、あるバージョン(おそらくv9あたり)からは「'||'」を使うと下記のようにエラーとなります。

testdb1=# SELECT * FROM product WHERE no='2'||'';
ERROR:  operator does not exist: integer = text
LINE 1: SELECT * FROM product WHERE no='2'||'';
                                      ^
HINT:  No operator matches the given name and argument type(s).
 You might need to add explicit type casts.

結果として文字列連結シグネチャ'||'」では検出できず、また(Blindの場合は)他のシグネチャでも検出できませんでした。

これに気づいた当時(2009年)はマイナーな問題だと考えてスルーしていましたが、年月が経つに連れ、検査対象のPostgresのバージョンも当然に上がってきています。本件による検出漏れの可能性も上がっているはずで、遅ればせながら今回のGWに対応を検討しました。

マニュアルを見たり自分の環境で試したりと試行錯誤したのですが、通常の四則演算の演算子(「+」「-」「*」「/」)だとエラーとなりうまくいかず、最終的に下記のパターンにたどりつきました。

    <値>                   <SQL処理>     <次へ進む条件>
イ:【元の値】'^cbrt(1)^'1  正常(と等価)  「'」への応答と異なる
ロ:【元の値】'^brct(1)^'1  SQLエラー     上(イ)応答と異なる
ハ:【元の値】'^brct(1)^'1  SQLエラー     上(ロ)応答に近い
二:【元の値】'^cbrt(1)^'1  正常(と等価)  上(ハ)応答と異なる
ホ:【元の値】'^brct(1)_'1  SQLエラー     上(二)応答と異なる
ヘ:【元の値】'^cbrt(1)^'1  正常(と等価)  上(ホ)応答と異なる
ト:【元の値】'^brct(1)_'1  SQLエラー     上(ヘ)応答と異なる
チ:【元の値】'^brct(1)_'1  SQLエラー     上(ト)応答に近い

Postgresでは「^」はべき乗を求める演算子、cbrt()は立方根を求める関数です。「^」は他のDBMS(特にMySQL)ではXOR演算子と解釈されてしまい気持ちが悪いので、Postgres固有の関数であるcbrt()と組み合わせています。

ただシグネチャを作ってみたものの、これが必要なのは、上記の3条件が満たされる場合だけです。さらに新たに追加した「' and ''='」で検出できるケースが多いはずなので有用性があまり高くないシグネチャです。できるなら他のシグネチャと統合したいところですが、妙案がなく暫定的にこのまま実装しました。

MySQLでの安全性向上のためシグネチャの順番を変更

MySQLには「暗黙の型変換」という親切な機能があり、「'||'」や「'+'」を含むパターンを送信すると問題が生じる場合があります。

この問題を(リアルな例を元に)説明しているのが、zaki4649さんのスライドです。

とある診断員とSQLインジェクション

スライドのP.46を見て、自分が過去の日記に「'||'」「'+'」の危険性について書いたことを思い出しました(6年前なのでもう忘れてた)。

'+'」はzaki4649さんのスライドで説明されているので、ここでは「'||'」について少し補足します。

これは前回(6年前)の日記に書いた例です。

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)

このクエリは、categoryがfruit、nameがappleのレコードを返すように思えますが、後半のnameに関する条件が無視されています(対象のテーブルに含まれるfruitは2件だけなので、上のクエリでfruitの全レコードが出力されています)。

これはMySQLにおいては「||」が「OR」と完全に等価であるからです。上のSQL文がどう解釈されるかは、下記のように考えれば分かりやすいと思います。

WHERE category='fruit'||'' AND name='apple';
↓
WHERE category='fruit' OR '' AND name='apple';
↓
WHERE category='fruit' OR ('' AND name='apple');
↓
WHERE category='fruit' OR (FALSE AND name='apple');
↓
WHERE category='fruit' OR FALSE;
↓
WHERE category='fruit'

このような挙動のため、UPDATE、DELETEのWHERE内に「'||'」(あるいは「'+'」)を入れると状況によっては危険です。そこで、この手の問題が生じにくくなるような変更をこのGWに実施しました(いまさらですが…)。

変更の内容は単純です。これまで、文字列連結系は「'||'」「' '」「'+'」の順番で実行していましたが、これを「' and ''='」「' '」「'||'」「'+'」の順番に変えました。「' and ''='」は前述の新規追加したシグネチャです。

順番を変えて、危険性が相対的に高いもの(「'||'」「'+'」)より前に、危険性が低いもの(「' and ''='」や「' '」)を実行して、そっちで先に脆弱性を見つけてしまおうということです(本ツールはある脆弱性を検出したら、以降は同種の脆弱性シグネチャを原則的にスキップする仕組みになっています)。

ところで、zaki4649さんのスライドの「全ユーザのPWリセット」的な問題ですが、私が過去に類似の問題を経験したことがあるかというと、おそらくあります。私がはっきり記憶しているのは自分が主犯だった1件だけです(その1件は「多分この辺のSQLが原因っぽい」というところまでしか原因調査をしてません)。ただ「意図しない範囲のデータまでが更新されたけれども誰も気づかなかった(診断専用の環境だったりして)」というケースは、実は他にもあったのかもしれません。

いずれにせよ、シグネチャの順番の入れ替えにより、この手の問題が生じる率は減るのではないかと思います。

なお「'+'」や「'||'」の危険性を無くすために、「'+/*!z*/'」的なパターンにすることも考えましたが、余計な文字列を入れることで入力値チェック等に引っ掛かる割合が増えるので、採用しませんでした。

C. Blind 数値型・カラム名

クォートされてない部分に値が挿入されるケースです。2つシグネチャがあり、ひとつは数値リテラル、もうひとつはその他雑多なもの(カラム名、ASC/DESC等のSQLキーワード、TRUE/FALSE/NULL等の特殊なリテラル、他)をターゲットとしています。

前者は「【元の値】*(1)」、後者は「【元の値】/*q*/」のようなパターンです。いずれも6年前と大きな変更はなく、誤検出を減らすための送信パターンの追加のみが変更点です。詳細は割愛しますが、文字列連結シグネチャでltrim()を足したのと同じように、数値型ではabs()の動作を確認します。

D. Time-based

Time-basedは6年前のツール開発当時には含めませんでしたが、その後程なくして追加しました。Time-basedは、Errorの有無等が応答の内容に(殆ど)影響を及ぼさない、したがってSQLエラーが発生したか否かすら応答の内容からは分からない場合に効力を発揮します。実際のところ頻度は高くありませんが、たまに脆弱性を見つけてくれます。

ご存知のように、遅延まわりは個々のDBMSによって関数等が全然違うため、DBMS毎にシグネチャがあります。もともとは、MySQL、Postgres、Oracleの3つに対応するシグネチャがありましたが、このGWに多少見直しを行うとともに、MSSQL用のシグネチャを追加しました。

順に見ていきます。

MySQLのTime-basedシグネチャ(旧)

最近まで使用していたのは、下記のようにbenchmark()を使った文字列です。

<遅延式>
0 regexp if(benchmark(100000000,md5(1)),1,0x28)

<非numeric>
【元の値】'-(遅延式)-'

<numeric>
【元の値】*(遅延式)/*'-(遅延式)-'*/

元の値がnumericか、非numericかによって送る値を変えています。この後に説明するMySQL以外のTime-basedシグネチャもnumericか否かにより値を変える点は同様です。

numericでない値の場合は割と単純な値を送ります。numericな値の場合は、値がSQLのクォート内に出力される場合、クォート外に出力される場合の両方に備えるため2つの遅延式を入れます。クォート内に出力される場合は2つ目の遅延式が評価され、クォート外の場合は1つ目の遅延式が評価されます。

もう一つ説明が必要だと思われるのは、遅延式内でregexp演算子を使っていることです。目的は遅延後にruntimeエラーを発生させるためです。benchmark()は0(false)を返すため、遅延後に「0 regexp 0x28」が実行されます。0x28は「(」と等価で正規表現として不正であるため、runtimeエラーが発生します。

runtimeエラーを発生させる目的は、①意図しないデータが更新されないようにする、②サーバに過剰な負荷を与えないようにする、という2つの意味での安全性のためです。

前者の危険性は「'+'」と同じ原理です。regexpがない場合「'-benchmark(...)-'」のような値が文字列リテラルに入ることになり、結果として数値への暗黙的な変換が行われます。仮にUPDATEやDELETE文のWHEREに入ると、想定外の範囲のレコードが更新されるかもしれません。しかしbenchmark()終了後にregexpでクエリをkillすれば、レコードが書き換えられることはなくなります。

後者は、サーバの負荷の話です。実は単一のSQL文を実行した場合であっても、regexp等を用いてクエリをkillしなければ、benchmark()は多数回実行される可能性があります。下記は、単純にbenchmark()だけを使用したSQL文の例です。

SELECT * FROM product WHERE name='carot'-benchmark(100000000,md5(1))-'';

UPDATE product SET price=100*benchmark(100000000,md5(1)) WHERE category='fruit';

仮にproductテーブルに500レコード存在するとして、上記のSELECT文を実行すると、benchmark()は何回実行されるでしょうか。自宅の少々古いバージョン(v5.0.26)では、500レコードあるとbenchmark()は500回実行されます。これでは負荷を掛け過ぎなので、一度のbenchmark()実行後にruntimeエラーによってクエリをkillしたいわけです。

この辺の挙動はDBMSの種類やバージョンによって異なります。MySQLの場合、v5.5, 5.6系で試すと上記とは違う挙動となり、上のSELECT文そのままだとbenchmark()は1回しか実行されないようです。これはbenchmark()の実行内容が固定であり、最適化が行われるためだと思われます。しかし、最近のバージョンでもUPDATE文の方は、更新行数分だけbenchmark()が実行されます。また、benchmark()の代わりにsleep()を使う場合、SELECT文であってもバージョンにかかわらずレコード数の分だけsleep()が実行されます。

ということで、delay & killするパターンを送るのが無難だろうと思います。本ツールのTime-based SQLIシグネチャでは、MySQL以外を含めてこの方針を採っています。

MySQLのTime-basedシグネチャ(新)

上記のbenchmark()を使うのは、1つ前のバージョンまでです。このGW期間中にsleep()を使うものへとシグネチャを更新しました。

benchmark()からsleep()に切り替えた理由は2つあります。ひとつは、benchmark()でスリープする時間が読みにくくなってきているからです。もともと「benchmark(100000000,md5(1))」を使っており、過去にはこれでそれなりの時間(数十〜数百秒)のdelayが得られていました(はずです)。

しかしMySQL自体のバージョンアップによる性能向上と、マシンパワーの向上により、同じ式で得られるdelayの時間は減ってきています。最近、自宅のPCのv5.5, 5.6で試したところ、十数秒のdelayしか得られませんでした。時間が読みづらいという意味で、benchmark()はシグネチャとして使いにくいです。

もうひとつは、sleep()に対応したバージョン(v5.0.12以上)が世の中のMySQLの多くをしめるようになってきたからです。そうなると、あえて使いづらく、サーバに負荷をかけすぎるリスクもあるbenchmark()を積極的に使う理由が減ってきています。

というわけで、最新版のsleep()を使うパターン(遅延式の部分のみ)は下記です。

sleep(NN)|(select 1 union select 2)

これはdelay後に「ERROR 1242 (21000): Subquery returns more than 1 row」エラーになります。実はこのシグネチャ作成時に、sleep後にkillさせる部分で少々苦労しました。当初はregexp等をsleep()と組み合わせれば簡単にkillできると思っていましたがなぜかうまくいかず、試行錯誤の上、最終的にサブクエリで複数行を返す方式としました。

ちょっと脇道にはずれますが、MySQLで動的にエラーを起こす(Conditional Errorとして使える)ものを下記にまとめます。

1. 0 regexp if(EXP,0,0x28)
  ERROR 1139 (42000): Got error 'parentheses not balanced' from regexp
  Probably discovered by Kanatoko around 2007

2. like 0 escape if(EXP,1,10)
  ERROR 1210 (HY000): Incorrect arguments to ESCAPE
  Probably discovered by me around 2009

3. if(EXP,1,(select 1 union select 2))
  ERROR 1242 (21000): Subquery returns more than 1 row
  Probably discovered by Elekt around 2007
  Works on v4.1 or above

4. extractvalue(1,if(EXP,1,0x21))
  ERROR 1105 (HY000): XPATH syntax error: '!'
  Probably discovered by me around 2009
  Works on v5.1 or above

5. if(EXP,1,row(1,1)=(select sum(1),round(rand(0))x from mysql.user group by x))
  ERROR 1062 (23000): Duplicate entry '1' for key 'group_key'
  Probably discovered by Qwazar around 2010
  Works on v4.1 or above

全てEXPが1(true)ならは1を返し、そうでなければkillします。4と5は一般にError-based用ですが、動的エラーにも転用できます。とりあえず1,2,4とsleep()の組み合わせを試してうまくいかず、最終的に3を選んだという経緯です。

OracleのTime-basedシグネチャ

Oracleについては、現状下記の遅延式を使っています。

httpuritype(3221225995||chr(58)||1).getclob()

3221225995||chr(58)||1」は「192.0.2.11:1」です。このIPアドレスは例示用として使うもので(RFC3330)、実際にこのIPを持つ機器は世の中に存在しません(正確には「存在するべきではない」)。結果としてhttpuritype()で接続できずに数十秒〜数分程度は遅延が生じ、最終的にはエラーとなる、ということを期待しています。

知られているように、Oracle 11g以降、デフォルトでは外部へのNW接続ができなくなっており、上記のようなシグネチャは機能しない可能性が高いです。11gでも確実に動くものとしては、JOINを重ねたいわゆるHeavy Queryがありますが、遅延時間が読みづらく、またDBサーバで多くのリソースを消費することが前提であるため、導入には二の足を踏んでしまいます。

ネット上にはHeavy Queryを使わない11g用のDelay方法に関する情報もありますが、設定に依存したり、すでにOracle側で対策されていたりで、確実なものが見つからないため現状はひとまずhttpuritype()のシグネチャを使い続けています。

ただ、比較的最近対策されたOracleのXXEバグ(Advisory: XXE Injection in Oracle Database (CVE-2014-6577))は動く環境が多そうなので、いずれこのバグを使用したdelay方法に変更するかもしれません。興味深いことに、先月リリースされたBurpのCollaboratorも、Blogの図から推測するにこのバグを利用する(OOB的に)シグネチャを持っているように見えます*1

PostgresのTime-basedシグネチャ

Postgresについては、現状下記の遅延式を使っています。

cast(chr(1)||pg_sleep(NN) as int)

cast()でkillしています。pg_sleep()を使っているので、v8.2以上が対象です。

MSSQLのTime-basedシグネチャ

このGWで新たに追加しました。MSSQLで遅延させる方法としては、Heavy Queryを使う方法と、WAITFOR DELAYを使う方法が知られています。Oracleと同じ理由でHeavy Queryは避けたい気持ちがあり、WAITFOR DELAYでシグネチャを作りました。

WAITFOR DELAYはサブクエリにできないため、複文(Stacked Query)にせざるをえません。つまりインジェクション対象のSQL文を途中で切って別の文を入れることになります。必然的に問題になるのは、途中で切られるSQLがUPDATEやDELETE文の場合の安全性です。

今回作成したシグネチャでは、先頭のSQL文をruntimeエラーにすることにより、この問題を解決しようとしています。

具体的には下記のような文字列を挿入します。

<遅延式>
declare @X char(6)=cast(0x303a303aXXXX as char);waitfor delay @X
--> @X は '0:0:NN' になる

<非numeric>
【元の値】'+cast(1/(select 0) as char);(遅延式)--

<numeric>
【元の値】-1/(select 0);(遅延式)--'+cast(1/(select 0) as char);(遅延式)--

まずゼロ除算で先頭のSQL文をruntimeエラーにします。これにより先頭の文がUPDATE/DELETEであってもデータの更新は実行されない(はず)です。都合が良いことに、先頭のSQLがruntimeエラーでも、2番目以降のSQL文は実行され遅延を得られます。

複文を利用しているため、挿入ポイントが括弧内の場合などは構文エラーとなり、遅延もしないという大きな制約があります。したがってINSERT文へのインジェクションでは絶対に動作しません。複文ではなくインラインのHeavy Queryのシグネチャにすれば、このような制限は無しにできますが、上述の理由から採用しませんでした。

E. その他

その他のものとしては下記のシグネチャがあります。

  1. 更新系クエリ用
  2. 文字コード
  3. 8000Byte超の値によるSQLエラー
  4. "」「\」によるBlind
  5. 全角/EncodedのBlind

1〜3は6年前とあまり変わっていないので割愛します。

4のシグネチャは「"」で文字列リテラルが括られている場合や、「\」がエスケープされないケースに対応するもので、現状では基本的にMySQLが対象です。5はWebアプリでURLデコードされるケースや、全角記号が半角に変換されるケースに対応するものです。

*1:BurpのScannerでは、Collaboratorがデフォルト有効のようです。ブログ記事のコメントに批判があります。