Inyección SQL a ciegas

octubre 11, 2024

La inyección SQL a ciegas es un tipo de inyección diferente a la explotada en el post Entendiendo la inyección SQL, en la cual la aplicación web mostraba el error retornado por el sistema gestor base de datos. En este tipo de vulnerabilidad a ciegas no tendremos la información adicional suministrada por los mensajes de error, sino que la aplicación web cambiará su comportamiento en base a la respuesta obtenida del sistema gestor base de datos, por lo tanto se puede entender la respuesta obtenida como una condiciona booleana, verdadero o falso, true o false, 1 o 0, en base al comportamiento de la aplicación.

Esta vulnerabilidad esta contemplada en el OWASP Top 10 dentro de la categoría A03:2021 – Injection y esta categorizada como una de las mas criticas que se puede encontrar en una prueba de penetración.

Para comprender esta vulnerabilidad vamos a trabajar con un laboratorio que esta integrado por una aplicación web vulnerable llamada DVWA, creada en PHP con vulnerabilidades de forma intencional para permitir espacios de practica sin afectar aplicaciones en ambiente productivo y que podrían acarrear repercusiones legales si se realizan sin la respectiva autorización de efectuar pruebas de seguridad. Para el laboratorio se trabajó con una maquina virtual que tiene desplegada la aplicación DVWA, llamada Cap’n Crunch Hacking Web.

Identificando la vulnerabilidad

Desde un navegador se accede a la aplicación DVWA y se inicia sesión en la misma mediante las credenciales admin / password y desde el menú lateral en la opción DVWA Security se pasa al nivel de seguridad bajo.

Posteriormente en la opción SQL Injection (Blind) del menú lateral verificamos la funcionalidad presentada por el formulario web que pide un identificador de usuario, al probar con números se evidencia que retorna un mensaje indicando si existe o no un usuario con el identificador enviado, además, se determina que la petición realizada por el formulario es tipo GET al enviar el parámetro id en la URL.

Si ingresamos un carácter especial obtenemos la misma respuesta que si con ese identificador el usuario no existiera, User ID is MISSING from the database. Esto nos indica que a nivel de backend la aplicación esta haciendo una consulta a la base de datos por el parámetro id ingresado por el usuario para averiguar si existe o no un registro con el valor indicado en la base de datos, siendo posiblemente una consulta SQL similar a:

SELECT * FROM users WHERE id='1';

Entendiendo que si la anterior consulta retorna un registro la aplicación muestra el mensaje User ID exists in the database. se puede alterar la consulta SQL jugando con la lógica booleana y los operadores lógicos AND y OR, mediante el ingreso del parámetro 1′ AND ‘1’=’1′ y 1′ AND ‘1’=’2′, obtendríamos unas consultas SQL como las siguientes:

SELECT * FROM users WHERE id='1' AND '1'='1'#';
SELECT * FROM users WHERE id='1' AND '1'='2'#';

Debido a que las anteriores consultas tienen dos condiciones y un operador lógico AND, por lógica booleana ambas condiciones tienen que cumplirse para retornar algún registro, en el primer caso las dos condiciones se cumplen, debido a que el usuario con identificador 1 ya sabemos que existe y el carácter ‘1’ siempre va a ser igual al mismo carácter. Caso contrario en la segunda consulta donde la segunda condición no se cumple, el carácter ‘1’ es diferente del carácter ‘2’. Por lo tanto la primera consulta nos retornará el mensaje User ID exists in the database. y la segunda el mensaje User ID is MISSING from the database.

De esta forma podemos ir jugando con la segunda condición mediante el uso de subconsultas con el fin de deducir información almacenada en la base de datos.

Identificando el sistema gestor base de datos implementado

Entendiendo el funcionamiento de la vulnerabilidad podemos tratar de identificar que sistema gestor base de datos tiene implementado la aplicación web, para ello es necesario trabajar con substrings y cambiando la segunda condición para comparar con los caracteres del abecedario. El parámetro malicioso a inyectar para el primer carácter seria 1′ AND substring((SELECT version()),1,1)=’1’#, esta inyección trae la versión de la base de datos por medio de la función version(), con la función substring() se corta el texto retornado por caracteres y se compara con el carácter ‘1’, cuando esta condicional retorne el texto User ID exists in the database. podremos inferir que el carácter de la comparación corresponde al carácter de la cadena de texto en la posición indicada.

Para continuar con el segundo carácter se inyecta el parámetro 1′ AND substring((SELECT version()),2,1)=’1’# y se comienza a cambiar el carácter de la comparación hasta obtener el texto User ID exists in the database. y continuar con los siguientes caracteres. Este proceso lo podemos automatizar un poco mediante el uso de la herramienta Burp Suite, para ello debemos configurar el navegador para hacer uso del proxy de Burp Suite, y realizar una consulta de usuario para interceptar esta petición y enviarla a la funcionalidad de intruder, una vez ahí indicamos la posición a ir alternando, siendo esta la del carácter de la comparación, cabe mencionar que en la petición es necesario cambiar los espacios en blanco por el carácter ‘+’ y el ‘#’ en codificación URL que seria %23

En la pestaña de Payloads es necesario especificar todos los caracteres a probar, se puede incluir minúsculas, mayúsculas, dígitos y caracteres especiales.

Para facilitar la identificación del carácter que cumpla la condición agregaremos una búsqueda del texto User ID exists in the database. en la respuesta de cada petición, para ello en la pestaña de settings, en la sección de Grep-Match limpiamos la lista, agregamos el texto y habilitamos la funcionalidad.

Lanzamos el ataque y en la tabla de las peticiones enviadas visualizamos que en la segunda posición se tiene el carácter ‘0’, debido a que la aplicación retorno un código de respuesta 200 al probar con este carácter y además la búsqueda del texto especificado nos indica que se encontró una vez dentro de la respuesta obtenida.

Para encontrar la cadena completa podemos ir cambiando la posición del substring() y lanzando ataques por medio de la funcionalidad de intruder de Burp Suite y encontraremos que el texto retornado corresponde a 10.1.37-MariaDB-0+deb9u1, indicando que se tiene implementado un sistema gestor base de datos MariaDB.

Obteniendo la cantidad de bases de datos

Ya que hemos identificado el sistema gestor base de datos implementado en la aplicación web, podemos consultar cuantas bases de datos tiene configuradas mediante el conteo de los registros de la tabla schemata de la base de datos information_schema. Esto debido a que MariaDB al igual que MySQL tienen definido para su funcionamiento la base de datos information_schema. El parámetro a inyectar en este caso seria 1′ AND (SELECT count(*) FROM information_schema.schemata)=1# donde iremos cambiando los numero hasta encontrar la condición que se cumpla y retorne el texto User ID exists in the database. esto lo podemos seguir realizando con la herramienta Burp Suite, en este caso únicamente definiendo en la pestaña de Payloads dígitos como valores a remplazar en cada petición.

Encontrando de esta forma que el sistema gestor base de datos tiene configuradas dos bases de datos a las cuales tiene acceso el usuario con el que la aplicación web se conecta.

Obteniendo el nombre de las bases de datos

Teniendo en cuenta que hay dos bases de datos a las cuales tiene acceso el usuario y que en la condicional debemos trabajar con cadenas de texto, no podemos realizar el ataque con una consulta que retorna múltiples registros, así que debemos tomar registro por registro e ir trabajando con la función substring() como lo hicimos anteriormente, para ello utilizaremos las propiedades LIMIT y OFFSET de las consultas SQL.

Lo primero que buscaremos es el tamaño del nombre de la primera base de datos, utilizando la función length() de MariaDB, el parámetro a inyectar quedaría 1′ AND (SELECT length(schema_name) FROM information_schema.schemata LIMIT 1 OFFSET 0)=1# en el cual iremos cambiando el numero de la condición hasta obtener el texto User ID exists in the database. en la respuesta.

Encontrando así que el tamaño del nombre de la primera base de datos es 4, para consultar sobre la segunda base de datos solo hay que cambiar el parámetro OFFSET por la siguiente posición y realizar de nuevo el ataque.

Para obtener el nombre de la base de datos debemos realizar el mismo proceso que se efectuó para identificar la versión del sistema gestor base de datos, el parámetro a inyectar seria 1′ AND (SELECT substring(schema_name,1,1) FROM information_schema.schemata LIMIT 1 OFFSET 0)=’a’# cambiando el carácter de la condición hasta obtener el código de respuesta 200 y el texto User ID exists in the database. encontrando que el primer carácter corresponde a la letra ‘d’.

Para cambiar al siguiente carácter la función substring quedaría substring(schema_name,2,1) y así sucesivamente hasta los 4 caracteres que tiene el nombre de la base de datos, para encontrar que la base de datos se llama dvwa, la otra base de datos es information_schema, utilizada por MariaDB y MySQL para su funcionamiento.

Obteniendo las tablas de una base de datos

Ya que sabemos el nombre de las bases de datos, podemos centrarnos en la base de datos dvwa y consultar cuantas tablas tiene creadas, para ello el parámetro a inyectar seria 1′ AND (SELECT count(*) FROM information_schema.tables WHERE table_schema=’dvwa’)=1# nuevamente variando el numero hasta obtener un código de respuesta 200 y el texto User ID exists in the database.

Encontrando que la base de datos dvwa tiene dos tablas creadas, para encontrar el nombre de las mismas se sigue el mismo proceso del nombre de las bases de datos, consultando primero el tamaño del nombre de la tabla con el siguiente parámetro a inyectar 1′ AND (SELECT length(table_name) FROM information_schema.tables WHERE table_schema=’dvwa’ LIMIT 1 OFFSET 0)=1#

Encontrando que el nombre de la primera tabla de la base de datos dvwa tiene 9 caracteres. Para encontrar el nombre se realiza el mismo proceso que con el nombre de las bases de datos, quedando el parámetro a inyectar 1′ AND (SELECT substring(table_name,1,1) FROM information_schema.tables WHERE table_schema=’dvwa’ LIMIT 1 OFFSET 0)=’a’#

Encontrando que la primera letra, del nombre de la primera tabla de la base de datos dvwa corresponde a la letra ‘g’, continuando con el ataque hasta completar la longitud del nombre de la tabla se obtiene que el nombre completo es guestbook. Realizando el mismo proceso para la segunda tabla de la base de datos dvwa, se obtiene que el nombre de la otra tabla es users.

Obteniendo las columnas de una tabla

La tabla users despierta interés por su nombre, por esta razón se procede a averiguar cuantas columnas integran la misma, para lo cual se efectúa el mismo procedimiento visto para las tablas, quedando el parámetro a inyectar 1′ AND (SELECT count(*) FROM information_schema.columns WHERE table_name=’users’)=1#

Encontrando que tiene 8 columnas, para ello se vuelve a identificar la longitud de los nombres de las columnas, para el nombre de la primera columna se usa el parámetro a inyectar 1′ AND (SELECT length(column_name) FROM information_schema.columns WHERE table_name=’users’ LIMIT 1 OFFSET 0)=1#

Encontrando que la longitud del nombre de la primera columna de la tabla users es de 7 caracteres, para lograr identificar el nombre de las columnas nuevamente se trabaja con la función substring() y los atributos LIMIT y OFFSET, el parámetro a inyectar en esta ocasión quedaría 1′ AND (SELECT substring(column_name,1,1) FROM information_schema.columns WHERE table_name=’users’ LIMIT 1 OFFSET 0)=’a’#

Encontrando que el primer carácter del nombre de la primera columna de la tabla users es ‘u’, continuando con el resto de los caracteres se obtiene que el nombre de la columna es user_id. Realizando el proceso para las otras columnas se obtiene el nombre del resto de columnas first_name, last_name, user, password, avatar, last_login, failed_login.

Obteniendo registros de una tabla

Prestando total interés en las columnas de user y password de la tabla users, inicialmente consultamos cuantos registros tiene esta tabla con el parámetro a inyectar 1′ AND (SELECT count(*) FROM users)=1#

Encontrando que la tabla users tiene 5 registros, de igual manera como hemos hecho anteriormente primero averiguamos la longitud del campo de la columna user del primer registro, para ello usamos el siguiente parámetro a inyectar 1′ AND (SELECT length(user) FROM users LIMIT 1 OFFSET 0)=1#

Encontrando que la longitud del campo user del primer registro de la tabla users tiene 5 caracteres. Para identificar el primer carácter se inyecta el parámetro 1′ AND (SELECT substring(user,1,1) FROM users LIMIT 1 OFFSET 0)=’1’#

Encontrando que la primera letra del campo user del primer registro de la tabla users es ‘a’, continuando con el resto de caracteres se obtiene que el user completo es admin, con paciencia y trabajando sobre las columnas user y password se obtienen los siguientes registros.

  • admin:5f4dcc3b5aa765d61d8327deb882cf99
  • gordonb:e99a18c428cb38d5f260853678922e03
  • 1337:8d3533d75ae2c3966d7e0d4fcc69216b
  • pablo:0d107d09f5bbe40cade3de5c71e9e9b7
  • smithy:5f4dcc3b5aa765d61d8327deb882cf99

Obteniendo de esta forma los usuarios y los hashes de las contraseñas de los usuarios registrados en la aplicación web, cabe resaltar que obteniendo los hashes se puede continuar con un proceso de password cracking para recuperar las contraseñas en claro, pero eso será tema de otro post.

De esta manera hemos logrado explicar en detalle y paso a paso en que consiste una vulnerabilidad de inyección SQL a ciegas y comprender el riesgo que puede representar tener esta vulnerabilidad en una aplicación web en producción.