09 marzo 2011

Mi España. Un vistazo rápido a sus servidores web.

Una de las charlas que más ganas tenía de ver en la Rooted2011 era la de @NigtherMan, titulada "Global Warfare". En ella contaron como han desarrollado una herramienta similar a Shodan, llamada hookle, pero con algunas nuevas funcionalidades adicionales muy interesantes. 

En noviembre del año pasado decidí hacer algo similar, pero acotando el resultado únicamente a España. Tenía interés en ver que escondíamos, que se encontraba detrás de cientos de ADSLs e IPs oscuras y raritas, y como siempre, sacar algunos números estadísticos y ridículos para divertirme.

Después de obtener los primeros resultados presente la propuesta, junto al análisis de contraseñas, al Call For Papers de la Rooted, a ver cuál les gustaba más. Se decidieron por la segunda, a la que también le había dedicado tiempo, así que yo y mis análisis se pararon y me dedique a otra cosa.

En esta entrada voy a intentar contar como lo hice, que problemas me encontré y hasta donde llegué. Aunque ahora pienso retomar el tema just 4 fun!

1.- Identificación de Direcciones IP de España.

La primera fase y más sencilla era acotar el análisis a todas las direcciones IP españolas así que debía consultar en RIPE que rangos pertenecían a este país y descartar todo lo demás. Ojo, que no es lo mismo una dirección IP española, que un dominio español. Yo puedo tener registrado alexissexy.es en un hosting en Francia, por ejemplo. Aunque su contenido sea mío y el dominio sea español, quedará excluido.

Por otra parte, por ser un primer análisis decidí únicamente centrarme en IPv4, en otra ocasión ya hablaremos de IPv6.

Para identificar el direccionamiento IP del país de Cervantes, lo más rápido que se me ocurrió fue descargar la base de datos de RIPE y aplicarle un grep, con cuidado, que el espacio entre "country:" y "ES" pueden ser espacios, tabulaciones o lo que sea y hay veces que pone "ES", "es", "Es"...

[aramosf@dmz ~]$ wget ftp://ftp.ripe.net/ripe/dbase/ripe.db.gz
[aramosf@dmz ~]$ zegrep -B6 -i "^country:.*ES" ripe.db.gz|grep inetnum \
|awk -F: '{print $2}'|sed -e 's/\s//g' >p0.txt

Para que me fuera más sencillo trabajar con las listas de redes y la herramienta nmap pudiese leerlas, convertí los rangos en notación CIDR:

[aramosf@dmz ~]$ cat p0.txt |perl -ne 'use Net::CIDR::Lite; 
my $cidr = Net::CIDR::Lite->new; $cidr->add_range("$_"); 
@cidr_list = $cidr->list; foreach (@cidr_list) { print "$_\n" };'
>p1.txt

Una vez tenía los resultados en el archivo p1.txt, conté el número de direcciones IP totales:

[aramosf@dmz ~]$ nmap -n -iL p1.txt -sL -oG p2.txt
[aramosf@dmz ~]$ grep "Host:" p2.txt.gnmap |wc -l

Del que se obtenía el resultado de 45.256.747 direcciones IPs.
También pensé que lo mismo me serviría luego o podría observar algo si resolvía las 45 millones de direcciones IP, así que sin tener demasiado claro por qué, hice las consultas y las guardé en otro fichero:

[aramosf@dmz ~]$ nmap -T5 -iL p1.txt -sL -oG p2-dns

Esto tardó 4 días (354855.44 segundos), aunque se podría optimizar con otras opciones y modificando las DNS para que use algún servidor más rápido, como por ejemplo el de Google.

Luego empecé el análisis a los rangos del puerto 80, pero tuve que repetir varias veces por distintos problemas:
  • Guardando resultados, el nmap que utilicé no estaba compilado para soportar ficheros mayores a 2Gbs por lo que los XML dejaban de almacenar datos. De esto te das cuenta después de una larga ejecución :-(
  • Desde el servidor, con un enlace de 100Mbits, desde el que lanzaba las pruebas, no conseguía optimizar los resultados, empecé con el más agresivo (-T5), pero obtenía demasiados falsos negativos. Así que lo configuré de tal forma que estaría entre el -T4 y el -T5.
  • Traté de utilizar el --resume para continuar un escaneo abortado pero no continuaba como se esperaba. Así que decidí dividir el archivo con los rangos en 4 ficheros e ir ejecutando poco a poco.
  • Quite la opción de comprobar si el sistema estaba levantado, ya que eso supone lanzar hasta 8 paquetes y esperar como responden, mientras que mirar si está directamente un único puerto abierto, es lanzar un syn y esperar la respuesta.
Así que dividí el fichero en 8 trozos (y uno más con el resto de la visión) y comencé el escaneo de puertos:

[aramosf@dmz ~]$ wc -l p0.txt
32443 p0.txt
[aramosf@dmz ~]$ echo $(( 32443 / 8 ))
4055
[aramosf@dmz ~]$ split -l 4055 p0.txt

Para finalmente buscar el puerto 80 abierto:

for i in xa?; do  
nmap -n -Pn -g53 --min-hostgroup 1024 --max-rtt-timeout 700ms \ 
--initial-rtt-timeout 500ms --max-retries 6 --max-scan-delay 8 \
--host-timeout 1m -p80 -sS -iL $i.txt --stats-every 30s -oA $i-nmap
done

Después de un tiempo en ejecución, me di cuenta lo poco útil que es tener una lista con direcciones IP con el puerto 80 abierto, así que decidí obtener las cabeceras HTTP añadiendo la opción: --script http-headers.

Ya que tenía que abrir el puerto y hacer una petición, mejor hacer un GET y guardar la página, que tener solo la cabecera. ¿no?. Así que modifiqué uno de los scripts de Nmap para guardarme el / de la raíz del servidor.

description = [[
Shows the index of the default page of a web server.
]]

author = "A. Ramos"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"default", "discovery", "safe"}
local url    = require 'url'
local dns    = require 'dns'
local http   = require 'http'
local ipOps  = require 'ipOps'
local stdnse = require 'stdnse'
portrule = function(host, port)
        local svc = { std = { ["http"] = 1, ["http-alt"] = 1 },
                                ssl = { ["https"] = 1, ["https-alt"] = 1 } }
        if port.protocol ~= 'tcp'
        or not ( svc.std[port.service] or svc.ssl[port.service] ) then
                return false
        end
        -- Don't bother running on SSL ports if we don't have SSL.
        if (svc.ssl[port.service] or port.version.service_tunnel == 'ssl')
        and not nmap.have_ssl() then
                return false
        end
        return true
end
action = function(host, port)
  local data, result, output
  local MAX_SIZE = 5000
  local request_options = {}
  request_options.header = {}
  request_options.header["Range"] = "Range: bytes=0-5000"
  result = http.get( host, port, '/')
   output = result.rawheader
   table.insert(output, result.body)
   return stdnse.format_output(true, output)
end
Una de las cosas raras que me encontré fueron varios servidores de streaming, como por ejemplo radios online, en las que el GET no terminaba nunca, generando error en Nmap. Para no meterle mano a la librería http.lua, hice un apaño-chapuza añadiendo la cabecera "Range" en la petición y así limitando el tamaño de la respuesta :S

El resultado final quedó:

for i in xa?; do  
nmap -n -Pn -g53 --min-hostgroup 1024 --max-rtt-timeout 700ms \
--initial-rtt-timeout 500ms --max-retries 6 --max-scan-delay 8 \
--host-timeout 1m -p80 --script=html-index.nse -iL $i \
--stats-every 30s -oA $i-nmap
done

La ejecución termino en 11,9 días o para ser más exactos: 1.030.133 segundos.

¿Los resultados? Muchos y muy variados. Por ejemplo la entrada sobre el QoS de IPTV, aprovechando el Real Madrid .vs. Barcelona. Os puedo adelantar que el servidor que más se encontró fue "micro_httpd", seguido por "RomPager" y luego "IIS" y finalmente "Apache". Los dos primeros son de routers ADSL, y con los dos siguientes hay truco.... Pero eso lo dejo para la entrada siguiente.

18 comments :

Alfon dijo...

Ya tengo algo con que entretenerme hoy que hay poco curro. Gracias Alejandro, muy interesante.

SPECTRAdo dijo...

Con 10 millones de ADSLs en España, de las cuales la mayoría tienen IP dinámica, y teniendo en cuenta q tardaste 11 días en realizar el escaneo, estoy convencido q a alguno le escaneaste dos veces.

Saludos.

Alejandro Ramos dijo...

@Alfon: muchas gracias!

@SPECTRAdo: puede ser, pero ¿realmente importa? esa es justamente la información que luego descartaré...

silverhack dijo...

Impresionante entrada Alejandro!
También se podría hacer algo parecido, para cotejar cambios y demás, con la base de datos de Maxmind, la cual se actualiza con bastante rapidez

GEOIP_Country
Saludetes!

Oscar dijo...

Tio, un artículo de lo más interesante y ocioso. Son cosas que siempre se plantea uno pero sólo a ti se te ocurre llevarlas a cabo. Enhorabuena, se abre un hilo muy curioso, espero nuevos aportes.

Vicente Motos dijo...

Respecto a estos escaneos masivos ¿podría haber ahora alguna repercusión legal después de las últimas modificación en nuestro queridísimo código y p.. artículo 197?

Esther dijo...

Artículo muy interesante... no es que vaya a probar algo así (estoy con Oscar), pero estoy esperando leer los resultados...

Lo dicho, muy muy interesante!

Alejandro Ramos dijo...

@silverhack, @oscar, @esther: muchas gracias a vosotros también!

@vicente: no, esto no es ningún tipo de delito, solo he hecho un GET al recurso por defecto de una página. Exactamente como hace el crawling de Google. No veo que vulnerabilidad podría estar explotando eso...

Anónimo dijo...

Muy interesante Alejandro, como lo es el que te percataras de que la charla era de quien era y no de quién parecía ser.

knx dijo...

Charla de @Nighterman? Te olvidas de alguien :P Para más info sobre hookle:

http://www.slideshare.net/rootedcon/david-lpez-paz-global-warfare-rootedcon-2011

Nosotros hemos utilizado GeoLite City, que te da precisión a nivel de ciudad, y viene con todas las coordenadas por si quieres pintarlo en un mapita (como hacemos en hookle)

http://www.maxmind.com/app/geolitecity

Saludos!

SPECTRAdo dijo...

Solo te lo comentaba xa q lo tuvieses en cuenta al hacer las estadísticas de los servidores web.

JAJAJA ojo con los GET!!! Leéte el post "10 negritos" de El lado del mal. :)

En cualquier caso buen curro!

Alejandro Ramos dijo...

@SPECTRAdo: conozco la historia. Lanzó un Acunetix a una página. Una herramienta para buscar vulnerabilidades. No tiene absolutamente nada que ver, con todo esto que se cuenta aquí.

Un abrazo!

Pepelux dijo...

Gracias Alejandro, muy interesante!

Tarea que dejo pendiente para jugar un poco xDDD

Sursum Corda dijo...

Muy bueno el artículo... se aprende mucho aqui...

Anónimo dijo...

Que chulada, espero ansioso el resto del artículo :)

Un saludo.
Manolo.

des dijo...

Súper chulo, espero ver pronto la continuación :) .

Coridora Seca dijo...

Me ha gustado mucho, muy buena idea y, como ya han dicho antes, sólo a ti se te ocurre llevarla a cabo.

Genial, como siempre.

Sebastián Guerrero dijo...

Felicidades Alejandro, yo siempre estuve con la mosca detrás de la oreja por hacer algo así pero al final por flojera lo dejé de lado.

Es un gustazo ver como otras personas tienen preguntas similares y se adentran a investigar.

Un saludo.