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_exen lugar deconnectpara 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
withpara 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,
ProcessPoolExecutorserí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.hashcatusa 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:
- 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_excontra un puerto y has visto el tiempo de espera. - Aprendes a leer el código de otras personas. Una vez que has escrito un descifrador de hash, leer el código fuente de
hashcatse vuelve manejable. El vocabulario es compartido. - 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.