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:
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 chain | Con chain (correlation) |
|---|---|
| "victim@gmail.com existe" | "victim@gmail.com es Juan Pérez, CTO en XYZ, padre de 2, vive en Madrid" |
| Severity Low | Severity High/Critical |
| $50-$500 | $2K-$15K |
Patrón 1 — enumeration via /forgot timing
Endpoint POST /forgot { email }:
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
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
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:
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:
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:
- Introspecta el schema (si está expuesto):
query { __schema { types { name fields { name } } } }. - Lista campos en
User. Frecuentemente hay campos protegidos por scope que se filtran por error. - Lanza
searchUserscon 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.
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:
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":
- Atacante encuentra que tu LinkedIn público muestra "+34 6XX..." en bio (común en perfiles de freelancers, consultores).
- App target.com permite "forgot password" via phone number alternativo:
http
POST /auth/forgot-by-phone {"phone": "+34 6XX XXX XXX"} - 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:
## 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=Npara 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
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Hay un payload extra al final
La query GraphQL exacta que extrae 100k usuarios con email + phone + employer en <2h sin trigger rate limits.
5 €/mes · cancela cuando quieras
Artículos relacionados
Mass PII Extraction vía GraphQL — 93 perfiles reales en 1 hora
Un endpoint GraphQL de sincronización de contactos sin rate limiting, sin verificación de propiedad y con batching de 200 números por petición. Resolución teléfono → identidad real.
Acortador de URL como PII leak masivo — extracción de ~300 personas/hora
Códigos de baja entropía + sin rate limiting + ticket sin auth = enumeración de teléfonos, tarjetas (BIN+últimos 4) y compras de clientes reales.