PythonでWebサーバーのディレクトリ探索をする

PythonのRequestsライブラリを使用して、Webサーバーのディレクトリ探索をします。
HTTP、HTTPSおよびProxy経由の場合を想定しています。
Proxyは、Burp SuiteやOWASP ZAP等を想定しています。
OSは、Parrot OS Security Editionです。
Requestsライブラリの使い方の備忘録です。

# プログラム
IPアドレス、ポート、HTTP、HTTPS、Proxy有り無しの設定は、変数の初期値の変更や、コメントアウトに位置を変更することで切り替えます。
```python:DirBursting.py
#!/usr/bin/python3
from signal import signal, SIGINT
from sys import exit
import requests

# 証明書の検証を行わないときにInsecureRequestWarningを表示させないため
import urllib3
from urllib3.exceptions import InsecureRequestWarning
urllib3.disable_warnings(InsecureRequestWarning)

# Ctrl+Cでプログラムを中止するため
def handler(signal_received, frame):
    # Handle any cleanup here
    print('   [+]Exiting...')
    exit(0)

signal(SIGINT, handler)

# 変数の初期化
# HTTPかHTTPSを選択する
hostname = "http://192.168.56.116"
#hostname = "https://192.168.56.116"
# デフォルトのポートから変更するとき
port = 80
#port = 443
# ワードリストファイルを指定する
wordlist_file = "/usr/share/dirb/wordlists/small.txt"
wordlist = ""
word = ""
# ヘッダファイルを指定する
header_file = "header.txt"
headrslist = ""
header = ""
headerlist = ""
header_result = {}
proxies = {
    'http': 'http://127.0.0.1:8080',
    'https': 'http://127.0.0.1:8080',
}

# メインルーチン
if __name__=="__main__":
    try:
        with open(wordlist_file) as f:
            wordlist = [s.rstrip() for s in f.readlines()]
            print("Wordlist: " + wordlist_file)
            try:
                with open(header_file) as f2:
                    headerslist = [s.rstrip() for s in f2.readlines()]
                    print("Headerlist: " + header_file)
                    for header in headerslist:
                        if header == '':
                            break
                        if (header.startswith('GET') == False
                        and header.startswith('POST') == False
                        and header.startswith('Host') == False
                        and header.startswith('Connection') == False
                        and header.startswith('Content-Type') == False
                        and header.startswith('Content-Length') == False):
                            headerlist = header.split(': ')
                            header_result[headerlist[0]] = headerlist[1]
            except FileExistsError:
                header = ""
                print("ヘッダーファイルが存在しません")
            except FileNotFoundError:
                header = ""
                print("ヘッダーファイルが存在しません")
            keep_alive = requests.Session()
            #keep_alive.proxies.update(proxies)
            for word in wordlist:
                # デフォルトのポートから変更するとき
                url = hostname + '/' + word + '/'
                #url = hostname + ':' + str(port) + '/' + word + '/'
                # 証明書を検証しないとき
                #response = keep_alive.get(url)
                response = keep_alive.get(url, headers=header_result)
                #response = keep_alive.get(url, verify=False)
                #response = keep_alive.get(url, headers=header_result, verify=False)
                status = response.status_code
                connection = response.headers['Connection']
                if status != 404:
                    print(word + " " + str(status))
                if connection == "close":
                    keep_alive.close()
                    keep_alive = requests.Session()
                    #keep_alive.proxies.update(proxies)
            keep_alive.close()
    except FileExistsError:
        print("Wordlistファイルが存在しません")
    except FileNotFoundError:
        print("Wordlistファイルが存在しません")
```

# 説明
#### モジュール
Ctrl+Cでプログラムを中止するために、signal, SIGINT
http通信のために、requests
をインポートしています。
```python
from signal import signal, SIGINT
from sys import exit
import requests
```
#### 警告表示をさせない
オレオレ証明書のWebサーバーでテストする際に、証明書の検証を無効にしただけでは、Warningが表示されてしまうので、警告表示させないようにします。
```python
import urllib3
from urllib3.exceptions import InsecureRequestWarning
urllib3.disable_warnings(InsecureRequestWarning)
```
#### Ctrl+Cでプログラムを中止する
これを書いておけば、Ctrl+Cでプログラムを中止できます。
```python
def handler(signal_received, frame):
    # Handle any cleanup here
    print('   [+]Exiting...')
    exit(0)

signal(SIGINT, handler)
```
#### ホストのIPアドレスおよびプロトコル
ホストのIPアドレス（ホスト名でもOKなはず）を記載します。
HTTPSの場合は、上側をコメントアウトして、下側を有効にします。
```python
hostname = "http://192.168.56.116"
#hostname = "https://192.168.56.116"
```
#### デフォルトのポートから変更するとき
デフォルトのポートから変更するときに設定します。
ただし、URLを作成するところのコードを有効にしないと変更されません。
```python
port = 80
#port = 443
```
#### ワードリストファイルを指定する
ワードリストのファイルを指定します。
wordlist_fileは、ワードリストが記載されたファイル名
wordlistは、ファイルからワードを読み込みリスト形式にしたリスト
wordは、wordlistから一つずつ取り出したワード
```python
wordlist_file = "/usr/share/dirb/wordlists/small.txt"
wordlist = ""
word = ""
```
#### ヘッダーファイルを指定する
ヘッダーを指定したい場合、ヘッダーファイルを指定します。
ヘッダーファイルのフォーマットは、通常のリクエストヘッダーと同じです。
Burp Suite等で、リクエストをそのままファイルに保存してもいいです。
リクエスト行、Hostヘッダー、Connectionヘッダー、Content-Typeヘッダー、Content-Lengthヘッダーは、追加せずに削除します。
header_fileは、ヘッダーが記載されたファイル名
headrslistは、ファイルからワードを読み込みリスト形式にしたリスト
headerは、headrslistから一つずつ取り出したヘッダー
headerlistは、headerをヘッダー名と値で分けたリスト
header_resultは、ヘッダー名をキーとした辞書
```python
header_file = ""
headrslist = ""
header = ""
headerlist = ""
header_result = {}
```
#### プロキシーの設定
プロキシーのアドレス、ポートを指定する。
ただし、セッションを作成するところのコードを有効にしないとプロキシーが有効になりません。
```python
proxies = {
    'http': 'http://127.0.0.1:8080',
    'https': 'http://127.0.0.1:8080',
}
```
#### ワードリスト読み込み
ディレクトリ名の一覧ファイルを読み込みます。
readlines()で読み込んだ時点では、改行コードが含まれているため、rstrip()で改行コードを削除しています。
```python
with open(wordlists) as f:
    wordlist = [s.rstrip() for s in f.readlines()]
    print("Wordlist: " + wordlists)
```
#### ヘッダーの読み込み
ヘッダーを読み込みます。
リクエスト行、Hostヘッダー、Connectionヘッダー、Content-Typeヘッダー、Content-Lengthヘッダーは、削除します。
読み込んだヘッダーは、ヘッダー名をキーとした辞書を作成します。
```python
for header in headerslist:
    if header == '':
        break
    if (header.startswith('GET') == False
    and header.startswith('POST') == False
    and header.startswith('Host') == False
    and header.startswith('Connection') == False
    and header.startswith('Content-Type') == False
    and header.startswith('Content-Length') == False):
        headerlist = header.split(': ')
        header_result[headerlist[0]] = headerlist[1]
```
#### セッションを作成
キープアライブのために、セッションを作成します。
プロキシーを有効する場合は、2行目を有効にします。
```python
keep_alive = requests.Session()
#keep_alive.proxies.update(proxies)
```
#### リクエストの送信
wordlistから1個ずつワードを取り出して、リクエストを送信します。
デフォルトポートから変更する場合は、urlを作成する式の上側をコメントアウトして、下側を有効にします。
ヘッダーを追加する場合は、headers=header_resultがある式を有効にします。
httpsの証明書の検証を無効にする場合は、verify=Falseがある式を有効にします。
statusは、HTTPステータスコードを取得します。
connectionは、レスポンスヘッダーのConnectionを取得します。
HTTPステータスコードが、404でなければ、その時のワードとステータスを表示します。
レスポンスヘッダーのConnectionが、closeになると、一旦、セッションをcloseして、再度、セッションを作成します。
プロキシーを有効する場合は、ここも有効にします。
```python
for word in wordlist:
    url = hostname + '/' + word + '/'
    #url = hostname + ':' + str(port) + '/' + word + '/'
    #response = keep_alive.get(url)
    response = keep_alive.get(url, headers=header_result)
    #response = keep_alive.get(url, verify=False)
    #response = keep_alive.get(url, headers=header_result, verify=False)
    status = response.status_code
    connection = response.headers['Connection']
    if status != 404:
        print(word + " " + str(status))
    if connection == "close":
        keep_alive.close()
        keep_alive = requests.Session()
        #keep_alive.proxies.update(proxies)
keep_alive.close()
```
# プロキシーを使用する場合
OWASP ZAPの場合は、上記のコードで問題ないですが
Burp Suiteの場合、上記のコードでは、予期しないときにBurp Suiteからセッションを閉じられて、エラーになります。
エラーにならないように、毎回セッションを閉じるコードです。
このコードでも、OWASP ZAP、Burp Suiteの両方とも、サーバーとの間は、キープアライブになるようです。
```python:DirBursting.py(キープアライブ無し)
#!/usr/bin/python3
from signal import signal, SIGINT
from sys import exit
import requests

# 証明書の検証を行わないときにInsecureRequestWarningを表示させないため
import urllib3
from urllib3.exceptions import InsecureRequestWarning
urllib3.disable_warnings(InsecureRequestWarning)

# Ctrl+Cでプログラムを中止するため
def handler(signal_received, frame):
    # Handle any cleanup here
    print('   [+]Exiting...')
    exit(0)

signal(SIGINT, handler)

# 変数の初期化
# HTTPかHTTPSを選択する
hostname = "http://192.168.56.116"
#hostname = "https://192.168.56.116"
# デフォルトのポートから変更するとき
port = 80
#port = 443
# ワードリストファイルを指定する
wordlist_file = "/usr/share/dirb/wordlists/small.txt"
wordlist = ""
word = ""
# ヘッダファイルを指定する
header_file = "header.txt"
headrslist = ""
header = ""
headerlist = ""
header_result = {}
proxies = {
    'http': 'http://127.0.0.1:8080',
    'https': 'http://127.0.0.1:8080',
}

# メインルーチン
if __name__=="__main__":
    try:
        with open(wordlist_file) as f:
            wordlist = [s.rstrip() for s in f.readlines()]
            print("Wordlist: " + wordlist_file)
            try:
                with open(header_file) as f2:
                    headerslist = [s.rstrip() for s in f2.readlines()]
                    print("Headerlist: " + header_file)
                    for header in headerslist:
                        if header == '':
                            break
                        if (header.startswith('GET') == False
                        and header.startswith('POST') == False
                        and header.startswith('Host') == False
                        and header.startswith('Connection') == False
                        and header.startswith('Content-Type') == False
                        and header.startswith('Content-Length') == False):
                            headerlist = header.split(': ')
                            result[headerlist[0]] = headerlist[1]
            except FileExistsError:
                header = ""
                print("Headerファイルが存在しません")
            except FileNotFoundError:
                header = ""
                print("Headerファイルが存在しません")
            for word in wordlist:
                keep_alive = requests.Session()
                keep_alive.proxies.update(proxies)
                # デフォルトのポートから変更するとき
                url = hostname + '/' + word + '/'
                #url = hostname + ':' + str(port) + '/' + word + '/'
                # 証明書を検証しないとき
                #response = keep_alive.get(url)
                response = keep_alive.get(url, headers=header_result)
                #response = keep_alive.get(url, verify=False)
                #response = keep_alive.get(url, headers=header_result, verify=False)
                status = response.status_code
                if status != 404:
                    print(word + " " + str(status))
                keep_alive.close()
    except FileExistsError:
        print("Wordlistファイルが存在しません")
    except FileNotFoundError:
        print("Wordlistファイルが存在しません")
```