BBLABS v2BBLABSv2
>Home>Labs
>New labs

Latest 3 labs

Loading…

View all labs →
>Creators>Ranking
>Learn

Learn bug bounty

AcademyGuides, cheatsheets and glossaryVulnerabilitiesXSS, SQLi, IDOR, SSRF and moreHacker RoadmapYour step-by-step bug bounty pathBlogBug bounty guides and news
>Business>Pricing
ES
Log inLog in
>Home>Labs>New labs>Creators>Ranking>Learn>Business>Pricing
ES
Sign inCreate account
  1. Home
  2. Labs
  3. SSRF en un webhook de media que bypassa su guard DNS por TOCTOU (check-then-use sin pinning)
HardVDP1h 30min

SSRF en un webhook de media que bypassa su guard DNS por TOCTOU (check-then-use sin pinning)

By @gorka

El webhook de media valida el host resuelto en el check, pero el fetch real re-resuelve el mismo hostname sin fijar la IP entre medias: alcanza infraestructura interna.

32 views1 completedUpdated Sep 2026
Log in to start

Learn to find this bug

A real HackerOne hack, reproduced for you to practice.

Create your account and practice real bugs that got paid. Download the environment, find it and learn the exact technique — your path to your first bounty.

650

hunters training

50

labs from real hacks

380

completions

$12,000

paid out for these bugs

40 flags captured this week
Create account
I already have an account

Access to all labs · no commitment · cancel anytime

Hackers who solved it· 1

w4tchw0lf1
@w4tchw0lf8 hours ago

Objectives

1
Descubrir el webhook público que descarga `media` server-side y confirmar el fetch real.
2
Confirmar que el guard bloquea IPs internas nombrando la IP resuelta y su rango.
3
Leer el directorio de servicios internos y anotar la dirección del servicio objetivo.
4
Detectar en el changelog que validación y descarga corren como pasos separados.
5
Comparar dos operadores equivalentes para distinguir cuál fija (pinea) la IP resuelta.
6
Armar un servidor DNS propio: primera resolución pública, siguientes hacia el interno, TTL=0.
7
Confirmar en el visor de consultas que hubo dos resoluciones distintas antes de dar por bueno el ataque.
8
Leer el adjunto capturado en la bandeja y extraer la flag del servicio interno.

Information

Platform
HackerOne
Difficulty
Hard
Duration
1h 30min
Bounty
VDP
Completed
1
Creator
gorka@gorka
Updated
Sep 2026

Download the environment

Reproduce it and find the bug yourself

Create account

Tools

curlBurp SuiteNavegadorToolkit DNS (dado)

Prerequisites

  • HTTP y APIs REST
  • Fundamentos de DNS
  • SSRF / TOCTOU básico

Tags

SSRFRace ConditionAPI Abuse

Achievement you'll earn

Solve this lab to unlock this shareable achievement

BBLABS.ESLab Solved
HardVDP
// achievement_unlocked

SSRF en un webhook de media que bypassa su guard DNS por TOCTOU (check-then-use sin pinning)

SSRFRace ConditionAPI Abuse
Sep 2026
gorka
solved_by@gorkaMember since Mar 2026
bblabs.es// real hacking practice

Community writeups

Contenido del lab

Escenario

Convergia es una plataforma SaaS omnicanal: SMS/MMS, email y chat de distintos operadores externos convergen en una única bandeja de agentes. Los operadores entregan mensajes entrantes por webhook y pueden adjuntar "media" — la foto de una entrega, un justificante, un adjunto de soporte — como URLs remotas que la plataforma descarga server-side para renderizarlas dentro de la conversación del agente.

Eres pentester con una cuenta de agente en Convergia. Tu misión: demostrar que ese descargador de media, pensado para traer imágenes inocuas de operadores externos, puede acabar golpeando infraestructura interna que nunca debería ser alcanzable desde fuera de la red del producto — y traerte de vuelta un secreto que vive ahí dentro.

Qué tienes

  • Cuenta demo: attacker@convergia.local / password123 — rol agente: puedes ver la bandeja, leer conversaciones y sus adjuntos. Es tu punto de partida para el read-back final, no es la vulnerabilidad.
  • Acceso a la bandeja completa de Convergia: páginas de Integraciones, Servicios internos y Changelog, más un historial ya seedeado de conversaciones (algunas con media pública ya adjunta, prueba de que el flujo de descarga funciona en producción sin ser el bug).
  • Un segundo servicio, ya desplegado y expuesto en el mismo origen bajo /toolkit: un toolkit de servidor DNS controlable, con una UI que trae un hostname listo y un formulario para "armar" qué IP responde a cada consulta.

Guía paso a paso

  1. Encuentra el webhook y confirma que descarga server-side. En Integraciones ves los dos canales de operador activos, la ruta pública POST /api/v1/channels/inbound/:provider, el shape exacto del payload ({from, to, body, received_at, media:[...]}) y un botón "Send test message" que dispara un ejemplo por ti. Lánzalo, o construye tu propio POST con un media apuntando a cualquier URL pública: la respuesta es 200 y aparece de inmediato una conversación nueva en tu bandeja con la media ya adjunta. Confirmas que el servidor hace un fetch real, server-side, sobre cualquier URL que le mandes — y sin sesión: el webhook es público.

  2. Apunta directo al interno y localiza el guard. En Servicios internos el directorio lista "Ops Metrics" con su dirección exacta (http://10.31.9.10:8080/ · internal network only), junto a 2-3 backends de acompañamiento. Prueba media apuntando esa misma dirección: el adjunto que se genera no es la respuesta del servicio, es un error que nombra la IP resuelta y su rango (media host 10.31.9.10 resolved to 10.31.9.10 (non-routable range: private (10.0.0.0/8)) — refusing to fetch). Conclusión: hay un guard, y valida la IP a la que resuelve el hostname, no la cadena de la URL — el camino directo está cerrado.

  3. Detecta que check y descarga son dos resoluciones separadas, y arma el rebind. El Changelog (v2.0.5, "Media validation pipeline hardening") lo dice con toda honestidad: "media validation and media download run as separate steps; a re-fetch may occur after validation". Repite el mismo experimento del paso 2 contra el segundo operador ("Hardened v2" en su ficha de Integraciones): el adjunto nunca trae datos internos, aunque el hostname resuelva exactamente lo mismo — ese operador resuelve una única vez y conecta directamente a esa IP (la fija/pinea), así que no hay una segunda resolución que aprovechar. La diferencia entre los dos operadores es la prueba de que fijar la IP tras el check es justo lo que falta en el primero.

    Abre el toolkit DNS del lab en /toolkit — un hostname ya preparado (mms-fetch.rbnd.test) y un formulario arm {host, safe_ip, internal_ip}. Ármalo con la IP pública que trae por defecto (198.51.100.20, pasa el check del guard) como primera respuesta, y la IP de "Ops Metrics" del paso 2 (10.31.9.10) como respuesta a partir de la segunda consulta, con TTL=0.

  4. Dispara el webhook contra el hostname armado — la vuln. Manda el mensaje al primer operador (el que NO fija la IP, sablewire) con media apuntando al hostname que acabas de armar en el puerto 8080. El guard resuelve el hostname para el check → obtiene la IP pública → pasa. El fetch real vuelve a resolver el mismo hostname (nadie fijó el resultado del check anterior) → esta vez el toolkit responde la IP interna → el fetch conecta directamente contra "Ops Metrics" y captura su respuesta completa.

  5. Confirma el punto ciego y lee el adjunto — la flag. Antes de dar el paso 4 por bueno, refresca el visor de consultas del toolkit (/log): verás dos entradas para el mismo hostname, una con la IP pública y la siguiente con la interna — la prueba de que hubo dos resoluciones reales y de que el fetch cogió la segunda. Luego ve a tu bandeja, abre la conversación que acaba de crear el webhook y descarga su adjunto: es el JSON completo de "Ops Metrics", con varios campos de acompañamiento (queue_depth, node_id, internal_signing_key, smtp_relay_password) y el campo service_token, que contiene la flag.

Canales de feedback (no vuelas a ciegas)

  • El webhook responde 200 de inmediato y la conversación aparece en la bandeja sin recargar nada (paso 1).
  • El adjunto de un intento bloqueado nombra la IP resuelta y su rango exacto — nunca un 400 genérico (paso 2).
  • El visor de consultas del toolkit lista cada A-query respondida en orden, con su IP — confirma en vivo si hubo una o dos resoluciones (paso 3-4).
  • El adjunto capturado renderiza el JSON completo del servicio interno, no un resumen — la flag está ahí en claro (paso 5).

Decoys (descártalos por razonamiento acotado)

  • El guard directo (funciona). Cualquier media que resuelva a un rango no enrutable (privado, loopback, link-local — incluida la IP de metadata en la nube — CGNAT) se bloquea con el error que nombra la IP y el rango. Un único intento con la IP del directorio de servicios basta para confirmarlo: el guard no está roto, así que ir directo al interno no es el camino.
  • El operador "Hardened v2" (pinning). Comparte el mismo guard, pero tras validar conecta a la IP exacta que resolvió — no vuelve a preguntar al DNS. Armar el mismo rebind y dispararlo contra este operador nunca trae datos internos: confirma que el fallo no es "cualquier operador se puede rebindear", es específicamente la ausencia de fijar la IP tras el check en el operador estándar.
  • Esquemas fuera de http(s). file://, data:, gopher://, etc. se rechazan con 400 unsupported media scheme antes de tocar ningún DNS. Un intento con file:///etc/passwd lo confirma: no hay atajo por esquema, el único camino es http(s) más la ventana entre dos resoluciones DNS.

La falla del laboratorio

Clase: CWE-918 (Server-Side Request Forgery) con CWE-367 (Time-of-Check Time-of-Use) como causa raíz, más CWE-350 (Reliance on Untrusted Inputs — una decisión de seguridad basada en una resolución DNS que el atacante controla) y CWE-306 (Missing Authentication, la condición habilitante: el webhook de entrada no exige sesión). OWASP A10:2021 (Server-Side Request Forgery) + A04:2021 (Insecure Design).

El guard (assertMediaTargetAllowed) es correcto: resuelve el hostname con dns.lookup(host, {family:4}) y clasifica la IP resultante contra la lista completa de rangos no enrutables (RFC1918, loopback, link-local incl. 169.254.169.254, CGNAT, reservados) antes de dejar pasar nada. El problema no está en esa clasificación — está en lo que ocurre después: el flujo estándar (fetchMedia) llama a fetch(url) directamente sobre la URL original, y ese fetch vuelve a resolver el hostname por su cuenta, en un instante distinto y sin conocer el resultado del check anterior. Nada conecta la IP que el guard validó con la IP a la que realmente se conecta la descarga.

Esa ventana es explotable porque el atacante controla el servidor DNS autoritativo del hostname del webhook (el toolkit del lab, o en el mundo real cualquier hostname bajo su propio dominio) y puede responder lo que quiera en cada consulta, con TTL=0 para evitar cualquier caché. Basta con servir una IP pública inocua a la primera consulta (la que hace el guard) y la IP interna real a partir de la segunda (la que hace el fetch) para que el check y el uso diverjan.

La mitigación existe en el propio código, aplicada al segundo operador (fetchMediaPinned): resolver una sola vez, validar esa dirección, y conectar explícitamente a esa IP (con el header Host original para no romper el vhost), en vez de volver a pasar el hostname a una librería HTTP que lo re-resolverá por su cuenta. Es la diferencia entre "confiar en que el nombre siga significando lo mismo" y "fijar lo que significaba en el momento en que lo comprobé".

Lección: cualquier control de seguridad que decida algo a partir de resolver un nombre (DNS, pero el patrón aplica igual a symlinks, rutas de fichero o tokens de un solo uso) deja de ser válido en el instante en que ese nombre puede volver a resolverse de otra forma. Si el check y el uso son dos operaciones distintas, hay que fijar el resultado del check y usar exactamente ese valor — nunca volver a preguntar por el nombre original.

Laboratorio basado en la reproducción anonimizada (empresa ficticia Convergia) de una técnica real de bug bounty sobre un guard SSRF basado en resolución DNS, bypasseado mediante DNS rebinding (TOCTOU) contra el downloader de media de un webhook de mensajería.

Contact

Practice, learn and hack

Bug bounty practice platform with labs based on real hacks. Learn ethical hacking in safe environments.

contact→

Follow us

YouTube
@0xGorka
X
@gorkaelbochi
LinkedIn
gorka-el-bochi-morillo
Instagram
@_.gorkaaa.b
Email
team@bblabs.es

Access every lab from €7.99/mo

New labs every week. Cancel anytime.

Create account

BBLabs is the bug bounty labs platform where you learn bug bounty with real vulnerabilities extracted from paid hacks on HackerOne, YesWeHack and Bugcrowd. Here you practice web hacking —XSS, SQLi, IDOR, SSRF, CSRF and more— in downloadable environments, capture the flag, read the writeup and apply the technique on active bug bounty programs.

BBLabs is the alternative to HackTheBox, TryHackMe and PentesterLab for those who want to practice bug bounty with real hacks instead of artificial CTFs. From €7.99/mo, no commitment.

→ Learn bug bounty from scratch→ How to do bug bounty step by step→ Real bug bounty reports→ BBLabs for companies and academiesLabsAcademyVulnerabilitiesToolsHacker rankingXSS labsIDOR labsSSRF labsCSRF labsHackTheBox alternativeHack4u alternativeTryHackMe alternativePortSwigger alternativePentesterLab alternativeBug Bounty Labs comparisonHackerOne to practiceOffSec / OSCP alternativeINE / eWPT alternativeHTB Academy alternativeDVWA alternativeJuice Shop alternativeVulnHub alternativePentesterAcademy alternativeRoot-Me alternativeHackTheBox vs TryHackMeBest bug bounty platforms 2026BlogSpoilersWhat is bug bounty?How much do you earn in bug bounty?OWASP Top 10 explainedBest sites to practice web hackingHow to become an ethical hacker from scratchBurp Suite tutorial (Spanish)OSCP guide and prepGoogle Dorks for bug bountyHow much an ethical hacker earns in SpainBug bounty tools 2026Best cybersecurity certifications 2026Burp Suite tutorialsqlmap tutorialffuf web fuzzingnuclei tutorialHTTP Request SmugglingWAF bypassPrompt injection (LLM)Google Dorks
Made withand code
TermsPrivacyCookiesComparisonES

© 2026 BBLABS v2 — All rights reserved