Ataques SQL Injection

30. January 2013 19:15 by Oscar.SS in Desarrollo Empresarial, Desarrollo Web  //  Tags: ,   //   Comments (4)

Como desarrolladores tenemos una gran responsabilidad sobre la seguridad de las aplicaciones en las que participamos y creo que no pensamos en ello (yo incluido) con la debida frecuencia o el debido respeto. Permitirme que me ponga un poco drástico. Cuando desarrollamos una aplicación sobre nóminas, una aplicación sobre informes médicos, o cualquier otro tipo de información sensible, lo que está en juego no es solo la integridad de la aplicación, detrás hay usuarios, hay vidas de personas.

Mi intención al escribir este artículo no es enseñaros nada nuevo sobre el tema, hay mucho escrito en internet y en libros sobre SQL Injection. El propósito de este artículo es que como desarrolladores tomemos conciencia de esta responsabilidad y por otro lado conocer los conceptos básicos de este tipo de ataques.

Estoy seguro que el lector, el que más o el que menos, conoce los peligros de un ataque por inyección de SQL. De hecho, la organización OWASP (Open Web Application Security Project) coloca esta vulnerabilidad la primera de la lista en su Top 10 de riesgos en aplicaciones web.

Las vulnerabilidades por inyección de SQL se caracterizan por tener un vector de ataque muy sencillo, el atacante simplemente envía texto a un intérprete. Así mismo es una vulnerabilidad frecuente y dependiendo de los casos, no es difícil detectarla. La última característica a mencionar es que tienen un impacto muy grande en las aplicaciones atacadas.

Tal y como se aprecia en la tabla anterior (Risk Rating Methodology) los ataques por inyección no solo afectan a las consultas de SQL. También pueden ser atacadas por inyección las consultas LDAP (Lightweight Directory Access Protocol), XPATH o comandos del sistema operativo entre otros. En este artículo centraremos nuestro interés en los ataques por inyección de SQL.

 

Introducción

En primer lugar, hablaremos de algunas versiones con las que se puede ejecutar este tipo de ataque, la mejor forma de "vacunarse" es sin duda el conocimiento, comprender cómo y porque es posible este tipo de debilidad en nuestras aplicaciones.

Anteriormente adelantamos que el vector de ataque sobre esta vulnerabilidad consiste en enviar un simple texto al intérprete de SQL. Es decir, todo dato, y repito otra vez, todo dato enviado a nuestra aplicación por un usuario, ya sea este humano o electrónico, es susceptible de contener código SQL que podría modificar el comportamiento esperado de nuestra aplicación. Por lo tanto, cualquier información que nuestra aplicación esté esperando desde fuera, debe tomarse como potencialmente peligrosa.

Para los ejemplos de código de inyección de SQL que vamos a ver a continuación, tomaremos como ejemplo el típico formulario de login que el usuario malintencionado podría usar como un posible punto de entrada a nuestra aplicación. Por lo tanto, los datos o información potencialmente peligrosa en este escenario, serán tanto el email del usuario como la contraseña.

Como es lógico, y por motivos puramente pedagógicos, para todos los ejemplos que veremos a continuación se ha supuesto que en la capa de acceso a datos existe un código de servidor que no previene contra este tipo de ataques. Podría ser algo parecido a lo mostrado a continuación.

            string query "SELECT * FROM Employees WHERE Email = "
                
"'" + email + "'"
                
" AND [Password] = "
                
"'" + pass + "';";

            using 
(var conn = new SqlConnection(ConnString))
            {
              
using (var cmd = new SqlCommand(query, conn))
              {
                cmd.CommandType 
CommandType.Text;
                
conn.Open();

                using 
(var reader cmd.ExecuteReader())
                {
                    
while (reader.Read())
                    {
                        
// etc...
                    
}
                    reader.Close()
;
                
}
              }
            }

 En este caso, la consulta ejecutada en la base de datos tendría el siguiente aspecto.

SELECT FROM Employees WHERE Email 'NombreEmpleado' and [Password] 'ContraseñaEmpleado';

Seguidamente comprobaremos las diferentes vías que podrían utilizarse desde fuera de nuestra aplicación para modificar el comportamiento de esta consulta. ¿Estáis preparados para poneros en la piel del atacante?

 

Comprobar si es vulnerable

Una forma rápida de comprobar si una aplicación es vulnerable a inyecciones de SQL podría ser enviar al intérprete una comillas simple ( ' ) para terminar la instrucción en ese momento y que todo lo demás sea código no ejecutable por el intérprete de SQL y esto provoque un error. Veamos un ejemplo.

Campo email = meloinvento y campo contraseña = '

Consulta construida:

SELECT FROM Employees WHERE Email 'meloinvento' AND [Password] ''';

He señalado en color Gold (según mi editor WYSIWYG) la parte de la consulta que el intérprete de SQL puede entender. Es decir, en esta parte se está comprobando que Email sea igual a 'meloinvento' y que Password sea igual a '', lo que es totalmente correcto gramaticalmente aunque no devolvería ningún resultado.

Pero al final de la consulta todavía tenemos una pequeña fracción de código ', que el intérprete no puede ejecutar correctamente. Lo que sin duda provocará un error en el intérprete de SQL y por extensión en la aplicación.

En este caso, el como la aplicación gestione este tipo de errores es lo de menos. Si nosotros como atacantes recibimos una amigable pantalla de error, igualmente habremos conseguido nuestro propósito, averiguar si la aplicación es susceptible de ser atacada.

 

Introducir una instrucción siempre True

Una forma típica de inyectar SQL en una aplicación y que seguramente el lector ya conoce es cuando inyectamos una instrucción siempre verdadera consiguiendo que la instrucción WHERE siempre se cumpla y nos devuelva siempre resultados sin conocer los datos auténticos.

Campo email = meloinvento y campo contraseña = ' or '1'='1

Consulta construida:

SELECT FROM Employees WHERE Email 'meloinvento' AND [Password] '' or '1'='1';

Quiero llamaros la atención aquí sobre el juego de comillas simples que se ha realizado. Comenzando nuestro código malicioso con una comilla simple hemos cerrado el contenido de la variable Password, y como al terminar nuestro código malicioso sin una comilla simple hemos aprovechado una comilla simple que seguramente, así lo hemos supuesto, exista ya en el código de la aplicación. De esta forma la instrucción comprueba que FirstName sea igual a 'meloinvento' y que Password sea igual a '', o que 1 sea igual 1 lo que siempre es cierto y nos permite entrar en el sistema.

Nota: si la aplicación consume datos desde MySQL bastaría con incluir OR 1 -- x. Con esto quiero mostraros que aunque para este artículo se ha elegido SQL Server, los conceptos son los mismos para cualquier tipo de base de datos.

 

Con datos numéricos

Hemos visto algunos sencillos ejemplos cuando las variables a atacar son de tipo alfanumérico, y como hemos podido comprobar el meollo del asunto consiste en ir jugando con las comillas simples que espera el intérprete de SQL. Pero cuando los datos son de tipo numérico, supongamos que la contraseña lo es, si no se tiene especial cuidado podemos tener total libertad para ejecutar instrucciones completas en nuestro sistema. También podría ser una comprobación por el campo ID o por cualquier otro concepto numérico.

Campo email = 'meloinvento' y campo contraseña = 00000; insert into employees (firstName, password) values ('Oscar', 85493)

Consulta construida:

SELECT FROM Employees WHERE Email 'meloinvento' AND [Password] 00000
insert into e
mployees (email, password) values ('Oscar'85493);

Como se puede observar realmente se han enviado dos instrucciones SQL al intérprete, una que comprueba un supuesto empleado y otra que directamente inserta un nuevo empleado en la aplicación permitiendonos tener acceso a la misma.

Evidentemente este tipo de ataque es mucho más difícil de realizar de lo que parece. Para empezar, deberíamos conocer el nombre de la tabla, aunque siempre se pueden probar nombres razonables como Users, Employees, Clients, etc, veremos más tarde como atajar este problema.

Otra dificultad añadida es que la tabla normalmente contendrá más campos que no podrán ser nulos, lo que provocará un error en la aplicación al no poder ejecutarse correctamente la instrucción SQL. Y como siempre, dependiendo de como estén gestionandos este tipo de errores, podríamos recibir información sobre el error y poco a poco afinar con la inyección de SQL. Sea como sea, es algo de lo que tenemos que ser conscientes como desarolladores.

 

Averiguar el número de columnas de la tabla

Como bien sabe el lector, la instrucción ORDER BY también puede ser utilizada como si de un array se tratara y en lugar de pasarle el nombre de la columna podemos pasarle un valor entero con la posición que ocupa la columna en la tabla empezando desde 1.

SELECT FROM Employees WHERE LastName 'Gomez' ORDER BY 2

Esta consulta ordena los resultados ascendentemente por la segunda columna en la tabla. Ahora bien, podríamos ir probando distintos valores (3, 4, 5, etc) y cuando la aplicación lance un error sabremos que nos acabamos de pasar por arriba en el número de columnas de la tabla.

Campo email = meloinvento y campo contraseña = ' ORDER BY 7; --

Consulta contruida:

SELECT FROM Employees WHERE Email 'meloinvento' AND [Password] '' ORDER BY 7--';

En el caso que nos ocupa esta consulta lanzaría un error del siguiente tipo.

Anteriormente ya he mencionado que no importa que no veamos esta información, es suficiente con que la aplicación muestre una agradable pantalla informandonos del error, igualmente sabremos que la tabla tiene 6 columnas.

 

Averiguar el nombre de una columna

La idea en este caso es ir probando nombre lógicos o esperables para los nombre de las columnas en un consulta que se ejecuta satisfactoriamente, es decir, al contrario que antes, cuando no recibamos un error, sabremos que hemos acertado.

Campo email = meloinvento' or firstname = ''; --

Consulta contruida:

SELECT FROM Employees WHERE Email 'meloinvento' or firstname ''; --' AND [Password] = '';

Si esta cosulta se ejecuta sin mostrarnos un error sabremos que hemos acertado con el nombre de la columna, en caso contrario nos tocará seguir provando otros nombres.

 

Averiguar el nombre de una tabla

Igual que antes provaremos distintos nombre para el nombre de la tabla y sabremos que el nombre es correcto cuando la consulta se ejecute satisfactoriamente.

Campo email = meloinvento' or 1 = (select count(*) from Employees); --

Consulta contruida:

SELECT FROM Employees WHERE Email 'meloinvento' or 1=(select count(*) from Employees); --' AND [Password] = '';

Fijaros en que nos importan en absoluto si la segunda condición que hemos introducido se cumple. Nos da lo mismo si la tabla tenga 1 o 100 filas, lo que nos interesa aquí es que la consulta es gramaticalmente correcta y por lo tanto hemos dado con el nombre de una tabla.

Ahora bien, de momento sabemos que en el sistema existe una tabla Employees y lo mismo podríamos hacer con otras tablas si conocemos un poco el negocio de la aplicación que estamos asaltando. Pero también podemos averiguar si el nombre de la tabla que hemos adivinado se está utilizando en la consulta actual.

Campo email = meloinvento' and employees.email is null; --

Consulta construida:

SELECT FROM Employees WHERE Email 'meloinvento' and employees.email is null; --' AND [Password] = '';

Esta forma de especificar el nombre de la columna Email en la segunda condición, solo funciona cuando el nombre de la tabla especificado es el mismo que se utiliza después de la cláusula FROM.

 

Enviar la contraseña de un usuario

Es habitual que antes de comenzar ningún ataque ya conozcamos el email de algún usuario resgistrado en el sistema. Ya sea porque se trata de una red social y el usuario es un conocido nuestro o de nuestros conocidos. O simplemente porque en la propia aplicación web exista en la zona de contacto uno o varios emails de usuarios adminstradores del sistema para poder contactar con ellos en caso de problemas, sugerencias o dudas.

Supongamos también que la aplación web tiene el típico enlace "Si no recuerdas la contraseña haz click aquí" en la que se nos pedirá un email para enviarnos la contraseña automáticamente por correo electrónico. Procedamos!!.

Campo email = meloinvento'; update employees set email = 'miEmail@hacker.com' where email = 'emailConocido@hostweb.com'; --

Consulta contruida:

SELECT FROM Employees WHERE Email 'meloinvento'
update 
employees set email 'miEmail@hacker.com' where email 'emailConocido@hostweb.com'--' AND [Password] = '';

Ahora solo tendremos que ir al formulario donde se nos pide el email para enviarnos la contraseña e introducir el email malicioso para al de unos minutos obtener la contraseña de este usuario.

 

Averiguar información del esquema

Cambiemos un poco el escenario del crimen. Imaginaros que hemos conseguido entrar en la aplicación con las credenciales de otro usuario/empleado. Al navegar por la aplicación nos encontramos con una página donde se muestra un listado de clientes del empleado que hemos suplantado.

Como podemos observar el listado permite realizar una búsqueda por el código postal del cliente que mostrará los resultados para el empleado que entró en la aplicación. Con lo que sabemos hasta ahora (los clientes pertenecen a un empleado específico y los clientes se buscan por código postal) podríamos suponer que la consulta tendría un aspecto parecido al mostrado a continuación.

SELECT FROM NombreTabla WHERE NombreColumna identificadorEmpleado AND OtraColumna 'codigoPostalCliente';

SQL Server al igual que MySQL proporciona información del esquema de las bases de datos por medio de la tabla de sistema INFORMATION_SCHEMA. Te recomiendo que ejecutes en cualquiera de tus bases de datos las dos consultas siguientes para que puedas observar la información que contienen.

SELECT FROM INFORMATION_SCHEMA.TABLES;

SELECT 
FROM INFORMATION_SCHEMA.COLUMNS;

Intentemos ahora averiguar el nombre de todas las tablas de la base de datos.

Campo búsqueda = ' union select 1, 2, table_name, 4, 5, 6, 7 from information_schema.tables; --

Consulta construida:

SELECT FROM Customers WHERE EmployeeID '6' AND PostalCode '' 
union select 12, table_name, 456from information_schema.tables--';

Las consultas que usen la clausula UNION (también INTERSECT y EXCEPT) deben tener el mismo número de columnas en las dos SELECT. Por ese motivo en la segunda SELECT se han añadido los índices ordinales de varias columnas para rellenar. Fijaros también que la columna que contendrá los nombres de las tablas, table_name, se encuentra en 3er lugar. Hay que hacerla coincidir con una columna que admita valores de texto y hemos supuesto que las dos primeras columnas contenían identificadores y por lo tanto eran probablemente de tipo numérico.

Al pulsar sobre el botón de búsqueda obtenemos la información esperada.

En la base de datos existen dos tablas, Employees y Customers, además de un diagrama de base de datos. Podríamos intentar hacer lo mismo con la información de las columnas de toda la base de datos.

Campo búsqueda = ' union select 1, 2, table_name, column_name, ordinal_position, data_type, is_nullable from information_schema.columns; --';

Consulta construida:

SELECT FROM Customers WHERE EmployeeID '6' AND PostalCode '' union 
select 
12, table_name, column_name, ordinal_position, data_type, is_nullable from information_schema.columns--';

El resultado cuando menos es espectacular.

Por order de aparición, de irquierda a derecha, vemos el nombre de la tabla, el nombre de la columna, el lugar que ocupa esta en la tabla, el tipo de datos que contiene la columna y si admite valores nulos. Mencionar por último que el poder ver la información del esquema de la base de datos depende de cómo estén configurados los permisos en el servidor de base de datos. No todos los usuarios tienen porque tener permisos para acceder a esta información.

 

Comentarios Finales

Evidentemente todo lo aquí visto ha sido posible gracias a un código de aplicación escrito a propósito con poca o ninguna consideración sobre estos temas, pero ha cumplido su cometido, que no era otro que didáctico. De todas formas, las inyecciones de SQL no son como el Unicornio Blanco, son muy reales. Durante la preparación de este artículo, el aquí presente, ha encontrado un par de webs reales susceptibles de ser atacadas con los conceptos que acabamos de estudiar.

Tener en cuenta que los ataques mostrados en este artículo quizás no han sido demasiado agresivos pero igualmente se podrían haber enviado sentencias para borrar registros, tablas o cualquier otra invasión relacionada con el negocio de la aplicación. Comentar también que por supuesto existen técnicas mucho más sofisticadas de este vector de ataque, aquí hemos visto quizás las más elementales.

Espero que encontréis de utilidad, o al menos de interesante, el artículo y sobre todo que os haya hecho pensar en esa gran responsabilidad que tenemos como desarrolladores sobre la seguridad de nuestras aplicaciones.

Comments (4) -

Eduardo Valle
Eduardo Valle
2/5/2013 3:50:19 PM #

Excelente articulo para tener muy en cuenta. y aplicar los correctivos necesarios

Oscar.SS
Oscar.SS
2/5/2013 5:32:37 PM #

Eduardo, me alegro de que sea de utilidad.

Ignacio
Ignacio
5/21/2013 9:04:19 PM #

Que tal Oscar, acabo de leer este artículo y realmente es muy interesante. Sabes si hoy en día con tecnologías como LINQ, es posible que haya ciertas vulnerabilidades?.

Óscar.SS
Óscar.SS
5/22/2013 12:13:18 AM #

Hola Ignacio,

Tengo pensado escribir un artículo sobre lo que has preguntado y no me gustaría estropear la sorpresa Wink

En principio los ORMs tienen sus defensas de serie para este tipo de ataques. Pero no existe la completa seguridad. Por ejemplo podrías estar ejecutando un procedimiento almacenado con parámetros desde un ORM.

msdn.microsoft.com/en-us/library/cc716760.aspx

Prometo escribir sobre ello en cuanto tenga tiempo.

Un saludo.

Recent Comments

Comment RSS

Month List