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

HTMLHTMLBeginner
今すぐ練習

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

前のプロジェクトでは、スレッドとソケットを利用してTCPポートをスキャンするPythonポートスキャナを開発しました。有効ではありますが、サードパーティのパッケージを使用することで改善の余地があります。

このプロジェクトでは、python-nmap ライブラリを統合することでポートスキャナを強化し、より堅牢なスキャン機能を提供します。さらに、Flaskを使ってWebアプリケーションを構築し、スキャナに対するユーザーフレンドリーなインターフェイスを提供します。この手順に沿ったプロジェクトでは、あなたが従って、既存の知識を基に構築できるように、プロセス全体を案内します。

👀 プレビュー

🎯 タスク

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

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

🏆 成果

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

  • Flaskを使ったWeb開発の基本的な理解を示すことができます。ルーティング、テンプレートレンダリング、フォーム処理を含みます。
  • PythonスクリプトとWebインターフェイスを統合する実際の経験を適用することができます。
  • ネットワークスキャンタスクに対してNmapライブラリを熟練して使用することができます。
  • Webアプリケーションにおけるフォーム作成と検証にFlask-WTFを利用することができます。
  • Webページのスタイリングとユーザーインターフェイスデザインの向上にTailwind CSSを使用することに慣れていることを示すことができます。
  • バックエンドの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/[email protected]/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" onsubmit="updateButton()">
        {{ 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つの主なフィールドが含まれています。1つはホストアドレス用で、もう1つはポート範囲を指定するためのものです。

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/[email protected]/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を使用してスタイリングしています。Jinja2テンプレートエンジン(Flaskと統合)の使用により、動的なコンテンツレンダリングが可能になり、スキャン結果がFlaskアプリケーションからテンプレートに渡され、テーブルを埋めるために反復処理されます。

✨ 解答を確認して練習

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

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

app.py に以下のコードを追加します。

## Import necessary modules
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

## Initialize Flask app
app = Flask(__name__)
app.config['SECRET_KEY'] = 'labex'

## Initialize Nmap PortScanner
nm = nmap.PortScanner()

## Define Flask-WTF form for scanning inputs
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攻撃からフォームを保護するために使用する実際のシークレットキーに置き換えてください。

✨ 解答を確認して練習

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

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

## Define route for the index page
@app.route('/', methods=['GET', 'POST'])
def index():
    form = ScanForm()  ## Instantiate the form
    if form.validate_on_submit():
        ## Get data from the form
        host = form.host.data
        ports = form.ports.data  ## Format: "start-end"
        ## Redirect to the scan route with form data
        return redirect(url_for('scan', host=host, ports=ports))
    ## Render the index page template with the form
    return render_template('index.html', form=form)

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

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

✨ 解答を確認して練習

スキャンルートの実装

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

## Define route for the scan results
@app.route('/scan')
def scan():
    ## Retrieve host and ports from the query string
    host = request.args.get('host')
    ports = request.args.get('ports')
    ## Perform the scan using Nmap
    nm.scan(hosts=host, ports=ports, arguments='-sV')  ## -sV for service/version detection
    scan_results = []

    ## Process scan results and store them in a list
    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', '')
                })

    ## Render the results page template with the scan results
    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によるネットワークスキャンの素晴らしい入門となり、両方のスキルを組み合わせた実用的なアプリケーションを提供します。