¡Esta es una revisión vieja del documento!
Tabla de Contenidos
Escaneo de puertos con Python-nmap
Módulo perteneciente al curso Python avanzado para proyectos de seguridad
En python podemos hacer uso de nmap a través de la librería python-nmap la cual nos permite manipular fácilmente los resultados de un escaneo, además, puede ser una herramienta perfecta para administradores de sistemas o consultores de seguridad informática a la hora de automatizar procesos de penetration testing.
Python-nmap es una herramienta que se utiliza dentro del ámbito de las auditorías de seguridad o pruebas de intrusión y su principal funcionalidad es descubrir qué puertos o servicios tiene en escucha un determinado host.
Para proceder con la instalación lo podemos hacer con alguno de los siguientes comandos:
sudo apt-get install python-pip nmap sudo pip install python-nmap
Además, para instalar el módulo es necesario ejecutar el comando con permisos de administrador/superusuario del sistema (sudo).
Una vez instalado podríamos invocar el módulo desde nuestros scripts o desde la terminal interactiva, por ejemplo:
>>> import nmap >>> nmap.__version__ '0.6.1' >>> dir(nmap) ['ET', 'PortScanner', 'PortScannerAsync', 'PortScannerError', 'PortScannerHostDict', '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 hayamos comprobado que la instalación ha sido correcta podemos empezar a realizar escaneos sobre un determinado host, para ello tenemos que instanciar un objeto de la clase PortScanner(), así podremos acceder al método más importante: scan. Una buena práctica para entender cómo trabaja una función, método u objeto es usar la función help() o dir() para saber las funciones/métodos disponibles en una clase u objeto.
>>> import nmap >>> portScanner = nmap.PortScanner() >>> dir(portScanner) ['_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']
Método de escaneo
Si ejecutamos el comando help(portScanner.scan) vemos que el método scan() de la clase PortScanner recibe tres argumentos, el host a analizar, los puertos y los argumentos, además al final añade como deben ser enviados los parámetros (todos deben ser en formato string):
>>> help(portScanner.scan)
IMAAAAAAAAAAAAAAAAAAAAAAAAGEN
Lo primero que tenemos que hacer es importar la librería de nmap y crearnos nuestro objeto para empezar a interactuar con PortScanner().
Lanzamos nuestro primer escaneo con el método scan("ip/rango","puertos",'argumentos'), donde solo el primer parámetro es obligatorio y el segundo y tercer parámetros son opcionales, de forma que si no lo definimos realiza un escaneo estándar de Nmap.
El siguiente código lo podemos encontrar en el fichero portScanner_inicial.py:
import nmap portScanner = nmap.PortScanner() resultados = portScanner.scan('scanme.nmap.org', '21,22,23,80','-sV') print(resultados) print(portScanner.command_line())
En el script anterior está realizando un escaneo sobre el dominio scanme.nmap.org en los puertos 21, 22, 23 y 80.
Lo primero que hacemos es instanciar un objeto de la clase PortScanner() y a través de los métodos scan() y command_line() para ver el comando que nmap está ejecutando por debajo.
Con el argumento -sV le estamos indicando que detecte las versiones cuando se realiza el escaneo. El resultado del escaneo es un diccionario que contiene la misma información que devolvería un escaneo hecho con Nmap directamente.
La ejecución del script anterior podría ser:
$ python3 portScanner_inicial.py
{'nmap': {'command_line': 'nmap -oX - -p 21,22,23,80 -sV scanme.nmap.org', 'scaninfo': {'tcp': {'method': 'connect', 'services': '21-23,80'}}, 'scanstats': {'timestr': 'Tue Jul 28 18:08:15 2020', 'elapsed': '8.32', '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': 'conn-refused'}, 'tcp': {21: {'state': 'closed', 'reason': 'conn-refused', 'name': 'ftp', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': ''}, 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'}, 23: {'state': 'closed', 'reason': 'conn-refused', 'name': 'telnet', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': ''}, 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'}}}}}
nmap -oX - -p 21,22,23,80 -sV scanme.nmap.org
En la salida del script anterior vemos como los puertos 22 y 80 son los que están abiertos, además muestra información sobre la versión del servicio que se está ejecutando en ese puerto.
El método all_hosts() nos devuelve información acerca de los hosts o direcciones ip que están activos.
>>>portScanner.all_hosts() ['45.33.32.156']
Con el método scaninfo() podemos ver los servicios que han dado algún tipo de respuesta en el proceso de escaneo, así como el método de escaneo.
>>>portScanner.scaninfo() {'tcp': {'method': 'connect', 'services': '21-23,80'}}
Obtener resultados del escaneo en formato CSV
Si queremos visualizar de una manera fácil el resultado del escaneo, disponemos de la función csv(), el cual nos devolverá la información en formato csv que lo separa por punto y coma.
>>> portScanner.csv()
Ejemplo de salida:
host;hostname;hostname_type;protocol;port;name;state;product;extrainfo;reason;version;conf;cpe 45.33.32.156;scanme.nmap.org;user;tcp;21;ftp;closed;;;conn-refused;;3; 45.33.32.156;scanme.nmap.org;PTR;tcp;21;ftp;closed;;;conn-refused;;3; 45.33.32.156;scanme.nmap.org;user;tcp;22;ssh;open;OpenSSH;"Ubuntu Linux; protocol 2.0";syn-ack;6.6.1p1 Ubuntu 2ubuntu2.13;10;cpe:/o:linux:linux_kernel 45.33.32.156;scanme.nmap.org;PTR;tcp;22;ssh;open;OpenSSH;"Ubuntu Linux; protocol 2.0";syn-ack;6.6.1p1 Ubuntu 2ubuntu2.13;10;cpe:/o:linux:linux_kernel 45.33.32.156;scanme.nmap.org;user;tcp;23;telnet;closed;;;conn-refused;;3; 45.33.32.156;scanme.nmap.org;PTR;tcp;23;telnet;closed;;;conn-refused;;3; 45.33.32.156;scanme.nmap.org;user;tcp;80;http;open;Apache httpd;(Ubuntu);syn-ack;2.4.7;10;cpe:/a:apache:http_server:2.4.7 45.33.32.156;scanme.nmap.org;PTR;tcp;80;http;open;Apache httpd;(Ubuntu);syn-ack;2.4.7;10;cpe:/a:apache:http_server:2.4.7</p>
Escaneo síncrono
En el siguiente ejemplo vamos a implementar una clase llamada NmapScanner que permite realizar un escaneo de una dirección ip y un puerto específico que se pasan por parámetro a la función nmapScan(ip,port) de la clase NmapScanner.
El siguiente código lo podemos encontrar en el fichero NmapScanner.py:
#!/usr/bin/env python3 import nmap class NmapScanner: def __init__(self): self.portScanner = nmap.PortScanner() def nmapScan(self, host, port): try: print("Comprobando puerto "+ port +" en la máquina "+host) self.portScanner.scan(host, port) # Command info print("[*] Ejecutando el comando: %s" % self.portScanner.command_line()) self.state = self.portScanner[host]['tcp'][int(port)]['state'] print(" [+] "+ host + " tcp/" + port + " " + self.state) print(self.portScanner[host].tcp(int(port))) self.server = self.portScanner[host].tcp(int(port))['product'] self.version = self.portScanner[host].tcp(int(port))['version'] print(" [+] "+ self.server + " " + self.version + " tcp/" + port) except Exception as exception: print("Error al conectar con " + host + " para escaner de puertos"+str(exception)) NmapScanner = NmapScanner() NmapScanner.nmapScan("45.33.32.156","80")
Ejecución escáner síncrono
En la ejecución del script anterior vemos como ha detectado que el puerto 80 está abierto en la dirección ip 45.33.32.156, mostrando información adicional sobre el comando ejecutado y la versión del sistema operativo.
La siguiente sería una ejecución del script anterior:
$ python3 NmapScanner.py
Comprobando el puerto 80 en la máquina 45.33.32.156
[*] Ejecutando el comando: nmap -oX - -p 80 -sV 45.33.32.156
[+] 45.33.32.156 tcp/80 open
{'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'}
[+] Apache httpd 2.4.7 tcp/80
Guardar resultado del escaneo en un fichero JSON
Además de realizar el escaneo de puertos y devolver el resultado por consola, podríamos generar un documento JSON donde almacenar el resultado con los puertos abiertos para un determinado host o dirección ip. En este caso utilizamos la función csv() que devuelve el resultado del escaneo en un formato fácil de tratar para recoger la información que necesitemos.
El siguiente código lo podemos encontrar en el fichero NmapScannerJSON.py:
#!/usr/bin/env python3 import nmap import json class NmapScannerJSON: def __init__(self): self.portScanner = nmap.PortScanner() def nmapScanJSON(self, host, ports): try: print("Comprobando puertos "+ str(ports) +" en la máquina "+host) self.portScanner.scan(host, ports) # Info del comando ejecutado print("[*] Ejecutando el comando: %s" % self.portScanner.command_line()) print(self.portScanner.csv()) results = {} for x in self.portScanner.csv().split("\n")[1:-1]: splited_line = x.split(";") host = splited_line[0] dns = splited_line[1] protocolo = splited_line[3] puerto = splited_line[4] estado = splited_line[6] results.update({'host': host,'dns': dns,'protocolo': protocolo,'puerto': puerto,'estado': estado}) # Almacenar info en json file_info = "scan_%s.json" % host with open(file_info, "w") as file_json: json.dump(results, file_json) print("[*] El fichero '%s' ha sido generado con los resultados del escaner" % file_info) except Exception as exception: print("Error al conectar con " + host + " para escaner de puertos"+str(exception)) NmapScannerJSON = NmapScannerJSON() NmapScannerJSON.nmapScanJSON("45.33.32.156","21,22,23,80")
Al final de script vemos cómo se instancia un objeto de la clase NmapScannerJSON y se realiza la llamada al método nmapScanJSON() pasando por parámetros la ip y la lista de puertos que queremos analizar.
Ejecución escáner formato json
En la ejecución del script anterior vemos como ha detectado como abiertos los puertos 22 y 80 en la dirección ip 45.33.32.156, mostrando información adicional sobre el comando ejecutado y la versión del sistema operativo.
$ python3 NmapSEcannerJSON.py Comprobando puertos 21,22,23,80 en la máquina 45.33.32.156 [*] Ejecutando el comando: nmap -oX - -p 21,22,23,80 -sV 45.33.32.156 host;hostname;hostname_type;protocol;port;name;state;product;extrainfo;reason;version;conf;cpe 45.33.32.156;scanme.nmap.org;PTR;tcp;21;ftp;closed;;;conn-refused;;3; 45.33.32.156;scanme.nmap.org;PTR;tcp;22;ssh;open;OpenSSH;"Ubuntu Linux; protocol 2.0";syn-ack;6.6.1p1 Ubuntu 2ubuntu2.13;10;cpe:/o:linux:linux_kernel 45.33.32.156;scanme.nmap.org;PTR;tcp;23;telnet;closed;;;conn-refused;;3; 45.33.32.156;scanme.nmap.org;PTR;tcp;80;http;open;Apache httpd;(Ubuntu);syn-ack;2.4.7;10;cpe:/a:apache:http_server:2.4.7 [*] El fichero 'scan_45.33.32.156.json' ha sido generado con los resultados del escáner
Como resultado del anterior script, además de mostrar la información por consola, lo que hace es generar un fichero json de nombre scan_45.33.32.156.json.
El contenido de este fichero es el siguiente:
{"host": "45.33.32.156", "dns": "scanme.nmap.org", "protocolo": "tcp", "puerto": "80", "estado": "open"}
Usando PortScannerYield
Si necesitamos mostrar los resultados conforme los vayamos obteniendo podemos utilizar su clase PortScannerYield, con el cual podemos ir obteniendo el progreso de nuestro escaneo. Esta clase útil cuando tenemos que escanear un segmento de red y necesitamos hacer operaciones con cada host que vayamos descubriendo.
El siguiente código lo podemos encontrar en el fichero PortScannerYield.py:
import nmap portScannerYield = nmap.PortScannerYield() for scan_yield in portScannerYield.scan('scanme.nmap.org', '21,22,23,25,80'): print(scan_yield[1]['nmap']['scanstats']['uphosts']) if scan_yield[1]['nmap']['scanstats']['uphosts'] == "1": print("Host ==> " + scan_yield[0]) print(scan_yield[1]['scan'])
La salida del script anterior sería la siguiente:
Host ==> 45.33.32.156
{'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': {21: {'state': 'closed', 'reason': 'conn-refused', 'name': 'ftp', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': ''}, 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'}, 23: {'state': 'closed', 'reason': 'conn-refused', 'name': 'telnet', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': ''}, 25: {'state': 'closed', 'reason': 'conn-refused', 'name': 'smtp', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': ''}, 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'}}}}
A continuación, vamos a realizar un escaneo utilizando otros módulos. Para realizar el escaneo con nmap desde python, también tendríamos la opción de realizarlo utilizando los módulos os y subprocess.
Usando nmap con el módulo os (operating system)
Para usar nmap con el módulo os (operating system) bastaría con lanzar el comando de la misma forma que lo lanzamos a través de la shell.
El siguiente código lo podemos encontrar en el fichero nmap_os.py:
import os nmap_command="nmap -sT 127.0.0.1" os.system(nmap_command)
La salida del script anterior podría ser la siguiente donde vemos como en nuestra máquina local tenemos abiertos los puertos 22 y 631:
Nmap scan report for localhost (127.0.0.1) Host is up (0.00014s latency). Not shown: 998 closed ports PORT STATE SERVICE 22/tcp open ssh 631/tcp open ipp
Usando nmap con el módulo subprocess
Con el módulo subproccess podemos proceder de la misma forma que hacíamos con el módulo operating system, pero en este caso podemos trabajar con las salidas STDOUT y STDERR de la consola, cosa que nos facilita el posterior tratamiento del resultado.
El siguiente código lo podemos encontrar en el fichero nmap_subprocess.py:
from subprocess import Popen, PIPE process = Popen(['nmap','-O','127.0.0.1'], stdout=PIPE, stderr=PIPE) stdout, stderr = process.communicate() print(stdout.decode())
Para ejecutar el script anterior necesitamos hacerlo con permisos de administrador (sudo en el caso de sistemas unix).
La salida del script anterior nos muestras además la versión del sistema operativo de nuestra máquina en local:
Nmap scan report for localhost (127.0.0.1) Host is up (0.000042s latency). Not shown: 998 closed ports PORT STATE SERVICE 22/tcp open ssh 631/tcp open ipp Device type: general purpose Running: Linux 2.6.X OS CPE: cpe:/o:linux:linux_kernel:2.6.32 OS details: Linux 2.6.32 Network Distance: 0 hops
Actividad práctica: Completa el siguiente script realiza un escaneo con python-nmap dada una lista de puertos y una dirección ip
Completa el siguiente script que realizar un escaneo con nmap con las siguientes condiciones en forma de argumentos. Sustituir las xxx por variables o clases definidas en el módulo python-nmap.
Condiciones del escaneo:
- Puertos a escanear: 21, 22, 23, 80, 8080
-npara no hacer resolución de DNS
Una vez obtenidos los datos del escaneo, guardarlos en un fichero scan.txt.
Por ejemplo si introducimos la ip 45.33.32.156 correspondiente al dominio scanme.nmap.org, el resultado sería:
Host scan: 45.33.32.156 nmap -oX - -n -p21,22,23,25,80,8080 45.33.32.156 45.33.32.156 up Port:21 State:closed Port:22 State:open Port:23 State:closed Port:25 State:closed Port:80 State:open Port:8080 State:closed
#!/usr/bin/python3 #importar nmap e inicializar portScanner import xxx nm = nmap.xxx() #pedimos al usuario el host que vamos a escanear host_scan = input('Host scan: ') #ejecutar nmap portlist="21,22,23,25,80,8080" nm.xxx(hosts=xxx, xxx='-n -p'+portlist) #mostrar comando nmap a ejecutar print(nm.xxx()) hosts_list = [(i, nm[i]['status']['state']) for i in nm.xxx()] #tratamiento fichero archivo = xxx('scan.txt', 'w') for host, status in xxx: print(xxx, xxx) xxx.write(host+'\n') #mostrar estado de cada puerto array_portlist=xxx.split(',') for port in xxx: state= xxx[host_scan]['tcp'][int(port)]['state'] print("Port:"+str(xxx)+" "+"State:"+xxx) xxx.write("Port:"+str(xxx)+" "+"State:"+xxx+'\n') #cierre fichero xxx.close()
Solución
#!/usr/bin/python3 #importar nmap e inicializar portScanner import nmap nm = nmap.PortScanner() #pedimos al usuario el host que vamos a escanear host_scan = input('Host scan: ') #ejecutar nmap portlist="21,22,23,25,80,8080" nm.scan(hosts=host_scan, arguments='-n -p'+portlist) #mostrar comando nmap a ejecutar print(nm.command_line()) hosts_list = [(x, nm[x]['status']['state']) for x in nm.all_hosts()] #tratamiento fichero archivo = open('scan.txt', 'w') for host, status in hosts_list: print(host, status) archivo.write(host+'\n') #mostrar estado de cada puerto array_portlist=portlist.split(',') for port in array_portlist: state= nm[host_scan]['tcp'][int(port)]['state'] print("Port:"+str(port)+" "+"State:"+state) archivo.write("Port:"+str(port)+" "+"State:"+state+'\n') #cierre fichero archivo.close()
Escaneo asíncrono
Podemos realizar escaneos asíncronos podemos hacer uso de la clase PortScannerAsync(). En este caso, al realizar el escaneo le podemos indicar un parámetro adicional de función callback donde definimos la función de retorno, que se ejecutaría al finalizar el escaneo.
El siguiente código lo podemos encontrar en el fichero PortScannerAsync.py:
import nmap portScannerAsync = nmap.PortScannerAsync() def callback_result(host, scan_result): print(host, scan_result) portScannerAsync.scan(hosts='scanme.nmap.org', arguments='-sP', callback=callback_result) while portScannerAsync.still_scanning(): print("Scanning >>>") portScannerAsync.wait(5)
Ejemplo de ejecución:
$ python3 PortScannerAsync.py
Scanning >>>
45.33.32.156 {'nmap': {'command_line': 'nmap -oX - -sP 45.33.32.156', 'scaninfo': {}, 'scanstats': {'timestr': 'Tue Jul 28 21:09:25 2020', 'elapsed': '0.19', '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'}}}}
De esta forma, podemos definir una función de callback que se ejecute cada vez que nmap disponga de un resultado para la máquina que estemos analizando.
Ejemplo de escáner asíncrono con python-nmap
<p>En el siguiente ejemplo lo que hacemos es implementar una clase que realiza el escaneo asíncrono a partir de la información introducida por el usuario en forma de argumentos al script.</p>
<p>El siguiente código lo podemos encontrar en el fichero <strong>NmapScannerAsync.py</strong>:</p>
#!/usr/bin/env python3 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)) 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)) 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)
