Edited at

ブラインドSQLインジェクションの脅威と対策

More than 1 year has passed since last update.


はじめに

こんにちは。CYBIRDエンジニア Advent Calendar 8日目担当の@daisuke-senmyouです。

勤続年数は10年を超えてから数えるのをやめたので分かりません。

7日目は@gucchonさんのWebサイト移転に役立つS3+CloudFrontでのリダイレクト設定まとめでした。

マニュアル通りの手順でやりたいことが実現できなかった時に、自らのアイデアで道を切り開いてしまうあたりはさすがですね。

確認方法のあたりは実践的なTipsが散りばめられていてありがたいですね。こういうのが役立つこと結構多いです。


1年を振り返って

私の2016年を振り返ると、セキュリティ関連の業務が多かったように思います。

思い出深いものだけ挙げておきます。


  • 社内セキュリティガイドライン策定

    基本的には社員で手分けして書きましたが、それだけでは不安だったのでHASHコンサルティングの徳丸氏にコンサルティングをお願いしました。


  • 脆弱性検出ツールを使ったWEBアプリケーション診断

    無償のツールだとOWASP ZAPがお勧めですが、弊社ではさらに強力な有償(年間ウン百万円!)のツールで片っ端から診断を掛けています。


  • セキュリティに関する社内勉強会

    ここでも登場するKali Linuxを使って、パスワード総当り攻撃やSHELL SHOCKを利用したサーバーへの侵入テストを実演しました。

    勉強会の最中に、検証用サーバーがリアルな攻撃を受けたりして楽しかったです。


  • 標的型攻撃に対する防災訓練

    本文中のURLをクリックしたら足跡が残るようなメールを作り社内に一斉配信しました。今回は残念な結果でしたが、攻撃に対する意識は高まったと信じてます!


さて、そんな訳で最後もセキュリティ関連で締めくくりたいと思います。


ブラインドSQLインジェクションについて

「SQLインジェクションがー」というと「はいはい、当然対策済みですよ。」と言われることが多いのですが

「ブラインドSQLインジェクションがー」というと「?」となることがあるので、このテーマにしました。

結論から言ってしまうと通常のSQLインジェクションと対策はなんら変わりません。

ただし脅威については大きく異なります。

通常のSQLインジェクションの脆弱性があってもレスポンスの仕方によってはデータ漏洩させるのはなかなか難しかったりします(それでも、改ざんや破壊などは可能で、大きな脅威であることには変わりません)。

が、ブラインドSQLインジェクションの場合、1箇所でも対策漏れがあると全データの漏洩につながる可能性が出てきます。

詳しくは以下のページなどをご覧ください。

http://www.atmarkit.co.jp/ait/articles/0608/26/news014.html

http://blog.tokumaru.org/2015/04/time-based-sql-injection.html


検証に使うもの


攻撃側


  • Kali Linux v2.0

  • sqlmap v1.0


Kali Linuxについて

Kali Linuxは「先進的なペネトレーションテスト(侵入試験)やセキュリティ監査を実施するためのLinuxのディストリビューション」です。

ここで使う sqlmap 以外にもいろんなツールがインストールされており、前述の社内勉強会で使ったものだけご紹介しておきます。

ツール名
概要

hping3
本来はさまざまなパケットを生成してリクエストするツールですが、勉強会ではこれを使ったDoS攻撃を実演しました。

OWASP ZAP
無償のWEBアプリケーション脆弱性検出ツールです。勉強会では外部非公開のはずのURLを検出するのに使用しました。

crunch
ブルートフォース(総当り)攻撃に使う辞書リスト生成ツール。

hydra
生成した辞書リストを使ってブルートフォース攻撃を行うツール。

metasploit
ペネトレーションテスト(侵入試験)のためのツール、というかフレームワーク。勉強会ではSHELL SHOCKによる侵入を実演しました。

http://ja.docs.kali.org/


sqlmapについて

通常のSQLインジェクションに加え、ブラインドSQLインジェクションについても検査できるツールです。


ターゲット側


  • MySQL v5.5

  • PHP v5.5

  • FuelPHP v1.6

※それぞれのバージョンに深い意味はありません。


ターゲットのAPI


  • パスワード認証を行うAPIを想定している。

  • 予め別のAPIでユーザーは特定されており、ユーザーID(数値)とパスワード(文字列)を受け取る。

  • 受け取ったパラメータに対しては、validationなどは一切行っていない。

  • ユーザーIDとパスワードでDBを検索し、該当レコードが存在すればtrueを、なければfalseを応答する。


診断手順

詳しくはこちらでご覧いただくとして、要点だけ。


  1. SQLインジェクションの脆弱性検出


    sqlmap -u "http://target.domain/login/submit" --dbms mysql --data "userid=1&password=password"


  2. データベース一覧取得


    sqlmap -u "http://target.domain/login/submit" --dbms mysql --data "userid=1&password=password" --dbs


  3. テーブル一覧取得


    sqlmap -u "http://target.domain/login/submit" --dbms mysql --data "userid=1&password=password" -D SEC_CHECK --tables


  4. データダンプ


    sqlmap -u "http://target.domain/login/submit" --dbms mysql --data "userid=1&password=password" -D SEC_CHECK -T users --dump


上記の手順で実際にsqlmapでインターネット越しに取得できたデータが以下です。

今回はuserテーブルを参照するAPIに脆弱性を仕込みましたが、userテーブル以外のデータ取得も可能です。

+---------------------------+-----------------+----------+------------+------------+

| id | email | username | password | created_at | updated_at |
+----+----------------------+-----------------+----------+------------+------------+
| 1 | cy_taro@cybird.co.jp | cy_taro | password | 1448007966 | 1448007966 |
+----+----------------------+-----------------+----------+------------+------------+

裏ではこんなSQLをひたすら投げています。

私には何をしているのかよく分かりませんが、ブラインドSQLインジェクションでデータを取得しようとしているようです。

SELECT * FROM `users` where `id` = 1 AND ORD(MID((SELECT IFNULL(CAST(email AS CHAR),0x20) FROM SEC_CHECK.users ORDER BY id LIMIT 0,1),1,1))>64 and `password` = 'password'

SELECT * FROM `users` where `id` = 1 AND ORD(MID((SELECT IFNULL(CAST(email AS CHAR),0x20) FROM SEC_CHECK.users ORDER BY id LIMIT 0,1),1,1))>96 and `password` = 'password'
SELECT * FROM `users` where `id` = 1 AND ORD(MID((SELECT IFNULL(CAST(email AS CHAR),0x20) FROM SEC_CHECK.users ORDER BY id LIMIT 0,1),1,1))>112 and `password` = 'password'
SELECT * FROM `users` where `id` = 1 AND ORD(MID((SELECT IFNULL(CAST(email AS CHAR),0x20) FROM SEC_CHECK.users ORDER BY id LIMIT 0,1),1,1))>104 and `password` = 'password'


各対策方法別の結果

弊社でスタンダードとなっている実装から、少し古い目の実装まで遡って行きたいと思います。


FuelPHP の ORM


  • ソースコード

$result = User::find('first', array(

'where' => array(
array('id', $userid),
array('password', $password)
),
));


  • DBに渡されるクエリー

SELECT `t0`.`id` AS `t0_c0`, `t0`.`username` AS `t0_c1`, `t0`.`password` AS `t0_c2`  FROM `users` AS `t0` WHERE `t0`.`id` = '1' AND `t0`.`password` = 'password' ORDER BY `t0`.`id` ASC LIMIT 1


  • 結果

    脆弱性は検出されませんでした。sqlmapでは以下のようなメッセージが表示されます。

    背景赤でCRITICALと表示されるので危ないのかと思いきや逆のようです。


FulePHP の QueryBuilder


  • ソースコード

$result = DB::select()->from('users')->where('id', $userid)->where('password', $password)->execute();


  • DBに渡されるクエリー

SELECT * FROM `users` WHERE `id` = '1' AND `password` = 'password'


  • 結果

    脆弱性は検出されませんでした。

    この辺までは流石に問題ありません。


Fuelphp DB::quote() によるエスケープ


  • ソースコード

$userid = DB::quote($userid);

$password = DB::quote($password);
$query = "SELECT * FROM `users` where `id` = $userid and `password` = $password";
$result = DB::query($query)->execute();


  • DBに渡されるクエリー

SELECT * FROM `users` WHERE `id` = '1' AND `password` = 'password'


  • 結果

    脆弱性は検出されませんでした。

    ここで1つ注目してほしいのは、数値型を期待している「1」がクォートされている点です。

    DB::escape() によってこれが行われています。


PDO::quote() によるエスケープ

ここからは FuelPHP から離れます。


  • ソースコード

$pdo = new PDO('mysql:host=localhost; dbname=SEC_CHECK; charset=utf8', 'user','password');

$userid = $pdo->quote($userid);
$password = $pdo->quote($password);
$query = "SELECT * FROM `users` where `id` = $userid and `password` = $password";
$result = $pdo->query($query)->fetchAll();


  • DBに渡されるクエリー

SELECT * FROM `users` where `id` = '1' and `password` = 'password'


  • 結果

    脆弱性は検出されませんでした。


プリペアードステートメント(mysqli)


  • ソースコード

$mysqli = new mysqli('localhost', 'user', 'password', 'SEC_CHECK');

$mysqli->set_charset("utf8");
$stmt = $mysqli->prepare("SELECT * FROM `users` where `id` = ? and `password` = ?");
$stmt->bind_param('is', $userid, $password);
$stmt->execute();


  • DBに渡されるクエリー

SELECT * FROM `users` where `id` = 1 and `password` = 'password'


  • 結果

    脆弱性は検出されませんでした。

    bind_param() で数値型を指定しているので、$userid はクォートされていません。


mysqli::real_escape_string() によるエスケープ


  • ソースコード

$mysqli = new mysqli('localhost', 'user', 'password', 'SEC_CHECK');

$mysqli->set_charset("utf8");
$userid = $mysqli->real_escape_string($userid);
$password = $mysqli->real_escape_string($password);
$query = "SELECT * FROM `users` where `id` = $userid and `password` = '$password'";
$result = $mysqli->query($query);


  • DBに渡されるクエリー

SELECT * FROM `users` where `id` = 1 and `password` = 'password'


  • 結果

    「boolean based」「time-based」というブラインドSQLインジェクションが2種類検出されてしまいました。

    クエリー中の \$userid はクォートされていません。

    mysqli::real_escape_string() ではクォートされないようです。

    クエリーを組み立てる時に \$passwod はクォートしましたが、\$userid は数値型なのでクォートしませんでした。

    ちなみに \$userid をクォートすると脆弱性は検出されなくなります。


  • 考察

    古いアプリケーションならありえる実装ではないかと思います。

    mysqli を利用して、かつクエリーを文字列連結で組み立てる場合にはちょっとしたミスでSQLインジェクションが作り込まれてしまいます。

    数値型をクォートしてDBに渡す点については、DB側で型変換が発生し意図せぬ挙動になることもあるようなので賛否の別れる部分ですが、少なくてもセキュリティ面では有効です。


まとめ


  • ブラインドSQLインジェクションは通常のSQLインジェクションより脅威が大きくなりやすい。

  • 対策としては通常のSQLインジェクションと同様で、極力、ORMやフレームワーク提供のクエリー生成機能を利用する。

  • のっぴきならない事情により、mysqliで文字列連結したクエリーをDBに渡す場合、プリペアードステートメントを利用する。

  • それもできない場合は、文字列はエスケープ、数値は数値型にキャストして数値以外の文字列が混入しないようにする。


最後に

セキュリティというと売上にもコスト削減にも貢献しないので敬遠されがちですが

弊社では比較的、協力的なエンジニアが多くて非常に助かっています。

これからもよろしくお願いします!

さて、CYBIRD エンジニア Advent Calendar 明日は、@sanjaponさんの○○です。

私のリアル上司にあたる @sanjapon さんは技術からマネジメントまで全方位的なスキルをお持ちなので、期待しましょう!