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.