Web ベースの TCP ポートスキャナーの構築

HTMLBeginner
オンラインで実践に進む

はじめに

前回のプロジェクトでは、スレッドとソケットを活用して TCP ポートをスキャンする Python ポートスキャナーを開発しました。非常に効果的ではありましたが、サードパーティ製のパッケージを利用することで、さらなる改善の余地があります。

このプロジェクトでは、python-nmap ライブラリを統合することでポートスキャナーを強化し、より堅牢なスキャン機能を提供します。さらに、Flask を使用して Web アプリケーションを構築し、スキャナーにユーザーフレンドリーなインターフェースを提供します。このステップバイステップのプロジェクトでは、既存の知識を基に構築を進められるよう、プロセス全体をガイドします。

👀 プレビュー

🎯 タスク

このプロジェクトでは、以下の内容を学びます:

  • Flask プロジェクトのセットアップとディレクトリ構造の整理方法
  • Flask-WTF を使用して Web フォームを安全に作成・処理する方法
  • Web ページの要求と送信を処理するための Flask ルート(Route)の実装方法
  • Python で Nmap ライブラリを利用してポートスキャンを実行する方法
  • Flask と HTML テンプレートを使用して、スキャン結果を Web ページに動的に表示する方法
  • Tailwind CSS を適用してフロントエンドのデザインを向上させる方法

🏆 達成できること

このプロジェクトを完了すると、以下のことができるようになります:

  • ルーティング、テンプレートレンダリング、フォーム処理を含む、Flask による Web 開発の基礎知識の習得
  • Python スクリプトを Web インターフェースと統合する実践的な経験の獲得
  • ネットワークスキャンタスクにおける Nmap ライブラリの使用習熟
  • Web アプリケーションでのフォーム作成とバリデーション(検証)のための Flask-WTF の活用
  • Tailwind CSS を使用した Web ページのスタイリングとユーザーインターフェースデザインの向上
  • バックエンドの Python スクリプトと連携してネットワークスキャンを実行する、機能的な Web ベースアプリケーションの作成

インデックスページの実装

まず、templates/index.html を開き、スキャンリクエストを送信するためのフォームが正しく設定されるよう、以下のコードを追加します。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TCP Port Scanner</title>
    <link
      href="https://cdn.jsdelivr.net/npm/tailwindcss@2.0.3/dist/tailwind.min.css"
      rel="stylesheet"
    />
    <script>
      function updateButton() {
        var submitButton = document.getElementById("scanButton");
        submitButton.value = "Scanning...";
        submitButton.classList.add("cursor-not-allowed", "opacity-50");
        submitButton.disabled = true;
      }
    </script>
  </head>
  <body class="bg-gray-100 flex items-center justify-center h-screen">
    <div class="bg-white p-8 rounded-lg shadow-md">
      <h1 class="text-2xl font-bold mb-4">TCP Port Scanner</h1>
      <form action="" method="post" class="space-y-4"
        {{ form.hidden_tag() }}
        <div>
          {{ form.host.label(class="block text-sm font-medium text-gray-700") }}
          {{ form.host(class="mt-1 block w-full rounded-md border-gray-300
          shadow-sm focus:border-indigo-500 focus:ring-indigo-500") }}
        </div>
        <div>
          {{ form.ports.label(class="block text-sm font-medium text-gray-700")
          }} {{ form.ports(class="mt-1 block w-full rounded-md border-gray-300
          shadow-sm focus:border-indigo-500 focus:ring-indigo-500") }}
        </div>
        <div>
          {{ form.submit(id="scanButton", class="w-full flex justify-center py-2
          px-4 border border-transparent rounded-md shadow-sm text-sm
          font-medium text-white bg-indigo-600 hover:bg-indigo-700
          focus:outline-none focus:ring-2 focus:ring-offset-2
          focus:ring-indigo-500") }}
        </div>
      </form>
    </div>
  </body>
</html>

index.html テンプレートは、スキャン対象のホストとポート範囲を送信するためのユーザーフレンドリーなインターフェースを提供します。HTML フォームには、ホストアドレス用とポート範囲指定用の 2 つの主要なフィールドが含まれています。

WTForms を扱うための Flask 拡張機能である Flask-WTF を使用して、テンプレート内にこれらのフィールドをレンダリングします。フォーム送信時には、JavaScript 関数 updateButton() によって送信ボタンのテキストが「Scanning...」に変わり、ユーザーにスキャンが進行中であることを視覚的に伝えます。

ページのスタイリングには、迅速な UI 開発を可能にするユーティリティファーストの CSS フレームワークである Tailwind CSS を使用しています。

スキャン結果ページの実装

templates/results.html を開き、スキャン結果を表示するために以下のコードが含まれていることを確認します。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Scan Results</title>
    <link
      href="https://cdn.jsdelivr.net/npm/tailwindcss@2.0.3/dist/tailwind.min.css"
      rel="stylesheet"
    />
  </head>
  <body class="bg-gray-100">
    <div class="flex flex-col items-center justify-center min-h-screen">
      <div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-4xl">
        <h1 class="text-2xl font-bold mb-4 text-center">
          Scan Results for {{ host }}
        </h1>
        <div class="overflow-x-auto">
          <table class="table-auto w-full text-left whitespace-no-wrap">
            <thead>
              <tr
                class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50"
              >
                <th class="px-4 py-3">PORT</th>
                <th class="px-4 py-3">STATE</th>
                <th class="px-4 py-3">SERVICE</th>
                <th class="px-4 py-3">VERSION</th>
              </tr>
            </thead>
            <tbody
              class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800"
            >
              {% for result in scan_results %}
              <tr class="text-gray-700 dark:text-gray-400">
                <td class="px-4 py-3 text-sm">{{ result.port }}/tcp</td>
                <td class="px-4 py-3 text-sm">{{ result.state }}</td>
                <td class="px-4 py-3 text-sm">{{ result.name }}</td>
                <td class="px-4 py-3 text-sm">
                  {{ result.product }} {{ result.version }} {{ result.extra }}
                </td>
              </tr>
              {% endfor %}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </body>
</html>

results.html テンプレートは、ポートスキャンの結果を表示する役割を担います。スキャン結果をテーブル形式で提示し、各ポートの状態、サービス名、および(利用可能な場合は)サービスのバージョンを一覧表示します。

このテンプレートでも Tailwind CSS を使用しており、結果ページがレスポンシブで視覚的に魅力的なものになるようにしています。Flask に統合されている Jinja2 テンプレートエンジンを使用することで、動的なコンテンツのレンダリングが可能になります。具体的には、Flask アプリケーションから渡されたスキャン結果をループ処理してテーブルに流し込みます。

Flask アプリケーションの初期化

このステップでは、Flask アプリケーションのメインとなる Python スクリプトを作成します。

app.py に以下のコードを追加してください。

## 必要なモジュールのインポート
from flask import Flask, render_template, request, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Regexp
import nmap

## Flask アプリの初期化
app = Flask(__name__)
app.config['SECRET_KEY'] = 'labex'

## Nmap PortScanner の初期化
nm = nmap.PortScanner()

## スキャン入力用の Flask-WTF フォームの定義
class ScanForm(FlaskForm):
    host = StringField('Host', validators=[DataRequired()])
    ports = StringField('Port Range', validators=[DataRequired(), Regexp(r'^\d+-\d+$', message="Format must be start-end")])
    submit = SubmitField('Scan')

このステップでは、Flask アプリケーションとフォームクラスが初期化されます。

app.py スクリプトは、Flask 本体、フォーム処理用の Flask-WTF、およびポートスキャンを実行するための Nmap を含む、必要なモジュールのインポートから始まります。Flask アプリケーションのインスタンスが作成され、CSRF 攻撃から保護するためのシークレットキーが設定されます。

ScanForm クラスは、ホストとポート範囲の入力フィールドを定義します。バリデーター(検証機能)を使用して、データが提供されていること、および正しい形式(特にポート範囲の形式)であることを確認します。

'your_secret_key' は、CSRF 攻撃からフォームを保護するために使用される実際のシークレットキーに置き換えてください。

インデックスルートの処理

このステップでは、ユーザーがスキャンしたいホストとポート範囲を送信できるインデックスルート(Index Route)を処理します。app.py に以下の関数を追加してください。

## インデックスページのルートを定義
@app.route('/', methods=['GET', 'POST'])
def index():
    form = ScanForm()  ## フォームをインスタンス化
    if form.validate_on_submit():
        ## フォームからデータを取得
        host = form.host.data
        ports = form.ports.data  ## 形式:"開始 - 終了"
        ## フォームデータと共にスキャンルートへリダイレクト
        return redirect(url_for('scan', host=host, ports=ports))
    ## フォームを含めてインデックスページのテンプレートをレンダリング
    return render_template('index.html', form=form)

app.py スクリプトのこの部分は、Web アプリケーションのインデックスページのルートを定義しています。index() 関数は、ScanForm インスタンスと共に index.html テンプレートをレンダリングします。

フォームが送信され、バリデーションチェックを通過すると、関数はユーザーをスキャンルートにリダイレクトし、URL パラメータを介してフォームデータ(ホストとポート範囲)を渡します。このリダイレクトによってスキャンプロセスが開始されます。

スキャンルートの実装

このステップでは、実際にスキャンを実行し、結果を表示するためのルートを作成します。app.py に以下の関数を追加してください。

## スキャン結果のルートを定義
@app.route('/scan')
def scan():
    ## クエリ文字列からホストとポートを取得
    host = request.args.get('host')
    ports = request.args.get('ports')
    ## Nmap を使用してスキャンを実行
    nm.scan(hosts=host, ports=ports, arguments='-sV')  ## -sV はサービス/バージョン検出用
    scan_results = []

    ## スキャン結果を処理してリストに保存
    for host in nm.all_hosts():
        for proto in nm[host].all_protocols():
            lport = nm[host][proto].keys()
            for port in lport:
                service = nm[host][proto][port]
                scan_results.append({
                    'port': port,
                    'state': service['state'],
                    'name': service.get('name', 'Unknown'),
                    'product': service.get('product', ''),
                    'version': service.get('version', ''),
                    'extra': service.get('extrainfo', '')
                })

    ## スキャン結果を含めて結果ページのテンプレートをレンダリング
    return render_template('results.html', scan_results=scan_results, host=host)

scan() 関数は、実際のポートスキャンを実行し、結果を表示するルートを処理します。URL で渡されたクエリ文字列パラメータからホストとポート範囲を取得します。

Nmap PortScanner インスタンスを使用して、指定されたホストとポートに対してスキャンを実行します。-sV 引数を使用することで、サービスのバージョンを検出します。

スキャン結果は処理され、スキャンされた各ポートの詳細を含む辞書のリストに整理されます。これらの詳細は results.html テンプレートに渡され、ユーザーに表示されます。

Flask アプリケーションの実行

すべてのコンポーネントが揃ったので、Flask アプリケーションを実行して TCP ポートスキャナーを起動する準備が整いました。app.py に必要な最後のコードは、スクリプトが直接実行された場合にのみ Flask アプリケーションが実行されるようにするものです(他のスクリプトにモジュールとしてインポートされた場合には実行されません)。これは、実行可能なスクリプトを含む Python アプリケーションにおける一般的なパターンです。

app.py ファイルの最後に以下のコードスニペットを配置してください。

if __name__ == '__main__':
    app.run(debug=True, port=8080, host='0.0.0.0')

このコードは、デバッグを有効にした状態でアプリケーションを開始するよう Flask に指示し、エラーの追跡を容易にします。アプリケーションはすべてのネットワークインターフェース (host='0.0.0.0') でリッスンし、ポート 8080 を使用します。デバッグモードは本番環境では安全ではない可能性があるため、開発中にのみ使用してください。

Flask アプリケーションを実行するには、app.py があるプロジェクトディレクトリにいることを確認してください。次に、ターミナルで以下のコマンドを実行します。

python app.py

Web 8080 タブに切り替えて、TCP ポートスキャナーにアクセスします。ホストとスキャンするポート範囲を入力し、結果ページで結果を確認できるようになります。

ポート 22 と 3306 はそれぞれ SSH および MySQL サービスに広く関連付けられており、ポート 3000 は WebIDE 環境で使用されています。

まとめ

このプロジェクトでは、Flask と Nmap を使用して、シンプルながらも強力な Web ベースの TCP ポートスキャナーを構築する方法を学びました。まずプロジェクト環境をセットアップし、必要な依存関係をインストールすることから始めました。その後、Flask アプリケーションの作成、フォーム送信の処理、ポートスキャンの実行、そして結果をユーザーフレンドリーな形式で表示するプロセスを進めました。このプロジェクトは、Flask による Web 開発と Nmap によるネットワークスキャンの両方のスキルを組み合わせた実用的なアプリケーションを提供しており、これら 2 つの分野への優れた入門となります。

✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習✨ 解答を確認して練習