SQL識別子の扱い

SQLの検査方法について書いた勢いで、SQLの識別子の扱いについて書いてみます。

議論としては、1年以上前に結論が出ている話ですw

間違いだらけのSQL識別子エスケープ | 徳丸浩の日記

徳丸さんの記事では、テーブル名が外部から指定可能な設定で説明がされていますが、少々エッジケース気味なので、今日の日記ではORDER BY句のカラム名が外から指定可能な前提で、どんな攻撃が可能なのか考えてみます。

例としてSNSサイトのユーザ検索機能のSQLをとりあげます。

SQL文は下記のようなイメージです。

SELECT nickname, prefecture, greeting
FROM members
WHERE nickname like ... AND prefecture=...
ORDER BY {$order}, nichname

検索結果画面にはSELECTした結果のニックネーム、都道府県、挨拶文のみが表示されます。このSNSでは、氏名や誕生日等の詳細なプロフィールは、許可した相手以外には表示されない仕様だとします。

この場合にどんな攻撃が可能か?という話です。

ちなみにSQL文の「$order」の部分は外から指定可能になっていますが、エスケープなり、入力チェック(「\A\w+\z」など)なりがされるとします。

攻撃の例としては「order=birthday」みたいなパラメータを与える攻撃が考えられます。membersテーブルにbirthdayカラムがあるなら、ユーザを誕生日順に並び替えることができます。

並び替えができるなら、あるユーザ(誕生日がわかっているユーザ)と、別のユーザの誕生日を比較することができます。攻撃者自身がダミーのアカウントを登録すれば、バイナリサーチなどを使うことで、楽に特定の人の誕生日を割り出せることになります(画面には直接表示はされませんが、大小関係から値を詰められる)。

誕生日だけでなく「order=telnumber」とか「order=salary」とか「order=password(_hash)」とかも嫌な感じです*1。これが商品検索の機能であればダメージは少なそうですが「order=cost(原価の意味)」とかは少し嫌な感じかもしれません。

上記の例でわかるように、この手の攻撃が問題になるのは、SELECT対象のテーブル内(JOIN対象も含む)に、秘密にすべき情報が含まれている場合です。またフォームのパラメータ名などから、カラム名が推測可能であることが前提となります。

ただし、カラム名の推測は不要な場合もあります。これは、$orderが半角英数等のチェックのみがされ識別子としてクォートされずにSQL文に入る、かつSELECT対象のカラムに秘密情報が含まれている(典型的には「SELECT * FROM ...」のケース)、の2条件がそろう時です。そんな場合は、「order=1」「order=2」といった番号指定が可能になります。*2

開発側の対策としては『要件として「どのカラムでのソートを許すか」を決めて、それをホワイトリストで実装する。望ましくは「1」->「preference」のようにカラムをコード化する』ということになるでしょう。ソートを許すカラムは、通常は値を見せて良いカラムのみです。要件検討の結果「全カラムOK」ということもあるでしょうが、制限が必要な場合もあるはずです。

結論としては、1年以上前の徳丸さんをはじめとした方々とほぼ同じだと思います。

*1:Password_hashの場合は、Saltが付きならば攻撃の意味がなさそうです。

*2:大抵のSQL教本に書いてある構文で、どのメジャーDBMSでも使えます。攻撃側としては、番号とカラムのマッピングを調べていく作業がめんどいと思いますが。

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がデフォルト有効のようです。ブログ記事のコメントに批判があります。

loopback

http://127.0.0.1/ と同じ意味となりうるURL。
ブラウザでアクセスするというよりは、HTTPクライアントとして機能するWebアプリに食わせます。
(一部のサーバ環境でしか動かないものもあります。)

http://127.0.0.1/               普通の表記
http://127.0.1/                 2,3番目のバイトをまとめる
http://127.1/                   2,3,4番目のバイトをまとめる
http://127.1.2.3/               2,3,4番目のバイトは何でもいい
http://127.66051/               上の2,3,4番目のバイトをまとめる
http://017700000001/            全バイトをまとめて8進数で
http://0017700000001/           0をもうひとつ
http://2130706433/              全バイトをまとめて10進数で
http://02130706433/             0を頭につけても10進数と解釈する環境も
http://2130772483/              127.1.2.3の全バイトをまとめて10進数で
http://0x7F000001/              全バイトをまとめて16進数で
http://0x37F000001/             32bitを超える数でoverflow 16進数
http://15015608321/             同上 10進数
http://0177.0.0.0x0001/         先頭バイトを8進数、4番目を16進数に
http://12%37.0.0.%31/           URLエンコード
http://12[TAB]7.0.[LF]0.1/      空白文字を入れる
http://a:a@127.0.0.1/           user/password付き
http://127.0.0.1./              お尻にドット
http://localhost./              上と同じ
http://localhost.localdomain/   hostsに書いてあるかも
http://example.jp/              DNSが127.0.0.1を返せば

ほとんどのは、How to Obscure Any URL にのってます

最近買った本

Web Application Obfuscation: '-/WAFs..Evasion..Filters//alert(/Obfuscation/)-'

Web Application Obfuscation: '-/WAFs..Evasion..Filters//alert(/Obfuscation/)-'

sla.ckers.orgのXSS板とかでよく見る人たちが著者に名前を連ねています。

行きの電車の中で読んでますが、まだ40ページくらい。先は長いです。

徳丸さんの本です。先ほどアマゾンでぽちりました。

Web開発者向けのセキュリティ本としてはこれ、という本になるんでしょうね。

目次を見ると、4章が盛りだくさんな感じです。

オープンリダイレクト検査:Locationヘッダ編

オープンリダイレクタを脆弱性とみなすべきかは議論が分かれるところです。Google等の一部のサイトは、自サイトのオープンリダイレクタを脆弱性としてはみていません。一方で、脆弱性検査の現場では、見つかれば脆弱性として報告することが多いと思います。

その辺の議論はおいておいて、オープンリダイレクタの検査は、ブラウザの特性もからんで意外とバリエーションが多くて面白いので、本日の日記で取り上げてみたいと思います。

大まかにいうと、リダイレクトは、302応答のLocationヘッダ、Refresh(HTTPヘッダ、METAタグ)、JavaScriptによるものがありますが、本日は302応答のLocationヘッダのリダイレクタについて取り上げます。

パターン1:サブドメイン部分に値が入る場合

以下のように、サブドメインの箇所が動的なケースです。

Location: http://{$u}.hatena.ne.jp/hoge

このリダイレクタ("redir.cgi" とします)を悪用して、"example.com" ドメインにリダイレクトさせることを目指します。なお、$uの正常な値は "www" で、LF(%0A U+000A)は使えないとします。

まずは、一番基本的な検査文字列3つです。

■1A: redir.cgi?u=example.com/www
 → Location: http://example.com/www.hatena.ne.jp/hoge

■1B: redir.cgi?u=example.com?www
 → Location: http://example.com?www.hatena.ne.jp/hoge

■1C: redir.cgi?u=example.com%23www  (%23 => #)
 → Location: http://example.com#www.hatena.ne.jp/hoge

上の1A、1B、1Cでは、URL内で区切りとして使用される "/", "?", "#" を使っています。

アプリがブラックリストでパラメータuの値チェックをしているとしても、上の3つの記号については対策されていることは割とあります。その場合は、以下の1D〜1Fを試します。

■1D: redir.cgi?u=example.com;www
 → Location: http://example.com;www.hatena.ne.jp/hoge

■1E: redir.cgi?u=example.com:80www
 → Location: http://example.com:80www.hatena.ne.jp/hoge

■1F: redir.cgi?u=example.com\www
 → Location: http://example.com\www.hatena.ne.jp/hoge

1D, 1Eは ";" と ":" を使用しています。いずれも、FirefoxOperaでホスト名の終端として認識され、"example.com" にリダイレクトします。":" を使う場合は、その直後に妥当なポート番号を付けて ":80" のような値にしなければリダイレクトしません。

1Fは "/" の代わりに "\" を使っています。IEChromeで動作します。

次からは制御文字を使う検査文字列です。

■1G: redir.cgi?u=example.com%00www
 → ① Location: http://example.com
 → ② Location: http://example.com[0x00]www.hatena.ne.jp/hoge

1GはNULL(%00 U+0000)を使っています。応答のLocationヘッダを2パターン書いていますが、Apache上のCGIPHPで試してみると、NULL以降が消えてなくなった①のLocationヘッダが返されます。その場合は、当然 "example.com" にリダイレクトします。

仮に、②のような応答が返っても、たいていのブラウザはHTTPヘッダのNULL以降を無視するので、"example.com" にリダイレクトします(Safariだけは違う)。

もう一つ制御文字を使う検査文字列です。

■1H: redir.cgi?u=example.com%0Dwww
 → Location: http://example.com[0x0D]www.hatena.ne.jp/hoge

IEOperaChromeでは、CR(%0D U+000D)以降が無視されて、"example.com" にリダイレクトします。正確に言うと、これらのブラウザはCRをヘッダ行の区切りとして認識しています。つまり、この検査文字列は、HTTP Header Injectionを利用しています。

パターン2:スラッシュが先頭に付けられる場合

余り見ませんが、こんなケースです。

Location: /{$u}

$uの正常な値は "foo/bar.cgi" で、LF(%0A U+000A)は使えないとします。

こんなケースでの基本的な検査文字列は以下です。

■2A: redir.cgi?u=/example.com/foo/bar.cgi
 → Location: //example.com/foo/bar.cgi

URLの先頭の、"http:" や "https:" は省略可能なので、2Aは "example.com" にリダイレクトします。

たいていのアプリは上の2Aで詰むのですが、先頭に "/" が使えない場合は以下のような検査文字列があります。

■2B: redir.cgi?u=\example.com/foo/bar.cgi
 → Location: /\example.com/foo/bar.cgi

■2C: redir.cgi?u=%09/example.com/foo/bar.cgi
 → Location: /[0x09]/example.com/foo/bar.cgi

2Bでは、"/" の代わりに "\" を使っています。IEChromeで動作します。2Cはタブ(%09 U+0009)を使っています。IEChromeは、URLに含まれるタブ等の文字を無視するため、2Aと同様に動作します。

パターン3:Locationヘッダの先頭に値が入る場合

こんなケースです。一番良く見るパターンです。

Location: {$u}

$uの正常な値は "http://www.hatena.ne.jp/foo/" で、LF(%0A U+000A)は使えないとします。

外部にリダイレクトしないように、パラメータuの値を何らかの方法でチェックしていても、それが不完全ならば下の3A、3Bが通ってしまうかもしれません(このような値が通ることは少なくありません)。

■3A: redir.cgi?u=http://www.hatena.ne.jp.example.com/foo/
 → Location: http://www.hatena.ne.jp.example.com/foo/

■3B: redir.cgi?u=http://example.com/http://www.hatena.ne.jp/foo/
 → Location: http://example.com/http://www.hatena.ne.jp/foo/

元のURL(http://www.hatena.ne.jp/)のサブドメイン(www)の箇所のチェックが不完全ならば、下の3Cが通ってしまうかもしれません。

■3C: redir.cgi?u=http://example.com?.hatena.ne.jp/foo/
 → Location: http://example.com?.hatena.ne.jp/foo/

「パターン1:サブドメイン部分に値が入る場合」で見たように、上の3Cの "?" の箇所を、";", "#", ":80", "\", NULL, CR などに変えるバリエーションがあります。

先頭が "/" で始まる値が許容される場合は、下の3Dのような検査パターンが通るかもしれません。

■3D: redir.cgi?u=//example.com/foo/
 → Location: //example.com/foo/

「パターン2:スラッシュが先頭に付けられる場合」でみたように、上の3Dの "/" を "\" にするなどのバリエーションがあります。

テストしたブラウザ

IE8、Firefox3.6、Opera10、Safari5、Chrome9(いずれもWindows Vista版)。

他人のCookieを操作する

脆弱性検査をしていてしばしば出くわすのは、他人のCookieの値を操作できるとXSSやセッション固定等の攻撃が成功するようなWebアプリケーションです。

このようなアプリがあると、業界的には「Cookie Monsterという問題がありまして、、、でも、、、基本的に現状のブラウザではリスクは低いです」みたいな話がされることが多いのではないかと思います。

本日の日記では、それ(Cookie Monster)以外にも状況によっては考慮すべきことがある、という話をしたいと思います(過去の日記でも少し書いた話ですが、もう少しちゃんと書いておこうと思います)。

通信経路上に攻撃者がいる

被害者のブラウザとサーバの通信経路上に、アクティブな攻撃者がいると想定しましょう。

そのような状況では、攻撃者は正規のサーバになりかわってブラウザと通信をしたり、ブラウザと正規のサーバで交わされる通信に介入することができます。もちろんそれが可能なのは、通信がHTTP(SSLではないということ)の場合に限られます。

言うまでもなく、そのような状況では、攻撃者は対象サイトのCookieを被害者のブラウザにセットすることができます。

HTTPを使うサイトの場合

通信経路上に攻撃者がいる状況では、そもそもCookieをブラウザにセットできるという以前に、攻撃者はHTTPのリクエスト・レスポンスを好きに盗聴・改竄できます。

WebサイトがHTTPを使用しており、通信経路上に攻撃者がいるリスクを考慮しないと決めている場合には、通信経路上の攻撃者によりCookieがセットされるリスクもまた考慮する必要はありません。

問題は、WebサイトがHTTPではなくHTTPSを使用している場合です。

HTTPSを使うサイトの場合

世の中には、通信経路に攻撃者がいてもセキュアで無ければならないサイトもあります。そのようなサイトは通常HTTPSを使用します。

HTTPSのサイトでは、以下のような攻撃のリスクを考慮する必要があります。

  1. 被害者をHTTPで対象サイトにアクセスさせる
    攻撃者が通信経路上にいる場合、これは難しいことではありません。攻撃者は偽のSet-Cookieを含む応答を被害者のブラウザに返し、そのままHTTPSのページにリダイレクトさせます。
  2. 被害者はHTTPSで対象サイトにアクセスする
    このリクエストでは1でセットされたCookieがサーバに送られてしまいます。

3点ほど補足します。

1点目は、上の(1)のリクエストはHTTPであり、セキュリティ警告のポップアップ等を出すことなく被害者のレスポンスを偽造しCookieを汚染することができるということです。

2点目は、対象サイトがHTTPSだけしか使っておらず、したがってHTTPのポートを開けていないとしても、(1)の攻撃は可能だということです。途中にいる攻撃者はHTTPが開いているかのごとくブラウザに応答することができるからです。

3点目は、(1)のCookieはHTTPでセットされたものですが、(2)のHTTPSのリクエストでサーバに送られてしまうということです。Cookieの仕組み上そうなってしまいますし、HTTPのCookieHTTPSCookieは区別することもできません。

CookieXSSする脆弱性がWebアプリにあるならば、通信経路上の攻撃者が上の(1)でCookieに植えつけた攻撃コードが(2)の応答で実行されます。ここで重要なのは、(2)がHTTPSであり、HTTPSのコンテキストでJavaScriptが実行されてしまうということです。

つまり通信経路上の攻撃者が、セキュア属性付きのCookieを盗んだり、HTTPSのページの内容を盗んだり改竄したりできるということになります(もはやCross-Site Scriptingとはいえない攻撃ではありますが、リスクとして考慮すべき攻撃です)。

XSSではなくセッション固定の脆弱性がWebアプリにあるならば、通信経路上の攻撃者がHTTPSで保護されたセッションを固定化し、なりすまし等ができるということになります。

まとめ

まとめると、通信経路に攻撃者がいてもセキュアであるべきサイト(HTTPSのサイト)については、「他人のCookieは操作できない」という前提でセキュリティを考えることはできないということです。

理由は(「Cookie Monster」等とは関係なく)、HTTPの通信に介入できる攻撃者はCookieを操作できるからです。