构建基于 Web 的 TCP 端口扫描器

HTMLBeginner
立即练习

介绍

在之前的项目中,我们开发了一个利用多线程和套接字(Socket)扫描 TCP 端口的 Python 端口扫描器。虽然那个扫描器已经可以工作,但通过引入第三方库,我们可以进一步提升其性能和功能。

在本项目中,我们将通过集成 python-nmap 库来增强扫描器,从而提供更强大的扫描能力。此外,我们还将使用 Flask 构建一个 Web 应用程序,为扫描器提供一个用户友好的交互界面。这个循序渐进的项目将引导你完成整个开发过程,确保你能够跟上进度并在现有知识的基础上进行扩展。

👀 预览

🎯 任务

在本项目中,你将学习:

  • 如何搭建 Flask 项目并组织其目录结构
  • 如何使用 Flask-WTF 安全地创建和处理 Web 表单
  • 如何实现 Flask 路由(Route)来处理网页请求和提交
  • 如何在 Python 中利用 Nmap 库执行端口扫描
  • 如何使用 Flask 和 HTML 模板在网页上动态展示扫描结果
  • 如何应用基础的 Tailwind CSS 来美化前端设计

🏆 成就

完成本项目后,你将能够:

  • 展示对 Flask Web 开发的基础理解,包括路由、模板渲染和表单处理
  • 获得将 Python 脚本与 Web 界面集成的实战经验
  • 熟练使用 Nmap 库执行网络扫描任务
  • 在 Web 应用中使用 Flask-WTF 进行表单创建和验证
  • 熟悉使用 Tailwind CSS 装饰网页并优化用户界面设计
  • 创建一个功能齐全的 Web 应用程序,通过与后端 Python 脚本交互来执行网络扫描

实现首页

首先,打开 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" 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 表单包含两个主要字段:一个用于输入主机地址,另一个用于指定端口范围。

我们使用了 Flask-WTF(一个用于处理 WTForms 的 Flask 扩展)在模板中渲染这些字段。表单提交功能通过一个简单的 JavaScript 函数 updateButton() 进行了增强,当用户点击提交时,按钮文本会变为「Scanning...」。这种视觉反馈能让用户知道他们的扫描请求正在处理中。

页面的样式由 Tailwind CSS 处理,这是一个采用「原子类优先」理念的 CSS 框架,能够实现快速的 UI 开发。

✨ 查看解决方案并练习

实现扫描结果页面

打开 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 进行美化,确保结果页面既具备响应式能力又美观大方。通过使用 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 应用实例,并配置了一个密钥(Secret Key)以防止跨站请求伪造(CSRF)攻击。

ScanForm 类定义了主机和端口范围输入的表单字段,并使用了验证器(Validators)来确保用户提供了数据,且数据格式正确(特别是针对端口范围的格式验证)。

请注意,在实际生产环境中,你应该将 'your_secret_key' 替换为一个真正的随机密钥。

✨ 查看解决方案并练习

处理首页路由

在这一步中,我们将处理首页路由,用户可以在此提交他们想要扫描的主机和端口范围。将以下函数添加到 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() 函数负责渲染 index.html 模板以及 ScanForm 实例。

当表单提交并成功通过验证后,该函数会将用户重定向到扫描路由,并通过 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 以调试模式(Debug Mode)启动应用,这有助于追踪错误。应用将监听所有网络接口 (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 网络扫描的绝佳入门,通过将这两项技能结合,实现了一个极具实用价值的应用程序。