HackTheBox – CDNio (Challenge Web/Easy)

En este post estaremos resolviendo el challenge web llamado «CDNio» de Hack The Box, calificado como Easy. Se trata de un challenge ya retirado.

Comenzamos descargando y analizando el código fuente de interés con VSCode:

Obviamente, cuando resaltamos ficheros de interés, nos referimos a la configuración del servidor (en este caso, nginx) y a los propios ficheros de la aplicación (en este caso, escrita en Python). Comenzamos con el fichero de configuración de nginx y vemos que la caché de nginx está activada para recursos estáticos (negrita):

user nobody;
worker_processes 1;
pid /run/nginx.pid;

events {
    worker_connections 768;
}

http {
    server_tokens off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    proxy_cache_path /var/cache/nginx keys_zone=cache:10m max_size=1g inactive=60m use_temp_path=off;

    server {
        listen 1337; 
        
        server_name _;  

        location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            proxy_cache cache;
            proxy_cache_valid 200 3m;
            proxy_cache_use_stale error timeout updating;
            expires 3m;
            add_header Cache-Control "public";

            proxy_pass http://unix:/tmp/gunicorn.sock;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            
        }

        location / {
            proxy_pass http://unix:/tmp/gunicorn.sock;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            
        }

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
    }
}

Esto siempre es interesante, ya que puede dar lugar a ataques de Web Cache Deception, especialmente si hay sistemas intermedios que puedan interpretar las rutas de manera diferente a los sistemas finales (discrepancias frontend-backend o proxy-backend, por ejemplo).

Continuamos con los ficheros de la aplicación y observamos que el fichero «challenge/app/blueprints/main/routes.py» define una ruta «mágica» compuesta por un path y un subpath. Es decir, una ruta como «http://[IP_CHALLENGE]/path/subpath» entrará por este manejador (aunque también «http://[IP_CHALLENGE]/path»). Tras ello, es especialmente importante que existe una expresión regular que parece pretender validar si el inicio del path es «profile».

from . import main_bp

from app.middleware.auth import jwt_required
from flask import jsonify, request

import re, sqlite3

def get_db_connection():
    conn = sqlite3.connect("/www/app/database.db")
    conn.row_factory = sqlite3.Row
    return conn

@main_bp.route('/<path:subpath>', methods=['GET'])
@jwt_required
def profile(subpath):
    
    if re.match(r'.*^profile', subpath): # Django perfection

        decoded_token = request.decoded_token

        username = decoded_token.get('sub')
        if not username:
            return jsonify({"error": "Invalid token payload!"}), 401

        conn = get_db_connection()

        user = conn.execute(
            "SELECT id, username, email, api_key, created_at, password FROM users WHERE username = ?",
            (username,)
        ).fetchone()
        conn.close()
        
        if user:
            return jsonify({
                "id": user["id"],
                "username": user["username"],
                "email": user["email"],
                "password": user["password"],
                "api_key": user["api_key"],
                "created_at": user["created_at"]
            }), 200

        else:
            return jsonify({"error": "User not found"}), 404
        
    else:
        return jsonify({"error": "No match"}), 404

Si tenemos en cuenta el código anterior y la caché, podemos inferir claramente que es posible llevar a cabo ataques de Web Cache Deception, dado que usando «testing.png» como supuesto recurso estático:

  • Una petición a la ruta http://[IP_CHALLENGE]/profile/testing.png devolverá los datos del usuario al entrar por el manejador anterior
  • Además, el contenido se quedará cacheado según las reglas de nginx

Por tanto, si logramos «engañar» a un usuario para que acceda a dicha URL, lograremos cachear el contenido que después podemos visualizar. Para lograr este «engaño», en la aplicación existe un código en «challenge/app/blueprints/bot/routes.py» que define una ruta «visit» que se puede usar por parte de un usuario autenticado para forzar un bot a realizar una petición HTTP:

from . import bot_bp

from app.utils.bot import bot_thread
from app.middleware.auth import jwt_required

from flask import request, jsonify


@bot_bp.route('/visit', methods=['POST'])
@jwt_required
def visit():

    data = request.get_json()

    uri = data.get('uri')
    
    if not uri:
        return jsonify({"message": "URI is required"}), 400

    bot_thread(uri)

    return jsonify({"message": f"Visiting URI: {uri}"}), 200

Este fichero hace referencia al fichero «challenge/app/utils/bot.py» que, claramente, hace una petición HTTP como se comentaba:

import time, os, threading, requests

base_url = "http://0.0.0.0:1337"

admin_passwd = os.getenv("RANDOM_PASSWORD")

base_headers = {
    "User-Agent": "CDNio Bot ()"
}

def login_and_get_token():
    session = requests.Session()

    login_url = f"{base_url}/"
    payload = {
        "username": "admin",
        "password": admin_passwd
    }
    response = session.post(login_url, json=payload, headers=base_headers)

    if response.status_code == 200:
        token = response.json().get("token")
        return token, session 
    else:
        return None, None  

def bot_runner(uri):

    token, session = login_and_get_token()
    
    headers  = {
        **base_headers,
        "Authorization": f"Bearer {token}"
    }

    r = requests.get(f"{base_url}/{uri}", headers=headers)

    time.sleep(5) 

def bot_thread(uri):
    bot_runner(uri)

Lo importante es que, esta petición, la hace como administrador. Así las cosas, tenemos ya la cadena de ataque, que será la siguiente:

  • Registramos un usuario en la aplicación
  • Utilizamos el endpoint «/visit» para forzar al bot de administrador a que haga una petición a un supuesto recurso estático aprovechando la ruta «mágica»
  • El contenido de la petición anterior se quedará cacheado en nginx
  • Posteriormente, accederemos a la misma ruta mágica que le hemos indicado al bot y podremos leer los datos del administrador

Es decir, construimos un ataque de Web Cache Deception (https://portswigger.net/web-security/web-cache-deception). En las siguientes figuras tienes la resolución al reto paso a paso. Comenzamos registrando el usuario y leyendo datos de manera normal:

Capturando las peticiones con Burp Suite, obtenemos el JWT de nuestro usuario, con el que podemos ya hacer las peticiones a «/visit» para forzar al bot a cachear sus datos y a la ruta «mágica» para leerlos:

Como vemos, hemos logrado cachear datos del administrador con la petición del bot y luego los leemos desde nuestra sesión de usuario normal. Ahora, para terminar, tenemos la API KEY con el flag y por tanto la incluimos como solución.

~km0xu95

Comments are closed.