Las denominadas "inyecciones SQL" son un tipo de ataque a una aplicación que aprovecha fallos de seguridad en la base de datos. En esta lección , vamos a ver ejemplos de inyecciones SQL. Esta lección está basada en el artículo de Steve Friedl SQL Injection Attacks by Example cuya lectura recomiendo, o la chuleta SQL injection cheat sheet. Por supuesto, el objetivo de esta lección es concienciar de la necesidad de proteger cualquier aplicación web de este tipo de ataques.
Utilizaremos como ejemplo una aplicación similar a la de los ejercicios Bases de datos 1.
Cuando el usuario escriba un nombre de usuario y contraseña, la aplicación responde uno de estos tres mensajes:
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Nombre de usuario y contraseña correctos.
Nombre de usuario incorrecto.
Contraseña incorrecta.
Para comprobar si la aplicación incluye los datos enviados por el usuario sin ningún tratamiento previo, podemos enviar una comilla (simple o doble) como dato.
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Nombre de usuario incorrecto.
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Error en la consulta.
Este último mensaje ("Error en la consulta") nos hace saber que no se tratan los datos y que además las consultas están delimitadas por dobles comillas. ¿Por qué?
Probablemente el código de la aplicación es algo similar a esto:
$usuario = $_REQUEST["usuario"];
$contraseña = $_REQUEST["contraseña"];
$consulta = "SELECT COUNT(*) FROM $dbTabla
WHERE campo1='$usuario'
AND campo2='$contraseña'";
$result = sqlite_exec($db, $consulta);
if (!$result) {
print "<p>Error en la consulta.</p>\n";
} elseif ($result[0][0] > 0) {
print "<p>Nombre de usuario y contraseña correctos.</p>\n";
} else {
...
Al escribir una comilla doble al principio del nombre de usuario, la consulta se convierte en
SELECT COUNT(*) FROM $dbTabla WHERE campo1='"loquesea' AND campo2='loquesea'
Esta consulta es correcta (no contiene errores de sintaxis) y cuando se ejecuta, la base de datos simplemente devuelve 0.
Sin embargo, al escribir una comilla simple al principio del nombre de usuario, la consulta se convierte en
SELECT COUNT(*) FROM $dbTabla WHERE campo1=''loquesea' AND campo2='loquesea'
Esta consulta no es correcta (contiene un error de sintaxis por culpa de la comilla dentro de las comillas de la segunda línea y cuando se ejecuta, la base de datos da error.
Ahora que sabemos que la consulta está delimitada por comillas dobles podemos escribir unos datos que modificarán la consulta y harán que la aplicación crea que hemos introducido datos de un usuario registrado.
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Nombre de usuario y contraseña correctos.
En este caso, la consulta a la base de datos será algo parecido a esto:
SELECT COUNT(*) FROM tabla WHERE campo1='loquesea' AND campo2='loquesea' OR '1'='1'
Esta consulta es correcta y, cuando se ejecuta, la base de datos devuelve el número total de registros en la tabla, puesto que la condición se cumple siempre, aunque el nombre de usuario y la contraseña sean incorrectos, porque la condición final OR '1'='1' se cumple siempre.
Los nombres de campos se pueden averiguar mediante prueba y error. La idea es introducir datos que construyan consultas en las que aparezcan posibles nombres de los campos. En caso de que las consultas den error significa que el nombre es incorrecto, si no es así significa que hemos acertado el nombre de los campos.
Por ejemplo, vamos a probar si el nombre de uno de los campos es "usuario".
Podríamos hacer algo parecido a lo visto en el punto anterior:
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Error en la consulta.
La respuesta de la aplicación es "Error en la consulta", lo que nos indica que no hay un campo que se llame "usuario". En este caso, la consulta a la base de datos habrá sido algo parecido a esto:
SELECT COUNT(*) FROM tabla WHERE campo1='loquesea' AND campo2='loquesea' AND usuario='loquesea'
Otra posible entrada podría haber sido la siguiente:
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Error en la consulta.
La respuesta de la aplicación es "Error en la consulta", lo que nos indica que no hay un campo que se llame "usuario". En este caso, la consulta a la base de datos habrá sido algo parecido a esto:
SELECT COUNT(*) FROM tabla WHERE campo1='loquesea' AND campo2='loquesea' AND usuario is NULL; --'
En SQL, los guiones son la marca de inicio de un comentario, de manera que la comilla final no se toma en cuenta en la consulta.
En ambos casos hemos obtenido "Error en la consulta", por lo que sabemos que ningún campo se llama "usuario".
Hacemos ahora un segundo intento, con el nombre "user"
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Nombre de usuario incorrecto.
En este caso, la consulta a la base de datos será algo parecido a esto:
SELECT COUNT(*) FROM tabla WHERE campo1='loquesea' AND campo2='loquesea' AND user is NULL; --'
Como hemos obtenido la respuesta "Nombre de usuario incorrecto", sabemos que uno de los campos se llama "user".
Los nombres de las tablas se pueden averiguar mediante prueba y error. La idea es introducir datos que construyan consultas en las que aparezcan posibles nombres de las tablas. En caso de que las consultas den error significa que el nombre es incorrecto, si no es así significa que hemos acertado el nombre de las tablas.
Por ejemplo, vamos a probar si el nombre de la tabla es "usuarios".
Podríamos hacer algo parecido a lo visto en el punto anterior:
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Error en la consulta.
La respuesta de la aplicación es "Error en la consulta", lo que nos indica que no hay una tabla que se llame "usuarios". En este caso, la consulta a la base de datos habrá sido algo parecido a esto:
SELECT COUNT(*) FROM tabla WHERE campo1='loquesea' AND campo2='loquesea' AND 1=(SELECT COUNT(*) FROM usuarios); --'
Hacemos ahora un segundo intento, con el nombre "tabla"
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Nombre de usuario incorrecto.
En este caso, la consulta a la base de datos será algo parecido a esto:
SELECT COUNT(*) FROM tabla WHERE campo1='loquesea' AND campo2='loquesea' AND 1=(SELECT COUNT(*) FROM tabla);--
Como hemos obtenido la respuesta "Nombre de usuario incorrecto", sabemos que una de las tablas se llama "tabla".
Una vez se conoce el nombre de la tabla de usuarios y los nombres de los campos se puede intentar conocer valores concretos de un registro mediante prueba y error. La idea es introducir datos que construyan consultas en las que aparezcan posibles contenidos de los campos. En caso de que las consultas den error significa que el contenido es incorrecto, si no es así significa que hemos acertado el contenido.
Por ejemplo, vamos a buscar nombres de usuarios.
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Nombre de usuario y contraseña correctos.
La respuesta de la aplicación es "Nombre de usuario y contraseña correctos.", lo que nos indica que hay un usuario cuyo nombre empieza por "a". En este caso, la consulta a la base de datos habrá sido algo parecido a esto:
SELECT COUNT(*) FROM tabla WHERE campo1='loquesea' AND campo2='loquesea' OR user LIKE 'a%';--
Podríamos ir alargando la cadena letra a letra hasta encontrar el nombre del usuario.
Nota: Este tipo de ataque no funciona en la aplicación de ejemplo del primer apartado de esta lección ya que la extensión PDO no permite ejecutar varias consultas de una sola vez, pero sí funciona en la aplicación vulnerable inyeccion_sql_2.zip que puede probar en su ordenador.
Una vez se conoce el nombre de la tabla de usuarios y los nombres de los campos se puede intentar editar la base de datos, por ejemplo, añadiendo un usuario.
La técnica consiste en incluir una sentencia SQL que inserte un registro.
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Nombre de usuario incorrecto
En este caso, la consulta a la base de datos será algo parecido a esto:
SELECT COUNT(*) FROM tabla WHERE campo1='loquesea' AND campo2='loquesea'; INSERT INTO tabla VALUES (NULL, 'hacker', 'hacker'); --
Para comprobar si el ataque ha tenido éxito, habría que probar a entrar como usuario "hacker" con contraseña "hacker".
Obviamente para que el ataque tenga éxito deberíamos haber acertado la estructura de la tabla, lo que podría exigir numerosas pruebas o ser demasiado difícil. Otra vía para conseguir los datos de un usuario sería averiguar el nombre de algún usuario (mediante sentencias LIKE y algo de paciencia) e inyectar una consulta que cambie su contraseña.
Nota: Este tipo de ataque no funciona en la aplicación de ejemplo del primer apartado de esta lección ya que la extensión PDO no permite ejecutar varias consultas de una sola vez, pero sí funciona en la aplicación vulnerable inyeccion_sql_2.zip que puede probar en su ordenador.
Una vez se conoce el nombre de la tabla de usuarios vamos a realizar una acción destructiva, como por ejemplo borrar la tabla de usuarios.
La técnica consiste en incluir una sentencia SQL que inserte un registro.
Para entrar en el sistema escriba su nombre de usuario y contraseña:
Usuario: | |
Contraseña: |
Nombre de usuario incorrecto
Error en la consulta.
En este caso, la consulta a la base de datos será algo parecido a esto:
SELECT COUNT(*) FROM tabla WHERE campo1='loquesea' AND campo2='loquesea'; DROP TABLE tabla; --
Si el ataque ha tenido éxito, la aplicación seguramente dejará de funcionar, puesto que ha desaparecido una de las tablas.
La excelente tira cómica xkcd publicó un chiste sobre este tema: