SpookTastic
4 minutos de lectura
Se nos proporciona el siguiente sitio web:
También tenemos el código fuente en Python del servidor (Flask).
Análisis del código fuente
El archivo relevante es app.py
:
import random, string
from flask import Flask, request, render_template, abort
from flask_socketio import SocketIO
from threading import Thread
app = Flask(__name__)
socketio = SocketIO(app)
registered_emails, socket_clients = [], {}
generate = lambda x: "".join([random.choice(string.hexdigits) for _ in range(x)])
BOT_TOKEN = generate(16)
def blacklist_pass(email):
email = email.lower()
if "script" in email:
return False
return True
def send_flag(user_ip):
for id, ip in socket_clients.items():
if ip == user_ip:
socketio.emit("flag", {"flag": open("flag.txt").read()}, room=id)
def start_bot(user_ip):
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
host, port = "localhost", 1337
HOST = f"http://{host}:{port}"
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-infobars")
options.add_argument("--disable-background-networking")
options.add_argument("--disable-default-apps")
options.add_argument("--disable-extensions")
options.add_argument("--disable-gpu")
options.add_argument("--disable-sync")
options.add_argument("--disable-translate")
options.add_argument("--hide-scrollbars")
options.add_argument("--metrics-recording-only")
options.add_argument("--mute-audio")
options.add_argument("--no-first-run")
options.add_argument("--dns-prefetch-disable")
options.add_argument("--safebrowsing-disable-auto-update")
options.add_argument("--media-cache-size=1")
options.add_argument("--disk-cache-size=1")
options.add_argument("--user-agent=HTB/1.0")
service = Service(executable_path="/usr/bin/chromedriver")
browser = webdriver.Chrome(service=service, options=options)
try:
browser.get(f"{HOST}/bot?token={BOT_TOKEN}")
WebDriverWait(browser, 3).until(EC.alert_is_present())
alert = browser.switch_to.alert
alert.accept()
send_flag(user_ip)
except Exception as e:
pass
finally:
registered_emails.clear()
browser.quit()
@app.route("/")
def index():
return render_template("index.html")
@app.route("/api/register", methods=["POST"])
def register():
if not request.is_json or not request.json["email"]:
return abort(400)
if not blacklist_pass(request.json["email"]):
return abort(401)
registered_emails.append(request.json["email"])
Thread(target=start_bot, args=(request.remote_addr,)).start()
return {"success":True}
@app.route("/bot")
def bot():
if request.args.get("token", "") != BOT_TOKEN:
return abort(404)
return render_template("bot.html", emails=registered_emails)
@socketio.on("connect")
def on_connect():
socket_clients[request.sid] = request.remote_addr
@socketio.on("disconnect")
def on_disconnect():
del socket_clients[request.sid]
if __name__ == "__main__":
app.run(host="0.0.0.0", port=1337, debug=False)
La única entrada de usuario en el sitio web está en la parte inferior de la página, donde se supone que debemos poner una dirección de correo electrónico:
Luego, el servidor responde y el navegador emite un mensaje de alert
.
Si miramos el código fuente, básicamente estamos llamando a /api/register
:
@app.route("/api/register", methods=["POST"])
def register():
if not request.is_json or not request.json["email"]:
return abort(400)
if not blacklist_pass(request.json["email"]):
return abort(401)
registered_emails.append(request.json["email"])
Thread(target=start_bot, args=(request.remote_addr,)).start()
return {"success":True}
La función blacklist_pass
Verifica que la dirección de correo electrónico no contenga la palabra script
:
def blacklist_pass(email):
email = email.lower()
if "script" in email:
return False
return True
Luego, el servidor agrega la dirección de correo electrónico a registered_emails
y ejecuta un bot usando un thread. Este bot lanza un navegador Chrome:
def start_bot(user_ip):
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
host, port = "localhost", 1337
HOST = f"http://{host}:{port}"
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-infobars")
options.add_argument("--disable-background-networking")
options.add_argument("--disable-default-apps")
options.add_argument("--disable-extensions")
options.add_argument("--disable-gpu")
options.add_argument("--disable-sync")
options.add_argument("--disable-translate")
options.add_argument("--hide-scrollbars")
options.add_argument("--metrics-recording-only")
options.add_argument("--mute-audio")
options.add_argument("--no-first-run")
options.add_argument("--dns-prefetch-disable")
options.add_argument("--safebrowsing-disable-auto-update")
options.add_argument("--media-cache-size=1")
options.add_argument("--disk-cache-size=1")
options.add_argument("--user-agent=HTB/1.0")
service = Service(executable_path="/usr/bin/chromedriver")
browser = webdriver.Chrome(service=service, options=options)
try:
browser.get(f"{HOST}/bot?token={BOT_TOKEN}")
WebDriverWait(browser, 3).until(EC.alert_is_present())
alert = browser.switch_to.alert
alert.accept()
send_flag(user_ip)
except Exception as e:
pass
finally:
registered_emails.clear()
browser.quit()
La parte relevante está en la parte inferior del cuerpo de la función, donde el navegador espera aceptar un mensaje de alert
y finalmente enviar la flag con send_flag
si no hay ningún error:
def send_flag(user_ip):
for id, ip in socket_clients.items():
if ip == user_ip:
socketio.emit("flag", {"flag": open("flag.txt").read()}, room=id)
La forma de enviar la flag es un poco avanzada para este reto, pero está hecho así para hacerlo más fácil. En resumen, el navegador y el servidor mantienen una conexión WebSocket para compartir información sobre el mensaje de alert
.
Solución
Entonces, solo necesitamos hacer que el bot obtenga un mensaje de alert
. Esto es posible porque el bot está leyendo esta plantilla HTML (templates/bot.html
):
{% for email in emails %}
<span>{{ email|safe }}</span><br/>
{% endfor %}
La palabra clave safe
indica al motor de plantilla que los datos de email
deben considerados como seguros, por lo que no hay necesidad de escapar de caracteres especiales HTML.
Como resultado, podemos inyectar código HTML arbitrario dentro de email
. Por lo tanto, podemos realizar un ataque de Cross-Site Scripting (XSS) escribiendo código JavaScript en un manejador de eventos. No podemos usar etiquetas script
, pero podemos usar eventos como onerror
u onload
en etiquetas como img
o svg
. Para más información, véase HackTricks.
Flag
Entonces, un simple payload de XSS con un alert
funcionará para obtener la flag:
HTB{al3rt5_c4n_4nd_w1l1_c4us3_jumpsc4r35!!}