6 may 2011

Inyecciones SQL, qué son, cómo funcionan y cómo deben filtrarse

Buenas a todos, por petición expresa de @Zipus hoy vamos a dedicar la entrada a las inyecciones SQL, hablaremos sobre qué son, cómo funcionan y cómo deben filtrarse en PHP.

Hoy en día las inyecciones SQL no son nada nuevo, pero si que hay aún numerosos desarrolladores que las desconocen, mismamente la semana pasada lo discutía con un conocido, desarrollador en una conocida empresa, el cual no solo desconocía ésta vulnerabilidad, si no también otras como los XSS, XPath Injection, Blind, etc. Pero por desgracia, el hecho de este compañero del sector no es un hecho aislado, y así lo certifica la última actualización del documento generado por SANS y MITRE, donde analizaba los 25 errores de programación más peligrosos cometidos por los desarrolladores:

http://cwe.mitre.org/top25/archive/2010/2010_cwe_sans_top25.pdf

En el documento comentan que los XSS encabezan la lista de los errores más comunes, seguidos inmediatamente en segundo lugar por las SQL Injection. Por lo que parece que todavía no existe demasiada concienciación sobre el tema ¿no pensáis?

Comencemos comentando que son las SQL Injection para los begginers en el tema. Pongamos como ejemplo que tenemos un portal Web en el que hay una sección privada para la que se requiere una autenticación mediante usuario y contraseña. El portal Web, programado en PHP tira contra una BBDD MySql donde se encuentran almacenadas en una tabla llamada "T_usuarios" todos los usuarios administradores del sistema con su contraseña. Por lo que tendríamos un escenario como el siguiente, en el que se supone que el sistema tiene un administrador llamado "Carlos" con la contraseña "123abc.":

 

Imaginemos ahora que tenemos en la página PHP de autenticación la siguiente query con la que intentaremos recuperar los usuarios que coinciden con el usuario y contraseña proporcionados por el usuario:

query = "SELECT * FROM T_usuarios WHERE nombre = '" . $nombreUsuario . "' and password = '" . $passwordUsuario . "'";
Donde las variables nombreUsuario y passwordUsuario contienen el valor introducito en los siguientes textbox:
<form id="form1" name="form1" method="post" action="login.php">User: <input type="text" name="user" id="user">Pass: <input type="password" name="pass" id="pass"><input type="submit" name="aceptar" value="Log in"></form>
Pongamos ahora que un usuario malicioso inserta el siguiente valor en cada cajetin del textbox:
' or '1'='1
Ocurriendo lo siguiente:Es decir la query quedaría de la siguiente manera:
query = "SELECT * FROM T_usuarios WHERE nombre = '' or '1' = '1' and password = '' or '1' = '1';

Con esto estamos diciéndole a la BBDD que queremos que nos devuelva todos los usuarios cuyo nombre sea igual a '', es decir, a vacío, esta sentencia nunca ocurrirá porque no existen usuarios con nombre vacío en la BBDD. La siguiente sentencia "or '1'='1', está anulando a la sentencia anterior gracias al operador "or", y dando el poder a la siguiente afirmación, que está indicando una sentencia que se va a cumplir siempre, que 1 es igual a 1. Lo mismo ocurre con la password.

En resumen, vamos a seleccionar todos los usuarios de la tabla "T_usuarios" Sí SU nombre es igual a vacío ó SÍ 1=1, y como la segunda sentencia es verdadera siempre, devolverá TODOS los usuarios de la BBDD. ¿Correcto no?

Por lo que si nuestro sistema de autenticación en el código PHP es algo tan simple como lo siguiente (donde no importa ni el tipo de usuario), nos habremos colado en el sistema:

$query = "SELECT * FROM T_usuarios WHERE nombre='".($user)."' AND password='".($pass)."'";$resultado = mysql_query($query);if(mysql_fetch_row($resultado)){$_SESSION['conectado']=1;}

Ahora pongamos otro caso, imaginemos que el desarrollador "es zurdo" y le gusta "programar las cosas de izquierdas" =)

query = "SELECT * FROM T_usuarios WHERE '" . $nombreUsuario . "' = nombre and '" . $passwordUsuario . "' = password";

Pues de la misma manera intentamos inutilizar las igualdades de la variable nombreUsuario con la columna nombre y de la variable passwordUsuario con la columna passwrord con la siguiente query:

1' = '1' or '
Otro ejemplo interesante y muy ilustrativo es el siguiente. Imaginemos que un usuario mete en el cajetín del usuario cualquier cosa y en el cajetin de la password la siguiente query:
algo';DROP TABLE T_usuarios;SELECT password FROM T_clientes where nombre='Carlos

En primer lugar se ejecutaría la selección del usuario para el login como ya se ha visto antes, en segundo lugar eliminaría la tabla T_usuarios, y en tercer lugar (y por finalizar la query para que no de error porqué quedó una comilla suelta. También podríamos haber comentado el resto del código con "--") seleccionaría, por ejemplo, la contraseña del cliente Carlos:

query = "SELECT * FROM T_usuarios WHERE nombre = 'algo' and password = 'algo';DROP TABLE T_usuarios;SELECT password FROM T_clientes where nombre='Carlos';

Pero no todo en las inyecciones SQL son las comillas, imaginamos que tenemos la siguiente query:

query = "SELECT * FROM T_usuarios WHERE id=" . $idUsuario;

El programador está suponiendo que la variable $idUsuario va a contener un número entero con el id de un usuario. Pero alguien podría insertar algo como lo siguiente:

1 or '1'='1'

Quedando la siguiente query:

query = "SELECT * FROM T_usuarios WHERE id=1 or '1'='1'";

Es decir, se seleccionarían todos los usuarios SI el id=1 o SI 1=1, y como la segunda sentencia es verdadera, devolverá todos los usuarios de la BBDD.

Ahora bien, ¿qué podemos hacer los desarrolladores para protegernos?

De una manera estándar a todos los lenguajes la solución sería validar SIEMPRE los datos de entrada y parametrizar las sentencias SQL, para que los trozos de código insertados por los usuarios no sean interpretados como código, si no únicamente tomados como el tipo de datos (números o letras) que esperamos que el usuario introduzca. Si por ejemplo queremos que el usuario introduzca un id numérico, podríamos "castear" la variable donde se asigna el texto del cajetin del textbox con un (int) para así verificar que no podrá meter texto.

Por ejemplo, para el caso concreto de PHP y MySql, como nos proponía @zipus, se utilizaría la siguiente función, que coloca barras invertidas antes de los siguientes caracteres para evitar la inyección: \x00\n\r\,'"\x1a:

mysql_real_escape_string($variable)
Quedando la query de la siguiente manera:
$query = "SELECT * FROM T_usuarios WHERE nombre='".mysql_real_escape_string($user)."' AND password='".mysql_real_escape_string($pass)."'";

Hace un tiempo, hasta PHP 5.3.0 si no me equivoco, existía una funcionalidad llamada magic quotes, que se encargaba de poner delante de las comillas simples y dobles una barra invertida, y así evitar la inyección. Esta operación era automática, sin necesidad de llamar a ninguna función, por lo que no era necesario llamar manualmente a la función addslashes() para anteponer la barra invertida a una comilla. Sin embargo, no ha sido muy querida esta funcionalidad, ya que si portas la aplicación PHP a otro server con otra configuración de las magic quotes puedes tener problemas. Y al revés, si controlas manualmente las comillas con addslashes() y portas la aplicación a un server con las magic quotes, podrían darse casos de que antes de una comilla se pusiesen dos barras invertidas, lo que podría hacer que se guardase un dato en la BBDD como \'.

Así que una manera de filtrar más estándar podría ser la siguiente, bloqueando el daño que puedan causar las magic quotes:

function filtrar($variable){    // Este if se encargará de retirar las barras en caso de que las comillas mágicas estén habilitadas    if (get_magic_quotes_gpc()) {        $variable = stripslashes($variable);    }    if (!is_numeric($variable)) {        $variable= "'" . mysql_real_escape_string($variable) . "'";    }    return $variable;}

NOTA: get_magic_quotes_gpc devuelve 1 si las magic_quotes están habilitadas y 0 si no lo están. En caso de que estén habilitadas, automáticamente se coloca una barra invertida \ antes de una  comilla simple, doble comilla, nulos, etc. Es decir, que si después hiciesemos un mysql_real_escape_string, estaríamos poniendo dos barras invertidas \\ antes de las camillas simples, dobles, nulos, etc. Por ello utilizamos la función stripslashes() que elimina las primeras barras invertidas que hayan podido poner las magic_quotes, para que solo se coloquen las de mysql_real_escape_string().

Nos quedaría la query de la siguiente manera:
$query = "SELECT * FROM T_usuarios WHERE nombre='".filtrar($user)."' AND password='".filtrar($pass)."'";

Para despedirme os dejo con éste enlace que me pasó hace algún tiempo Oca con algunas inyecciones curiosas:

http://websec.wordpress.com/2010/03/19/exploiting-hard-filtered-sql-injections/

Eso es todo por hoy, espero que os haya gustado el post.

Saludos!

12 comentarios:

  1. Una publicación casi tan importante como desayunar!Gracias juanan, pero imagino que esto no terminara aqui o si ??Saludos!

    ResponderEliminar
  2. Gracias @anbuitachi17. En principio terminaba aquí, pero si os gusta el tema intentaré profundizar más y extenderlo a otros lenguajes de programación y BBDD. A ver si tengo tiempo para escribir un poquito más! =)saludos

    ResponderEliminar
  3. Exelente articulo, igual que todos los que posteas!, ahora me surge una duda, estuve haciendo sql injection para probar en sitios que son vulnerables a traves de parametros GET. Mi duda es la siguiente: como sería la query en sitios que toman el valor desde el GET?, ya que si por ej el sitio toma ?id=1, y esa vble numerica no está bien validada, entonces en teoría si pongo ?id=1 -- debería dar error ya que estoy comentando todo el codigo que sigue, incluyendo el ";" que cierra la consulta, incluso lo mismo si toma una vble ?category=games, si yo le pongo ?category=games -- no veo ningun error, a pesar de que estaría comentando el resto, lo mismo si trato de hacer ?category=games'; -- ... ahi me indica que la consulta sql no es validaTe molestaría mucho explicarme un poquito como sería el tema de la validación por parametros GET?Gracias!!!

    ResponderEliminar
  4. Gracias @LeChabon!Si no muestra mensaje de error podría tratarse de un caso de Blind Sql Injection.Prueba a poner algo como lo siguiente (seguiría sin tener que devolverte error):?id=1 or 1=1 y luego pon lo siguiente (te debería devolver un mensaje, o cambiar algo el comportamiento de la página):?id=1 or 1=2Si esto ocurre, efectivamente es un caso de inyección a ciegas.Comentas aparte el caso de ?category=games --, que no te da error y si pones ?category=games’; — sí te da error. ¿Imagino que no sabes como está construida la select de la BBDD que devuelve los valores de la categoría "games" verdad?, para poder analizar detenidamente el caso concreto. El tema de la validación de parámetros GET debería ser exactamente la misma que para los valores introducidos en un textbox. Si por ejemplo nos encontrasemos en el caso de leer noticias de un períodico virtual, en el que cada noticia sea identificada por un id numérico pasado por GET y obtenido de la siguiente manera:$id = $_GET['id'];Deberíamos verificar SIEMPRE que el parámetro introducido sea un número, por ejemplo con:if (!is_numeric($id)){ Mostrar mensaje de error por pantalla y bloquear la ejecución}saludos!

    ResponderEliminar
  5. mmm creo que voy entendiendo, ahora mi consulta es la siguiente, tengo un sitio web que se que es vulnerable a sql-i porque me da un resultado, puntualmente esta para ejemplificar alguna (http://bit.ly/ko4UHi), si nos fijamos toma por variable el parametro ?id=news ... si ponemos una comilla al final (quedando ?id=news' ) tira error de sentencia sql, por lo que detecto que es vulnerable a injection.. ahora si pongo lo siguiente ?id=news' order by 5 -- ; ... en ese caso me ejecuta bien la consulta, puedo ver que no tiene 5 columnas, si pruebo con 4 la pagina carga normal por lo que se que tiene 4 columnas esa tabla... ahora mi duda es la siguiente... por qué si no pongo el ";" luego de los "--" para comentar no anda la consulta, si supuestamente estaría comentado ese ";"... lo mismo para las inyecciones en sitios donde levanta de un ?id=1 y yo introduzco por ej ?id=1 select 1,2,3,4,5 -- ... si comento el resto de la consulta con los "--" estaría tambien comentando el ";" que tiene embebido la query desde el codigo no? como puede ser que ande comentando tambien eso que indicarìa el fin de la sentencia?, por ahí me estoy mareando solo, si pudieras sacarme esa duda te lo agradecería mucho!, y mil gracias por la forma de validar el parametro por GET

    ResponderEliminar
  6. [...] Inyecciones SQL, qué son, cómo funcionan y cómo deben filtrarse [...]

    ResponderEliminar
  7. La verdad que no caigo ahora mismo en porqué ese ; puede hacer que la query funcione, tendría que analizarlo más en profundidad, pero de todas maneras mi consejo es que pruebes los conceptos en sitios web preparados para ello como badstore, ya que es un delito inyectar código sin el permiso del dueño del sitio Web...saludos

    ResponderEliminar
  8. genial!, hay algun sitio en el que pueda hacer XSS y demás para probar legalmente? ya que lo unico que quiero es aprender para justamente poder defenderme bien de eso... muchas gracias por el consejo!

    ResponderEliminar
  9. En la badstore puedes probar XSS, incluso XSS persistente desde una seccion donde puedes añadir comentarios estilo foro.Saludos

    ResponderEliminar
  10. [...] Inyecciones SQL, qué son, cómo funcionan y cómo deben filtrarse [...]

    ResponderEliminar
  11. Usen store procedures en sus BD y creen usuarios que solo ejecuten esos sp asi sera mas facil evitar el sql injection y claro diseñen rutinas para validar sus querystrings y sobre todo validaciones para sus inputs que si bien sabemos existen muchas ya hechas con jscript nunca esta demas usarlas

    ResponderEliminar
  12. Hola, soy muy muy nuevo, y no se acceder al código PHP.... :( ayuda por favor.

    ResponderEliminar