Construcción de un Escáner de Puertos TCP Basado en Web

HTMLBeginner
Practicar Ahora

Introducción

En el proyecto anterior, desarrollamos un escáner de puertos en Python que utilizaba hilos y sockets para escanear puertos TCP. Aunque es efectivo, existe un gran potencial de mejora si aprovechamos paquetes de terceros.

En este proyecto, potenciaremos nuestro escáner integrando la librería python-nmap, lo que nos permitirá ofrecer capacidades de escaneo mucho más robustas. Además, construiremos una aplicación web con Flask para proporcionar una interfaz de usuario intuitiva y accesible. Este proyecto guiado paso a paso te permitirá avanzar sobre tus conocimientos previos de manera fluida.

👀 Vista Previa

🎯 Tareas

En este proyecto, aprenderás:

  • Cómo configurar un proyecto Flask y organizar su estructura de directorios.
  • Cómo utilizar Flask-WTF para crear y gestionar formularios web de forma segura.
  • Cómo implementar rutas en Flask para manejar peticiones de páginas y envíos de datos.
  • Cómo utilizar la librería Nmap en Python para realizar escaneos de puertos profesionales.
  • Cómo mostrar los resultados del escaneo de forma dinámica en una página web usando Flask y plantillas HTML.
  • Cómo aplicar Tailwind CSS básico para mejorar el diseño de la interfaz de usuario.

🏆 Logros

Al finalizar este proyecto, serás capaz de:

  • Demostrar una comprensión fundamental del desarrollo web con Flask, incluyendo el enrutamiento, el renderizado de plantillas y el manejo de formularios.
  • Aplicar experiencia práctica en la integración de scripts de Python con interfaces web.
  • Mostrar competencia en el uso de la librería Nmap para tareas de auditoría de red.
  • Utilizar Flask-WTF para la creación y validación de formularios en una aplicación web.
  • Utilizar Tailwind CSS para dar estilo a páginas web y mejorar el diseño de la experiencia de usuario.
  • Crear una aplicación web funcional que interactúe con scripts de Python en el backend para realizar escaneos de red en tiempo real.

Implementación de la Página de Inicio

Para comenzar, abre templates/index.html y añade el siguiente código para asegurar que el formulario esté correctamente configurado para enviar las solicitudes de escaneo:

<!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>

La plantilla index.html proporciona una interfaz amigable para que el usuario introduzca el host y el rango de puertos a escanear. El formulario HTML incluye dos campos principales: uno para la dirección del host y otro para especificar el rango de puertos.

Utilizamos Flask-WTF, una extensión de Flask para trabajar con WTForms, para renderizar estos campos en la plantilla. El envío del formulario se mejora con una pequeña función de JavaScript llamada updateButton(), que cambia el texto del botón a "Scanning..." al hacer clic. Esta retroalimentación visual informa al usuario que su solicitud de escaneo está siendo procesada.

El diseño de la página se gestiona con Tailwind CSS, un framework de CSS basado en utilidades que permite un desarrollo de interfaces extremadamente rápido.

✨ Revisar Solución y Practicar

Implementación de la Página de Resultados

Abre templates/results.html y asegúrate de incluir el siguiente código para mostrar los resultados del escaneo:

<!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>

La plantilla results.html es la encargada de presentar el desenlace del escaneo de puertos. Muestra la información en formato de tabla, enumerando cada puerto con su estado correspondiente, el nombre del servicio y la versión del mismo si está disponible.

Esta plantilla también utiliza Tailwind CSS para el estilo, garantizando que la página de resultados sea responsiva y visualmente atractiva. El uso del motor de plantillas Jinja2 (integrado en Flask) permite el renderizado de contenido dinámico: los resultados del escaneo se pasan desde la aplicación Flask a la plantilla y se recorren mediante un bucle para rellenar la tabla.

✨ Revisar Solución y Practicar

Inicialización de la Aplicación Flask

En este paso, crearemos el script principal de Python para nuestra aplicación Flask.

Añade el siguiente código en 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')

En esta etapa, se inicializan tanto la aplicación Flask como la clase del formulario.

El script app.py comienza importando los módulos necesarios, incluyendo Flask, Flask-WTF para la gestión de formularios y Nmap para realizar los escaneos. La instancia de la aplicación Flask se configura con una clave secreta (SECRET_KEY) para proteger el sitio contra ataques de falsificación de petición en sitios cruzados (CSRF).

La clase ScanForm define los campos de entrada para el host y el rango de puertos, utilizando validadores para asegurar que los datos sean obligatorios y tengan el formato correcto (específicamente para el rango de puertos).

Sustituye 'your_secret_key' por una clave secreta real que se utilice para proteger los formularios.

✨ Revisar Solución y Practicar

Gestión de la Ruta de Inicio

En este paso, manejaremos la ruta principal donde los usuarios pueden enviar el host y el rango de puertos que desean escanear. Añade la siguiente función a 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)

Esta sección del script app.py define la ruta para la página de inicio de la aplicación web. La función index() renderiza la plantilla index.html junto con la instancia de ScanForm.

Cuando el formulario se envía y supera las validaciones, la función redirige al usuario a la ruta de escaneo, pasando los datos del formulario (host y rango de puertos) a través de parámetros en la URL. Esta redirección es la que inicia el proceso técnico de escaneo.

✨ Revisar Solución y Practicar

Implementación de la Ruta de Escaneo

Este paso consiste en crear la ruta que realizará el escaneo real y mostrará los resultados. Añade la siguiente función a 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)

La función scan() gestiona la ruta encargada de ejecutar el escaneo de puertos y mostrar los hallazgos. Recupera el host y el rango de puertos de los parámetros de la cadena de consulta pasados en la URL.

Utilizando la instancia de Nmap PortScanner, realiza un escaneo en el host y puertos especificados, empleando el argumento -sV para detectar las versiones de los servicios.

Los resultados del escaneo se procesan y organizan en una lista de diccionarios, cada uno con detalles sobre un puerto escaneado. Estos detalles se pasan luego a la plantilla results.html, donde se presentan al usuario.

✨ Revisar Solución y Practicar

Ejecución de la Aplicación Flask

Con todos los componentes en su lugar, ya estás listo para ejecutar la aplicación Flask y dar vida a tu escáner de puertos TCP. El último fragmento de código necesario en app.py asegura que la aplicación Flask solo se ejecute si el script se invoca directamente, y no si se importa como un módulo en otro script. Este es un patrón estándar en aplicaciones Python.

Coloca el siguiente fragmento de código al final de tu archivo app.py:

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

Este código le indica a Flask que inicie la aplicación con el modo de depuración activado, lo que facilita la localización de errores. La aplicación escuchará en todas las interfaces de red (host='0.0.0.0') y utilizará el puerto 8080. Ten en cuenta que el modo de depuración solo debe usarse durante el desarrollo, ya que puede ser inseguro en entornos de producción.

Para ejecutar tu aplicación Flask, asegúrate de estar en el directorio del proyecto donde se encuentra app.py. Luego, ejecuta el siguiente comando en la terminal:

python app.py

Cambia a la pestaña Web 8080 para acceder al escáner de puertos TCP. Ahora puedes introducir un host y un rango de puertos para escanear, y ver los resultados en la página correspondiente.

Los puertos 22 y 3306 están asociados comúnmente con los servicios SSH y MySQL, respectivamente, mientras que el puerto 3000 es utilizado para los entornos WebIDE.

✨ Revisar Solución y Practicar

Resumen

En este proyecto, has aprendido a construir un escáner de puertos TCP basado en web, sencillo pero potente, utilizando Flask y Nmap. Comenzamos configurando el entorno del proyecto e instalando las dependencias necesarias. Luego, avanzamos en la creación de la aplicación Flask, la gestión del envío de formularios, la ejecución del escaneo de puertos y la visualización de los resultados de manera intuitiva. Este proyecto sirve como una excelente introducción al desarrollo web con Flask y al escaneo de redes con Nmap, ofreciendo una aplicación práctica que combina ambas disciplinas.