Python セキュリティ

【Python】Webハッキングで使われる基礎知識の解説【辞書攻撃】

前回の記事ではサーバ上で不用意に配置されたファイルがないかを調査するためのスキャナーを作成し、その結果を元にデータベースのテーブルや認証に関する情報を取得するところまでデモンストレーションを行いました。

スクリプトをどんと張って終わりにしたので、今回はPythonのソースコードで実行している内容の大まかな流れを解説していきます。不要なファイルの調査やパスワード解析を辞書ファイルで行う場合、今回紹介するようなファイルを読み込み後、スレッド処理を使用してリクエストを複数回試行するといった処理はよく使われますので、これを機に基本を抑えておきましょう。

前回に引き続き今回も脆弱性診断士やペネトレーションテスターなど、サイバーセキュリティ関連業務に従事する人に向けた内容になっていますが、悪用すると法律に問われる可能性がありますのでくれぐれも常識の範囲内で参考にするようにしてください。

辞書用ファイルの読み込み関数を作成

前回の記事では2つの関数をPythonで実装しました。辞書ファイルを読み込むgetWords関数、読み込んだ語句を使ってリクエストを送信するbruteForce関数です。最初に解説するのはgetWords関数になります。以下のようなPythonスクリプトで実装を行いました。

import queue

WORDLIST = r"C:\Users\owner\Desktop\Others\学習\Python\all.txt"

def getWords():
    words = queue.Queue()

    with open(WORDLIST) as f:
        wordList = f.read()

    for word in wordList.split():
        words.put(word)
        print(f'Loading words... {word}')

    return words

Open関数

まずgetWords関数中で使われているOpen関数について解説します。Open関数というのはファイルを読み込むための関数であり、以下のように記述することで指定したファイルを読み込むことができます。

open関数で読み込んだファイルの中身をread()関数を使ってwordListという変数に代入することができました。試しにprint関数でwordListの中身を表示すると以下のように読み込んだall.txtの中身が表示されます。

all.txtの内容

ただwordList変数にはall.txtの中身が全て代入されていますから、この変数をそのままリクエストに入れて送信した場合、ファイルの内容が丸ごと送信されることになってしまいます。そのため読み込んだ内容をsplit関数を使用して1行ごとに分割を行います。

for word in wordList.split():
    print(word)

//Output
common
CVS
root
Entries
lang
(以下略)

Queueモジュール

words = queue.Queue()と関数の先頭で書いていますが、これはキューを利用するためのオブジェクト宣言を行っています。キューというのはIT系の資格の勉強をしたことがあるならなんとなく分かっているかと思いますが、多くの場合先入れ先出しのデータ構造を指します。Queueオブジェクトはput()とget()メソッドを持っており、これを使って中身を入れたり出したりすることが可能です。

こちらの画面はwords.put(word)の箇所にブレークポイントを置き、その直後のprint(f'Loading words… {word}')のところまで実行した時のデバッグ画面です。open関数で読み込んだ辞書ファイルの最初の行の語句「common」をwords.put(word)でキューに投入したところで止まっています。この時点で各変数の中身を確認するとqueueの中に「common」という値が投入されていることが確認できます。

試しに以下の通りgetメソッドで取り出してみます。

再びqueueの中身を確認すると「common」が取り出され中身が消えていることが確認できました。

ブルートフォース攻撃を行う関数の実装

では辞書攻撃の根幹をなすbruteForce関数の実装を解説していきましょう。

#補足
#TARGET = "http://testphp.vulnweb.com"
#

def bruteForce(words):
    while not words.empty():
        url = f'{TARGET}/{words.get()}'
        try:
            r = requests.get(url)
        except:
            sys.stderr.write('Oops, an error has occurred.')
            continue

        if r.status_code == 200:
            print(f'\nDetected! -> {url}')
            urlList.put(f'200 SUCCESS -> {url}')
        elif r.status_code == 404:
            print(f'.', end="")
        else:
            print(f'.', end="")
            urlList.put(f'[*] Check the following URL -> {url}, Status code: {r.status_code} ')

        if words.qsize() % 100 == 0:
            print(f"\n{words.qsize()} words remaining.")

url = f'{TARGET}/{words.get()}'でリクエストを送信するURLを生成

今回TARGET変数には「http://testphp.vulnweb.com」の値が入っていますから、f'{TARGET}/{words.get()}'とすることでwordsのqueueオブジェクトに例えば「common」の文字列が入っていた場合、「http://testphp.vulnweb.com/common」というURLを生成します。これが実際にリクエストを送信するURLになります。

requestsモジュール

上で生成したURLに対してはrequestsモジュールを使用してリクエストを送信します。requestsモジュールはPythonでHTTP通信を行うためのライブラリで直感的に書くことができ、以下のようなスクリプトで指定したURLに対して通信を行うことが可能です。

#sample
import requests

url = "https://www.google.com"
r = requests.get(url)

content = r.content
print(content.decode())

#出力結果
#<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head><meta content="&#1999#0;&#30…(略)…

content変数にはバイト文字列が代入されることに注意しましょう。decode関数を呼び出さずに表示した場合以下の通りバイト文字列のまま表示されてしまいます。

#sample
url = "https://www.google.com"
r = requests.get(url)

content = r.content
print(content)

#b'<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head><meta content="&#19990;&#3

有効なステータスコードか確認する

リクエストを送信して有効なレスポンスが得られればステータスコード200が返ってきますが、辞書ファイルに戻づいて生成したランダムなURLなので実際は存在しなかったり何らかの制御で見られないこともあるため、そういった場合の多くは404(ファイルが存在しない)エラーが返ってきます。またリダイレクト(302)が行われるケースなどは中身を精査しないと判断がしづらいので、キュー(urlList変数)に一旦追加し後で精査を行えるようにしておきます。

ステータスコードに応じて有効なケース(200)、無効なケース(400)、後で確認した方が良いケース(302等)の3つに分類を行います。今の内容を実装した部分が以下の部分です。

        if r.status_code == 200:
            print(f'\nDetected! -> {url}')
            urlList.put(f'200 SUCCESS -> {url}')
        elif r.status_code == 404:
            print(f'.', end="")
        else:
            print(f'.', end="")
            urlList.put(f'[*] Check the following URL -> {url}, Status code: {r.status_code} ')

進捗状況を表示する

辞書ファイルに登録されている語句の行数分だけリクエスト送信を行うことになりますが、こういった辞書ファイルは試行する数が多いだけに数分~数十分とかなり時間を要する場合があります。そのため100回試行するごとに進捗を表示し現時点でどれだけ進んでいるのかをわかりやすくしましょう。既に紹介したqueueオブジェクトのqsize()を利用することでキューにあとどれだけ値が入っているのか、その個数を調べることができます。

        if words.qsize() % 100 == 0:
            print(f"\n{words.qsize()} words remaining.")

#words.qsize()はwordsのキューにどれだけ値が入っているかを示す

実装した関数をスレッド処理で実行

ここまでで辞書攻撃に必要な処理は大体実装することができました。ただ辞書ファイルは4万行以上あることもあり、リクエストを送信してレスポンスが取得されるまで1回1回待っていると処理に時間が掛かりすぎてしまいます。そのためレスポンスが取得されるまでの待ち時間も次のリクエストを送信し続け、可能な範囲で並行して処理を実行し続けるようにしましょう。Pythonではこの処理をThreadingモジュールを使用して実装することができます。今回はbruteForce関数の内容を並列実行しますが、これを実装するPythonスクリプトは以下の通りです。

#THREAD_NUM = 30

if __name__ == '__main__':
    words = getWords()
    for _ in range(THREAD_NUM):
        t = threading.Thread(target=bruteForce, args=(words,))
        t.start()
        threads.append(t)

    for t in threads:
        t.join()

    while not urlList.empty():
        print(urlList.get())

Threadオブジェクトを生成し並列実行する

Pythonスクリプトの先頭でいくつのリクエストを並行処理するのか定義しています。先頭の「THREAD_NUM=30」で記述している通り、今回は30個のリクエストを並行して送信するようにします。以下のスクリプトのthreading.Threadのtarget、args引数にはそれぞれ並列実行したい関数の名前と検査用URLの入っているwords変数を指定しています。args引数にはタプルを指定しなければならないため、「args=(words,)」と少し独特な表記をしていることに注意してください。

    for _ in range(THREAD_NUM):
        t = threading.Thread(target=bruteForce, args=(words,))
        t.start()
        threads.append(t)

joinで並列実行処理が終わるのを待つ

bruteForce関数の中で発見した不要なファイルが存在しているURLをurlListのキューに入れていましたが、キューの中身を表示する処理を実行する前に並列処理が終了するのを待たなければいけません。並列処理の終了を待たずにキューの中身を表示しても、まだ未処理の部分の結果がキューには反映されていないためです。そのため以下のjoin関数をそれぞれのスレッドオブジェクトに対して実行することで、実行した並列処理が全て終了するのを待ってから後続の処理を実行するようにします。

    for t in threads:
        t.join()

実行した結果を表示する

bruteForce関数のすべての処理が終わったら検査結果を表示します。検査結果を表示するスクリプトは以下の通りです。「while not 変数名.empty()」の部分はQueueオブジェクトの中身をループで取り出す際によく利用される表現になります。

    while not urlList.empty():
        print(urlList.get())

まとめ

いかがでしたでしょうか?一見辞書攻撃と聞くと難しく感じてしまいますが、その中で使われている要素をひとつひとつ分解すると基礎事項の積み重ねであることが理解できたと思います。今回作成したのはとてもシンプルな例ですので、工夫して自分なりの機能などを追加できないか工夫してみてください。

-Python, セキュリティ