Python para el Pentest. OPCat: Vigilando el puerto 80

Siguiendo con la serie Python para el Pentest vamos hoy a hablar de una herramienta desarrollada por los integrantes del grupo ENIGMA [1]. Continuando con la línea argumental, se pone de manifiesto la potencia que tiene este lenguaje de programación en tareas de pentest.

La herramienta en cuestión se ha denominado OPCat (Open Port Catcher) y su funcionalidad principal es hallar servicios web abiertos corriendo bajo el puerto 80 en direcciones IP generadas de manera aleatoria (o en una IP especifica) y tomar una captura de pantalla de la página encontrada, además de obtener información sobre la misma.

La herramienta puede resultar bastante interesante para la tarea de reconocimiento durante un pentest: búsqueda de servicios web expuestos a un ataque de diccionario o de fuerza bruta, dentro del rango de IPs que se esté auditando, descubrimiento de interfaces de administración de dispositivos de red expuestas al exterior, etc. Además, puede servirnos como nuestro pequeño “Shodan on steroids” con un mínimo de cambios.

En primer lugar, antes de entrar en detalles de implementación y funcionalidad, vamos a mostrar los paquetes utilizados en el desarrollo (en cursiva los que vienen por defecto y que no habrá que instalar):

random
ipaddress
socket
optparse
sqlite3
os
sys
ipwhois
selenium
dns

Para evitar importar todos los paquetes completos, agregaremos solo aquellos módulos que nos sean necesarios de cada uno de ellos:

from random import randint
from ipaddress import IPv4Address
from socket import socket, AF_INET, SOCK_STREAM, setdefaulttimeout, getfqdn
from os import path
from optparse import OptionParser
from selenium import webdriver
from ipwhois import IPWhois
from dns import resolver, reversename
from sqlite3 import connect
from sys import exit

Una vez que hemos instalado todos los paquetes necesarios e importado aquello que nos hace falta, vamos a ver las partes de las que está compuesta la herramienta. Lo primero que hemos implementado es una función que genera direcciones IP aleatorias válidas. Esto es muy sencillo ya que solo tenemos que generar cuatro números aleatorios entre 0 y 255, darle formato de dirección IP y comprobar que es válida, es decir, comprobar que no es una IP de multicast, privada, de loopback, etc., tal y como se muestra a continuación:

def ipFactory():

    ip = IPv4Address('{0}.{1}.{2}.{3}'.format(randint(0,255),randint(0,255),randint(0,255),randint(0,255)))

    if(ip.is_multicast or ip.is_private or ip.is_unspecified or  ip.is_reserved or ip.is_loopback or ip.is_link_local):
        return ipFactory()

    return ip

En esta función hemos introducido un concepto realmente útil, “recursión”. Como podrá observar el lector, en el bloque encargado de comprobar si la dirección IP no se adapta a nuestras necesidades, se devolverá la misma función, invocándose nuevamente hasta que la condición se cumpla y nos devuelva nuestra IP.

Después, hemos definido una función que comprueba, dada una IP, si el puerto está abierto. A ésta le pasamos tres parámetros: la dirección IP que hemos generado anteriormente (o bien la dirección IP especificada de manera estática al inicio), un puerto (que en este caso será el 80) y un timeout (que será el tiempo máximo de espera para intentar establecer la conexión). Esta función, básicamente, abre un socket e intenta conectar con el puerto 80 de dicha IP. Si tiene éxito, adquiere el banner, llama a la función que realiza el screenshot e invoca a otra función que obtiene más información sobre la IP para posteriormente tratarla (de estas funciones hablaremos más adelante). Si no tiene éxito, significa que esa IP no tiene el puerto 80 abierto, por tanto, cierra el socket y devuelve falso. El código de la función es el siguiente:

def isOpenPort(host, port, timeout):
    
    setdefaulttimeout(timeout)
    template = "{0:16}{1:3}{2:40}"
	
    #Intentamos establecer una conexión a un puerto, en caso positivo realizamos las funciones pertinentes
    try:
        # Definimos conexión
        #   AF_INET: Representa conjunto (host, port)
        #   SOCK_STREAM: Establece protocolo de la conexión TCP
        connection = socket(AF_INET, SOCK_STREAM)

        # Establecemos par de dirección IP y puerto
        connection.connect((host, port))
		
        # Obtenemos el banner y lo parseamos
        connection.send(b'HEAD / HTTP/1.0\r\n\r\n')
        banner = connection.recv(1024)
		
        # Imprimimos mensaje Puerto abierto junto a la dirección IPv4
        print(template.format(host, '->', 'Open Port'))
		
        # Adaptamos salto de línea a HTML
        aux = str(banner).replace('\\r\\n','<br/>')
		
        # Obtenemos banner eliminando carácteres especiales del principio y del final
        banner = aux[2:len(aux)-3]
		
        # Cerramos la conexión
        connection.close()
        
        #Intenamos realizar la captura de la página web, si se realiza con éxito generamos el fichero de información.
        screenshot = takeScreenshot(host, str(port))

        if generateInformationDB( host, port, banner, screenshot ) :
            return True

    except Exception as e:
        print(template.format(host, '->', 'Closed Port: ('+str(e)+')'))
        connection.close()
        return False

Vamos ahora a hablar de la función que realiza los screenshots. Esta es de las más interesantes por el hecho de que para su implementación se ha hecho uso de Selenium. Más concretamente, se ha utilizado Selenium Webdriver, que nos permite enviar comandos al navegador y automatizar su comportamiento, lo cual puede llegar a ser muy útil y práctico. Esta librería es muy potente y puede servir para automatizar tareas mucho más complejas que las que se muestran aquí. El código de la función es el siguiente:

def takeScreenshot(host, port):

    # Realizamos una captura del servicio web
    setdefaulttimeout(200)        

    try:
        browser = webdriver.Firefox(timeout=200)
        browser.implicitly_wait(200) # Segundos
        browser.set_page_load_timeout(200)
        browser.get('http://{0}'.format(host))
        screenshot = browser.get_screenshot_as_png()
        state = True
        browser.quit()
    
    except selenium.common.exception.WebDriverException as e:
        print("ERROR: Do you have Firefox installed?")
        exit(1)

    except Exception as e:
        state = False
        print("[Error] takeScreenShot: {0}".format(e))
        browser.quit()
  
    if state:
        return screenshot
    else:
        return None

Esta función lo primero que hace es abrir una instancia del navegador (en nuestro caso, se ha utilizado Firefox, pero se puede utilizar cualquier otro), con la que iremos trabajando posteriormente. Además, se definen unos tiempos de espera para el socket, para el navegador y para la carga de la página web. Este tiempo de espera total nunca superará los 200 segundos. A continuación, la instancia del navegador abre la web, y guarda el screenshot en binario dentro de una variable. Si todo ha ido bien, la función devolverá el contenido de esta variable, en caso contrario no devolverá nada.

Como hemos comentado anteriormente, además de tomar el screenshot de la página web, obtenemos más información sobre la dirección IP en cuestión. De ello se encarga la siguiente función:

def generateInformationDB(host, port, banner, screenshot):
    
    #Obtenemos el Nombre de Dominio y realizamos un Whois para obtener más información
    domainName = getfqdn(host) 
    whois      = whoisFormatter( IPWhois(host).lookup() )
    aux        = domainName.split('.')
    dnsQuery   = '{0}.{1}'.format(aux[-2],aux[-1])

    addr = reversename.from_address(host)
    dns = "<h5>Nombre reverso</h5>"
    dns += str(addr) + "<br/>"  

    try:

        ptrText = "<br/><h5>PTR</h5>"
        for ptr in resolver.query( addr, "PTR" ):
            ptrText += str(ptr)+"</br/>"

        dns += ptrText

    except Exception as e:
        pass

    try:

        nsText = "<br/><h5>Servidores de nombre</h5>"
        for server in resolver.query(dnsQuery, 'NS'):
            nsText += str(server).rstrip('.')+'<br/>'

        dns += nsText

    except Exception as e:
        pass
    
    sqlite_insert_data(host, port, banner, dns, whois, domainName, screenshot)

    return True

Esta función obtiene información sobre el DNS, Whois y nombre de dominio, la parsea y después la envía a una base de datos SQLite, para que quede allí almacenada. Esto último es opcional, ya que perfectamente se podría almacenar esta información en un simple fichero de texto. La función sqlite_insert_data simplemente recoge como parámetros el host, puerto, banner, DNS, Whois, nombre de dominio y screenshot de la web en binario para almacenarlos en la base de datos que se ha creado. Por tanto, esta función solo contiene consultas de inserción a las tablas de dicha base de datos.

Como decimos, en este caso se ha optado por almacenar toda la información obtenida de la IP en una base de datos SQLite, tecnología que permite hacer esto de manera muy simple, además de dotar de portabilidad a la aplicación. Esto nos ha permitido mostrar toda la información recopilada de distintas direcciones IP escaneadas de manera muy visual a través de una interfaz web. Además, se ha hecho uso de Tornado Web Server para el aplicativo web, aunque reiteramos que ésto es opcional y solo se ha utilizado para mejorar la presentación de los resultados obtenidos (perfectamente podemos almacenar la información recopilada en un fichero y visualizarlo después). En las siguientes ilustraciones se muestran, a través de capturas, los resultados de escanear la dirección IP correspondiente al dominio securityartwork.es:

opc

Hemos visto como con unas pocas líneas de código y nociones básicas de Python, se puede llegar a desarrollar una herramienta útil de una forma relativamente sencilla. La herramienta se encuentra en una fase beta de desarrollo y estamos trabajando en implementar nuevas funcionalidades, ya que queremos aprovechar el potencial que tiene para su aplicación en la fase de Information Gathering de un pentest, ¡se admiten sugerencias!

El código completo de la herramienta se puede clonar del siguiente repositorio de GitHub.

[1] ENIGMA es el proyecto formativo de S2 Grupo en materia de ciberseguridad. Está integrado por alumnos de último año de carrera o recién licenciados a los cuales se les facilita una beca remunerada que consiste en asistir a un curso de formación presencial impartido en S2 Grupo. Gracias Joan, Jesús, Alberto, Pablo, Vizu, Pedro, Enrique, Asier y Jose G.

 

Comments

  1. ¡Felicidades por el trabajo!

    Muy chula la herramienta y con muchas posibilidades de mejora y expansión :)

  2. Buen trabajo chicos.