Solución al reto zeropwn #wgsbd2

hashkey.jpg

Esta prueba, como parecía de las difíciles no la intenté hasta que vi que pusieron una pista :). Y vaya pista, el código fuente del programa que actuaba de servidor y un fichero de texto con muuuuuchas palabras. El típico fichero diccionario de claves.

Lo que teníamos era un servicio escuchando por el puerto 8008 en wargame.securitybydefault.com. Cuando nos conectábamos a dicho puerto recibíamos algo como:

DIGEST-MD5bm9uY2U9IjEzMTExMzA1NzAiLHFvcD0iYXV0aCIsY2hhcnNldD11dGYtOCxh
bGdvcml0aG09bWQ1LXNlc3M=

El servidor nos está mandando algo codificado en Base64.

bm9uY2U9IjEzMTExMjI3OTMiLHFvcD0iYXV0aCIsY2hhcnNldD11dGYtOCxhbGdvcml0aG09bWQ1LXNlc3M=

Si lo decodificamos, obtenemos:

nonce="1311122793",qop="auth",charset=utf-8,algorithm=md5-sess

Si miramos el código fuente del servidor, vemos que los campos qop, charset y algorithm son constantes. El único campo que es variable es nonce, que es el tiempo (en segundos) en el que el servidor nos manda el mensaje. Como veremos más adelante, será el único valor que tenemos que sacar de la primera respuesta que recibimos.

Una vez el servidor nos manda los datos arriba mencionados, espera respuesta por nuestra parte. De nuevo, mirando el código fuente del servidor, vemos que lo que espera es una cadena de texto codificada a Base64 con la siguiente estructura:

username="zero_cool",realm="war.game.sbd",nonce="XXXX",cnonce="XXXX",nc="XXX",qop="auth",digest-uri="xmpp/war.game.sbd",response="XXX",charset="utf-8"

Expliquemos cada campo:

El username debe ser zero_cool, este dato se nos daba como parte inicial del reto.

El realm lo podemos sacar de la respuesta inicial (……), aunque al ser un valor constante, lo usaremos como tal en nuestro script.

El campo nonce como dije antes es variable y lo tenemos que sacar de la primera respuesta del servidor. Este es el código que usaremos para extraer dicho dato:

chal = xml.xpath("//challenge").text
dec_chal = Base64.decode64(chal)
nonce = dec_chal.split(",")[0].split("=")[1].gsub("\"","")

Los campos cnonce y nc son dos datos que manda el cliente y que el servidor usa como tales, así que podemos mandarle lo que queramos. Los únicos requisitos son que cnonce debe ser del tipo numérico y nc del tipo texto.

Los campos qop, digest-uri y charset, de nuevo deben tener los valores que mostramos más arriba, puesto que son los valores que espera el servidor.

Y por último tenemos el campo response, que es el que tiene más miga de todos y es donde vamos a mandar nuestro password. Para calcular este campo tomé prestada la función que usa el servidor :) para comparar lo que mandamos. La función tiene esta pinta:

def sasl_md5_chall(uname, pass, realm, nonce, cnonce, nc, digest_uri, qop)

    hA1data = Digest::MD5.digest("#{uname}:#{realm}:#{pass}")
    hA1data = hA1data + ":#{nonce}:#{cnonce}".force_encoding("UTF-8")
    hA1 = Digest::MD5.hexdigest(hA1data)
    hA2 = Digest::MD5.hexdigest("AUTHENTICATE:#{digest_uri}")
    hash = Digest::MD5.hexdigest("#{hA1}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{hA2}")

    return hash
end

Como podemos ver, tenemos todos los parámetros que la función espera, menos la contraseña (pass). Aquí es donde entra en juego el fichero de passwords que os comenté al principio.

Veamos un ejemplo de la cadena que tendríamos que mandar con el password 12345 y nonce = 1311128886:

ddXNlcm5hbWU9Inplcm9fY29vbCIscmVhbG09Indhci5nYW1lLnNiZCIsbm9u
Y2U9IjEzMTExMjg4ODYiLGNub25jZT0iMSIsbmM9IjEiLHFvcD0iYXV0aCIs
ZGlnZXN0LXVyaT0ieG1wcC93YXIuZ2FtZS5zYmQiLHJlc3BvbnNlPSI4ZTlh
Yzc5YzFiNjViMmQ0Yjk2NDE2MWYwYTk2YmNjMCIsY2hhcnNldD0idXRmLTgi

Y si el password es incorrecto el servidor respondería con:

Invalid password, keep trying

Y si fuera correcto:

XXXXXXXXXXX

Donde las Xs será nuestro token codificado en Base64.

En resumidas cuentas, lo que tenemos que hacer es:

  1. Leer password del fichero de texto
  2. Conectamos al servidor
  3. Leemos la primera respuesta para sacar el valor de nonce
  4. Preparar y mandar nuestra respuesta
  5. Si el servidor responde con <error> volver a empezar con el siguiente password
  6. Si responde con success decodificar el texto en Base64 y ya tenemos nuestro token.
Este es el script que yo usé:
#!/usr/bin/env ruby

require ’nokogiri' require “digest/md5” require “socket” require “base64” require “timeout”

$server = “wargame.securitybydefault.com” $port = “8008” $login_user = “zero_cool” $login_pass = “12345” $realm = “war.game.sbd” $qop = “auth” $charset = “utf-8” $digest_uri = “xmpp/war.game.sbd” $cnonce = 1 $nc = “1”

def sasl_md5_chall(uname, pass, realm, nonce, cnonce, nc, digest_uri, qop)

hA1data = Digest::MD5.digest("#{uname}:#{realm}:#{pass}")
hA1data = hA1data + ":#{nonce}:#{cnonce}".force_encoding("UTF-8")
hA1 = Digest::MD5.hexdigest(hA1data)
hA2 = Digest::MD5.hexdigest("AUTHENTICATE:#{digest_uri}")
hash = Digest::MD5.hexdigest("#{hA1}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{hA2}")

return hash

end

def check(pass) s = TCPSocket.open($server, $port) response = s.gets response += s.gets

xml = Nokogiri::HTML(response)

chal = xml.xpath("//challenge").text
dec_chal = Base64.decode64(chal)
nonce = dec_chal.split(",")[0].split("=")[1].gsub("\"","")

hashed_resp = sasl_md5_chall($login_user, pass, $realm, nonce, $cnonce, $nc, $digest_uri, $qop)

resp_back = "username=\"zero_cool\",realm=\"war.game.sbd\",nonce=\"#{nonce}\",cnonce=\"#{$cnonce}\",nc=\"#{$nc}\",qop=\"auth\",digest-uri=\"xmpp/war.game.sbd\",response=\"#{hashed_resp}\",charset=\"utf-8\""

resp_back2 = "<response>"+Base64.encode64(resp_back)+"</response>"
s.write resp_back2 + "\n"
response = ""
while l = s.gets
	response += l
end
return response

end

f = File.new(“password.txt”, “r”) while (line = f.gets) $user_pass = line.chop puts “Trying: #{$user_pass}” result = check(line.chop) puts result if result.include?(‘success’) then puts result break end end f.close

Para concluir, decir que el password correcto era: unix y la repuesta recibida fue:

SGVyZSBpcyB5b3VyIGRhbW4gZmxhZzogSV9Mb1ZlX0FuZ2VsaW5hX0owTGllXw==

Y si decodificamos la cadena de texto obtenemos:

Here is your damn flag: I_LoVe_Angelina_J0Lie_