pg_sleepをSQLインジェクション検査に応用する

SQLインジェクションの進化形として、ブラインドSQLインジェクションという手法があります。通常のSQLインジェクションは、検索結果表示やエラー表示のところに、アプリケーションの想定とは別のテーブル・列の値を表示するものですが、ブラインドSQLインジェクションは、SQLの結果がエラーになる・ならないを1ビットの情報として悪用し、これを積み重ねることで、データベース内の任意情報を得ることができるというものです。
1ビットの情報が得る手段としては、SQLのエラー表示に限らないわけで、現実問題として、SQLのエラーが外部からは判別しにくい場合もあります。そのような場合の究極形として、時間差を利用するという手法があります。
MS SQL Serverには、waitfor delayという命令があって、時・分・秒指定でスリープさせることができます。金床本には、MySQLPostgreSQLの場合のスリープ代替手段として、MySQLのbenchmark関数や、PostgreSQLMD5関数の利用が紹介されています。しかしながら、これら代替手段の欠点として、適度なスリープ時間になるようなパラメータの調整が難しいことや、攻撃対象サーバーに過負荷をかけることから、攻撃がばれやすいという問題があります。ブラインドSQLインジェクション手法は、脆弱性検査手段としても有効ですが、検査対象に過負荷を掛けることは好ましくなく、特に本番環境に対してはまず使えないと考えた方がよいでしょう。脆弱性検査がDoS攻撃になってはしゃれになりません。
幸いなこと(?)に、金床本が書かれた後になって、MySQLPostgreSQLともに、スリープ機能を持つようになりました。
まず、MySQLでは、Ver5.0.12以降でsleep関数がサポートされています。これは指定した秒数だけスリープするというもので、戻り値は通常0です。PostgreSQL8.2以降では、pg_sleep()という関数が用意されており、これも指定秒数だけスリープするというものです。ここでは、pg_sleepの検査への応用について考えてみます。
pg_sleep()は関数とはいうものの値を返しません。以下のように利用できます。


shop=# select pg_sleep(3) where version() > ' PostgreSQL 8.3';
pg_sleep
----------

(1 row)

shop=#

これを検査に応用するとなると、どのようにpg_sleep()を注入するかが問題になります。
PostgreSQLに対するSQLインジェクションでは、複文が利用できる場合が多いので、簡単な方法としては、セミコロンを使って第二のSQLとして実行させることができます。例えば以下のようなSQLがあったとして、


SELECT name FROM users WHERE id='$id' and pwd = '$pwd'
ここに、$idとして「';select pg_sleep(3)--」を指定します。SQLは以下のようになります

SELECT name FROM users WHERE id='';select pg_sleep(3)--' and pwd = ''
SQLインジェクションに成功すると3秒余計の処理時間が掛かることから、脆弱性があることが判定できます。--以降はSQLのコメントとして無視されます。

SQLコメントの使用は危険な場合がある

ところが、上記のような検査パターンは、時として危険な場合があります。例えば、以下のようなパスワード変更のSQLがあったとします。


UPDATE users SET pwd='$pwd' WHERE ID='$id'
ユーザIDは現在ログイン中のIDがセッション変数から指定されるとして、外部から指定できるパラメータは新しいパスワード$pwdです。ここで、先ほどの検査パターンを$pwdに適用すると以下のようなSQLができます。

UPDATE users SET pwd='';select pg_sleep(3)--' WHERE ID='ockeghem'
このSQL全てのユーザのパスワードを空にした後3秒待つというものです。本番環境でこれをやったら大惨事ですね。
対象がUPDATE文だと分かっていれば、対処のしようもありますが、検索系と更新系の両方のSQLが動く場合もあり、外部からは判定できないので、ブラックボックス検査では、できるだけ安全な検査パターンが求められます。

WHERE句でのpg_sleepの利用

再びSELECT文を想定した検査パターンを考えますが、万一UPDATEやDELETE FROMなどが混じっていた場合にも極力被害が出ないようなパターンとして、WHERE句にpg_sleep()をもってくる方法を考えます。ただし、 MySQLのsleep()関数は値を返すのでWHERE句におくことが容易ですが、pg_sleep()は値を返さないので、WHERE句におくことが容易ではありません。
試行錯誤の結果、select pg_sleep()を副問い合わせとして使用することで、値を返させることができました。例えば、副問い合わせをvarcharにキャストする方法。以下は、空文字列を返します。


cast( (select pg_sleep(3)) as varchar);
また、EXISTSの利用。以下はtrueを返します。

exists(select pg_sleep(3));
キャストの方を先ほどのSELECTに応用してみましょう。検査パターンは「' and cast( (select pg_sleep(3)) as varchar) = '」とします。このとき、SQLは以下のようになります。

SELECT name FROM users WHERE id='' and cast( (select pg_sleep(3)) as varchar) = '' and pwd = ''
これにより、SQLとしてはエラーにならず、3秒遅延することにより、SQLインジェクション脆弱性があることが検査できます。
一方、UPDATEの方に当てはめると以下のようになります。

UPDATE users SET pwd='' and cast( (select pg_sleep(3)) as varchar) = '' WHERE ID='ockeghem'
こちらはSQLの文法違反になるので、検査はできませんが、データベースを破壊することもありません。

まとめ

ブラインドSQLインジェクション手法は脆弱性検査手法としても有望です。しかし、検査パターンを不用意に選択すると、更新系SQLが実行された場合に、データベースの内容を破壊する可能性があります。本エントリでは、PostgreSQL8.2以降で利用できるpg_sleep()関数に注目し、安全な検査パターンについて考察しました。
最近、開発会社様などで、セキュリティ検査を検査会社だけに頼らず、自ら検査するところも徐々に増えてきたようです。HASHコンサルティング株式会社への依頼の中にも、自社の検査パターンをレビューして欲しいという内容が出てきました。検査に使用できるパターンはインターネット上からも得られますが、上記で考察したような安全性までは中々配慮されていない場合が多いようです。安定した検査を行うためには、効果的に脆弱性を発見できることも重要ですが、同時に検査対象アプリケーションに対する安全性も重要だと考えます。