Nuevos labs cada semana — Accede a todos desde 5€/mes

Nivel IntermedioCon cuenta

Account enumeration via LinkedIn + phone — exposure de millones de usuarios

Caso real: account enumeration en API combinada con scraping de LinkedIn + reverse phone lookups para correlacionar identidades reales con accounts privados. Privacy impact crítico.

Gorka El Bochi11 de mayo de 202614 min

Respuesta rápida

Account enumeration parece "bug low severity" en informes superficiales. Pero el chain — enumerar accounts en una API privada + correlacionar con LinkedIn público + reverse phone lookup → conviertes "user privado" en "Juan Pérez, CTO de XYZ, móvil +34 6XX XXX XXX, esposa María". Privacy impact masivo. El bug class que más bounties grandes generó en 2026 en apps dating, fintech, salud, y networking. Reportado a 5+ unicornios con bounties combinados >€80K. Severity media-alta cuando lo enmarcas con regulación GDPR/CCPA/PIPL.


El framing — por qué importa más allá de "user exists"

Una respuesta enumeration típica:

json
GET /api/user/check?email=victim@gmail.com
{"exists": true}

Reports superficiales: "Severity Low, just informational". Reward: $50-$500.

El framing correcto: enumeration + correlation = de-anonymization. Apps con users "privados" (dating, mental health, recovery, activism) → mapear identidad real a perfil "anónimo" es Critical privacy violation.

Sin chainCon chain (correlation)
"victim@gmail.com existe""victim@gmail.com es Juan Pérez, CTO en XYZ, padre de 2, vive en Madrid"
Severity LowSeverity High/Critical
$50-$500$2K-$15K

Patrón 1 — enumeration via /forgot timing

Endpoint POST /forgot { email }:

graphql
victim@gmail.com    → 200 OK in 850ms ("email enviado si existe")
random@test.tld     → 200 OK in 120ms ("email enviado si existe")

Mismo response body, diferente timing. El email real ejecuta más código (lookup DB, generación token, envío email) que el falso → diferencia medible.

Detección automatizada

python
import requests, time

def time_request(email):
    start = time.time()
    requests.post("https://target.com/forgot", json={"email": email})
    return time.time() - start

print(time_request("victim@gmail.com"))   # 0.85s
print(time_request("fake_user_xx@gmail.com"))  # 0.12s

Tras 20 muestras de control, distinguir 200ms de 800ms es trivial.

Mitigación que se rompe

Devs suelen "fix" añadiendo setTimeout(800ms) al endpoint falso → pero olvidan que existe setTimeout real → la variancia distingue 800±50ms (artificial) de 800±200ms (natural).


Patrón 2 — enumeration via different response codes

http
POST /signup
{"email": "exists@target.com", "password": "x"}
→ 400 "Email already in use"

POST /signup
{"email": "new@target.com", "password": "x"}
→ 200 "OK"

Trivial. Pero menos obvio:

http
POST /api/2fa/send-otp
{"email": "exists@target.com"}      → 200 (otp sent)
{"email": "new@target.com"}          → 200 (otp sent, fake)

Misma respuesta — pero solo el real genera audit log, costo SMS, y rate limit per-user. Pivota a side channels: ¿hay un counter de "X emails sent" visible en algún endpoint admin/billing? Cada call al primero incrementa, al segundo no.


Patrón 3 — GraphQL introspection + enumeration

GraphQL multiplica la superficie. Queries de búsqueda parcial → enumeration friendly:

graphql
query {
  searchUsers(query: "victim", limit: 50) {
    edges {
      node {
        id
        username
        # campos sensibles según el schema
        publicProfile {
          fullName
          jobTitle
          company { name }
        }
        contactInfo {
          email
          phoneNumber
        }
      }
    }
  }
}

Test:

  1. Introspecta el schema (si está expuesto): query { __schema { types { name fields { name } } } }.
  2. Lista campos en User. Frecuentemente hay campos protegidos por scope que se filtran por error.
  3. Lanza searchUsers con queries cortas (1-2 chars) → resultados extensos.

Bug típico

User.email y User.phoneNumber están "protegidos" en el query directo user(id) (chequea ownership) pero no en searchUsers (devuelve la lista sin chequeo per-field) → leak masivo.

<!-- PAYWALL -->

Patrón 4 — chain con LinkedIn scraping

El verdadero salto de severity. Tras enumerar 10K emails en target:

Step 1 — bulk LinkedIn lookup

LinkedIn permite contact import por email. Si controlas una cuenta LinkedIn con red profesional moderada, puedes:

  • Subir CSV de emails → LinkedIn devuelve "estos están en LinkedIn, estos no".
  • Para los que matchean: nombre completo + empresa actual + cargo + ubicación + foto.
  • Tools: Hunter.io, Apollo.io, ZoomInfo, Lusha, Snov.io — todos consumen email → devuelven perfil enriquecido.

Step 2 — reverse phone lookup

Si la API target.com expone teléfono:

  • Truecaller (web search, no API pública pero scraping doable)
  • WhitePages.com (US)
  • Yellow.es (España)
  • Showcaller / Hiya
  • Otros services agregadores

Result: número anónimo → "Juan Pérez, Madrid, edad ~35".

Step 3 — correlation engine

Para cada user enumerado:

yaml
target_id: 184729
email: jperez@gmail.com
phone: +34 6XX XXX XXX

 LinkedIn lookup:
   name: Juan Pérez
   company: XYZ Corp
   title: Senior Engineer
   location: Madrid, ES
   
 Reverse phone:
   carrier: Movistar
   region: Madrid centro
   public listings: Juan Pérez (esposa María)

 Combined dossier:
   {full_name, employer, title, family, location, target_userId}

→ Ahora el "user anónimo 184729" en target.com es Juan Pérez con todos sus datos. Es el ataque que las regulaciones de privacidad clasifican como severe — un user que pensó estar usando la app "anónimamente" está completamente identificado.


Patrón 5 — phone + LinkedIn email reset chain

Documentado en el report "An attacker forgets your email and LinkedIn gives him your phone number":

  1. Atacante encuentra que tu LinkedIn público muestra "+34 6XX..." en bio (común en perfiles de freelancers, consultores).
  2. App target.com permite "forgot password" via phone number alternativo:
    http
    POST /auth/forgot-by-phone
    {"phone": "+34 6XX XXX XXX"}
    
  3. Si la app no requiere otros factores → SMS reset al phone del atacante (porta-SIM si conseguiste swap, o si target SMS llega a phone visible vía LinkedIn).

Severity

Cuando esto incluye recovery via phone del atacante (no de la víctima) → ATO. Cuando solo permite descubrir el phone de la víctima → privacy violation high.


Patrón 6 — Bulk extraction sin trigger rate limit

Endpoints "informativos" suelen tener rate limit relajado (100 req/min) porque "no leak sensible". Pero si filtras phone/email/PII → rate limit lax × millones de IDs = bulk extraction.

Técnicas para no trigger rate limit

  • Distributed IPs: residential proxy network (legal: ProxyMesh, Bright Data; bug bounty use: NO).
  • Pagination with offset: ?limit=100&offset=N — cada request es "diferente" en logs.
  • Field selection (GraphQL): solo pide los campos sensibles, evita "expensive" fields que disparen monitoring.
  • Off-peak time: lanzar attack de madrugada local del target → menos detección manual.

[!warning] Bug bounty scope NO ejecutes bulk extraction contra usuarios reales para PoC. La mayoría de programs prohíbe explícitamente — extrae 5-10 records de TUS cuentas + 1-2 de control (que tu sepas que existen), documenta el patrón. El report con "y aquí 50K records reales extraídos" suele resultar en ban del program.


Reporting framework

El report tiene que narrar el chain:

markdown
## Summary
Combined account enumeration in /api/forgot + bulk lookup via /graphql 
searchUsers allows correlating private user accounts with full real-world
identities via LinkedIn email lookup.

## Steps
1. Enumeration: POST /api/forgot exhibits timing oracle (850ms real vs 120ms fake)
2. Bulk: GraphQL searchUsers(query) returns phone+email without ownership check
3. Correlation: LinkedIn email lookup (CSV import) yields full name + employer

## Impact
"Anonymous" user IDs 100% de-anonymizable. PII covered by GDPR Art. 6/9.
Specific cases (mental health, dating apps, recovery): severity Critical.

## Demonstration
Attached: 3 test accounts I control + 1 control account explicitly verified
with the program team. Total records: 4. Method generalizable to entire dataset.

Reportar el chain completo y no cada pieza por separado. Pieza individual = $200; chain = $5K-$15K.


Hunting checklist

  • Endpoints donde response delata existencia de user: forgot, signup, login error messages, 2FA send.
  • Timing oracle: mide tiempo real vs fake (10 samples cada uno).
  • GraphQL: introspección + searchUsers / nodos por filter parcial.
  • Bulk APIs: contact import, batch lookup, suggested friends.
  • Campos sensibles en respuesta: email, phone, full name, address, jobs.
  • Pagination: ?offset=N para extracción incremental sin spike.
  • LinkedIn lookup chain: ¿el target tiene email o phone públicos?
  • Reverse phone: si la API leakea phone, ¿es correlationable con identidad real?
  • Framing privacy: identifica regulación aplicable (GDPR, CCPA, LGPD).
  • Report con chain narrativo + PoC mínimo (3-5 records).

Labs relacionados

Practica enumeration timing, GraphQL bulk extraction y PII correlation en labs de enumeration y PII.

Practica esto en un lab

Mass Pii Extraction Graphql Enumeration

Resolver

Sigue aprendiendo · cuenta gratis

Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.

Crear cuenta
Premium · 1 técnica más

Hay un payload extra al final

La query GraphQL exacta que extrae 100k usuarios con email + phone + employer en <2h sin trigger rate limits.

Desbloquear

5 €/mes · cancela cuando quieras

Artículos relacionados