Hackvens 2022 - The Boat Blog
Challenge details
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| Hackvens 2022 | Le JSON des mers | Web | 92 | 7 |
As a fan of photos of boats at sea, I developed an online gallery to share my passion. I made sure to use secure tokens for the session, so that no one could access the admin section. Feel free to leave a review!
https://json-des-mers.hackvens.fr/
TL;DR
The website provided was built in Flask. Sending a message on the Contact page using a nickname allows authentication as well as the generation of a JWT token containing the supplied nickname. The user is then redirected to a voting page whose features are not implemented but which contains an HTML comment disclosing the administrator’s name admin_mouss_adm. It is not possible to create a token in the name of admin_mouss_adm; however, it turns out that it is possible to pollute the token’s parameters during its creation by specifying 2 user attributes.
The difficulty of the challenge lies in the rather unrealistic nature of the parameter pollution, since the creation request uses a traditional content-type (and not JSON), resulting in an injection worthy of an SQL payload. You therefore had to supply a nickname like premieruser", "user": "adam_mouss_adm in order to obtain an administrator token. The token generated by default was then blacklisted, but by tweaking the padding of the token’s signature it was possible to bypass this check. Finally, the administration section offered a “Show flag” feature with a form and file=flag.txt parameters, resulting in an error message indicating that the file “flag.txt” is not accessible. This form was vulnerable to Server Side Template Injections (Jinja), and using a traditional payload with subprocess allowed remote command execution and reading the flag.
Le JSON des mers
On 7 October 2022, the Hackvens event organized by Advens took place. Together with several colleagues from Imineti by Niji, we had the opportunity to take part in the challenge (CTF) offered at the end of the event. This article recounts our solution for the “Le JSON des mers” challenge.
Entry point

The challenge’s website offers several pages. The voting page is inaccessible to an unauthenticated user, and the administration page returns a 401 error:

The contact page, for its part, lets you send a message to the administrator and results in the creation of a JWT token stored in a cookie:

Analysis of the JWT token indicates that a single user attribute is stored in the Data section. The HS256-format signature is not crackable, and the standard attacks related to JWT signatures do not work.

Submitting the form unlocks the voting page. The latter is not functional but contains an HTML comment disclosing the name of an administrator adam_mouss_adm.

After many attempts, the idea of performing a JWT attribute pollution came to mind. The idea is therefore to have a second “user” field in our token:

Assuming that our nickname is simply concatenated during the generation of a token (as in the case of an SQL injection), we went with the payload Zeecka", "user": "adam_mouss_adm:

This strategy pays off, since the application now seems to partially recognize us as an administrator. Likewise, the HTML comment previously discovered is once again available in its entirety.

Bypassing the blacklist
The generated token as well as the one provided in the comment are not functional for accessing the admin section.

By tweaking the padding of the token’s signature, it is possible to bypass the blacklists. To do this, you simply need to increment or decrement the last character of the signature:

Server-side code execution
Once a valid token has been obtained, it is possible to test the “Show flag” form of the administration section. It appears not to work and returns the error message “Désolé vous ne pouvez pas accéder au fichier flag.txt” or “Le fichier $fichier n’existe pas”.

Since the application is written in Flask and the file name is displayed, it is possible to attempt a Jinja 2 template injection with the payload {{7*7}}:

The SSTI vulnerability is confirmed. To finalize our exploitation, we will use the subprocess.Popen module to execute our commands. This technique can be found on the Payload all the things repo.

Flag
HACKVENS_{__1n_J$0N_W3_Tru$t__}
Bonus - Source Code
from flask import Flask, render_template, render_template_string, redirect, url_for, make_response, request
from flask_jwt_extended import JWTManager
import jwt
import time
import json
#CONGRATS :)
app = Flask(__name__)
app.config["JWT_SECRET_KEY"] = "eKz7nR61g0fsm1C35eEbEZCLyuJdzGcq3DOiOPMC"
app.config["JWT_ALGORITHM"] = "HS256"
ADM_KEY = "4G2klzstPXfeQM6Ee3zR2jOEGGeE6msjPim9nvIj"
jwt_manager = JWTManager(app)
def jwt_verify(token):
return(jwt.decode(token, app.config["JWT_SECRET_KEY"], algorithms=['HS256']))
def jwt_verifyAdmin(token):
try:
return(jwt.decode(token, ADM_KEY, algorithms=['HS256']))
except Exception as e:
raise e
def generateToken(username):
payload = {'alg':'HS256'}
print("payload : " + str(payload))
body = '{"user": "' + str(username) + '", "createdat": "' + str(time.time()) + '"}'
print("body : " + str(body))
return(jwt.encode(json.loads(body), app.config["JWT_SECRET_KEY"], algorithm='HS256'))
def generateTokenAdmin():
payload = {'alg':'HS256'}
body = '{"user": "adam_mouss_adm", "createdat": "970351200"}'
return(jwt.encode(json.loads(body), ADM_KEY, algorithm='HS256'))
@app.route('/')
def home():
return render_template('home.html')
@app.route('/admin',methods=['GET','POST'])
def admin():
if request.method == 'GET':
if request.cookies.get('token') is not None :
token = request.cookies.get('token')
decoded = jwt_verifyAdmin(token)
username = decoded['user']
if token == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRhbV9tb3Vzc19hZG0iLCJjcmVhdGVkYXQiOiI5NzAzNTEyMDAifQ.x-TwOUeNr3ZhX83czsx92sR5eGsNDUqAdTdf2ingzTI":
return render_template('401.html', message="Des pirates ont volé mon token, j'ai dû le blacklister par mesure de sécurité")
elif token[-1] == "=" :
return render_template('401.html', message="Serais-ce toi le hacker ? Je t'enverrai par le fond!")
elif token[-1] != "=" and "=" in token :
return render_template('401.html', message="Serais-ce toi le hacker ? Je t'enverrai par le fond!")
elif username == "adam_mouss_adm" :
return render_template('admin.html')
else:
return render_template('401.html', message="Seul l'administrateur du site peut accéder à cette page.")
elif request.method == 'POST':
if request.cookies.get('token') is not None :
token = request.cookies.get('token')
decoded = jwt_verifyAdmin(token)
username = decoded['user']
if token == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRhbV9tb3Vzc19hZG0iLCJjcmVhdGVkYXQiOiI5NzAzNTEyMDAifQ.x-TwOUeNr3ZhX83czsx92sR5eGsNDUqAdTdf2ingzTI":
return render_template('401.html', message="Des pirates ont volé mon token, j'ai dû le blacklister par mesure de sécurité")
elif token[-1] == "=" :
return render_template('401.html', message="Serais-ce toi le hacker ? Je t'enverrai par le fond!")
elif token[-1] != "=" and "=" in token :
return render_template('401.html', message="Serais-ce toi le hacker ? Je t'enverrai par le fond!")
elif username == "adam_mouss_adm" :
if request.form['file'] is not None :
file = request.form['file']
if file == "flag.txt" :
return render_template('admin.html',file="flag.txt")
else :
response = '''
{% extends 'index.html' %}
{% block content %}
<header class="bg-dark py-5">
<div class="container px-4 px-lg-5 my-5">
<div class="text-center text-white">
<h1 class="display-4 fw-bolder">Oh non ma gallerie prend l'eau :(</h1>
<div class="text-center text-white">
<h5>Le fichier ''' + file + ''' n'existe pas</h5>
</div>
</div>
</div>
</header>
{% endblock %}'''
return render_template_string(response)
else :
return render_template('admin.html')
else:
return render_template('401.html', message="Seul l'administrateur du site peut accéder à cette page.")
@app.route('/about')
def about():
return render_template('about.html')
@app.route('/votes',methods=['GET'])
def votes():
if request.method == 'GET':
if request.cookies.get('token') is not None :
token = request.cookies.get('token')
decoded = jwt_verify(token)
username = decoded['user']
return render_template('votes.html', username=username)
else:
return render_template('401.html', message='Vous devez laisser un avis avant de pouvoir voter.')
@app.route('/contact',methods=['GET','POST'])
def contact():
if request.method == 'GET':
return render_template('contact.html')
if request.method == 'POST':
if request.form['username'] is not None and request.form['message'] is not None:
username = request.form['username']
if username == "adam_mouss_adm" :
return render_template('contact.html', token_response='NOK1')
else:
try:
token = generateToken(username)
print(token)
res = make_response(render_template('contact.html', token_response='OK'))
res.set_cookie('token', token, secure=False, httponly=True, samesite="Lax")
return res
except Exception as e:
if "Expecting ',' delimiter" in str(e) or "Expecting ':' delimiter" in str(e) :
return render_template('contact.html', token_response='NOK2')
else:
print(e)
return render_template('contact.html', token_response='NOK3')
else:
return render_template('contact.html', token_response="Votre nom/message est vide :(")
@app.errorhandler(404)
def page_not_found(e):
return redirect(url_for('home'))
@app.errorhandler(jwt.exceptions.InvalidSignatureError)
def InvalidSignatureError(e):
return render_template('401.html', message="Signature du token invalide")
@app.errorhandler(jwt.exceptions.InvalidTokenError)
def InvalidTokenError(e):
return render_template('401.html', message="Token invalide")
@app.errorhandler(jwt.exceptions.DecodeError)
def DecodeError(e):
return render_template('401.html', message="Token invalide")
@app.errorhandler(jwt.exceptions.InvalidKeyError)
def InvalidKeyError(e):
return render_template('401.html', message="Token invalide")
@app.errorhandler(jwt.exceptions.InvalidAlgorithmError)
def InvalidAlgorithmError(e):
return render_template('401.html', message="Algorithme non autorisé")
@app.errorhandler(jwt.exceptions.MissingRequiredClaimError)
def MissingRequiredClaimError(e):
return render_template('401.html', message="Token invalide")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80, debug=False)