Ataques a bases de datos
Metodología
Se recomienda seguir usando cherrytree para ir guardando las evidencias.
Referencias usadas
MySQL y MariaDB
FALTA LA INTRODUCCIÓN
Iniciamos el servicio de mysql:
Accedemos desde Kali a MySQL usando el comando:
mysql -u root -p
Pulsamos Intro cuando nos pregunte la contraseña:
para logarnos:
Por convención, los comandos de SQL se escriben con mayúsculas. El primer comando que ejecutaremos es SHOW DATABASES para que nos muestre las bases de datos disponibles. Acabamos las sentencias SQL con punto y coma (;). Entre las bases de datos disponibles, vemos 2 que nos serán de interés más adelante:
- mysql
- information_schema
Vamos a crear una base de datos para aprender un poco de SQL antes de comenzar a realizar inyecciones. Para crear una base de datos, usaremos el comando CREATE DATABASE seguido del nombre de la base de datos. En este caso, llamaremos a la base de datos pruebas:
Si volvemos a ejecutar el comando SHOW DATABASES, podremos ver la base de datos que hemos creado:
Para interactuar con la base de datos que hemos creado, usaremos el comando USE seguido de las base de datos que queremos usar. En este caso pruebas:
Podemos confirmar que hemos seleccionado correctamente la base de datos usando el comando SELECT DATABASE(). El comando SELECT sirve para consultar información en las tablas de una base de datos y la función DATABASE me devuelve el nombre de la base de datos que se está utilizando en este momento:
Veremos más ejemplos de cómo usar SELECT muy pronto pero primero necesitamos tablas. Para ver las tablas que tiene la base de datos que estamos utilizando, usamos el comando SHOW TABLES:
En este caso, como la base de datos acaba de ser creada, no tiene tablas. Vamos a crear una tabla usando el comando CREATE TABLE. Antes de ver cómo crear la tabla, veamos un par de tipos de datos para poder crearla:
- INT => Números enteros
- VARCHAR(n) => n carácteres
Es decir, VARCHAR nos va a permitir introducir texto hasta un número máximo de caracteres y INT números enteros de hasta 4 bytes. Con esto ya podemos crear la tabla:
CREATE TABLE colores_favoritos_alumnos_sabados (
nombre VARCHAR(20),
apellido VARCHAR(25),
edad INT,
color_favorito VARCHAR(25)
);
Ahora si volvemos a ejecutar el comando SHOW TABLES, veremos que la base de datos se ha creado correctamente:
Para ver las columnas de la base de datos que hemos creado usaremos el comando SHOW COLUMNS FROM seguido del nombre de la tabla:
Ya que tenemos una tabla, podemos usar el comando SELECT para consultar información de la misma. Para ello, indicamos a SELECT las columnas de la tabla que queremos, por ejemplo, nombre y edad, y el nombre de la tabla:
SELECT nombre, edad FROM colores_favoritos_alumnos_sabados;
En este caso, no hay datos. Vamos a introducir datos en la tabla de la forma más sencilla. Para ello usaremos el comando INSERT INTO seguido del nombre la tabla, después pondremos VALUES y los valores que queramos insertar. Por ejemplo:
INSERT INTO colores_favoritos_alumnos_sabados VALUES ('pepita','perez',30,'azul');
Ahora tras insertar valores en la tabla, si ejecutamos la consulta anterior:
SELECT nombre, edad FROM colores_favoritos_alumnos_sabados;
Veremos los resultados. Si en vez de querer columnas concretas, quisiéramos seleccionar todas las columnas, podemos usar el símbolo asterisco:
Añadamos unas cuantas filas más a nuestra tabla. Para ello, utilizaremos el comando INSERT de la siguiente forma:
INSERT INTO colores_favoritos_alumnos_sabados VALUES
('pepito','perez',30,'rosa'),
('laura','garcia',25,'negro'),
('eduardo','manostijeras',33,'morado'),
('elena','de troya',36,'negro'),
('saul','soul',25,'rosa'),
('violeta','coello',45,'negro'),
('juan','rubio',50,'gris');
Podemos contar el número de filas que hay en nuestra base de datos usando la función COUNT de MySQL:
SELECT COUNT(*) FROM colores_favoritos_alumnos_sabados;
Veremos que hay 8 filas. Nuevamente podemos usar SELECT para ver el contenido de las 8 filas:
SELECT * FROM colores_favoritos_alumnos_sabados;
Muchas veces necesitamos usar filtros para poder recuperar sólo la información de la base de datos que nos interese. Los filtros van comprobando fila por fila si se cumple una condición y, en caso de que en esa fila se cumpla, muestran los resultados de esa fila. Para realizar filtros vamos a usar el comando WHERE seguido de la condición o condiciones por las que queramos filtrar. Por ejemplo, imaginemos que queremos saber las personas cuyo color favorito es el negro. Para ello, realizamos la siguiente busqueda:
SELECT *
FROM colores_favoritos_alumnos_sabados
WHERE color_favorito = 'negro';
Como hemos mencionado, también podemos realizar busquedas con varias condiciones apoyandonos del comando:
- OR si queremos que nos devuelva el resultado de la fila si se cumple alguna de las condiciones
- AND si queremos que nos devuelva el resultado de la fila sólo si se cumplen ambas condiciones
Veamos ejemplos. Seleccionemos las personas cuyo color favorito es el negro o el rosa:
SELECT *
FROM colores_favoritos_alumnos_sabados
WHERE color_favorito = 'negro' OR color_favorito = 'rosa';
o seleccionamos las personas cuyo color favorito sea el negro y sean menores de 40 años:
SELECT *
FROM colores_favoritos_alumnos_sabados
WHERE color_favorito = 'negro' AND edad < 40;
También podemos poner condiciones imposibles. Por ejemplo, seleccionemos las personas que tienen a la vez 30 y 50 años. Es decir, es algo imposible porque o bien tienen 30 o bien 50 pero no ambas:
SELECT *
FROM colores_favoritos_alumnos_sabados
WHERE edad = 30 AND edad = 50;
El resultado de la consulta son 0 filas porque hemos dado una condición imposible de cumplir. Sin embargo, vamos a hacer ahora lo contrario, vamos a dar una condición que pase lo que pase, siempre se cumpla. Por ejemplo, podemos comparar 2 valores que sean iguales. En este caso, podemos comparar la palabra trasto consigo misma:
SELECT *
FROM colores_favoritos_alumnos_sabados
WHERE 'trasto'='trasto';
y como un trasto es un trasto, pues nos devolverá todos los resultados. Como hemos comentado anteriormente, el filtro va, fila por fila, evaluando si es verdad la condición del filtro y, si lo és, nos muestra los datos de esa fila. En este caso, como la condición del filtro es siempre verdadera, nos devuelve todos los datos. Como veremos más adelante, esto es un problema para la seguridad de una aplicación web en la que no se validen correctamente los datos ya que un usuario con malas intenciones puede aprovecharse de esto para añadir condiciones que siempre sean verdad utilizando el operador OR. Veamos un ejemplo. Seleccionemos todos los usuarios cuyo color favorito sea el morado:
SELECT *
FROM colores_favoritos_alumnos_sabados
WHERE color_favorito = 'morado';
Vemos que sólo hay una persona cuyo color favorito sea morado. Ahora podemos sacar partido del operador OR para añadir una segunda condición que siempre sea verdad:
SELECT *
FROM colores_favoritos_alumnos_sabados
WHERE color_favorito = 'morado' OR 'trasto'='trasto';
Veremos que se muestran todos los resultados de la tabla ya que la consulta devuelve los resultados que cumplan una de las siguientes condiciones:
- que el color favorito sea morado
- que la palabra trasto sea igual a la palabra trasto
Ira comprobando fila a fila si cumplen alguna de las condiciones y, como una de las condiciones es verdad siempre, se mostrarán todos los resultados. Utilizaremos está técnica más adelante con distintos fines.
Sentencias UNION
Las bases de datos nos permiten unir datos de distintas tablas. La forma de unirlos es usando sentencias JOIN y sentencias UNION. JOIN se sale del alcance de esta pequeña guía, por lo que nos centraremos en UNION. Para poder experimentar con UNION creamos una segunda tabla que se llame colores_favoritos_alumnos_viernes:
CREATE TABLE colores_favoritos_alumnos_sabados (
nombre VARCHAR(20),
apellido VARCHAR(25),
edad INT,
color_favorito VARCHAR(25)
);
INSERT INTO colores_favoritos_alumnos_viernes VALUES
('pepita','perez',30,'azul'),
('pepito','perez',30,'rosa'),
('laura','garcia',25,'negro'),
('eduardo','manostijeras',33,'morado'),
('elena','de troya',36,'negro'),
('saul','soul',25,'rosa'),
('violeta','coello',45,'negro'),
('juan','rubio',50,'gris'),
('maria','castillo',27,'azul'),
('marco','polo',27,'rosa');
Ahora que tenemos 2 tablas, podemos unirlas. UNION representa la unión matemática de dos conjuntos. Es decir, va a mostrar todos los resultados que haya en ambas tablas pero, si hubiese filas repetidas, sólo las mostrará una vez. Veamos un ejemplo:
SELECT * FROM colores_favoritos_alumnos_viernes
UNION
SELECT * FROM colores_favoritos_alumnos_sabados;
Debido a que todos los alumnos que asisten el sábado a clase, también asisten el viernes, sólo se muestran 10 resultados. Si quisiéramos que se mostrase todo, incluyendo los repetidos, debemos usar la sentencia UNION ALL. Veamos un ejemplo:
SELECT * FROM colores_favoritos_alumnos_viernes
UNION ALL
SELECT * FROM colores_favoritos_alumnos_sabados;
Vemos que ahora nos muestra 18 filas. Las 10 de la tabla colores_favoritos_alumnos_viernes y las 8 de la tabla colores_favoritos_alumnos_sabados. Hemos aprendido a unir tablas usando UNION. UNION nos une una tabla con otra, poniendo las filas de una tabla debajo de la otra. Esto quiere decir que necesitamos que las tablas tengan el mismo número de columnas. Por ejemplo, vamos a seleccionar los datos de la columnas edad y color favorito de ambas tablas:
SELECT edad, color_favorito FROM colores_favoritos_alumnos_viernes
UNION
SELECT edad, color_favorito FROM colores_favoritos_alumnos_sabados;
Veamos ahora que ocurre, si seleccionamos en una tabla un número distinto de columnas que en la otra. Por ejemplo, omitimos la columna edad en la tabla colores_favoritos_alumnos_viernes:
SELECT color_favorito FROM colores_favoritos_alumnos_viernes
UNION
SELECT edad, color_favorito FROM colores_favoritos_alumnos_sabados;
Como vemos no es posible y MariaDB nos muestra un error. Es decir, que como ponemos una tabla debajo de otra, ambas deben tener el mismo número de columnas. Hasta ahora, hemos utilizado UNION para seleccionar datos que esten en una tabla pero, si os recordamos ejemplos anteriores, MariaDB cuenta con funciones que podemos USAR con las sentencias SELECT. Por ejemplo, hemos visto las funciones DATABASE() y COUNT(). Vamos a utilizarlas en la siguiente consulta:
SELECT edad, color_favorito FROM colores_favoritos_alumnos_viernes
UNION
SELECT DATABASE(), COUNT(*) FROM colores_favoritos_alumnos_sabados;
Como vemos, en la última fila de los resultados, bajo la columna edad tenemos el nombre de la base de datos y bajo la columna color_favorito, tenemos el número de filas de la tabla colores_favoritos_alumnos_sabados. Esta técnica también la usaremos para extraer información de la base de datos. En estos ejemplos, estamos consultando información en las tablas de la base de datos pruebas, pero también podemos consultar datos de tablas que se encuentren en otras bases de datos. Por ejemplo, podemos consultar datos de la tabla user de la base de datos mysql. Cuando queramos acceder a una tabla de una base de datos distinta a la que estamos usando, debemos indicarlo con la notación basededatos.tablaALaQueQuieroAcceder:
SELECT edad, color_favorito FROM colores_favoritos_alumnos_viernes
UNION
SELECT user, password FROM mysql.user;
En este caso, sólo esta dado de alta el usuario de root y no tiene contraseña. Lo último que vamos a ver es la forma de realizar comentarios en mysql. Aunque existen 3 formas de hacer comentarios en una base de datos MySQL, vamos a ver el más conocido que es usar doble guión. Todo lo que venga despues del doble guión será ignorado. Veamos un ejemplo:
SELECT edad, color_favorito FROM colores_favoritos_alumnos_viernes -- UNION SELECT user, password FROM mysql.user;
;
DVWA SQLi
Vamos a practicar lo que hemos aprendido con dvwa. Para ello, hacemos click sobre el botón SQL Injection:
buscamos un usuario y pulsamos el botón Submit:
vemos que nos devuelve los datos del usuario correspondiente al ID:
Pensemos cómo puede ser esta query en SQL. Probablemente será algo así:
SELECT * FROM users WHERE user_id = 'el id que busquemos';
El formulario que estamos usando, modifica el filtro user_id. Si ponemos un 1, la consulta quedaría así:
SELECT * FROM users WHERE user_id = '1';
Si metemos un 2, la consulta quedaría así:
SELECT * FROM users WHERE user_id = '2';
Es decir, nosotros podemos modificar todo lo que haya entre las comillas:
Pero que pasa si, en vez de escribir un número en el formulario, ponemos una comilla:
Al poner una comilla, cerramos el texto que podemos cambiar, y se abre una nueva comilla que nunca se cierra:
Lo cual provoca un error en la consulta a la base de datos. Si probamos en dvwa a poner una comilla y pulsamos el botón Submit:
Veremos el mensaje del error:
esto podemos arreglarlo, añadiendo una segunda comilla que cierre el texto que antes hemos señalado en amarillo:
de esta forma el punto y coma (;) no queda como si fuera texto y, si probamos en dvwa:
la sentencia se ejecuta correctamente y no da mensajes de error. Ahora que hemos aprendido a modificar la consulta SQL, podemos utilizar nuestro conocimiento del operador OR para cambiar los filtros de la consulta y que nos devuelva todas las filas de la tabla que estamos consultando. Para ello, como vimos en el paso anterior, podemos comparar la palabra trasto con si misma. Lo que tenemos que teclear es:
' OR 'trasto'='trasto
Veamos el porqué. Si la consulta aproximadamente es así:
y nosotros añadimos el payload a la consulta:
Vemos que cerramos la primera comilla:
y después realizamos una comparación que siempre será verdad:
Debido a esto, como el filtro siempre será verdad, debería devolvernos todos los resultados. Probemosló en dvwa. Tecleamos el payload en el formulario y pulsamos el botón Submit:
y veremos como se muestran todos los resultados de la tabla:
Como vemos que podemos alterar la consulta del programador, vamos a intentar usar sentencias UNION para intentar sacar información de la base de datos. Para ello, tenemos que averiguar el número de columnas. Comencemos probando si la tabla que devuelve la consulta tiene sólo una columna. Para ello, por ejemplo, vamos a utilizar el siguiente payload:
' UNION SELECT 'a ver si tiene una' --'
Si metemos el payload en la consulta, quedaría así:
Es decir, la última parte de la sentencia quedaría comentada:
Probemoslo en dvwa:
y veremos que tiene más de una columna:
Como no tiene una, probemos a ver si tiene 2:
' UNION SELECT 'no tiene una', 'a ver si tiene 2' --'
Volvemos a teclear el payload en el formulario y pulsamos el botón Submit:
Vemos que tiene dos columnas porque, en vez de darnos error, nos muestra los resultados:
De hecho, podemos probar a ver si tiene 3 columnas, para confirmar que nos vuelve a mostrar el mensaje de error. Esta vez, en vez de frases, usaremos números en el payload:
' UNION SELECT 1, 2, 3 --'
Metemos el payload en el formulario y pulsamos el botón Submit:
veremos que vuelve a mostrar el mensaje de error:
Una vez que confirmamos que podemos extraer datos utilizando sentencias UNION, vamos a comenzar a averiguando el nombre de la base de datos que se está utilizando con la función DATABASE() y la versión de MySQL con la función VERSION():
' UNION SELECT DATABASE(), VERSION() --'
y veremos que la base de datos se llama dvwa y que la versión de MySQL es la 5:
Normalmente la versión de la base de datos suele ser más que un número. Como no se nos ha mostrado correctamente vamos a ayudarnos de la función CONCAT de MySQL que nos permite unir dos cadenas de texto para sacar la versión de la base de datos correctamente. Para ello, tecleamos en el formulario:
' UNION SELECT CONCAT(DATABASE()," ",VERSION()), 'da igual' --'
y pulsamos el botón Submit:
Podemos sacar a continuación el nombre del usuario que se esta usando para realizar las consultas en la base de datos:
' UNION SELECT CURRENT_USER(), 'da igual' --'
y veremos que se está usando el usuario root de MySQL:
Lo cual es muy mala práctica. MySQL tiene una función que se llama LOAD_FILE que nos va a permitir leer ficheros. Vamos a utilizar esta función para extraer todos los usuarios de la máquina leyendo el archivo /etc/passwd:
' UNION SELECT LOAD_FILE('/etc/passwd'), 'da igual' --'
Nuevamente, usamos este payload en el formulario:
y veremos que hemos podido leer el archivo:
Si recordamos del principio del capítulo, en mysql, por defecto, vienen creadas las bases de datos information_schema y mysql. La base de datos information_schema contine información sobre el resto de bases de datos como, por ejemplo, qué tablas tiene cada base de datos o qué columnas tiene cada tabla. Una de las tablas interesante de esta base de datos es columns. La tabla columns de la base de datos information_schema almacena el nombre de todas las tablas y todas las columnas de cada tabla de todas las bases de datos en el servidor MySQL. Para ampliar información sobre la tabla columns, podemos consultar la documentación oficial de MySQL. En este caso, vamos a consultar todas las columnas de cada tabla de cada una de las bases de datos. Para ello, solicitaremos las columnas:
- table_schema
- table_name
- column_name
de la tabla columns de la base de datos information_schema. Como sólo podemos sólicitar 2 columnas, nos apoyaremos en la función CONCAT de MySQL para poder pedir las tres de golpe:
' UNION SELECT CONCAT(table_schema," - ",table_name), column_name FROM information_schema.columns -- '
Copiamos la consulta en el formulario y pulsamos Submit:
Veremos que se nos muestran los resultados:
Por ejemplo, encontramos la base de datos dvwa y vemos que tiene una tabla que se llama users que, entre otras, contiene las columnas user y password:
Asimismo también podemos ver la información de la otra base de datos que viene creada por defecto, que se llama mysql, y que tiene una tabla que se llama user con las columnas user y password:
Con esta información, por ejemplo, vamos a consultar los usuarios y las contraseñas de la tabla users de la base de datos dvwa. De esta forma, podremos tener acceso a todos los usuarios de la aplicación dvwa. Para ello, copiamos la consulta:
' UNION SELECT user, password FROM dvwa.users -- '
al formulario y pulsamos Submit:
Para ver los usuarios y sus contraseñas:
Las contraseñas han sido hasheadas. Copiamos el hash que queramos crackear:
y, como aprendimos en un tema anterior, podemos averiguar de qué hash se trata utilizando los programas hash-identifier o hashid:
Vemos que nos da 2 resultados posibles. Vamos a comenzar probando con MD5. Copiamos el hash a un fichero:
y lo crackeamos:
para ver la contraseña:
Explotando SQLi inyections con sqlmap
Una vez que hemos visto como hacerlo manualmente, veamos cómo podríamos hacerlo de forma automática usando el programa sqlmap. Para ello, activamos el proxy de burpsuite:
Nos aseguramos que lo tenemos configurado en el navegador:
escribimos un número en el formulario y hacemos click en el botón Submit:
El proxy de burpsuite capturará la petición HTTP. Pulsamos Forward para enviar la petición al servidor:
y nuevamente Forward para enviar la respuesta del proxy al navegador:
A continuación, desde la pestaña HTTP History, hacemos click con el botón derecho sobre la petición (Request) y seleccionamos Copy to file del menú desplegable:
Le damos un nombre y pulsamos el botón Save:
Una vez tenemos guardada la petición post en ese fichero, vamos a lanzar sqlmap. Para ello, usamos las opciones:
- -r para indicarle a sqlmap el fichero donde se encuentra la petición HTTP
- --dbms para indicarle el motor de base de datos que estamos atacando. En este caso MySQL
- -p para indicarle a sqlmap cuál es el parámetro vulnerable
sqlmap -r dvwa.req --dbms=mysql -p id
y veremos que el parámetro es vulnerable:
al final de ejecutarse, nos mostrará un breve resumen de lo que ha encontrado:
ahora que sabemos que explotable a través de UNION, vamos a indicarle que use esta técnica con la opción --technique=U y vamos a pedirle que nos muestre el usuario de la base datos con --current-user:
sqlmap -r dvwa.req --dbms=mysql -p id --technique=U --current-user
Podemos ahora usar --current-db en vez de current-user, para que nos muestre la base de datos que se esta usando:
sqlmap -r dvwa.req --dbms=mysql -p id --technique=U --current-db
Si quisiéramos saber, todas las bases de datos utilizaríamos la opción --dbs:
sqlmap -r dvwa.req --dbms=mysql -p id --technique=U --dbs
Una vez sabemos las bases de datos, podemos intentar listar todas las tablas de una de ellas. Por ejemplo, intentemos listar todas las tablas de la base de datos dvwa. Para ello, incluimos la opción --tables para que nos devuelva las tablas y la opción -D para restringir los resultados a la base de datos que queramos:
sqlmap -r dvwa.req --dbms=mysql -p id --technique=U --tables -D dvwa
Una vez que vemos las tablas, podemos ver las columnas de una tabla. Por ejemplo, intentemos ver las columnas de la tabla users. Para ello, utilizaremos la opción --columns para indicar que queremos las columnas y la opción -T para indicar la tabla:
sqlmap -r dvwa.req --dbms=mysql -p id --technique=U --columns -T users -D dvwa
Una vez que tenemos la tabla, si queremos extraer su contenido, usaremos la opción --dump:
sqlmap -r dvwa.req --dbms=mysql -p id --technique=U --dump -T users -D dvwa
Como detectará hashes, nos preguntará si queremos crackearlos. En este caso pulsamos Intro para aceptar que intente crackear los hashes y cuando nos pregunte qué diccionario usar, tecleamos 1 y pulsamos Intro:
Veremos que en pocos segundos, los hashes son crackeados y podemos ver las contraseñas en texto plano:
OWASP Juice Shop - Inyecciones SQL en formularios de autenticación
Muchos de los formularios de las aplicaciones que sirven para identificar y autenticar a los usuarios, comprueban las credenciales facilitadas contra una base de datos SQL. Si los datos no se validan correctamente, un usuario con malas intenciones puede modificar la consulta original del programador para poder entrar. Vamos a ver un ejemplo. Accedemos al formulario para autenticarnos pulsando en la opción Login:
Cuando nos enfrentamos a una aplicación de este tipo, aunque no sabemos exactamente cuál es la consulta SQL que hace el programador para comprobar las credenciales que le pasamos, normalmente es algo así:
Es decir, podemos controlar los datos que se pasarán como usuario y como contraseña. La idea es intentar encontrar un usuario válido y hacer que no se compruebe la contraseña. En este caso, en vez de un usuario válido, vamos a buscar una condición que siempre sea verdad para el campo usuario y comentaremos lo demas. Por ejemplo, podemos usar el payload
' oR 'quiero entrar'='quiero entrar' -- '
Si copiamos este payload y lo pegamos en la sentencia SQL:
Veremos que tenemos una condición siempre válida y que la comprobación de la contraseña no se ejecuta por estar comentada:
Es decir, podemos copiar el payload y poner cualquier contraseña en el formulario que, cuando pulsemos el botón Log in:
entraremos sin problemas a la aplicación: