¡Esta es una revisión vieja del documento!
Tabla de Contenidos
Introducción a los sockets
Módulo perteneciente al curso Python avanzado para proyectos de seguridad
Los sockets son el componente principal que nos permite aprovechar las capacidades del sistema operativo para interactuar con la red. Puede pensar en los sockets como un canal de comunicación punto a punto entre un cliente y un servidor.
Los sockets de red son una manera fácil de establecer una comunicación entre procesos que están en la misma máquina o en máquinas diferentes. El concepto de un socket es muy similar al de los descriptores de archivos UNIX. Los comandos como read() y write() que nos permiten trabajar con el sistema de archivos, funcionan de manera similar a los sockets. Una dirección de socket de 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
La comunicación entre distintas entidades en una red se basa en el clásico concepto de sockets. Los sockets son un concepto abstracto con el que se designa al punto final de una conexión. Los programas utilizan sockets para comunicarse con otros programas, que pueden estar situados en computadoras distintas.
Un socket queda definido por la dirección IP de la máquina, el puerto en el que escucha y el protocolo que utiliza. Los tipos y funciones necesarios para trabajar con sockets se encuentran en Python en el módulo socket. Los sockets se clasifican en sockets de flujo TCP (socket.SOCK_STREAM) o sockets de datagramas UDP (socket.SOCK_DGRAM) dependiendo de si el servicio utiliza TCP, que es orientado a conexión y fiable, o UDP, respectivamente. En esta unidad didáctica solo cubriremos los sockets orientados a conexión TCP, que cubren un 90 % de las necesidades comunes.
Para crear un socket se utiliza el constructor socket.socket() que puede tomar como parámetros opcionales la familia, el tipo y el protocolo. Por defecto se utiliza la familia AF_INET y el tipo SOCK_STREAM. La sintaxis general del método de socket es la siguiente:
s = socket.socket (socket_family, socket_type, protocol = 0)
Estos argumentos representan las familias de direcciones y el protocolo de la capa de transporte. Dependiendo del tipo de socket, los sockets se clasifican en sockets de flujo (socket.SOCK_STREAM) o sockets de datagramas (socket.SOCK_DGRAM), en función de si el servicio utiliza TCP o UDP. socket.SOCK_DGRAM se usa para comunicaciones UDP, y socket.SOCK_STREAM para conexiones TCP.
Los sockets también se pueden clasificar según la familia. Tenemos sockets UNIX (socket.AF_UNIX) que se crearon antes de la concepción de las redes y se basan en ficheros, sockets socket.AF_INET que son los que nos interesan, sockets socket.AF_INET6 para IPv6, etc.
En la siguiente imagen vemos el constructor de la clase socket:
IMAAAAAAAAAAAAAAAAAAAAGEN
Módulo socket en Python
Los tipos y funciones necesarios para trabajar con sockets se pueden encontrar en Python en el módulo de sockets. El módulo socket expone todas las piezas necesarias para escribir rápidamente clientes y servidores TCP y UDP.
El módulo de socket tiene casi todo lo que necesita para construir un servidor o cliente de socket. En el caso de Python, el socket devuelve un objeto al que se pueden aplicar los métodos de socket. Este módulo viene instalado por defecto cuando instala la distribución Python. Para verificarlo, podemos hacerlo desde el intérprete de Python.
Este módulo viene instalado por defecto cuando instalas la distribución de Python. Para comprobarlo podemos hacerlo desde el intérprete de Python:
>>>import socket >>>dir(socket)
IMAAAAAAAAAAAAAAAAAAAAAAAAAGEN
En esta imagen vemos todas las constantes y métodos que tenemos disponibles en este módulo. Las constantes las vemos en primera instancia dentro de la estructura que ha devuelto el objeto. Entre las constantes más utilizadas podemos destacar:
socket.AF_INETsocket.SOCK_STREAM
Para abrir un socket en una determinada máquina utilizamos el constructor de la clase socket que acepta por parámetros la familia, el tipo de socket y el protocolo. Una llamada típica para construir un socket que funcione a nivel TCP es pasando como parámetros la familia y el tipo de socket:
IMAAAAAAAAAAAAAAAAAAAAAAAGEN
Socket.socket(socket.AF_INET,socket.SOCK_STREAM)
Recopilación de información con sockets
Los métodos útiles para recopilar más información son:
gethostbyaddr(dirección): nos permite obtener un nombre de dominio a partir de la dirección IP.gethostbyname(hostname): nos permite obtener una dirección IP a partir de un nombre de dominio.
Podemos obtener más información sobre estos métodos con el comando de help(socket):
IMAAAAAAAAAAAAAAAAAAAAAAAAGEN
Ahora vamos a detallar algunos métodos relacionados con el host, la dirección IP y la resolución del dominio. Para cada uno, mostraremos un ejemplo simple:
socket.gethostbyname(hostname)
Este método convierte un nombre de host al formato de dirección IPv4. La dirección IPv4 se devuelve en forma de cadena. Este método es equivalente al comando nslookup que podemos encontrar en muchos sistemas operativos:
>>> import socket >>> socket.gethostbyname('packtpub.com') '83.166.169.231' >>> socket.gethostbyname('google.com') '216.58.210.142'
socket.gethostbyname_ex(nombre)
Este método devuelve muchas direcciones IP para un solo nombre de dominio. Significa que un dominio se ejecuta en múltiples IP
>>> socket.gethostbyname_ex('packtpub.com') ('packtpub.com', [], ['83.166.169.231']) >>> socket.gethostbyname_ex('google.com') ('google.com', [], ['216.58.211.46'])</pre> <p>Otro de
Otr de los métodos que disponemos en la clase sockets es el que permite obtener el nombre cualificado de un domiinio:
>>> socket.getfqdn('google.com')
socket.gethostbyaddr(ip_address)
Este método devuelve una tupla (hostname, nombre, ip_address_list) donde hostname es el nombre de host que responde a la dirección IP dada, el nombre es una lista de nombres asociados con la misma dirección ip_address_list es una lista de direcciones IP para la misma interfaz de red en el mismo host.
>>> socket.gethostbyaddr('8.8.8.8') ('google-public-dns-a.google.com', [], ['8.8.8.8'])
socket.getservbyname(servicename [,nombre_protocolo])
Este método le permite obtener el número de puerto del nombre del puerto:
>>> import socket >>> socket.getservbyname('http') 80 >>> socket.getservbyname('smtp','tcp') 25
socket.getservbyport(puerto [, nombre_protocolo])
Este método realiza la operación inversa de la anterior, lo que le permite obtener el nombre del puerto a partir del número de puerto:
>>> socket.getservbyport(80) 'http' >>> socket.getservbyport(23) 'telnet'
Ejemplo para los métodos de socket
El siguiente script es un ejemplo de cómo podemos usar estos métodos para obtener información de los servidores de Google.
Puede encontrar el siguiente código en el archivo sockets_metodos.py:
# -*- encoding: utf-8 -*- import socket import sys try: print("gethostbyname") print(socket.gethostbyname_ex('www.google.es')) print("\ngethostbyaddr") print(socket.gethostbyaddr('216.58.211.228')) print("\ngetfqdn") print(socket.getfqdn('www.google.com')) except socket.error as error: print (str(error)) print ("Error de conexion") sys.exit()
socket.getfqdn obtiene el nombre de dominio que identifica unívocamente a un servidor en Internet.
Búsqueda inversa
Este comando obtiene el nombre de host de la dirección IP. Para esta tarea, podemos usar la función gethostbyaddr().
En este script, obtenemos el nombre de host de la dirección IP de 8.8.8.8.
Puede encontrar el siguiente código en el archivo socket_reverse_lookup.py
#!/usr/bin/env python # --*-- coding: UTF-8 --*-- import sys, socket try : result=socket.gethostbyaddr("8.8.8.8") print("The host name is:") print(" "+result[0]) print("\nAddress:") for item in result[2]: print(" "+item) except socket.herror as e: print("error for resolving ip address:",e)
Actividad práctica
Dado un nombre de dominio introducido por la entrada estándar por parte del usuario, obtener información relación con dicho dominio como dirección IP, host asociado y nombre cualificado del dominio.
import socket dominio = input() try: print("Obtener ip a partir del nombre dominio:") ip = socket.xxx(xxx) print(ip) print("\nObtener host a partir de la direccion ip") print(socket.xxx(str(ip))) print("\nObtener nombre cualificado de un dominio") print(socket.xxx(dominio)) except socket.error as error: print (str(error)) print ("Error de conexion")
Completar el script anterior sustituyendo las xxx por variables y métodos del módulo socket.
Solución:
import socket dominio = input() try: print("Obtener ip a partir del nombre dominio:") ip = socket.gethostbyname(dominio) print(ip) print("\nObtener host a partir de la direccion ip") print(socket.gethostbyaddr(str(ip))) print("\nObtener nombre cualificado de un dominio") print(socket.getfqdn(dominio)) except socket.error as error: print (str(error)) print ("Error de conexion")
Implementar en Python un escáner de puertos con sockets
En esta sección revisaremos cómo podemos implementar el escaneo de puertos con sockets y cómo administrar las excepciones cuando trabajamos con sockets.
Los sockets son el bloque de construcción fundamental para las comunicaciones de red y de manera fácil podemos verificar si un puerto específico está abierto, cerrado o filtrado al llamar al método connect_ex().
Método connect_ex()
El método socket.connect_ex(dirección,puerto) se usa para implementar el escaneo de puertos con sockets. Este script muestra que los puertos están abiertos en la máquina localhost con la interfaz de dirección IP loopback de 127.0.0.1.
Puede encontrar el siguiente código en el archivo socket_ports_open.py:
import socket ip ='127.0.0.1' portlist = [22,23,80,912,135,445,20] for port in portlist: sock= socket.socket(socket.AF_INET,socket.SOCK_STREAM) result = sock.connect_ex((ip,port)) print(port,":", result) sock.close()
result es un número. Posibles valores:
111: puerto cerrado (conexión rechazada)0: puerto abierto
Escáner de puertos con sockets
Por ejemplo, podríamos tener una función que acepte por parámetros una IP y una lista de puertos y devuelva para cada puerto si está abierto o cerrado.
En este caso necesitamos importar los módulos socket y sys. El módulo sys lo utilizamos para salir del programa con la instrucción sys.exit() y devolver el control al intérprete en caso de error de conexión. Si ejecutamos la función desde nuestro programa principal vemos como comprueba cada uno de los puertos y nos devuelve si está abierto o cerrado para una ip determinada. El primer parámetro puede ser tanto una ip como un nombre de dominio ya que el módulo es capaz de resolver un nombre a partir de una ip y viceversa.
Puede encontrar el siguiente código en el archivo comprobarListaPuertos.py:
import socket import sys def comprobarListaPuertos(ip,portlist): try: 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 ("Puerto {}: \t Abierto".format(port)) else: print ("Puerto {}: \t Cerrado".format(port)) sock.close() except socket.error as error: print (str(error)) print ("Error de conexion") sys.exit() comprobarListaPuertos('www.google.es',[80,8080,443,22])
Ejemplo de ejecución:
Puerto 80: Abierto Puerto 8080: Cerrado Puerto 443: Abierto Puerto 22: Cerrado
Explicación función escáner de puertos
La forma de ejecutar esta función es mediante la llamada:
comprobarListaPuertos(dominio|direccion_ip,[lista_puertos])
Si ejecutamos la función con una ip o nombre de dominio que no exista, nos devolverá un error de conexión junto con la excepción que ha devuelve el módulo socket al no poder resolver la dirección IP.
comprobarListaPuertos("local",[80,8080,443])
[Errno 11004] getaddrinfo failed.Error de conexion
La parte más importante de la función del ejemplo anterior la encontramos cuando comprueba si el puerto está abierto o cerrado:</p>
# ... sock= socket.socket(socket.AF_INET,socket.SOCK_STREAM) sock.settimeout(5) result = sock.connect_ex((ip,port)) if result == 0: print ("Puerto {}: \t Abierto".format(port)) else: print ("Puerto {}: \t Cerrado".format(port)) sock.close()
En el código anterior vemos cómo utilizamos el método settimeout() para indicarle un tiempo de intento de conexión en segundos.
IMAAAAAAAAAAAAAAAGEN
Escáner de puertos avanzado
El siguiente código de Python le permitirá escanear un host local o remoto en busca de puertos abiertos. El programa busca puertos seleccionados a partir de una determinada dirección IP introducida por el usuario y refleja los puertos abiertos de regreso al usuario. Si el puerto está cerrado, también muestra información sobre el motivo, por ejemplo, por timeout de la conexión.
Puede encontrar el siguiente código en el archivo socket_scanner_puertos.py. El script comienza con información relacionada con la dirección IP y los puertos introducidos por el usuario:
# Escaner de puertos con sockets # Importamos modulo socket from socket import * # Preguntamos por la IP ip = input("Introduce IP : ") # Preguntamos por el puertos puerto_inicio = input("Introduce puerto de inicio : ") puerto_fin = input("Introduce puerto de fin : ") print ("Escaneando IP {} : ".format(ip)) #recorrer cada uno de los puertos for port in range(int(puerto_inicio),int(puerto_fin)): print ("Probando puerto {} ...".format(port)) # Crea el objeto socket s = socket(AF_INET, SOCK_STREAM) s.settimeout(5) # Comprobar conexion e imprimimos si el puerto está abierto if(s.connect_ex((ip,port))==0): print("El puerto " , port, "está abierto") # Cierra el socket s.close() print("Escaneo finalizado!")
Ejemplo de ejecución:
Introduce IP : 192.168.0.1 Introduce puerto de inicio : 22 Introduce puerto de fin : 80 Escaneando IP 192.168.0.1 : Probando puerto 22 ... Probando puerto 23 ... El puerto 23 está abierto Probando puerto 24 ... Probando puerto 25 ... (...) El puerto 53 está abierto Probando puerto 54 ... Probando puerto 55 ... Probando puerto 56 ... (...) Probando puerto 71 ... Probando puerto 72 ... Probando puerto 73 ... Probando puerto 74 ... Probando puerto 75 ... Probando puerto 76 ... Probando puerto 77 ... Probando puerto 78 ... Probando puerto 79 ... Escaneo finalizado!
Escáner de puertos a partir de un dominio
El siguiente script de Python nos permitirá escanear una dirección IP con las funciones portScanning y socketScan. El programa busca puertos seleccionados en un dominio específico resuelto a partir de la dirección IP ingresada por el usuario por parámetro.</p>
En este script, el usuario debe ingresar como parámetros obligatorios el host y un puerto, separados por una coma:
IMAAAAAAAAAAAAAAAAAAAAGEN
Puede encontrar el siguiente código en el archivo socket_portScan.py:
#!/usr/bin/python # -*- coding: utf-8 -*- # Para jugar con los argumentos que introduce el usuario: import optparse from socket import * # Lanzar diferentes hilos de ejecución: from threading import * def socketScan(host, port): try: socket_connect = socket(AF_INET, SOCK_STREAM) socket_connect.settimeout(10) socket_connect.connect((host, port)) print('[+] %d/tcp open \n' % port) except Exception as error: print(error) print('[-] %d/tcp closed \n' % port) 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('\n[+] Scan Results for: ' + name[0]) except: print('\n[+] Scan Results for: ' + ip) for port in ports: # Usamos el módulo 'Thread' para poder crear un hilo de ejecución # distinto para cada puerto t = Thread(target=socketScan,args=(ip,int(port))) t.start() 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()
Función principal escáner de puertos a partir de un dominio
Este podría ser nuestro programa principal donde obtenemos los parámetros obligatorios de host y puertos para la ejecución del script. Una vez que hayamos obtenido estos parámetros, llamaremos a la función portScanning que resolverá la dirección IP y el nombre de host, y llamaremos a la función socketScan que usará el módulo de socket para determinar el estado del puerto:</p>
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()
Ejecución:
python socket_portScan.py -H www.google.es -P 80,21,22,23
En la ejecución del script anterior, podemos ver el estado de todos los puertos que están del dominio:
[+] Scan Results for: mad41s10-in-f3.1e100.net [+] 80/tcp open timed out [-] 21/tcp closed timed out [-] 23/tcp closed timed out [-] 22/tcp closed
Actividad práctica
import socket import sys # Preguntamos por la IP ip = input("Introduce IP:") #Preguntamos por los puertos de inicio y fin listaPuertos = list(int(num) for num in input("Introduce rango de puertos separados por comas:").strip().split(",")) print ("Escaneando IP {} en los puertos {} : ".format(xxx,xxx)) try: for port in xxx: s = socket.socket(socket.AF_INET,xxx) s.settimeout(5) if (s.connect_ex((xxx,xxx))==0): try: serv = socket.getservbyport(xxx) except socket.error: error_servidor="not-found" print(("El puerto %s:está abierto Servicio:%s "%(xxx,xxx))) print("Scanning Completed") except KeyboardInterrupt as exception: print(exception) sys.exit()
Completa el siguiente script para obtener un escáner de puertos introduciendo desde teclado la IP y los puertos de inicio y fin. Sustituir las xxx por variables definidas.
Solución:
import socket import sys # Preguntamos por la IP ip = input("Introduce IP:") #Preguntamos por los puertos de inicio y fin listaPuertos = list(int(num) for num in input("Introduce rango de puertos separados por comas:").strip().split(",")) puerto_inicio = listaPuertos[0] puerto_fin = listaPuertos[1] print ("Escaneando IP {} en los puertos {} : ".format(ip,listaPuertos)) try: for port in range(int(puerto_inicio),int(puerto_fin)+1): s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.settimeout(5) if (s.connect_ex((ip,port))==0): try: serv = socket.getservbyport(port) except socket.error: error_servidor="not-found" print(("El puerto %s:está abierto Servicio:%s "%(port,serv))) print("Scanning Completed") except KeyboardInterrupt as exception: print(exception) sys.exit()
Implementar en Python un servidor HTTP
Podríamos crear un socket del tipo TCP y vincularlo a un puerto. Podríamos utilizar “localhost”, para aceptar conexiones desde la misma máquina. El puerto podría ser 80, pero como necesita privilegios de root, usaremos uno mayor o igual que 8080.
Métodos de socket del servidor
Estos son los principales métodos que podemos usar desde el punto de vista del servidor:
socket.bind(dirección): 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(numero_conexiones): este método acepta como parámetro el número máximo de conexiones de los clientes e inicia la escucha TCP para las conexiones entrantes.????socket.accept(): este método nos permite aceptar conexiones del cliente. Este método devuelve dos valores:client_sockety la dirección del cliente.client_socketes un nuevo objeto de socket utilizado para enviar y recibir datos. Antes de usar este método, debe llamar a los métodossocket.bind(dirección)ysocket.listen(numero_conexiones).
Implementación del servidor
De los métodos comentados anteriormente podríamos utilizar el método bind() que acepta como parámetros la dirección ip y el puerto.
El módulo socket proporciona el método listen() que permite poner en cola hasta un máximo de n solicitudes. Por ejemplo podríamos establecer en 5 el número máximo de peticiones con la instrucción mysocket.listen(5).
Posteriormente, podríamos establecer la lógica de nuestro servidor cada vez que recibe la petición de un cliente. Utilizamos el método accept() para aceptar conexiones</strong>, leer datos entrantes con el método recv y respondemos una página HTML con el método send().
import socket mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) mySocket.bind(('localhost', 8080) mySocket.listen(5) while True: print('Esperando conexiones...') (recvSocket, address) = mySocket.accept() print('Petición HTTP recibida:') 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()
Probando el servidor creando un cliente HTTP
Si queremos probar el funcionamiento del servidor HTTP, podríamos crearnos otro script que nos permite obtener la respuesta envía el servidor que hemos creado.
#!/usr/bin/python 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())
Con el servidor HTTP funcionando, ejecutaríamos el cliente y veríamos una respuesta similar a la siguiente:
Contacting localhost on port 8080 ... Response from localhost: HTTP/1.1 200 OK</p>
Actividad práctica
Completa el siguiente script para implementar un servidor HTTP que esté escuchando en el puerto 8080 y acepte como máximo 5 conexiones de forma simultánea.
import socket # Crear un socket del tipo TCP y vincularlo a un puerto # Utilizamos 'localhost', por lo tanto, solo aceptamos conexiones desde la misma máquina # El puerto podría ser 80, pero como necesita privilegios de root, # usemos uno por mayor o igual que 8080 mySocket = socket.socket(xxx,xxx) mySocket.bind(('localhost', 8080)) # Poner en cola un máximo de 5 solicitudes de conexión TCP xxx.listen(5) # Aceptar conexiones, leer datos entrantes y responder una página HTML (en un bucle) while True: print('Waiting for connections') (recvSocket, address) = mySocket.xxx() print('HTTP request received:') print(recvSocket.xxx(1024)) recvSocket.xxx(bytes("HTTP/1.1 200 OK\r\n\r\n <html><body><h1>Hello World!</h1></body></html> \r\n",'utf-8')) xxx.close()
Solución:
import socket # Crear un socket del tipo TCP y vincularlo a un puerto # Utilizamos 'localhost', por lo tanto, solo aceptamos conexiones desde la misma máquina # El puerto podría ser 80, pero como necesita privilegios de root, # usemos uno por mayor o igual que 8080 mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) mySocket.bind(('localhost', 8080)) # Poner en cola un máximo de 5 solicitudes de conexión TCP mySocket.listen(5) # Aceptar conexiones, leer datos entrantes y responder una página HTML (en un bucle) 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()
