miércoles, 5 de septiembre de 2012

Stripe Capture The Flag: Web Edition - Solutions (Part III)

Hoy doy por finalizada la serie "Solucionario del Capture The Flag: Web Edition". A continuación voy a explicar los dos últimos niveles del reto 7 y 8.

En estos niveles al igual que en los anteriores, los iniciaré con una pequeña descripción que ofrecían en el reto, el código fuente para que podáis hacer las pruebas localmente y por último la forma en que resolví el reto, que no quiere decir que sea la única ni la mejor, os lo aseguro.

******************************************************************************
- Stripe Capture The Flag: Web Edition - Solutions (Part I)
- Stripe Capture The Flag: Web Edition - Solutions (Part II)
- Stripe Capture The Flag: Web Edition - Solutions (Part III)
******************************************************************************

image

Descripción: WaffleCopter is a new service delivering locally-sourced organic waffles hot off of vintage waffle irons straight to your location using quad-rotor GPS-enabled helicopters. The service is modeled after TacoCopter, an innovative and highly successful early contender in the airborne food delivery industry. WaffleCopter is currently being tested in private beta in select locations.

Your goal is to order one of the decadent Liège Waffles, offered only to WaffleCopter's first premium subscribers.

Log in to your account at https://level07-2.stripe-ctf.com/user-gcoiiqmuxq with username ctf and password password. You will find your API credentials after logging in. You may find the sample API client in client.py particularly helpful.

Código fuente: [Descarga_codigo_fuente_nivel_7]

Solución: Si abrimos el enlace que nos indican nos topamos con un formulario de login, que introduciendo las credenciales indicadas por el reto {“ctf” : “password”} nos redirige al siguiente portal:

image

El objetivo esta muy claro, nosotros disponemos de un usuario no premium y necesitamos realizar una petición de la “liege waffles” para la cual es necesario disponer de una cuenta premium.

Para hacer las peticiones es necesario utilizar la API “client.py” que nos ofrecen. Realizamos una prueba haciendo una petición con nuestro usuario y pidiendo una “liege waffle”:

image

Como respuesta, nos indican que el gofre pedido requiere usuario premiun, no iba a ser tan fácil..

Después de hacer unas cuantas peticiones contra el servidor intentando inyectar código, falsificar las peticiones, cambiando parámetros, etcétera, lo único que conseguimos es un log repleto de peticiones realizadas por el usuario 5. (Los logs son accesibles desde el portal principal a través del enlace “API Request logs”).

image

Si os fijáis el mecanismo que tienen de firmado de los paquetes es muy simple:

Parte del cliente:

- Se crea una petición con los campos necesarios (count, latitud, user_id, logitud, waffle) con el siguiente formato: “count=1&lat=37.351&user_id=5&long=-119.827&waffle=liege

- Se concatena a esta petición la password del usuario y se realiza un SHA1: SHA1(password + petición), en nuestro caso quedaría del siguiente modo: SHA1(“dL6MTExB6gCeKC”+”count=1&lat=37.351&user_id=5&long=-119.827&waffle=liege”) dando como resultado: 4cbe76bf6ade7c8ffa75d384c93ac254fe24b7a7

- Y se envía al servidor con el siguiente formato: “count=1&lat=37.351&user_id=5&long=-119.827&waffle=liege|sig: 4cbe76bf6ade7c8ffa75d384c93ac254fe24b7a7

Parte del servidor:

- Recibe la petición y extrae de su base de datos la password del user_id

- Realiza un SHA1 del password y la primera parte “count=1&lat=37.351&user_id=5&long=-119.827&waffle=liege

- Compara el SHA1 generado con el recibido, en caso de que sean iguales la petición del gofre es aceptada, de lo contrario es rechazada.

El mecanismo utilizado para la firma de las peticiones, en un principio es seguro si no sabemos el password de ningún usuario. Una posibilidad sería realizar peticiones realizando fuerza bruta en el password, pero esto es poco viable.

Después de mirar y mirar el código fuente en busca de alguna vulnerabilidad, se me vino a la cabeza un fallo que salió hará ya un par o tres de años en los algoritmos de HASH más conocidos en una situación concreta.

Se trata de la vulnerabilidad “Hash Length Extension Attacks” o también conocida como “Hash padding Attack”. A grandes rangos dicha vulnerabilidad permite calcular el hash resultante de (secreto + mensaje) sin conocer el secreto, únicamente conociendo el resultado de un hash anterior (secreto + mensaje’) y la longitud del secreto.

Imaginaros que tenemos un mecanismo en el cual para firmar un texto generado por un usuario se calcula el HASH del texto concatenándole un secreto, pues a través de esta vulnerabilidad es posible calcular el resultado de un nuevo HASH válido como si este hubiera firmado el usuario únicamente conociendo un HASH anterior y la longitud de su clave.

¿Os resulta familiar?, es exactamente el mecanismo de cifrado que disponemos en nuestra aplicación. Únicamente nos falta un mensaje anteriormente generado por un usuario premium, para así poder generar una nueva firma(hash) realizando una petición de un “liege waffle”.

Si recordáis disponíamos de un sistema de logs que nos informaban de los pedidos que habíamos hecho:

image

Si observamos la forma de llamar a nuestros logs, se está haciendo una petición con nuestro “user_id” al directorio “/logs”, por lo que vamos a modificar el valor en busca de logs de otros usuarios, a ser posibles premium.

image

Si modificamos el valor del “user_id” por 1 conseguimos acceder a las peticiones realizadas de este usuario (claro fallo de seguridad), de este modo ya tenemos un mensaje valido generado por el usuario. Ahora vamos a crear el nuestro.

Para generar nuestro mensaje utilizaré el script “sha-padding.py” (referencias), una prueba de concepto que se generó para probar la vulnerabilidad.

image

Para hacer funcionar “sha-padding.py” es necesario añadirle la longitud del secreto (14), el mensaje anterior (count=10&lat=37.351&user_id=1&logn=-119.827&waffle=eggo), el hash del mensaje anterior (4053ae88…) y lo que se desea añadir al mensaje (&waffle=liege).

Hemos añadido una nueva variable de “waffle”, de este modo la petición se hará a la última variable añadida. Por si os lo preguntáis, la longitud del secreto es posible extraerlo del código fuente de la aplicación, ya que hace un random de 14, si este no lo conociéramos, podríamos haber realizado fuerza bruta de la longitud y probar los distintos mensajes. Una vez ejecutado nos devuelve el nuevo mensaje que deberemos enviar y la firma del mismo.

Modificamos el cliente.py para siempre envíe el nuevo mensaje con la nueva firma:

image

Y si lanzamos una nueva petición contra la aplicación mediante el cliente:

image

Hemos conseguido falsificar la una petición del usuario 1 mediante la vulnerabilidad “Hash padding Attack” y obtener la password que nos da acceso al nivel 8 “qkviWfMeqD”.

Referencias:

- https://blog.whitehatsec.com/hash-length-extension-attacks/ (Explicación de la vulnerabilidad)
- http://force.vnsecurity.net/download/rd/sha-padding.py (Script de la PoC)
- http://force.vnsecurity.net/download/rd/shaext.py (Dependencia del script)
- http://force.vnsecurity.net/download/rd/sha.py (Dependencia del script)

 

image

Descripción: Welcome to the final level, Level 8.

HINT 1: No, really, we're not looking for a timing attack.

HINT 2: Running the server locally is probably a good place to start. Anything interesting in the output?

Because password theft has become such a rampant problem, a security firm has decided to create PasswordDB, a new and secure way of storing and validating passwords. You've recently learned that the Flag itself is protected in a PasswordDB instance, accesible at https://level08-3.stripe-ctf.com/user-roolhuajbw/.

PasswordDB exposes a simple JSON API. You just POST a payload of the form {"password": "password-to-check", "webhooks": ["mysite.com:3000", ...]} to PasswordDB, which will respond with a {"success": true}" or {"success": false}" to you and your specified webhook endpoints.

(For example, try running curl https://level08-3.stripe-ctf.com/user-roolhuajbw/ -d '{"password": "password-to-check", "webhooks": []}'.)

In PasswordDB, the password is never stored in a single location or process, making it the bane of attackers' respective existences. Instead, the password is "chunked" across multiple processes, called "chunk servers". These may live on the same machine as the HTTP-accepting "primary server", or for added security may live on a different machine. PasswordDB comes with built-in security features such as timing attack prevention and protection against using unequitable amounts of CPU time (relative to other PasswordDB instances on the same machine).

As a secure cherry on top, the machine hosting the primary server has very locked down network access. It can only make outbound requests to other stripe-ctf.com servers. As you learned in Level 5, someone forgot to internally firewall off the high ports from the Level 2 server. (It's almost like someone on the inside is helping you — there's an sshd running on the Level 2 server as well.)

To maximize adoption, usability is also a goal of PasswordDB. Hence a launcher script, password_db_launcher, has been created for the express purpose of securing the Flag. It validates that your password looks like a valid Flag and automatically spins up 4 chunk servers and a primary server.

Código fuente: [Descarga_codigo_fuente_nivel_8]

Solución: A este reto podría titularle “El gran fracaso del CTF” (al final de la solución lo entenderéis), pero bueno, ya no se puede hacer nada.

Como bien dice en el segundo consejo, este reto hay que pasárselo primero en local y luego en remoto. Por lo que ejecutamos la aplicación localmente.

Ataque al servidor local:

image

Tal y como nos indican en la descripción, al ejecutar la aplicación este crea cuatro “chunk_server” que almacenaran cada uno de ellos un trozo de la password inicial “123456789012”, (chunk_server_1 = “123”, chunk_server_2 = “456”, chunk_server_3 = “789”, chunk_server_4 = “012”).

Si lanzamos diferentes peticiones contra el servidor preguntando por distintas contraseñas:

image

Observamos como nos devuelve un mensaje erróneo en caso de ser la password incorrecta, y un mensaje correcto en caso de ser la password buena. Si observamos que pasa en el servidor:

image

Vemos como el server divide en cuatro partes la password y le envía a cada “chunk_server” su trozo correspondiente para que este verifique si es el correcto, en caso de no ser el correcto el “chunk_server” enviaría un mensaje erróneo y ya ni se preguntarían a los demás.

Una de las cosas que me llamó más la atención era el concepto de “webhook” a la hora de hacer peticiones, por lo que vamos a generarnos un script en python que escuche en un puerto y lo asignamos como “webhook” en las peticiones.

image

La respuesta que obtenía el “webhook” era directamente algo similar a que obteníamos al enviar la petición. En este momento se me ocurrió que pasaría si enviamos distintas peticiones con distintas combinaciones. Por ejemplo: (3 peticiones con la respuesta incorrecta “111111111111”, 3 peticiones con un solo trozo correcto “123111111111”, 3 peticiones con dos trozos correctos “123456111111” y así sucesivamente), para esto cree un script en python que mediante urllib enviara las peticiones.

image

Si os fijáis, la única diferencia que existe entre las respuesta es el puerto de origen. Después de analizarlo, es posible observar como las primeras tres respuestas tiene una diferencia de 4 puertos, las siguientes tres, 5 puertos, las siguientes tres, 6 puertos y las siguientes tres, 7 puertos. Esto es debido a que la respuesta es enviada por “chunks_servers” distintos:

- Diferencia de 4 puertos = Respuesta del “chunk_server” 1

- Diferencia de 5 puertos = Respuesta del “chunk_server” 2

- Diferencia de 6 puertos = Respuesta del “chunk_server” 3

- Diferencia de 7 puertos = Respuesta del “chunk_server” 4

Por lo que, podemos modificar el script para cuando detecte que el puerto cambia de 4 a 5, nos indique la petición que se había realizado y esto querrá decir que hemos encontrado el primer “chunk” correcto. Y así con los distintos “chuncks”, veamos un ejemplo.

image

Se puede observar como se ha detectado que en la petición “123” se ha cambiado el puerto, por lo que ya sabemos como sacar la password!!

En los siguientes enlaces podéis descargar los scripts cliente_local.py y webhook_local.py utilizados.

Ataque al servidor en producción:

Ahora solo nos queda ejecutar directamente los scripts contra el servidor en producción. Pero tenemos un problema, el servidor solo permite conexiones salientes a servidores de su propio dominio, por lo que, no podemos recibir peticiones a nuestro “webhook”.

Recordando la descripción, se hablaba de que en el servidor del nivel 2 (recordad que es el servidor que dispone de un formulario de subida de ficheros y disponíamos de una webshell), se encuentra en ejecución el servicio SSH. Si conseguimos hacernos con el servicio, tendremos resuelto el problema ya que estaremos actuando directamente desde dentro del dominio.

Si intentamos realizar una conexión contra el servicio:

image

Nos salta un error de “Permissino denied” debido a que el servicio SSH solo admite conexiones mediante clave pública. Así que tenemos que generarnos una par de claves en local para el usuario del nivel 2 “user-qnriiemgks” y subir la clave pública (mediante la webshell) al directorio “/home/user-qnriiemgks/.ssh/authorized_keys” de este modo ya tendremos acceso al servicio SSH desde nuestra máquina.

Una vez accedido al servicio SSH y subidos los scripts nos encontramos con un gran problema, a continuación se puede observar una ristra de peticiones erróneas y los puertos que devuelven.

image

No puede ser… Los puertos ya no son saltos de 4 en 4!!! Los puertos ya no siguen ningún orden concreto. Esto es debido a que el servidor 8 está siendo utilizado por más gente para hacer peticiones contra el mismo servicio, por lo que, los puertos origen de las respuestas van cambiando continuamente.

Si os fijáis, existen algunas respuestas (marcadas en rojo) que si que siguen un orden de saltos de dos en dos, estas peticiones son las que nosotros hacemos sin que nadie se interponga entre nuestras peticiones. Así que hemos de modificar los nuestros scripts para que puedan convivir con las interferencias generadas.

Para resolver este problema se me ocurrió un sencillo algoritmo de descartes de peticiones mediante la compartición de un fichero (posiblemente sea una aberración para más de un programador, pero había que hacer algo rápido :))

image

Con esta implementación conseguía ir descartando peticiones que vayan emparejadas de dos en dos, hasta llegar un momento que solo me quedara una petición que sería la buena.

Vamos en busca del primer chunk (trozo del password) en el servidor en producción:

Aquí podemos ver al cliente funcionando:

image

Y Aquí al WebHook descartando peticiones:

image

Y al cabo de aproximadamente 25 minutos, lo tengo, el primer “chunk” del password es “169”. Solo nos quedan tres.

Vamos en busca del segundo chunk (trozo del password) en el servidor en producción:

Aquí podemos ver al cliente funcionando:

image

Y al cabo de aproximadamente 25 minutos, lo tengo, el segundo “chunk” del password es “726”. Solo nos quedan dos.

Vamos en busca del tercer chunk (trozo del password) en el servidor en producción:

Aquí podemos ver al cliente funcionando:

image

Y al cabo de aproximadamente 25 minutos, lo tengo, el tercer “chunk” del password es “016”. Solo nos queda uno.

Este fue el momento en que miré el reloj y vi que quedaban exactamente 15 minutos para terminar el reto.. Modifico corriendo el cliente para que envíe peticiones y detecte cuando la respuesta contiene la palabra “true” (Ya que el crackeo del último “chunk” no era necesario hacerlo mediante el algoritmo, la respuesta contendría la palabra true y solo necesitamos hacer fuerza bruta de 1000 peticiones).

image

Este último tenía una demora de más de un segundo entre peticiones, tendrían puesto un timeout en las respuestas del “server_chunk” o algo para retrasar las peticiones. Aquí es cuando aproximadamente iba por la petición 509 y el reloj marcó las 21:00 horas, el reto había acabado.. y la conexión del SSH había sido cerrada. :(

Me quedé a escasos 10 minutos de pasarme el último nivel del CTF!! Y lo más importante!! Me quedé sin mi camiseta de premio por pasarte los 9 niveles!!

image

Bueno.. como reflexión diré que para mi lo más importante era conseguir saber pasármelo y eso lo conseguí ;)

Por último agradecer a la gente de Stripe por el CTF, sinceramente me lo he pasado genial y creo que ha sido muy completo. Gracias!!

Un Saludo y espero que os haya gustado!!

3 comentarios:

  1. Estos dos niveles eran complejos. No creo que hubiera podido pasar el de los Waffles, porque hasta ahora mismo no conocía la vulnerabilidad de los hashes. Una lástima que no llegaras a tiempo, pero la solución era buena :)

    ResponderEliminar
    Respuestas
    1. La verdad que sí, fue una lástima que no lo acabase, pero bueno a ver si en el próximo puedo dedicarle más tiempo ;)

      Eliminar
  2. Este viernes le sacamos tiempo al tema y lo petamos ;)

    ResponderEliminar