Saltar al contenido
0xrcosRyan Camargo — home
Todas las publicaciones
programming~8min readRyan Camargo

Python para Herramientas de Seguridad: Un Esbozo de Escáner de Puertos y un Descifrador de Hash

Una introducción práctica y amigable para principiantes a la escritura de pequeñas herramientas de seguridad en Python — un escáner de puertos concurrente y un descifrador de hash por diccionario, con ética incorporada.

Ética y responsabilidad: Solo escanea sistemas que poseas o para los que tengas permiso explícito y por escrito para probar. Un escáner de puertos dirigido al destino equivocado es, en muchas jurisdicciones, tratado de la misma manera que cualquier otro intento de acceso no autorizado. Las herramientas a continuación son para aprendizaje, para defender tu propia infraestructura y para su uso dentro de compromisos autorizados. Si no estás seguro de si tienes permiso para apuntar una herramienta a un host, la respuesta es no.

Nota: Este es un contenido de marcador de posición de ejemplo creado para demostrar el blog. Reemplázalo con tu propio escrito.

Hay un momento, al principio de cualquier carrera en seguridad, cuando te das cuenta de que la mayoría de las herramientas en las que confías — nmap, hashcat, dirb, sqlmap — son en sí mismas solo programas. Una vez que eso te queda claro, la siguiente pregunta obvia es: "¿Podría escribir yo mismo una versión pequeña de esto?". La respuesta es sí, y hacerlo es una de las formas más rápidas de realmente entender lo que hacen las herramientas reales. Esta publicación recorre dos herramientas pequeñas en Python: un escáner de puertos concurrente y un descifrador de hash por diccionario.

Herramienta 1 — Un Escáner de Puertos Concurrente

El objetivo es modesto: dado un host y un rango de puertos, informar qué puertos están aceptando conexiones TCP. La versión ingenua — abrir un socket, intentar conectar, repetir — funciona, pero es dolorosamente lenta porque hace todo un puerto a la vez. La solución es la concurrencia, y la API más moderna y limpia de Python para esto es concurrent.futures.ThreadPoolExecutor.

"""portscanner.py — un pequeño escáner de puertos TCP concurrente para uso autorizado."""

from __future__ import annotations

import argparse
import socket
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass


@dataclass
class PortResult:
    port: int
    open: bool


def probe(host: str, port: int, timeout: float = 1.0) -> PortResult:
    """Intenta una conexión TCP a (host, puerto). Devuelve un PortResult."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(timeout)
        # connect_ex devuelve 0 en éxito, un errno en otro caso.
        result = s.connect_ex((host, port))
        return PortResult(port=port, open=(result == 0))


def scan(host: str, ports: range, workers: int = 100) -> list[PortResult]:
    results: list[PortResult] = []
    with ThreadPoolExecutor(max_workers=workers) as pool:
        futures = {pool.submit(probe, host, p): p for p in ports}
        for future in as_completed(futures):
            results.append(future.result())
    results.sort(key=lambda r: r.port)
    return results


def main() -> None:
    parser = argparse.ArgumentParser(description="Pequeño escáner de puertos TCP.")
    parser.add_argument("host", help="host de destino (solo escanea lo que poseas)")
    parser.add_argument("--start", type=int, default=1)
    parser.add_argument("--end", type=int, default=1024)
    parser.add_argument("--workers", type=int, default=100)
    args = parser.parse_args()

    print(f"Escaneando {args.host} puertos {args.start}-{args.end} "
          f"con {args.workers} trabajadores...")
    for r in scan(args.host, range(args.start, args.end + 1), args.workers):
        if r.open:
            print(f"  {r.port:>5}/tcp  abierto")

if __name__ == "__main__":
    main()

Algunas notas sobre las decisiones tomadas:

  • Se usa connect_ex en lugar de connect para que una conexión rechazada devuelva un código de error en lugar de generar una excepción. Las excepciones en 1.000 puertos se acumulan.
  • El socket se abre con un bloque with para que los descriptores de archivo se liberen promptly. En Linux, el límite predeterminado de fd es 1024 por proceso; un escáner descuidado puede alcanzarlo rápidamente.
  • Los hilos están bien aquí porque el trabajo es limitado por E/S — estamos esperando a que se agote el tiempo de espera de conexión, no quemando CPU. Para trabajo limitado por CPU, ProcessPoolExecutor sería la herramienta adecuada.
  • 100 trabajadores es un valor predeterminado razonable. Mucho más alto y comienzas a activar reglas de IDS o a agotar los puertos efímeros en el lado del escáner.

Una regla general rápida para la elección entre hilos y procesos:

El trabajo es... Usar Por qué
I/O-bound ThreadPoolExecutor Los hilos liberan el GIL durante las llamadas al sistema de E/S
CPU-bound ProcessPoolExecutor Los procesos evitan el GIL a través de los núcleos
Mixed Hilos, con cuidado La mayoría de las herramientas de seguridad son pesadas en E/S
CPU + nativo multiprocessing o ext. C Cuando el rendimiento del hashing de Python es el cuello de botella

Ejecútalo en tu propia máquina:

# Escanea los puertos 1-1024 de localhost con 100 trabajadores.
python3 portscanner.py 127.0.0.1

# Escanea un rango más amplio con más trabajadores.
python3 portscanner.py 127.0.0.1 --start 1 --end 65535 --workers 500

Una versión más agradable de esta herramienta te permitiría cargar una lista de objetivos desde un archivo YAML:

# targets.yaml — lista de objetivos de escaneo autorizados.
targets:
  - host: 127.0.0.1
    ports: [22, 80, 443, 8080]
  - host: 192.168.1.10
    ports: [1, 1024]

Dejo el código de carga YAML como un ejercicio porque es el tipo de tarea pequeña y satisfactoria que hace que una herramienta se sienta como tuya.

Lo que esta Herramienta No Hace (Aún)

Un escáner real — nmap, masscan, zmap — hace mucho más. Algunas cosas a considerar si quieres extenderlo:

  • Huella de servicio (Service fingerprinting). Después de que un puerto está abierto, envía una específica del protocolo y analiza el banner. Así es como funciona nmap -sV.
  • Escaneo SYN. Este escáner hace una connect() completa, que es registrada por el destino. El escaneo SYN (nmap -sS) envía solo un SYN y lee el SYN+ACK, luego RST. Es más rápido y sigiloso pero requiere sockets raw y generalmente root.
  • Limitación de velocidad. Un escáner educado limita sus paquetes por segundo para que no accidentalmente DoSee el destino. Para una herramienta de aprendizaje esto es excesivo; para cualquier cosa apuntada a infraestructura real es obligatorio.

Herramienta 2 — Un Esbozo de Descifrador de Hash por Diccionario

La segunda herramienta es un ataque de diccionario contra un solo hash sin sal. La configuración: tienes un hash (digamos, de un desafío CTF o de una imagen forense de tu propio sistema) y una lista de palabras, y quieres encontrar el texto plano que produjo el hash.

"""hashcracker.py — ataque de diccionario contra un solo hash sin sal.

Solo para aprendizaje. Las herramientas reales de cracking de contraseñas (hashcat, john)
son órdenes de magnitud más rápidas porque se ejecutan en GPUs y usan SIMD.
"""

from __future__ import annotations

import argparse
import hashlib
import sys
from concurrent.futures import ProcessPoolExecutor, as_completed


def hash_word(word: str, algorithm: str) -> str:
    """Devuelve el resumen hexadecimal de `palabra` bajo el algoritmo dado."""
    h = hashlib.new(algorithm)
    h.update(word.encode("utf-8"))
    return h.hexdigest()


def crack(target: str, wordlist: str, algorithm: str = "sha256") -> str | None:
    """Devuelve la primera palabra cuyo hash coincida con `target`, o None."""
    with open(wordlist, "r", encoding="utf-8", errors="ignore") as f:
        words = [line.rstrip("\n") for line in f]

    with ProcessPoolExecutor() as pool:
        futures = {
            pool.submit(hash_word, w, algorithm): w for w in words
        }
        for future in as_completed(futures):
            word = futures[future]
            if future.result() == target.lower():
                # Cancela el resto lo mejor que puedas; ya están en ejecución.
                return word
    return None


def main() -> None:
    parser = argparse.ArgumentParser(description="Descifrador de hash por diccionario.")
    parser.add_argument("hash", help="hash de destino (hex)")
    parser.add_argument("wordlist", help="ruta a la lista de palabras")
    parser.add_argument("--algo", default="sha256", help="nombre del algoritmo hashlib")
    args = parser.parse_args()

    print(f"Descifrando hash {args.algo} con {args.wordlist}...")
    result = crack(args.hash, args.wordlist, args.algo)
    if result is None:
        print("No se encontró coincidencia.")
        sys.exit(1)
    print(f"Coincidencia: {result}")

if __name__ == "__main__":
    main()

Genera un hash de destino a partir de una palabra conocida y pruébalo:

# Genera un sha256 de "letmein" — la contraseña que estamos intentando recuperar.
echo -n "letmein" | sha256sum
# 9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee6845628c12b338f3b8c4b8c4b8c  (ilustrativo)

# Ejecuta el descifrador contra una lista de palabras.
python3 hashcracker.py \
    9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee6845628c12b338f3b8c4b8c4b8c \
    /usr/share/wordlists/rockyou.txt \
    --algo sha256

Algunas cosas a notar:

  • Esto usa ProcessPoolExecutor, no hilos, porque el hashing es limitado por CPU. Los hilos no ayudarían aquí debido al GIL.
  • La coincidencia es insensible a mayúsculas/minúsculas en el resumen hexadecimal porque algunas herramientas emiten mayúsculas y otras minúsculas.
  • No hay manejo de sal. Si el hash que apuntas fue producido con una sal, necesitarías saber la sal e incorporarla en hash_word. El salado derrota la precomputación (tablas arcoíris) pero no los ataques de diccionario en sí — simplemente hace que cada suposición sea más costosa.
  • Esto es dramáticamente más lento que hashcat. hashcat usa GPUs y puede hacer miles de millones de hashes SHA-256 por segundo en hardware de consumo. El punto de este script es la comprensión, no el rendimiento.

Por qué Escribir Estos Tú Mismo

Hay algunas buenas razones para escribir versiones pequeñas de herramientas conocidas, incluso cuando ya existen mejores versiones:

  1. Aprendes lo que la herramienta realmente está haciendo. Un escáner de puertos deja de ser mágico en el momento en que has escrito connect_ex contra un puerto y has visto el tiempo de espera.
  2. Aprendes a leer el código de otras personas. Una vez que has escrito un descifrador de hash, leer el código fuente de hashcat se vuelve manejable. El vocabulario es compartido.
  3. Construyes el hábito de herramientas pequeñas y afiladas. La mayor parte del trabajo real de seguridad es pegamento — un script que toma la salida de una herramienta, la transforma y la alimenta a otra. Escribir herramientas pequeñas desde cero es cómo te vuelves bueno en el pegamento.

Dónde Ir a Continuación

Si estos dos te resultaron interesantes, los siguientes proyectos naturales son:

  • Un fuerza bruta de directorios para HTTP, similar a gobuster. La estructura es la misma: un objetivo, una lista de palabras, un grupo de trabajadores y una función de sondeo que devuelve abierto/cerrado.
  • Un analizador de registros que lee un registro de acceso de Apache/Nginx y marca patrones sospechosos (agentes de usuario anómalos, picos de 4xx, escaneos de una sola IP).
  • Un IDS simple que observa un pcap y alerta sobre escaneos SYN — un excelente ejercicio para leer las banderas TCP.

El proyecto OWASP Python Security y las guías de concurrencia de Real Python son buenos lugares para seguir cavando. Y, como siempre, la documentación de los módulos hashlib y socket vale la pena leerla de principio a fin al menos una vez — no son largas y desmitifican muchas cosas.

En esta página