CSP bypass

少し前に公開されたものだが、PortswiggerのDaily swig経由で見つけた、MSの小勝さんが報告したChromeのCSP bypassバグについて。

やられる側は例えば下記のようなページ。nonceのCSPを使っている。

<meta http-equiv="content-security-policy" content="script-src 'nonce-testrandom'">
<body>
<?= $_GET['param'] ?>    ← エスケープ無し
</body>

攻撃者は以下のようにiframeを挿入してやる(ここでは簡単のためsrcdocを使っている)。

<meta http-equiv="content-security-policy" content="script-src 'nonce-testrandom'">
<body>
<iframe srcdoc='<script>alert(window.origin)</script>'></iframe> ← 攻撃者が挿入したiframeタグ
</body>

攻撃者はiframe内でalertを動かそうとするが、iframeの親ページのCSPがiframeにも継承されるため、正しいnonceが無いJSの実行はブロックされる。

しかし古いChromeでは以下のような方法で回避できた、というのが小勝さんのレポート。

①attackerのページ[A]内のiframeに、victimのページ[V]を入れてやる([V]は上記のiframeが挿入されたCSP付きページ)。

┌─────────────────┐
│attackerのページ[A]        │
│                 │
│┌───────────────┐│
││victimのページ[V]       ││
││               ││
││┌─────────────┐││
│││XSSで挿入したiframe[I]  │││
││└─────────────┘││
│└───────────────┘│
└─────────────────┘
 この時点で[I]には[V]のCSPが適用されている。

②[A]は[I]のURLをdata:text/html,<script>history.back()</script>に操作する。
 この時点で[I]には[A]のCSP(空っぽ)が適用されるようになる。

③[I]のJSが動作してhistory backされる。[I]はXSSで挿入した内容(以下)に戻る。
 <iframe srcdoc='<script>alert(window.origin)</script>'></iframe>

④backした後も、[I]には[A]のCSP(空っぽ)が適用される。
 一方で[I]のオリジンは[V]を継承している。

こんな感じで、CSPのiframeへの継承というメンドクサイところを突いた攻撃です。

修正後は、④にて[I]には[V]のCSPが継承されるようになった模様。Daily swigによると修正にはHTML仕様の追加が必要だったらしい。

他のブラウザではどうかというと、Firefoxは昔から修正後のChromeと同じ挙動だったと思われる。

面白いことに、Safariは挙動が異なっていて③のJS(history.back)は実行されない。これは②で[A]が[I]のURLを変えてもCSPはそのまま[V]のものが適用されるから。それはそれで何となく気持ち悪くはある。

読書

Flatt securityの米内氏による、クライアント=ブラウザに特化したセキュリティ本。

すごく幅が広いトピックだが、想定読者であるWebアプリ開発者に必要な範囲で整理されている。中身的には、ベストプラクティスをまとめたものではなく、原理とか経緯が説明されている。最近の情報も書かれていて、セキュリティ畑にいる大抵の人(私を含め)が読んでも、知らないことが書いてある。私も1回半くらい読んだ。

参考文献の量がかなりおおい。その中に私が昔仕事で書いた資料が2つあった。ありがとうございます m(__)m。

中国人作家によるSF作品。三部作で2~3000ページある超大作で、世界的にも日本でも非常に売れた本。

一言で言うと宇宙戦争ものだがスケールがおおきい。宇宙のエコシステムというか、宇宙における生存競争はどのような形に行きつくかという骨格をなすアイディアがあり、侵略者に対する人間社会と個人の反応がつづられていく。

ⅢはⅡまでよりもさらにスケールが大きい話になっている。スケールが大きすぎてよく分からんくらい。

山本周五郎の時代物だが、現代もののような雰囲気の長編作品。若く才能がある浄瑠璃作家の短い生涯を描く。創作への純粋さ、ひたむきさ、喜びと苦悩。創作にもがき苦しんで身も心も蝕まれていく後半は、読んでいる方も苦しい。

坂の上の雲」(日露戦争)の講和会議における全権代表、小村寿太郎の綱渡りの交渉を描く。条約締結後に世論が弱腰外交と猛烈に非難するところは、現代でも同じようなことが起こりうると思うとおそろしい。

誘拐した幼児を連れて逃亡生活をする女の話。逃亡生活につきまとう緊迫感と、強い母性(誘拐してきた子に対するものではあるが)に引き込まれ、ラストシーンでジーンと来る。映画にもなっている作品。

特許に見る診断技術

WebアプリのDASTツール(スキャン技術関連)の日本国特許について少し調べました。

面白そうな出願がいくつかあったので下にまとめてみます。

権限周りの診断

特開2020-071637(2018年出願) イエラエセキュリティ/Archaic
特許6636222(2017年出願) 三菱電機

認可制御の自動診断。①と②で違いはあるが、ざっくり以下のようなイメージ。

1. ユーザA,Bそれぞれでログインした状態でサイトを巡回して、それぞれのリクエスト情報を集める。
2. ユーザA,Bのリクエスト情報の差を取り、相互に無いリクエストを抽出する。
 以下のような差があったとする。
 URL1: /order?id=1 -> ユーザAの巡回結果にのみ含まれている
 URL2: /order?id=2 -> ユーザBの巡回結果にのみ含まれている
3. URL1にユーザBのCookieでアクセスしてみる。
 応答をエラー応答等と比較して、脆弱性の疑いを判定する。

②は一般ユーザが特権ユーザ向け機能にアクセスできるかの診断を前提としている。

参照系の権限周りの診断では、あるページに認可制御があるべきか否かを判定する必要がある。本来はページに表示される情報の意味を考慮しないと判定出来ないが、上の2(巡回結果の差分)により簡易的な判定を行なう。

①は今のところ審査請求されていない。②は特許になっている。

②には関連しそうな論文あり。それによると、2つの異なる権限レベルのアカウントで巡回する方法は米国特許(US20170149782A1)にあるらしい。

応答差分解析

上の出願①では、3の応答比較の方法として、共通ワード数、bag-of-words、N-gram、潜在意味解析、WMD、str2vec、編集距離、木構造編集距離等を例示している(知らんものがいくつかある)。

Blind系の診断では、ロジック/テクニカル系にかかわらず、複数の応答間に有意な差があるかを判定する必要がある。その辺の技術について調べてみた。

特許4619867(2005年出願) 三菱電機
特許4193196(2007年出願) ファイブドライブ
特許5522850(2010年出願) KCCS

③⑤は、差分判定の誤りにつながりうる応答内のランダムな部分(例えば広告)等をやりすごすためのもので、③は応答のタグ構造の変化、⑤は正常応答に共通する部分、にそれぞれ着目する。④は主に送信する値に関するもの。

新しい話題ではないだけに最近のものは無さそう。③④は年金未納で抹消、⑤は有効。

その他

特開2010-39613(2008年出願) 個人

名称は「予約語の変更によるSQLインジェクション対策」。スキャン技術とは関係ないが、金床本(懐かしい)の「ONION SELECT」を彷彿とさせる。審査請求されておらず、みなし取下になっている。

特開2019-148917(2018年出願) NTTコミュニケーションズ

DASTツールの結果の精査(正誤判定)を機械学習する。例えば人間による正誤判定結果を教師データとする。うまくいけば、DASTツールの結果を与えると、自動で正誤判定結果が出てくることになる。関連する論文がある(有料)。

まとめ

Webアプリスキャン関連の国内特許出願は余り多くないので、新しい技術を探すためならば、US特許出願や学術論文を見ていった方がいいかもしれない。当然だが、特許より論文の方が情報は多いし読みやすい(加えて大抵の論文は専門家の査読を経る)。

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