Ejemplo de Inyección de SQL

El siguiente ejemplo que hubo en la plática de seguridad en aplicaciones Java fue de inyección de SQL. Este ataque es posible cuando se capturan datos y se concatenan directamente dentro de una sentencia SQL, sin validarse; este método de hecho también es el fundamento de los ataques XSS (cross-site scripting).

En el caso específico de SQL, es muy sencillo solucionarlos. Pero primero veamos un ejemplo de código vulnerable a este ataque. Supongamos que tenemos un método para validar un usuario, que se invoca en un panel de login:

 

La idea es que si pasamos por ejemplo   y   como parámetros, la cadena SQL queda así:

 

Pero, ¿qué pasa si pasamos como parámetros   en username y   en password? Pues la cadena de SQL quedaría así:

 

Lo cual va a devolvernos todos los registros de la tabla usuario, ya que aunque no haya un usuario con username x y password en blanco, la condición de cadena vacía siempre se cumple. El código vulnerable va a obtener el primer registro y creará el usuario con esa info, devolviéndolo al código que seguramente al ver que el usuario no fue nulo, lo deja entrar al sitio, crea su sesión etc. Con esto ya fue secuestrada una cuenta de usuario (no se sabe cual). Para secuestrar una cuenta específica simplemente en el password se puede enviar   y con eso podriamos entrar como el usuario admin.

La manera de resolver esto es muy sencilla. JDBC ofrece los PreparedStatements que resuelven este problema tomando los datos como parámetros que van en ciertas partes del SQL y eso se pasa directamente al servidor de base de datos quien ya los utiliza de una forma más directa, sin tener que concatenarlos en el SQL. De modo que nuestro código queda ahora así usando PreparedStatement:

 

Con este simple cambio, ahora si mandamos   y   como usuario y password, se va a buscar exactamente eso, y solamente se tendrá entrada al sistema si existe un usuario llamado   y su password es  .

Por increíble que parezca, este es un problema muy grave en lenguajes como PHP donde es muy común que se concatene el SQL para invocarlo, y por más que se le han puesto parches al lenguaje, siguen existiendo sistemas en producción con código que hace concatenaciones para SQL porque fue escrito con versiones previas de PHP donde no existían las soluciones que tienen ahora para usar parámetros en SQL.

Pero lo peor es que en Java también de repente se puede uno encontrar código así, a pesar de que JDBC desde sus inicios ha tenido el PreparedStatement.

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

Re: Ejemplo de Inyección de SQL

Antes que nada una saludo y una disculpa por no quedarme hasta el final de la platica por compromisos personales.

También es importante mencionar que no toda la gente usa SQL para el manejo de su seguridad, generalmente se hace uso de un repositorio LDAP corporativo donde se administran los perfiles y los roles de todos los usuarios dentro de la empresa. En este caso también existe un ataque igual de peligroso que el SQL inyeccion, el ataque LDAP inyeccion donde la filosofía del ataque es la misma que en (sql, binary, remote code) inyeccion. por lo cual es importante tenerla en cuenta. Algunos frameworks de seguridad validan la mayoría de los ataques y sus variantes, inclusive al nivel de verificar que no viajen ofuscados en caracteres Unicode. Acegi es un buen ejemplo de ello, creo que ahora se llama Spring Security.

Algunos puntos que ese día ya no quise tocar para no hacer polémica y distraernos de la platica son:

** El que un usuario ejecute 20,000 veces un query en una base de datos no significa que el manejador de la base ejecute los 20,000 comandos, eso depende del manejador ya que por ejemplo Oracle maneja un cache temporal para las consultas realizadas recientemente.

** PreparedStatement no el 100% infalible ya que depende de la implementacion del Driver y del Manejador de la base de datos, aqui un ejemplo de como hacer un SQL Inyection a pesar de usar PreparedStatement

Saludos

Imagen de ezamudio

Autenticación != seguridad

El ejemplo es usando un método de login, pero se puede inyectar SQL en cualquier query que no use PreparedStatements parametrizados sino concatenación de SQL con datos capturados sin validar.

No mencioné un problema con que un usuario ejecute 20mil veces un query; tal vez te refieres a la parte de validar los resultados de consultas potencialmente largas. Ahí me refiero a que un usuario lea 20mil registros. Ahí también la base de datos usa caches y al siguiente usuario que pida esos mismos 20mil registros (o un subconjunto de ellos, etc) le va a responder mucho más rápido porque los tiene en cache. Ese no es el problema, el problema es que seguramente el primer usuario lee 20mil registros que se convierten en 20mil instancias de Map o de alguna clase de Java si se usa un ORM; el siguiente usuario que pida 20mil registros va a tener otros 20mil objetos para representar los mismos datos; el tercer usuario va a crear otros 20mil registros. Hay casos en que es inevitable, hay casos en que se puede remediar con caches como OScache (que Hibernate puede usar por ejemplo), cuando son objetos inmutables; hay veces que simplemente se le puede avisar al usuario que su consulta tiene demasiados resultados.

En cuanto al ataque de SQL injection con PreparedStatements que mencionas, no me extraña pero ni tantito que sea el driver de MySQL el que tiene esa debilidad...

Re: Autenticación != seguridad

El problema de manejar resultados muy largos es muy común ya que ningún ser humano puede ser capaz de manejar tal volumen de datos, para esos casos se pueden hacer paginaciones de lado del servidor, pero para esto nuevamente dependemos del manejador. Oracle por ejemplo si soporta el uso de cursores para poder pedirle mediante la variable ROWNUM el set de registros del 5100 al 5200 en determinada consulta, cosa que en otros manejadores es imposible hacerlo y del lado del cliente como bien comentas es un desperdicio de memoria. El uso de caches como Ehcache y demás es buena practica para acelerar nuestras aplicaciones, solo que hay que tener en cuenta si tenemos un ambiente distribuido hay que implementar un cache distribuido, o si las afectaciones a la base vienen de diferentes aplicativos y plataformas ya no es fiable el cache, al menos que se implemente a una capa mas cercana de la BD como por ejemplo Times ten in Memory Database de oracle (Chales hasta parezco vendedor de esos monos) . En general felicidades por la exposición y creo que cumplió con su objetivo ya que muchos de los miembros no conocían la cara de la seguridad en el desarrollo de aplicaciones y se fueron con un grato sabor de boca. Y como dices la seguridad es una carrera armamentista y hay que tratar estar al día.

Imagen de ezamudio

queries largos

En general ya las bases de datos aceptan LIMIT y OFFSET en sus queries, que es más recomendable. Supongamos que esta consulta te da 20mil registros:

 

Si avientas ese query a la base de datos, va a obtener todos los registros y te los va a empezar a devolver. Pero ya preparó lo necesario de su lado para darte todos esos resultados. Si en tu aplicación tienes un ciclo donde vas leyendo registros en tu ResultSet pero estás paginando y digamos que quieres obtener la tercera página (de 100 en 100) entonces te saltas los primeros 200 registros con puro   y luego lees 100 registros y luego simplemente cancelas el ResultSet, lo cierras, etc. El problema de esto es que la base de datos ya tenía listos los 20mil registros, no importa que ya no se los pidas. Si en cambio envias este query:

 

Eso te va a devolver solamente los 100 registros que necesitas y el servidor de base de datos solamente alojará los recursos necesarios para darte esos 100 registros. De esa manera puedes paginar consultas con muchos resultados. El problema es cuando no estás mostrando datos en una página web o aplicación interactiva sino que estás generando un reporte en PDF o Excel...

ROWNUM y LIMIT/OFFSET

Desafortunadamente cada manejador de base de datos tienes sus propios Features particulares y complementan el ANSI SQL que si es soportado por cualquier manejador que merezca ese nombre.
En el caso de Limit y Offset no todos lo soportan, igual que rownum, pero sirven para lo mismo, es decir el query

SELECT * FROM tabla_grande WHERE fecha>'2008-01-01' AND tipo=1 LIMIT 100 OFFSET 200;

En Oracle se escribiria

SELECT id, fecha, tipo FROM
(SELECT id, fecha, tipo, ROW_NUMBER() AS row_num
FROM tabla_grande WHERE fecha>'2008-01-01' AND tipo=1 )
WHERE row_num > 200 AND row_num <= 300

Hibernate soporta la paginacion en Oracle y wrapea el código como el query anterior.

En Sql Server hay que ingeniarselas mas para poder hacer algo por el estilo ya que no tiene ni rownum ni limit ni offset

Saludos

Re: queries largos

En general ya las bases de datos aceptan LIMIT y OFFSET en sus queries, que es más recomendable.

Care to explain?

Saludos

Javier

Sobre LIMIT y WHERE

Si avientas ese query a la base de datos, va a obtener todos los registros y te los va a empezar a devolver....

LIMIT y OFFSET no impiden el procesamiento de las filas. La razón es que LIMIT y OFFSET sólo dan resultados consistentes si se utilizó ORDER BY, entonces de todas maneras hay que ordenar esos 20,000 registros. Si hay un índice, qué bueno, si no, hay que hacer un "full scan" de todo el producto cartesiano para entregar un resultado más o menos consistente.

El problema de esto es que la base de datos ya tenía listos los 20mil registros, no importa que ya no se los pidas.

Las 20,000 filas *ya* fueron procesadas por la base de datos. Lo que LIMIT garantiza *no* es que no se procesen todas las filas (por la razón que ya expliqué) sino que no se *devuelvan* al cliente más de cierto número de filas, aunque el cliente las solicite.

Saludos

Javier Castañón

Edit: De hecho, es el predicado WHERE el que realmente limita el número de filas procesadas en un query. Y esto es relevante, pues hay gente que se "autoataca" no usando dicha cláusula y recuperando todas las filas de un query, iterándolas en Java hasta que encuentra la fila buscada. La existencia de material educativo como el publicado por ezamudio es más que bienvenida para tenerlo como referencia.

Imagen de ezamudio

LIMIT

De acuerdo con lo del ORDER BY, se me fue incluirlo pero es muy cierto. Aparte ya nos estamos metiendo a cosas de índices y performance en base de datos, se sale del tema un poco pero lo quería enfatizar es que la diferencia entre usar LIMIT/OFFSET/ROWNUM y saltarse registros + cortar el query desde Java es esto:

1. Con LIMIT/OFFSET/ROWNUM la base de datos procesa los 20mil registros pero solamente prepara los 100 que se obtienen como resultado y es lo que le entrega al proceso que los pidió (nuestra aplicación Java). Si se hace esto dentro de una transacción que va a bloquear los registros leidos por ejemplo, solamente se bloquean los 100, no los 20mil.
2. Sin usar esa opción, la base de datos procesa los 20mil registros y los prepara para entregarlos a la aplicación. Aunque la aplicación solamente lea 100 registros y cierre el ResultSet y hasta la conexión, ya se usaron recursos en la base de datos (memoria, CPU, disco, etc) para tener listos esos 20mil registros para devolverlos a la aplicación. Si se hizo dentro de una transacción que bloquea los registros leidos, se bloquean los 20mil; y aunque se liberan cuando se cancela y cierra el ResultSet, ya se bloquearon desde antes de entregarlos.

Era todo. Es obvio que LIMIT/OFFSET no son sustitutos del WHERE, no estaba tratando de decir eso (de hecho el ejemplo que puse usa WHERE primero solito y luego WHERE + LIMIT/OFFSET). Me faltó el ORDER BY, que es muy importante para asegurar que siempre se vayan obteniendo los registros en el mismo orden.

Imagen de jali

En informix

Nadamas para contribuir. En informix no existe el offset(que yo sepa jeje) y cambia un poco la sintaxis. En este caso se utiliza:
 
Donde el SKIP nos indica cuantos registros nos saltaremos y el limite es el num de registro al que se llegara en el query

Saludos

Imagen de ezamudio

SKIP == OFFSET

SKIP en Informix tiene entonces el mismo efecto que OFFSET en PostgreSQL (y segun yo tambien jalaba en SQL Server)

Imagen de jali

Gracias

ahh gracias por el datos de esos motores de BD, es bueno saberlo.

Acerca de Ejemplo de Inyección de SQL

Que buena información. Yo personalmente ya había leído algo acerca del tema, pero me parece que aquí se aborda el tema muy bien con los ejemplos expuestos. Gracias por compartir la información.

Saludos!

Diferencias entre bases de datos

Por lo que respecta a base de datos, tómalo como heurísticas, no como reglas a seguir, pues cada base de datos tiene su propia implementación. ¿Es más recomendable usar LIMIT y OFFSET si la base de datos lo soporta? Depende. En Oracle, que no los soporta, se reemplaza su uso por ROWNUM, que no requiere el uso de un predicado ORDER BY para aplicar ROWNUM. Si se procesa un millón de registros y se solicitan sólo los 50 primeros, Oracle efectivamente ordena sólo 50 registros, no un millón. Bonito, ¿no? (el predicado ORDER BY sí es obligatorio sin embargo para hacer paginación). En cambio, otras bases de datos, deberán ordenar el millón de registros, lo cual seguramente desbordará sus buffers de procesamiento de cursores y generará muchísimo I/O; aunque se limite el número de filas a devolver al cliente, el daño ya está hecho, por ejemplo, de la documentación de Postgresql 8.3, sección 7.6. LIMIT and OFFSET:

The rows skipped by an OFFSET clause still have to be computed inside the server; therefore a large OFFSET might be inefficient.

En bases de datos que emplean versiones para los cambios en las filas (MVCC), como Oracle, Postgresql, Firebird o H2, las lecturas no bloquean, aún en una transacción (salvo que se utilice SELECT FOR UPDATE por supuesto), lo cual no viene gratis, tiene por supuesto un precio.

SQL Server no soporta (al menos hasta la versión 2005) ni LIMIT, ni OFFSET, ni ROWNUM.

Saludos

Javier Castañón

Jajaj me recordó

Jajaj me recordó XKCD:

Traducción:

1.
Escuela:- Hola, hablamos de la escuela de su hijo, estamos teniendo un problema de "computadoras"
2.
Mamá:- O cielos, rompió algo?
Escuela:- De cierta forma
3.
Escuela:- Realmente le puso a su hijo: Robert' ); Drop table Students; - - ?
Mamá:- Oh, si, el pequeño "Bobby tables" le decimos.
4.
Escuela:- Bueno, pues hemos perdido los registros de todos los estudiantes de este año, espero que esté feliz!!!
Mamá:- Y yo espero que hayan aprendido a sanitizar sus entradas a la base de datos!

Imagen de paranoid_android

Paginación de query

Dejo una entrada de blog para este tema como traducir querys de una base de datos a otra
piedra roset de querys

Imagen de paranoid_android

Paginación de query

Para evitar una inyección SQL también se puede evitar que el usuario ingrese los caracteres:
' comilla simple: Es un delimitador de cadena para sql se puede transformar como '' para que sql lo interprete como comilla simple
- guion: Indica que es comentario
o equivalentes en cada base de datos.

Imagen de ezamudio

inhibir caracteres

Evitar caracteres o escaparlos es una medida muy poco efectiva y no te da seguridad. Evitas que un O'Neil o un Gordon-Levitt o alguien con apellidos similares se puedan registrar, mientras que un atacante puede escribir el equivalente Unicode o darle la vuelta de otra forma para enviar la comilla. Si usas los Statements con parametros usando el ? ya le puedes pasar el texto que quieras con guiones y comillas y lo que sea y asi se va a guardar en la base de datos.

Esto de inhibir o escapar algunos caracteres no sirve y ya ha quedado demostrado mas de una vez con PHP y MySQL, donde los primeros parches que salieron de PHP para tratar de aliviar tanto problema de inyeccion de SQL que hay en tantos sistemas de PHP, fue poner funciones para escapar comillas y guiones y cosas asi, y se demostro que no sirve porque se le puede dar la vuelta y lo unico que se logra es incomodar a usuarios legitimos.

Imagen de paranoid_android

Caracteres escapados

Pienso que en esfuerzos de seguridad informatica siempre hay que estar al día.
Una pequeña regla o mejora quizás no es significativa por si sola pero en conjunto el sistema se hace cada vez más seguro.

Tendrías que usar métodos de seguridad más robustos como bloquear cuentas al 3 intento.
Servidores Proxy de peticiones.
Cambio periódico de passwords, cuentas que expiran, etc.
Mandar tramas de auditoria al log y registros de auditoría en base de datos y tener alguien que este revisando para ver si alguien se trató de saltar la seguridad.

Y en efecto se vuelve una carrera armamentista, por cada politica nueva hay una manera nueva de saltarla.

Imagen de ezamudio

Cambio periódico de passwords

Nos salimos del tema porque ya no tiene nada que ver con inyección de SQL todo lo que mencionas, pero en particular para lo del cambio periódico de passwords quiero mencionar que ya está comprobado que esa medida no solamente NO incrementa la seguridad sino que al contrario, la puede debilitar más. La gente que usa malos passwords no va a empezar a usar buenos passwords porque la obliguen, y cuando por fin tengan un buen password, lo van a tener que cambiar después de un tiempo. Y la gente que usa buenos passwords no puede estar creando buenos passwords todo el tiempo, si los obligas a cambiarlo, tarde o temprano eligirán un mal password porque ya se hartaron de tener que inventar uno cada X tiempo.

Los servidores proxy de peticiones no sé en qué incrementan la seguridad, o a qué te refieras. En lo demás (logs, monitoreo, bloquear cuentas al 3er intento fallido) estoy de acuerdo, aunque lo del login es más delicado porque el sólo bloquear cuenta al tercer intento crea la oportunidad para un ataque de negación de servicio. Por ejemplo si en este sitio bloqueamos la cuenta al tercer intento y ya, entonces para bloquearte y no permitirte usar más el sitio solamente tengo que intentar entrar con tu cuenta hasta que el sistema la bloquee. Lo importante es tener un buen sistema de desbloqueo y eso depende de la naturaleza del sistema, no hay remedio universal.

Imagen de paranoid_android

RE: Cambio periódico de passwords

En el cambio de passwords digamos ... la cuenta después de X meses no puedes acceder a menos que cambies tu password.

He visto reglas similares a estas para passwords.

Mínimo de 8 caracteres,
Usar Números y Letras
Usar Mayúsculas y minúsculas
No repetir letras
Uso de algunos símbolos permitido

Seguramente hay más

En cuanto a un "tipo de servidor proxy" no es un proxy hablando en términos de redes es en términos de que es un servidor que se ve en internet y atrás de el existen N cantidad de servidores que si atienden peticiones, pero filtran tramas que pueden ser maliciosas como una especie de filtro interceptor e impiden conocer la dirección interna de los aplicativos.

Y para retomar el tema de la inyección SQL también existen nuevas modalidades como inyección de json o inyección de parámetros manipulando una trama.

Imagen de truenosilencioso

Evitar el Injection SQL

Cordial saludo muy interesantes sus aportes. Ahora bien el objetivo es "evitar el injection Sql" y hasta el momento lo que plantean es resolverlo mediante perfección en el código, más pensando de nuevo en el objetivo considero que se puede abordar desde otra perspectiva, es decir no desde el código sino desde la lógica, me explico para iniciar sesión o loguearse se puede hacer por medio de un teclado virtual donde introduzcamos los caracteres de nuestro login, por supuesto en dichos caracteres no aparecería el : ' , por lo que de una manera inteligente esatriamos cuumpliendo con el objetivo, además se puede realizar el proceso de login en dos paso: 1 se introduce el username y luego de que este sea validado se carga una nueva pantalla donde se pide el password. Les invito a ver este sitio
que tiene estas contramedidas y que ayude a mejorar después de hackear... mejor dicho después de mostrarles un montón de debilidades al personal de seguridad del banco. aprecio sus observaciones al respecto

Imagen de ezamudio

Distintos problemas

No evitas inyección de SQL con teclados virtuales porque te los puedes saltar por completo haciendo tu propio cliente HTTP o HTTPS que directamente envía datos al servidor. Cualquier cosa que pongas en la página web con javascript, Flash, HTML5, etc solamente funciona para la gente que usa la página de manera legítima; un atacante que vea la estructura de la página, los campos dentro del FORM, etc, podrá armar un HTTP POST a mano para enviarlo directamente, y si no se están limpiando estos datos ya en el servidor, vas a tener problemas.

Imagen de truenosilencioso

Distintos problemas

Cordial Saludo ezamudio, primero deseo felicitarte por los aportes tan interesantes que haces y la calidad de los mismos. Me interesaría muchísimo que anexaras un ejemplo de la técnica que mencionas:"armar un HTTP POST a mano para enviarlo directamente"

En cuanto a lo de evitar la inyección de Sql con java, yo lo que hago es preparar ua sentencia con PreparedStatemente y luego le doy los values del login asi;
try {
String consulta = "select user,pwd from usuarios where user = ? and pwd = ? ";
PreparedStatement cmd = conexion.prepareStatement(consulta);
cmd.setString(1, txtUser.getText());
cmd.setString(2, TxtPwd.getText());

rs = cmd.executeQuery();
if (rs.next()) {
JOptionPane.showMessageDialog(null, "Bienvenido " + txtUser.getText() + ", Puedes ingresar", "Inicio de Sesión", JOptionPane.WARNING_MESSAGE);
//out.println("Successfully logged in");
txtUser.setText(rs.getString(1));
TxtPwd.setText(rs.getString(2));
} else {
intentos++;
JOptionPane.showMessageDialog(null, txtUser.getText() + ", No estás registrado como usuario\n" + "ó La contraseña está mal escrita\n" + intentos + " Intentos Fallidos", "Inicio de Sesión", JOptionPane.QUESTION_MESSAGE);
//out.println("Username and/or password not recognized");

}
if (intentos > 2) {
JOptionPane.showMessageDialog(null, txtUser.getText() + ", verifique bien su contraseña \n" + "ya que está mal escrita\n" + "3 Intentos Fallidos, este programa se cerrará", "Inicio de Sesión", JOptionPane.ERROR_MESSAGE);
System.exit(0);
}
} catch (SQLException e) {
JOptionPane.showMessageDialog(null, e.getMessage(), "Error en la conexión con la BD",
JOptionPane.ERROR_MESSAGE);
}
Como puedes ver, incluso cuento los intentos y al tercero hago que se cierre el frame; no obstante creo que pueda existir alguna forma de hackear el preparedStatement, tu que opinas y que bueno que me dieras un ejemplo de lo que mencionaste.
Estaré atento a tu comentario