jueves, febrero 21, 2008

Solucionario Reto Hacking VI por Dani Kachakil - Parte I

***************************************************************************************
- Solucionario Reto Hacking VI por Dani Kachakil - Parte I
- Solucionario Reto Hacking VI por Dani Kachakil - Parte II
***************************************************************************************

Introducción

Este documento describe una solución al Reto Hacking VI de Informática 64, el primero de la segunda temporada, que se publicó el 1 de febrero de 2008 en la siguiente dirección web: http://retohacking6.elladodelmal.com

Pistas

Esta vez las pistas prometían ser útiles… Se publicaron unas horas antes del reto, adelantando la existencia de dos partes y diciendo lo siguiente:

Primera parte

Sé que cuando lleguéis a esta parte vais a estar un poco... descolacados. Como un piloto de Formula 1 con el coche cruzado en la salida. Ya sabéis lo que mi primo el nano piensa sobre qué se debe hacer para ganar, ya que nunca es algo hecho. Si fuera fácil no habría que hacer tanto trabajo. Sí, sé que el reto será fácil, pero ganar nunca.

Una vez lleguéis allí, debéis saber que yo me siento agradecido por todos los que os habéis sumado a esta edición del reto. Sé que restar tiempo a vuestras horas de ocio, trabajo y en definitiva vida dividiéndoos entre la familia y amigos y el Reto Hacking es un gran trabajo, pero espero que con este reto se multiplique un poco vuestro conocimiento sobre lo ya conocido con una forma nueva de trabajo.

Segunda parte

Para la segunda parte... la cosa está de partes otra vez. Deberéis agudizar la Vista en este examen de reflejos. El que tenga más reflejos lo sacará antes....

Parte 1: Un poco de práctica para aprobar el examen

Para acceder a la primera fase, lo primero que teníamos que hacer era registrarnos eligiendo nuestro nombre de usuario e introduciendo una dirección de correo válida, donde recibiremos la contraseña correspondiente generada aleatoriamente. Accediendo con estos datos en la página de login, observamos que cambia el menú de navegación, apareciendo tres nuevos elementos:

• Práctica: En este apartado encontramos un formulario con un campo de texto y un botón de enviar que nos invita a practicar con el álgebra de Garrupito, preguntándonos cual es el valor de 1000+1.

• Examen: Aquí encontramos un formulario similar al anterior, preguntándonos por el valor de Garrupito.

• Subir nota: Para poder subir nota, primero tendremos que aprobar el examen. (Esta sección corresponde a la segunda fase y se habilitará cuando tengamos aprobado el examen).

Tras echarle un vistazo a las secciones disponibles, decidimos empezar por el principio, practicando un poco. La primera prueba necesariamente tenía que ser 1001, por ser la más lógica y evidente, pero no conseguimos nada, al igual que tampoco sirve de nada interpretar el valor como binario, o el operador de suma como concatenación y otras pruebas de ese estilo. Probamos con diferentes cadenas de texto aleatorio, comillas simples y dobles, operadores lógicos (and, or, not, xor), matemáticos (+, -, *, /, ^), de manejo de cadenas (len, trim, rtrim) y comprobamos que todas ellas son procesadas con normalidad. De momento, podemos asegurar las siguientes observaciones:

• Al introducir un valor numérico o expresión booleana (ya sea verdadera o falsa), obtenemos como respuesta "Vamos, vamos, garrupintizate!" [R1]

• Al introducir cualquier cadena o expresión errónea, obtenemos como respuesta "ACCESS: Error al convertir valor numérico" [R2]

Llegados a este punto, Chema publica un post con una pista adicional (más bien insistiendo en la pista existente), lo que nos lleva a pensar que no vamos por buen camino, ya que si no interpretamos la pista adecuadamente, parece ser que no lograremos nada. Retomando la primera pista, deducimos inmediatamente que se trata del piloto de Fórmula 1 Fernando Alonso y según la nueva pista, tendremos que buscar unas declaraciones que nos indicarán el camino a seguir para ganar. Tras haber leído decenas de declaraciones del piloto, me quedé con unas que decían que había que dar el máximo, otras que hablaban de vueltas y otras en las que decía que había que ir siempre al límite, pero no sabía qué hacer con ello.

De todas formas, para mí fue fundamental el tercer post ("O resolvéis el misterio de la primera pista o vais a tener que hacer algo más de dos millones de peticiones algo así como...."). Directamente me vino a la cabeza el valor máximo que se puede almacenar en un entero de 32 bits con signo (al que llamaremos MAXINT), que no son dos millones, sino más de dos mil millones: 2147483647 (231 – 1). Al introducir este valor, obtenemos una nueva respuesta: "Uy que cerca, casi estás garrupintizado!" [R3]

Esto me llevó a pensar que iba bien, que había que buscar un límite pero que no era exactamente ese valor, así que fui probando inútilmente diferentes valores en busca de ese otro límite inexistente. En ese momento me di cuenta de que el mensaje de error contenía una pista clave (la palabra "ACCESS") y eso parecía indicar que ahí detrás existía un motor de bases de datos Access (Microsoft Jet) procesando nuestras peticiones, por lo que intentaremos encontrar la forma de verificar la existencia de este motor de base de datos, buscando algún mecanismo que nos diferencia dos tipos de respuesta para los valores verdadero y falso (proceso de "booleanización"), realizando posteriormente las subconsultas que nos interesen sobre ese mecanismo.

Como primer paso, ya vimos que obteníamos tres posibles respuestas según el tipo de entrada (valor numérico [R1], valor no numérico [R2] o MAXINT [R3]), por lo que únicamente nos falta relacionar un par de ellas con una subconsulta adecuada. Si hacemos caso a la pista, parece que debemos usar las respuestas R1 y R3, de forma que nuestra subconsulta devuelva un valor MAXINT si es verdadera y cualquier otro valor si es falsa (o viceversa). Por ejemplo, introduciendo la siguiente subconsulta:

2147483647 * (select max(1) from tabla)

La mayoría de funciones de agregado (MAX, MIN, AVG, etc) devuelven el valor nulo si no tienen datos de entrada (es decir, si la consulta no devuelve registros). Las operaciones matemáticas que involucran al valor nulo no provocan ningún error y devuelven también el valor nulo. Sin embargo, la función COUNT devolvería cero en ese caso. Para la consulta anterior, si la tabla tiene algún registro, la subconsulta devolverá 1, que multiplicado por el valor MAXINT lógicamente dejará intacto este valor y obtendremos la respuesta R3. Por el contrario, si la tabla no tiene registros, entonces la subconsulta devolverá el valor nulo, que multiplicado por MAXINT también devuelve el valor nulo y obtendremos R1. Si la tabla no existe, la consulta devolverá un error (R2), por lo que la existencia de una tabla se puede determinar fácilmente, simplemente ejecutando una consulta de este tipo. Una consulta equivalente a la anterior podría ser esta (en la que seleccionamos un solo registro, con valor 1):

2147483647 * (select top 1 1 from tabla)

Sobre este tipo de subconsultas también se podrían haber hecho otras variaciones que consiguen el mismo resultado, como utilizar una suma o una resta en vez de la multiplicación. Por ejemplo:

2147483646 + (select max(1) from tabla)
2147483648 – (select max(1) from tabla)


También existía otra posibilidad, que tal vez resultara más evidente y natural, ya que en este caso no necesitamos usar la pista, ni llegar a ningún tipo de valor especial. El método se basa en que una división por cero devuelve un resultado que no está definido como numérico (por lo que nos devolverá el mensaje del error R2), mientras que una división por cualquier otro valor no nulo, devolverá un número (es decir, como respuesta obtendremos el mensaje R1). Si la tabla no existe, o no tiene registros, esta consulta fallará:

1 / (select count(*) from tabla)

Otro tipo de variación de este método es el de provocar un desbordamiento, multiplicando un valor muy alto por cero (obteniendo R1 como respuesta) o por un valor mayor que uno (obteniendo R2). Por ejemplo:

(2^1023) * (select max(2) from tabla)

Habiendo concluido el reto, Chema me comentó que la consulta que ejecutaba el servidor estaba basada en la conversión al tipo numérico a través de la función de valor absoluto (ABS), lo cual me llevó a deducir que otro tipo de inyección también podría ser válido, siempre que no exista alguna mecanismo que escape o duplique los paréntesis (cosa que tampoco se hacía). Por tanto, esta consulta también funcionaría, siendo tal vez la más flexible de todas ellas:

0) or (select count(*) from tabla

Como última opción, también podríamos haber recurrido a las consultas pesadas, en las que no importa el mensaje que se nos devuelva, sino el tiempo que tarde el procesamiento de nuestra petición. De todas formas, este método es demasiado invasivo y no me parece razonable su uso en un reto ni en otro tipo de servidores, ya que el retardo se consigue consumiendo muchos recursos del servidor, teniendo como efecto colateral que todas las peticiones restantes (las de los demás participantes y usuarios en general) se demoren mientras se ejecuta nuestra consulta pesada. En el momento en que se lanza una consulta mientras se está ejecutando una consulta pesada, nunca sabremos cual de las dos es la que ha generado el retardo y el método deja de ser válido. Una consulta pesada no es comparable a ejecutar instrucciones del tipo waitfor o sleep (que en Access no tienen su equivalente), ya que éstas han sido diseñadas específicamente para dejar pasar el tiempo de espera indicado, pero sin consumir gran cantidad de recursos del servidor, por lo que su uso sería perfectamente válido.

Ahora que tenemos tantas posibilidades a nuestro alcance, vamos a resolver esta fase utilizando la que se suponía que teníamos que utilizar, es decir, la que hace uso del valor MAXINT, pero antes que nada nos aseguraremos de la existencia del motor de base de datos de Access. Sabemos que Access tiene algunas tablas de sistema que son accesibles por defecto. Dependiendo de la versión, nos podremos encontrar con las siguientes tablas: MSysAccessObjects (versiones 97 y 2000), MSysAccessStorage (versiones 2003 y 2007). Comprobamos que en nuestro caso tenemos éxito con la segunda prueba y de momento podemos suponer que vamos por buen camino:

2147483647 * (select max(1) from MSysAccessObjects) -> R2 (Error)
2147483647 * (select max(1) from MSysAccessStorage) -> R3 (Verdadero)


Recordemos que el examen consiste en introducir el valor correcto de Garrupito, por lo que todo apunta a que dicho valor se encuentra en la base de datos a la que tenemos acceso a través de la técnica de Blind SQL Injection descrita. Las bases de datos de Access no tienen catálogo accesible, por lo que tendremos que adivinar el nombre de la tabla y de sus campos. Sería razonable ejecutar este tipo de consultas:

2147483647 * (select max(1) from garrupito) -> R3 (Verdadero)
2147483647 * (select count(*) from garrupito) -> R3 (Verdadero)
2147483647 * (select max(1) from garrupito where garrupito > '') -> R2 (Error)
2147483647 * (select max(1) from garrupito where valor > '') -> R3 (Verdadero)


Ya tenemos la tabla (garrupito), el número de registros (1), el campo que nos interesa (valor) y su tipo (texto), así que ahora podemos aplicar esa técnica que ya conocemos, a la que hacía referencia la primera pista. Primero obtenemos la longitud del valor o número de caracteres, como siempre (búsqueda binaria):

2147483647 * (select max(1) from garrupito where len(valor) > 64) -> R1 (F)
2147483647 * (select max(1) from garrupito where len(valor) > 32) -> R3 (V)
2147483647 * (select max(1) from garrupito where len(valor) > 48) -> R1 (F)
2147483647 * (select max(1) from garrupito where len(valor) > 40) -> R1 (F)
2147483647 * (select max(1) from garrupito where len(valor) > 36) -> R1 (F)
2147483647 * (select max(1) from garrupito where len(valor) > 34) -> R1 (F)
2147483647 * (select max(1) from garrupito where len(valor) > 35) -> R3 (V)
2147483647 * (select max(1) from garrupito where len(valor) = 36) -> R3 (V)


Aunque la última consulta realmente es innecesaria, si hacemos el procedimiento a mano nunca está de más una comprobación adicional por si nos hemos equivocado en algún punto. Por último, iremos obteniendo el valor de cada carácter utilizando el mismo método, actuando sobre los valores ASCII. Siempre que se pueda, es preferible evitar una consulta con el operador LIKE, ya que éste no distingue entre las mayúsculas y las minúsculas.

2147483647 * (select max(1) from garrupito where asc(mid(valor,{i},1) > {Xi}

Una vez hayamos obtenido los 36 valores ASCII los reconvertimos de nuevo en caracteres para formar la cadena que buscamos. Este es el valor de Garrupito (como siempre, os dejo la parte final para que terminéis los deberes en casa):

9DB7D157-CD68-42a3-A366-C32BE377????

A pesar de su formato hexadecimal de 32 caracteres (que podría ser una hash MD5, por ejemplo), antes de intentar hacer algo raro con esa cadena vamos a probar lo más sencillo, es decir, introducirla directamente como respuesta a la pregunta del examen. Finalmente comprobamos que la solución era así de simple, teniendo en cuenta que teníamos que introducir la cadena exacta, respetando las mayúsculas (y cómo no, respetando también esa "a" minúscula que había por ahí). ;-)

Como comentario adicional, Palako descubrió que el valor MAXINT estaba almacenado en un registro de una tabla llamada "valores", en el campo "valor", lo cual también podría ser aprovechado para construir consultas algo más complejas.

***************************************************************************************
- Solucionario Reto Hacking VI por Dani Kachakil - Parte I
- Solucionario Reto Hacking VI por Dani Kachakil - Parte II
***************************************************************************************

8 comentarios:

Anónimo dijo...

desde el principio encontré la tabla valores y el campo valor, pero desde ahí me volví loco probando y buscanco cosas...

casualidad que el valor binario de 2147483647 fuese 1111111111111111111111111111111??
no sabéis las burradas que he llegado a intentar porque no me podía creer que fuese casualidad ese número binario, y encima el 1000 y el 1... ><
pero ahora que me has hablado de MAXINT lo entiendo :(

por cierto que usando el IIF de access todo se simplifica, con sentencias como iif(select yyy from xxx, valor, 0) se podía averiguar casi todo :)

creo que todos deberíamos colaborar y hacer una gran hoguera para quemar a chema xDDD

espero el reto vii :D

kabracity dijo...

gracias por el solucionario Dani y sobre todo porque...por fin entiendo la pista!!hasta Fernando Alonso y las declaraciones(creo que eran después de Silverstone) llegué, pero no me había fijado en lo de "llegar al límite" ¬¬.Chema, a veces las pistas son = de complicadas que los retos grrrrr!Como dice Dani, menos mal que diste la otra pista de 2 millones (que al final resultaron ser 2k millones xo weno, por ahí fuimos encaminados)

Brayan M. Blanco dijo...

yo probaba con el método "1) or (consulta" gracias al post-pista de hace unos días, pero he visto que mi error ha sido desconocer como funciona esto del access, pues probé la tabla MSysAccessObjects pero no MSysAccessStorage, y al devolverme el mensaje de error pensé que estaba haciendo algo mal en la construcción de la cadena... lo del max int ni se me ocurrió.

Gracias por el solucionario

penyaskito dijo...

¡Segunda parte ya!

Anónimo dijo...

ostias y cosas así porque no las enseñan en la asignatura de seguridad de las Ing. informáticas?? en lugar de hacer paridas...

aaaah coño, es verdad, la uni española es un negocio...

:(

Anónimo dijo...

Buen pues para la segunda parte la cosa es mas facil de lo que se piensa...

Lo primero que se te viene a la cabeza es alguna forma de esteganografia con PNG inventada con el chema pero luego de fijarte con mucha atencion ves que es un chiste...

Mis recomendaciones que vean con suma atencion el fichero especialmente bajo un editor hexadecimal... si sufren del mal de ojo como su servidor (xDDD) talvez les interese volver a sacar una copia de la imagen asi talvez se les abran los ojos.

Saludos

Anónimo dijo...

jaja k bueno!!! cada dia ando mas perdido... me he hexo todos los anteriores... para aprender.. y no he sabido ni empezar... mas cosas de estas debian enseñar en la carrera y menos matematicas!!! jaja muxas gracias x la sol!!

kabracity dijo...

la esteganografía en ficheros comprimidos(como el PNG) es algo más complicadilla que en un bmp por ejemplo...a mi fue lo que me llevó a comenzar por lo más común y básico.

Por cierto, como añadido a la indicación de g3org3_x, si no quieres mirar con tanta atención basta con echarle un vistazo a las especificaciones del formato PNG :P

Entrada destacada

Cibercriminales con Inteligencia Artificial: Una charla para estudiantes en la Zaragoza

Hoy domingo toca ir a participar en un evento, con una charla y una pequeña demo. Ahora mismo sí, así que el tiempo apremia, os dejo una cha...

Entradas populares