SpookTastic
4 minutes to read
We are given the following website:
We also have the Python source code of the server (Flask).
Source code analysis
The relevant file is 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)
The only user input in the website is at the bottom of the page, where we are supposed to put an email address:
Then, the server responds and the browser pops an alert
message.
If we check the source code, we are basically calling /api/register
under the hood:
@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}
The function blacklist_pass
verifies that the email address does not contain the word script
:
def blacklist_pass(email):
email = email.lower()
if "script" in email:
return False
return True
Then, the server adds the email address to registered_emails
and runs a bot using a thread. This bot launches a Chrome browser:
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()
The relevant part is just the bottom of the function body, where the browser expects to accept an alert
message and finally send the flag with send_flag
if there is no exception:
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)
The way to send back the flag is a bit advanced for this challenge, but it’s like this to make the challenge easier. In brief, the browser and the server maintain a WebSocket connection to share information about the alert
.
Solution
So, we only need to make the bot get an alert
message. This is possible because the bot is reading this HTML template (templates/bot.html
):
{% for email in emails %}
<span>{{ email|safe }}</span><br/>
{% endfor %}
The safe
keyword indicates the template engine that the data inside email
must be considered safe, so there is no need to escape HTML especial characters.
As a result, we can inject arbitrary HTML code inside email
. Therefore, we can perform a Cross-Site Scripting (XSS) attack by adding JavaScript code in an event handler. We cannot use script
tags, but we can use events such as onerror
or onload
in tags like img
or svg
. For more information, take a look at HackTricks.
Flag
So, a simple XSS payload with an alert
will work to get the flag:
HTB{al3rt5_c4n_4nd_w1l1_c4us3_jumpsc4r35!!}