← Writeups

SSRF with whitelist-based input filter

Bypass de filtro SSRF con whitelist de dominio usando el patrón userinfo '@': la URL se normaliza en el cliente y resuelve a localhost dentro de la whitelist.

Target Overview & Vulnerability Summary

El lab ssrf/lab-ssrf-with-whitelist-filter valida el parámetro stockApi contra una whitelist de dominios: solo permite stock.weliketoshop.net. El filtro se puede eludir con el componente userinfo de la URL: http://host@whitelisted-domain — el parser del servidor interpreta el host real como el que precede a @ mientras que la whitelist valida el dominio que le sigue. Combinado con URL-encoding, el host atacante (localhost) queda enmascarado dentro del dominio permitido.

Campo Valor
Severidad Alta (SSRF hacia recursos internos / admin con acceso a red interna)
Endpoint vulnerable GET /product/stock (parámetro stockApi)
Root cause Validación de URL por whitelist de dominio sin considerar userinfo ni la normalización de encoding

Technical Breakdown & Exploitation Flow

1. La request legítima

El stock checker consulta la API interna de stock:

stockApi=http://stock.weliketoshop.net:8080/product/stock/check?productId=1&storeId=1

URL-encoded:

stockApi=http%3A%2F%2Fstock.weliketoshop.net%3A8080%2Fproduct%2Fstock%2Fcheck%3FproductId%3D1%26storeId%3D1

La whitelist permite solo el host stock.weliketoshop.net (y el puerto 8080).

2. Bypass con userinfo #@

El payload combina userinfo + encoding del # para confundir la validación y el parser:

stockApi=http://localhost:80%2523@stock.weliketoshop.net/admin/delete?username=carlos

Desglose:

  • http:// — esquema.
  • localhost:80 — el host real que el servidor termina resolviendo (internamente). El puerto 80 apunta al servidor interno del admin.
  • %2523 — el carácter # codificado dos veces. Según el orden de decodificación del servidor, # (fragmento) separa el host atacante de la whitelist.
  • @ — separador userinfo: todo lo que precede a @ es usuario/contraseña para el host que sigue.
  • stock.weliketoshop.net — el dominio que la whitelist valida.

La validación de la whitelist examina el componente de host de la URL después del @ (porque el parser las ignora las reglas de RFC 3986 respecto al userinfo), ve stock.weliketoshop.net y aprueba. El componente de red real —localhost— es el que el backend usa para abrir la conexión, alcanzando el panel admin interno:

GET /admin/delete?username=carlos

La request interna elimina a carlos.

Root Cause Analysis

El filtro implementa la whitelist evaluando el dominio al final de la URL, ignorando el componente userinfo definido en RFC 3986. Un parser conforme extrae localhost como host real cuando aparece userinfo@host; el código de validación, sin embargo, toma stock.weliketoshop.net (el último segmento) como el host a comprobar. El encoding doble (%2523) explota además diferencias de normalización: el # decodificado rompe el parsing en un punto que mantiene la whitelist satisfecha. El resultado es una discrepancia validación-vs-resolución: se valida un host, se conecta a otro.

Secure Remediation

  • Parsear la URL con un parser conforme a RFC 3986 y validar el host neto (tras resolver userinfo y decodificar), nunca una substring del string original:
from urllib.parse import urlparse, unquote

allowed_hosts = {"stock.weliketoshop.net"}

def check_stock_api(raw_url):
    url = urlparse(unquote(raw_url))
    host = url.hostname                       # extrae el host real, ignora userinfo
    if host not in allowed_hosts:
        abort(403)
    if url.scheme not in ("http", "https"):
        abort(403)
    # además: resolver el host y re-validar la IP para evitar SSRF por DNS rebinding
  • Prohibir el uso de userinfo en URLs de negocio, o rechazar cualquier URL cuyo userinfo no esté vacío.
  • Revalidar la IP resuelta contra una lista de rangos privados/loopback tras la resolución DNS (SSRF definitivo), y bloquear rangos no públicos (127.0.0.0/8, 10.0.0.0/8, 169.254.0.0/16, etc.).