Inyección SQL
Es hora de echar un vistazo a la inyección SQL. Durante mucho tiempo, fue el rey indiscutible del Top 10 de OWASP, estamos hablando de años seguidos. A pesar de ser tan antigua (como más de 20 años), y aunque ha caído ligeramente del primer puesto de esa lista, sigue siendo una vulnerabilidad increíblemente popular y peligrosa.
Siendo una vulnerabilidad de seguridad web, la inyección SQL (SQLi) sigue siendo una de las técnicas de 'hacking' más utilizadas por los atacantes, ya que les permite manipular una base de datos y extraer información crucial de ella. Y lo que es aún más alarmante, un atacante puede hacerse pasar por administrador del servidor de la base de datos y hacer cosas realmente devastadoras, como destruir bases de datos, manipular transacciones, revelar datos y hacerla vulnerable a más problemas.
Veamos brevemente cómo se produce
SQL (o Lenguaje de Consulta Estructurado) es el lenguaje utilizado para comunicarse con las bases de datos relacionales; es el lenguaje de consulta utilizado por desarrolladores, administradores de bases de datos y aplicaciones para gestionar las enormes cantidades de datos que se generan cada día.
En una aplicación existen dos contextos: uno para los datos y otro para el código. El contexto de código indica a los ordenadores qué ejecutar y lo separa de los datos que deben procesarse. La inyección SQL se produce cuando un atacante introduce datos que el intérprete SQL trata erróneamente como código, lo que le permite obtener información valiosa de la aplicación.
Efectos de un ataque de inyección SQL
Una inyección SQL puede ser extremadamente dañina para cualquier aplicación web y ha sido la técnica preferida detrás de tantas brechas de alto perfil porque proporciona a los atacantes acceso no autorizado a datos críticos. Pueden ver muchísima información, desde nombres de usuario y contraseñas hasta datos de tarjetas de crédito y números de identificación personal.
Tras obtener acceso a estos datos, los atacantes pueden hacerse con el control de cuentas, restablecer contraseñas, realizar compras en línea prolongadas o cometer otros tipos de fraude (mucho peores).
Pero quizás lo más alarmante de SQLi es que un atacante puede, si no es detectado, mantener una puerta trasera en el sistema durante largos periodos de tiempo. Como se puede imaginar, eso llevaría a repetidas violaciones de datos durante el tiempo que se mantenga abierta esa puerta trasera. Algo aterrador.
Veamos algunos ejemplos para entender mejor cómo se ve esto en acción.
Ejemplos de SQLi
SQLi incluye varias técnicas de vulnerabilidad que pueden hacer frente a diferentes situaciones. Lo que sigue a continuación son sólo algunos de los ejemplos más comunes de SQLi:
Tipos SQLi
Bien, veamos ahora los tres tipos diferentes de SQLi.
SQLi en banda
Este es uno de los tipos más comunes, simples y eficientes de inyección SQL. En este tipo de ataque, se utiliza el mismo canal de comunicación para atacar y recuperar el resultado.
A continuación se describen los dos tipos de ataques SQLi en banda:
- SQLi basado en la unión - El ataque basado en la unión utiliza el operador union para combinar dos o más consultas SQL, como sentencias SELECT, para obtener la información deseada y los resultados en una respuesta HTTP GET.
- SQLi basado en errores: el atacante utiliza los mensajes de error de la base de datos para comprender su estructura. En este ataque, el atacante puede enviar peticiones falsas o realizar acciones para que el servidor muestre mensajes de error y así poder recibir información de la base de datos. Por eso es importante que los desarrolladores eviten enviar errores o mensajes de registro en el entorno en vivo; en su lugar, deben almacenarse con acceso restringido.
SQLi inferencial
Los ataques SQLi inferenciales o ciegos son más complicados y puede llevar más tiempo explotarlos. Además, el atacante no obtiene los resultados del ataque de inmediato, por lo que se trata de un ataque ciego.
El atacante envía las cargas útiles a través de peticiones HTTP al servidor de base de datos para reestructurar la base de datos del usuario, luego observan la respuesta y el comportamiento de la aplicación para ver si el ataque tuvo éxito o no.
Se trata de dos tipos de ataque SQLi inferencial:
- SQLi ciego basado en booleanos - En este ataque, se envía una consulta a la base de datos obteniendo el resultado booleano (verdadero o falso), y el atacante observa la respuesta HTTP para predecir el resultado booleano.
- SQLi ciego basado en el tiempo - En este ataque, el atacante envía una consulta a la base de datos para hacerla esperar unos segundos antes de enviar la respuesta, y el atacante juzga los resultados de la consulta a partir del tiempo de respuesta de la petición HTTP.
SQLi fuera de banda
Este es un tipo de ataque SQLi más raro que depende de las características habilitadas del servidor de base de datos. Se produce en casos en los que el atacante no puede utilizar realmente los otros tipos de ataque.
Por ejemplo, si no puede utilizar el mismo canal de comunicación para el ataque en banda, o la respuesta HTTP no es lo suficientemente clara como para que pueda averiguar los resultados de la consulta.
Además, no es tan común debido a su dependencia masiva de la capacidad del servidor de base de datos para hacer peticiones HTTP o DNS para enviar los datos requeridos al atacante.
Cómo defenderse de SQLi
Afortunadamente, el lado positivo de que la inyección SQL sea tan antigua y tan común es que hay formas de evitar que ocurra. El uso de este tipo de técnicas de prevención no sólo es un buen hábito de codificación, sino que realmente reforzará la seguridad de una organización contra SQLi.
Hay muchas formas de proteger los servidores de bases de datos de este tipo de ataques, como la validación de entradas, el uso de un cortafuegos de aplicaciones web (WAF), la protección de las bases de datos, el empleo de equipos o sistemas de seguridad de terceros y la redacción de consultas SQL infalibles.
Veamos un ejemplo de prevención de inyecciones SQL en Python empleando una de las medidas de seguridad mencionadas.
Ejemplo de Python
En este ejemplo, el atacante utilizará una inyección SQL ciega basada en booleanos para obtener información importante del sistema.
Python: Vulnerable
Supongamos que hay una tabla llamada "datos_de_muestra" en la base de datos. Esta tabla almacena los nombres de usuario y las contraseñas de los usuarios de la aplicación.
Ahora permita al usuario encontrar un valor de esta tabla de base de datos mediante los siguientes comandos:
import mysql.connector
db = mysql.connector.connect
#Mala práctica. ¡Evita esto! Esto es sólo para aprender.
(host="localhost", user="newuser", passwd="pass", db="sample")
cur = db.cursor()
name = raw_input('Enter Name: ')
cur.execute("SELECT * FROM sample_data WHERE Name = '%s';" % name) for row in cur.fetchall(): print(row)
db.close()
Inyección SQL
Aquí, si el usuario introduce un nombre en la búsqueda, por ejemplo, Alicia, no habrá ningún problema con la salida.
Sin embargo, si el usuario introduce algo como Alicia'; DROP TABLE sample_data; afectará significativamente a la base de datos.
Python: Remediación
La sentencia SQL debe cambiarse por la siguiente para evitar que se produzca el ataque:
cur.execute("SELECT * FROM datos_de_muestra WHERE Nombre = %s;", (nombre,))
Ahora, el sistema tratará la entrada del usuario como una cadena, incluso si el usuario intenta inyectar cualquier consulta SQL en ella, y tratará la entrada del usuario sólo como el valor del nombre.
Este sencillo cambio puede evitar actividades maliciosas en futuras consultas y proteger el sistema de ataques de entrada de usuario.
Ejemplo Java
Para este ejemplo, también utilizaremos una tabla de base de datos llamada "sample_data" que almacena los datos de usuario de la aplicación.
Una página de inicio de sesión básica toma un nombre de usuario y una contraseña y el archivo java, que es un servlet (LoginServlet), los valida contra la base de datos para permitir la operación de inicio de sesión.
Java: Ejemplo de vulnerabilidad
Utilizando la tabla "sample_data" de la base de datos, el sistema permite a los usuarios realizar operaciones de inicio de sesión tomando sus credenciales como entrada.
Hay una consulta en el archivo LoginServlet para acomodar la operación de inicio de sesión, que es:
//Bad Example. Do not use string concatenation.
String query = "select * from sample_data where username='" + username + "' and password = '" + password + "'";
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);
if (rs.next()) {
// Login Successful if match is found
success = true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {}
}
if (success) {
response.sendRedirect("home.html");
} else {
response.sendRedirect("login.html?error=1");
}
}
A continuación se muestra la consulta para el inicio de sesión de usuario:
select * from datos_de_muestra where nombre_usuario='nombre_usuario' and contraseña ='contraseña'
Inyección SQL
El sistema funcionará perfectamente si la entrada es válida. Por ejemplo, diremos que el nombre de usuario es Alicia otra vez, y la contraseña es secreta.
El sistema devolverá los datos del usuario con estas credenciales. Sin embargo, un atacante puede manipular la petición del usuario usando Postman y cURL para inyección SQL.
Por ejemplo, el hacker puede enviar un nombre de usuario ficticio ( Alicia) y la contraseña 'o '1'='1'.
En este caso, el nombre de usuario y la contraseña no coincidirán, pero la condición '1'='1' siempre será verdadera, por lo que la operación de inicio de sesión se realizará correctamente.
Java: Prevención
Para prevenirlo, necesitamos modificar el código de LoginValidation y utilizar PreparedStatement en lugar de Statement para la ejecución de la consulta. Este cambio evitará concatenar el nombre de usuario y la contraseña en la consulta y los tratará como datos setter para evitar la inyección SQL.
A continuación se muestra el código modificado para LoginValidation:
String query = "select * from sample_data where username=? and password = ?";
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
stmt = conn.prepareStatement(query);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
success = true;
}
rs.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {
}
}
En este caso, el PreparedStatement, los setters y la API JDBC subyacente se encargarán de la entrada del usuario y evitarán la inyección SQL.
Ejemplos
Ahora veremos algunos ejemplos más en varios idiomas para entender mejor cómo se ve esto en acción.
C# - Inseguro
Este ejemplo es inseguro debido al uso de `FromRawSql`. Este método no vincula los parámetros ni intenta escapar de ellos. Como tal, este método debe evitarse a toda costa.
var blogs = context.Posts
.FromRawSql("SELECT * FROM Posts WHERE state = {0} AND author = {1}", state, author)
.ToList();
C# - Seguro
Este ejemplo es seguro gracias a `FromSqlInterpolated`, que toma los valores interpolados y los parametriza.
Aunque esto es generalmente seguro, corre el riesgo de ser muy similar a `FromRawSql` que no es seguro.
var blogs = context.Posts
.FromSqlInterpolated($"SELECT * FROM Posts WHERE state = {state} AND author = {author}")
.ToList();
Java - Seguro: Hibernate - Named Query + Native Query
Hibernate ofrece dos métodos para construir consultas de forma segura: `Native Query` y `Named Query`. Ambos permiten especificar ubicaciones para los parámetros.
@NamedNativeQuery(
name = "find_post_by_state_and_author",
query =
"SELECT * " +
"FROM Post " +
"WHERE state = :state" +
" AND author = :author",
resultClass = Post.class)
java
List<Post> posts = session.createNativeQuery(
"SELECT * " +
"FROM Post " +
"WHERE state = :state" +
" AND author = :author" )
.addEntity(Post.class)
.setParameter("state", state)
.setParameter("author", author)
.list();
Java - Seguro: jplq
Anotando un atributo `Query` en una interfaz de repositorio jplq, Pueden tomar múltiples formas, y son parametrizadas.
@Query("SELECT p FROM Post p WHERE u.state = ?1 and u.author = ?2")
Post findPostByStateAndAuthor(String state, int author);
@Query("SELECT p FROM Post p WHERE u.state = :state and u.author = :author")
Usuario findPostByStateAndAuthor(@Param("state") String state, @Param("author") int author);
Javascript - Seguro: pg
Cuando se utiliza la biblioteca `pg`, el método `query` permite la parametrización proporcionando valores de parámetros a través de su segundo parámetro.
const { posts } = await db.query('SELECT * FROM Post WHERE state = $1 AND author = $2', [state, author])
Javascript - Seguro: Sequelize
La biblioteca `sequelize` proporciona una forma de parametrizar una consulta a través de su segundo argumento, que toma parámetros para la consulta. Esto incluye una lista de valores para vincular a la consulta como un parámetro, ya sea por nombre o índice.