martes, 6 de mayo de 2014

Tuenti Challenge 2014 -5ª Parte

Challenge 10 - Random Password


Y llegamos, al que en mi caso se puede considerar como el GRAN MURO del concurso. 3 días. 3 días dedicados al problema, para finalmente tener que pulsar el dichoso botón de SKIP. Y lo que es peor, con la sensación de estar a las puertas de solucionar el problema, y no comprender el porqué de no ser capaz de entrar en la fortaleza. Frustrante cuanto menos.

Pero como dijo Jack El destripador "Por partes vamos mejor" (sí, la he modificado, para darle un toque más andaluz xd), así que pasaré a relatar mi odisea por el desierto, que nada tiene que envidiar a la de Moisés xd.
El problema pertenecía al conjunto de problemas que podríamos englobar como de seguridad-astucia, dado que no requieren altos conocimientos de seguridad informática, sino unos conocimientos básicos acompañados de una buena cantidad de astucia (y para que vamos a negarlo, de suerte).

Se nos daba la siguiente web http://random.contest.tuenti.net/?input=INPUT, a la que tras entrar en ella podíamos ver un único mensaje de texto que nos saludaba con "Wrong". Rapidamente uno podía echar un vistazo a la URI y fijarse en que tenemos como argumento de entrada la palabra "INPUT" para el parámetro "input". Lo primero que a cualquiera se le pasa por la cabeza es probar otros argumentos como "hola", "adios", "123456", "root", o incluso dejarlo vacío. 

 

Tras ver que nada funciona, y echar un pequeño vistazo al test-script de los chicos de tuenti, pude ver como este nos daba una entrada que claramente iba dirigida a introducir en dicho input. Por lo tanto, nuestro objetivo no debía ser realizar ningún cambio en este, sino que más bien los tiros debían ir por otro lado.

 

El siguiente paso fue trastear un poco la web. Para ello comencé invocandola sin ningún parámetro: "http://random.contest.tuenti.net". El resultado idéntico, el puñetero mensaje de Wrong. Por lo que probé a invocar "http://random.contest.tuenti.net/index.html" y con ello llegó la primera revelación:


sabíamos ya por tanto el servidor http que utilizaba la máquina (nginx), pero lo que era mejor, la web nos indicaba aquellas páginas que existían en dicha máquina (en muchas ocasiones se suele desactivar, ya que muchos admins lo consideran como un 'bug' de revelación de información), y por tanto lo podíamos aprovechar para encontrar otros archivos en la máquina.

 

Lo primero averiguar el nombre de la página que nos estaba sirviendo el mensaje_de_las_narices "wrong": index.php. Vamos que se había dejado por defecto. A continuación probar otras muchas opciones como "robots.txt", index[x].php, default.php, admin.php... Incluyendo otras muchas variedades como búsqueda de logs y directorios fuera del directorio raiz. Pero nada, mis resultados no tuvieron sus frutos.

 

Decidí tomarme un descanso, y echarle un vistazo a la estructura de directorios del servidor Nginx. Una búsqueda rápida en google, unos cuantos ejemplos de servidores mal configurados y al rato volvía con ideas frescas. Pero nuevamente los intentos fueros inútiles. Pensé por tanto, que quizás la gente de Tuenti esperase una explotación del sistema en toda regla. Por lo que comencé mi búsqueda de exploits para nginx: CVE-2013-2028 Nginx HTTP Server 1.3.9-1.4.0 Chunked, CVE-2014-0133, CVE-2013-2070, en concreto este último viable en la versión 1.1.19, y fácil de explotar. Tras comprender un poco más como llevarlo a cabo, e intentar replicarlo contra el servidor, nuevamente en una encrucijada sin salida. Nada había funcionado.

 

Pensé entonces en atacar PHP, para ello levanté Wireshark y me dispuse a ver con más detenimiento los paquetes que nuestro amado nginx nos enviaba:



¡Aja! ¡Ya te tengo! Teníamos a php 5.3.10 precompilado para ubuntu. Nuevo vistazo a las vulnerabilidades de PHP, nuevo intento de explotación, en esta ocasión acompañado de nuestro amigo MetaSploit, pero nuevo tropiezo y primeros momentos de desesperación del concurso.

 

Reflexioné. Me paré y pensé. Intenté utilizar los conocimientos que durante la carrera de informática había aprendido: matemáticas discretas, algebra, estadística, redes neuronales... Mierda, estaba perdido

 

Estaba claro que nada de esto me iba a ayudar lo más mínimo ante un reto como este. Pero había algo que sí que había aprendido durante la carrera, y que precisamente no me lo habían enseñado los profesores, y es que los alumnos que tienen interés por la programación y el desarrollo, suelen no tener mucho interés por la telemática y la seguridad, y a la inversa (para atentar contra mi persona los comentarios chicos/as). Y realmente esto se puede seguir extrapolando al ámbito profesional, donde la mayoría se encasilla claramente en una categoría, y suele pillarle un poco lejana la otra rama. ¿Y por qué iba a ser diferente para este caso? A fin de cuentas la gran mayoría de los desafíos tienen una alta carga algorítmica, por lo que seguramente sus autores hayan escrito un reto de seguridad "light". Olvidémonos de ASLR, DEP y demás lindezas y volvamos a un terreno más cercano al de un programador.


Así que nuevamente  a probar alternativas sobre la URL: algún "directory traversal", algún truquito de la vieja escuela, y por fin a la tercera fue la vencida: PHP Source era la clave. Estos archivos con código fuente php, que su utilizan para poner tu código maquetado con colorines y que todo el mundo pueda ver lo buen programador que eres, tenían la solución a nuestros problemas.

"Index.phps" El archivo que acababa con toda una tarde de búsqueda infructuosa, y que nos abría las puertas del cielo (o más bien del infierno viendo lo que se me venía encima). 

De acuerdo, el código era muy simple, hasta para alguien como yo con un mediocre nivel de PHP. Al comienzo se generaba la semilla ('srand'), para el número aleatorio que se generaba al final ('rand') y que se comparaba con la contraseña, que debíamos pasar mediante el parámetro 'password'.

Pero recordemos que cuando hablamos de aleatoriedad, en realidad hacemos referencia a pseudo-aleatoriedad, y cuantas veces habremos oído repetido eso de que a la función de inicialización de aleatoriedad solo hay que llamarla una vez, dado que si no nuestra secuencia aleatoria se hace previsible (dado que si es la única semilla siempre producirá la misma secuencia para dicha semilla).

Pues bien, para colmo además, el fallo se produce en un servidor web, donde cada vez que invoquemos a index.php, el servidor generará un proceso hijo (o hebra) para atender a dicha petición, y por tanto se generará en cada una de nuestras peticiones http una nueva semilla.

 

Ahora solo queda averiguar el valor de dicha semilla, que se forma con la hora en formato timestamp de Unix (con los segundos a 0) multiplicado por el PID del proceso padre. La hora podría ser un problema, ya que el servidor realmente podría tener la que le diese en gana, pero para mayor facilidad han incluido en la cabecera http la hora del servidor. Por tanto solo nos queda ya averiguar el PID del proceso padre.

 

Ya que antes hemos visto que se trata, de un servidor ubuntu (es decir linux), seguramente el PID del padre se encuentre entre los 10000 primeros, por lo que la búsqueda no debería ser demasiado complicada. Por fuerza bruta probaremos los 10000 primeros PID, y a ver si tenemos suerte.

 

Para ello antes de nada, realicé las pruebas sobre mi propio servidor web, con el mismo código que había en el index.phps del servidor. En mi servidor web tardó apenas un segundo en encontrar la solución (lógico si pensamos que estaba en la propia máquina, y no hay retardos de red), con un PPID 6637. Ahora quedaba realizar la prueba contra el servidor web del desafió. Os dejo aquí el código php que utilicé para ello: 

<html>
 <head>
  <title>Por la fuerza</title>
 </head>
 <body>
 <?php 

/* LOCURA DE PROBLEMA XD */

//Evitamos que muestre los Warnings
error_reporting(E_ERROR | E_PARSE);

 
//$server = 'http://127.0.0.1/probe.php';
$server = 'http://random.contest.tuenti.net/';

$contents = file_get_contents($server.'?password=2036317783&input=326b5d287a');

for ($ppid = $_GET['i']; $ppid <= $_GET['f']; $ppid++){


 $pos = strpos($contents, "wrong!");

 if ($pos !== false){

  $nHeaders = count($http_response_header);
  for ($i = 0; $i <= $nHeaders; $i++){
   if(strpos($http_response_header[$i], "Date") !== false){
    break;
   }
  }

  $hora = substr($http_response_header[$i], 23, -10);
  $minuto = substr($http_response_header[$i], 26, -7);


  srand(mktime($hora, $minuto, 0) * $ppid);
  $rand = rand();

  echo $rand." ";

  $contents = file_get_contents($server.'?password='.$rand.'&input=326b5d287a');

 }else{
  echo "El ppid valido era ".($ppid-1)." fecha:".$hora." ".$minuto. "<br/>";
  echo $contents;
  break;
 }
}
 ?>
 </body>
</html>

El código era muy simple, solamente leía la hora y minuto que nos enviaba el servidor, y lo multiplicaba por un supuesto ppid que iba desde _GET(i) hasta _GET(f) (que eran pasados como argumentos). Después comprobaba si la respuesta que recibía contenía el mensaje 'Wrong' o no.

 

El mismo código funciono perfecto, sobre el mismo index.phps en mi servidor. Pero contra el servidor del concurso no tuve suerte. Intenté entonces probar los pid hasta 35000. Pero fallé nuevamente. Y empecé a mosquearme, y a pensar que mi aproximación tenía que ser erronea. A pensar que quizás el retardo pudiese estar estropeando mi solución. Pero lo descarté porque yo leía la hora actualizada para cada petición.

 

Me acordé entonces de una cosa que leí hace un tiempo sobre un módulo llamado Suhosin encargado de maximizar la seguridad en PHP. Y entonces pensé que probablemente la función srand() fuese diferente para diferentes versiones de PHP con diferentes características (era una de las características de Suhosin).

Me fuí entonces a http://sandbox.onlinephpfunctions.com/ y probé el siguiente código sobre 2 versiones distintas de PHP:


 

¡Lo que me esperaba! La función srand funciona de diferente forma en diferentes versiones de PHP, por lo que me esperaba intentar replicar las condiciones que tenía la máquina del servidor. Así que monté una máquina virtual, le metí Ubuntu y PHP 5.3.10 y probé nuevamente. Y nuevamente el vacío. Nada de nada. No sabía que podía estar fallando en una solución que creía haber encontrado. Me fuí a dormir, y como último intento dejé el programa toda la noche probando PPID entre 0-500000 con varias threads en paralelo, y con un poco de miedo por si provocaba un DOS en el servidor (nah, es coña).

 

Al día siguiente, el servidor seguía vivo (que alivio xd), pero ninguno de los PPID parecía valido.

 

Comenzaba entonces mi travesía por el desierto de intentos disparatados y desesperados, entre los que se incluían un escaneo completo con NMAP por si algún pequeño resquicio me daba algo. 

 

No encontré nada, pero después me enteré que intentando resolver este problema había resuelto el desafío 18 (que tenía que ver con realizar ingeniería inversa sobre un bytecode de python. Os dejo el código por si os interesa:

import json, sys
print 'Content-Type: text/txt\n'
q = sys.argv[-1]

def error(m):
    print m
    sys.exit()


if len(q) == 0:
    error('Input missing')
try:
    h, p = q.split(':')
except ValueError:
    error('Password missing')

try:
    i = json.load(open('../keys.json'))[h]
except:
    error('Invalid input')

print 'Right!' if p.isdigit() and len(p) < 15 and pow(i[0], int(p), i[1]) == i[2] else 'Wrong!\n' + '\n'.join(map(str, i))

 

Trataba de jugar con potencias y módulos (problema recurrente en programación competitiva), para de nuevo obtener una clave en un archivo "keys.json".

 

Por supuesto también llegué al problema de la calavera, antes de haber leído su anunciado, en mi proceso de exploración. Y como eso muchas otras cosas que aprendí acerca de los servidores de Amazon en mi camino de búsqueda. Pero nada que me sirviese para resolver el problema.

 

Tras 3 días de pruebas, me daba por vencido. Con la sensación de haber estado cerca pero resignándome a tener que saltarme dicho desafío, y sobre todo lamentándome por haber desperdiciado tanto tiempo en un solo problema.

 

Pd: Después pude saber gracias a @dvil88 que mi enfoque era correcto y que el PPID era 1336, no sé si fijado también con premeditación y utilizando "argot haxor".

Desafío 11  http://programacioncompetitiva.blogspot.com.es/2014/05/tuenti-challenge-2014-6-parte.html

4 comentarios:

  1. Te felicito por el currazo del post. También creo que has hecho un buen trabajo en este desafío aunque parece que solo cuentan los resultados y es una putada después de todo quedarse a falta del maldito PPID.

    Después de ver cosas como esta me siento menos ludópata de los desafíos, te lo dice una persona que ha dormido 3-4 horas todos los días de la semana intentando compaginar el Tuenti Challenge con trabajo y estudios.

    También creo que hay que felicitar y darles las gracias a ellos por tenernos enganchados como si estuviéramos mal de la cabeza jajaja

    ResponderEliminar
  2. Sí, la verdad es que da rabia quedarse a las puertas, pero se disfruta también en el camino, aunque a veces las cosas no salgan.

    Respecto a lo del tiempo llevas toda la razón, y más aún en un concurso como este que dura una semana, y que da pie a que le dediques "tiempo de más", en mi caso como en el tuyo, compaginado con facultad, novia y familia (con comuniones de por medio). Pero cuando algo te apasiona realmente se hace más llevadero.

    Ahora también te digo, que después de este concurso, me tomaré un respiro hasta el siguiente xd.

    Un saludo

    ResponderEliminar
    Respuestas
    1. Es curioso, yo lo hice con mi PHP 5.5.3-1ubuntu2.3 y me funcionó.

      Eliminar
    2. A ver si alguien da con el origen de mi problema, y lo comenta, porque no termino de ver donde está el error.

      Un saludo

      Eliminar